from __future__ import annotations import json import shutil from pathlib import Path from retrobuilder.model import BaseSpec, FeatureSpec, ProfileSpec LIVE_DIR_STRUCTURE = ( 'config/chroot_local-packageslists', 'config/chroot_local-packages', 'config/chroot_local-includes', 'config/chroot_local-hooks', ) def ensure_live_structure(live_dir: Path) -> None: for relative in LIVE_DIR_STRUCTURE: (live_dir / relative).mkdir(parents=True, exist_ok=True) def clear_directory_contents(path: Path) -> None: path.mkdir(parents=True, exist_ok=True) for child in path.iterdir(): if child.name == '.gitkeep': continue if child.is_dir() and not child.is_symlink(): shutil.rmtree(child) else: child.unlink() def copy_tree_contents(src: Path, dst: Path) -> None: if not src.exists(): return dst.mkdir(parents=True, exist_ok=True) for child in src.iterdir(): target = dst / child.name if child.is_dir(): shutil.copytree(child, target, dirs_exist_ok=True) else: shutil.copy2(child, target) def copy_glob(src_dir: Path, pattern: str, dst_dir: Path, prefix: str = '') -> None: if not src_dir.exists(): return dst_dir.mkdir(parents=True, exist_ok=True) for src in sorted(src_dir.glob(pattern)): if src.is_dir(): continue name = f'{prefix}{src.name}' if prefix else src.name shutil.copy2(src, dst_dir / name) def save_json(path: Path, payload: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(payload, indent=2, sort_keys=True) + '\n', encoding='utf-8') def save_profile_metadata( path: Path, profile_name: str, profile: ProfileSpec, base_name: str, base: BaseSpec, base_chain: list[tuple[str, BaseSpec]] | None = None, ) -> None: save_json(path, { 'profile_name': profile_name, 'profile': profile.to_dict(), 'base_name': base_name, 'base': base.to_dict(), 'base_chain': [ {'name': item_name, 'spec': item_spec.to_dict()} for item_name, item_spec in (base_chain or [(base_name, base)]) ], }) def save_feature_metadata(path: Path, feature_name: str, feature: FeatureSpec) -> None: save_json(path, {'feature_name': feature_name, 'feature': feature.to_dict()}) def apply_profile_common_configuration(root: Path, live_dir: Path, profile_name: str, profile: ProfileSpec) -> None: ensure_live_structure(live_dir) notes_dir = live_dir / 'builder-notes' notes_dir.mkdir(parents=True, exist_ok=True) notes = [ f'profile={profile_name}', f'base={profile.base}', f'features={"|".join(profile.features)}', ] if profile.splash: splash_src = root / profile.splash if splash_src.exists(): splash_dst = notes_dir / splash_src.name shutil.copy2(splash_src, splash_dst) notes.append(f'splash={profile.splash}') (notes_dir / 'profile-common.txt').write_text('\n'.join(notes) + '\n', encoding='utf-8') def inject_module_resources(module_dir: Path, live_dir: Path, prefix: str) -> None: copy_tree_contents(module_dir / 'packageslists', live_dir / 'config' / 'chroot_local-packageslists') copy_tree_contents(module_dir / 'packages', live_dir / 'config' / 'chroot_local-packages') copy_tree_contents(module_dir / 'chroot', live_dir / 'config' / 'chroot_local-includes') copy_glob(module_dir / 'hooks', '*.sh', live_dir / 'config' / 'chroot_local-hooks', prefix=f'{prefix}_') def profile_pre_build(live_dir: Path, profile_name: str) -> None: marker = live_dir / 'builder-notes' / 'pre-build.txt' marker.parent.mkdir(parents=True, exist_ok=True) marker.write_text(f'pre-build profile={profile_name}\n', encoding='utf-8') def profile_finalize(root: Path, profile_name: str) -> None: live_dir = root / 'live' out_dir = root / 'artifacts' / 'profiles' / profile_name / 'final' out_dir.mkdir(parents=True, exist_ok=True) summary = out_dir / 'summary.txt' summary.write_text( 'Profile finalization complete.\n' f'profile={profile_name}\n' f'live_exists={live_dir.exists()}\n', encoding='utf-8', )