"""Initial portage sync and profile setup (run inside chroot).""" import re from pathlib import Path from .utils import info, success, error, fatal, prompt, run, run_quiet, emerge DESIRED_PROFILE = "default/linux/amd64/23.0/desktop" SWAP_MAPPER = "swap" # System defaults DEFAULT_TIMEZONE = "America/New_York" DEFAULT_LOCALE = "en_US.UTF-8" DEFAULT_HOSTNAME = "legion" def get_swap_partition() -> Path | None: """Read swap partition from /etc/conf.d/dmcrypt.""" dmcrypt_conf = Path("/etc/conf.d/dmcrypt") if not dmcrypt_conf.exists(): return None content = dmcrypt_conf.read_text() for line in content.split("\n"): if line.startswith("source="): # Extract path from source='...' or source="..." match = re.match(r"source=['\"]?([^'\"]+)['\"]?", line) if match: return Path(match.group(1)) return None def activate_swap() -> None: """Activate encrypted swap for use during compilation. In a chroot, OpenRC isn't running, so we manually set up swap. """ info("=== Activating Encrypted Swap ===") swap_part = get_swap_partition() if swap_part is None: error("Could not find swap partition in /etc/conf.d/dmcrypt") error("Skipping swap activation - builds may be slower") return mapper_path = Path(f"/dev/mapper/{SWAP_MAPPER}") # Check if already active if mapper_path.exists(): info("Swap already active") run("swapon", "--show") return info(f"Setting up encrypted swap on {swap_part}...") # Create encrypted swap with random key run( "cryptsetup", "open", "--type", "plain", "--cipher", "aes-xts-plain64", "--key-size", "256", "--key-file", "/dev/urandom", str(swap_part), SWAP_MAPPER, ) # Format as swap run("mkswap", str(mapper_path)) # Activate run("swapon", str(mapper_path)) success("Encrypted swap activated.") run("swapon", "--show") def deactivate_swap() -> None: """Deactivate encrypted swap before exiting chroot.""" mapper_path = Path(f"/dev/mapper/{SWAP_MAPPER}") if not mapper_path.exists(): return info("Deactivating encrypted swap...") run("swapoff", str(mapper_path), check=False) run("cryptsetup", "close", SWAP_MAPPER, check=False) success("Swap deactivated.") def emerge_webrsync() -> None: """Initial portage tree sync via webrsync.""" info("=== Syncing Portage Tree (webrsync) ===") run("emerge-webrsync") success("Portage tree synced.") def list_profiles() -> list[tuple[int, str, bool]]: """List available profiles and return parsed list. Returns list of (number, profile_name, is_selected). """ result = run_quiet("eselect", "profile", "list") profiles = [] for line in result.stdout.strip().split("\n"): # Format: " [N] profile/name (stable) *" # The * indicates currently selected match = re.match(r'\s*\[(\d+)\]\s+(\S+)(?:\s+\(.*\))?(\s+\*)?', line) if match: num = int(match.group(1)) name = match.group(2) selected = match.group(3) is not None profiles.append((num, name, selected)) return profiles def find_profile_number(profiles: list[tuple[int, str, bool]], target: str) -> int | None: """Find profile number for a given profile name.""" for num, name, _ in profiles: if name == target: return num return None def select_profile() -> None: """Interactive profile selection.""" info("=== Profile Selection ===") profiles = list_profiles() # Display profiles print() info("Available profiles:") run("eselect", "profile", "list") # Find desired profile target_num = find_profile_number(profiles, DESIRED_PROFILE) if target_num is None: error(f"Could not find profile: {DESIRED_PROFILE}") print() choice = prompt("Enter profile number manually: ") try: target_num = int(choice) except ValueError: fatal("Invalid profile number") else: print() success(f"Recommended: [{target_num}] {DESIRED_PROFILE}") response = prompt(f"Set profile to {DESIRED_PROFILE}? (y/n/other number): ").strip() if response.lower() == "n": fatal("Profile selection cancelled") elif response.lower() != "y": try: target_num = int(response) except ValueError: fatal("Invalid input") # Set the profile info(f"Setting profile to [{target_num}]...") run("eselect", "profile", "set", str(target_num)) # Verify print() info("Current profile:") run("eselect", "profile", "show") success("Profile set.") def setup_repositories() -> None: """Install eselect-repository and enable guru overlay.""" info("=== Setting Up Repositories ===") # Install eselect-repository info("Installing eselect-repository...") emerge("app-eselect/eselect-repository", ask=False, verbose=False) success("eselect-repository installed.") # Enable guru overlay info("Enabling guru overlay...") run("eselect", "repository", "enable", "guru") success("guru overlay enabled.") # Sync guru info("Syncing guru overlay...") run("emerge", "--sync", "guru") success("guru overlay synced.") def setup_timezone(tz: str = DEFAULT_TIMEZONE) -> None: """Set system timezone.""" info(f"=== Setting Timezone: {tz} ===") tz_file = Path(f"/usr/share/zoneinfo/{tz}") if not tz_file.exists(): error(f"Timezone {tz} not found") return localtime = Path("/etc/localtime") # Check if already set correctly (idempotency) if localtime.is_symlink(): try: if localtime.resolve() == tz_file.resolve(): info(f"Timezone already set to {tz}") return except OSError: pass # Broken symlink, continue to fix it if localtime.exists() or localtime.is_symlink(): localtime.unlink() localtime.symlink_to(tz_file) success(f"Timezone set to {tz}") def setup_locale(locale: str = DEFAULT_LOCALE) -> None: """Configure locale.gen and generate locales.""" info(f"=== Configuring Locale: {locale} ===") locale_gen = Path("/etc/locale.gen") # Read existing content if locale_gen.exists(): content = locale_gen.read_text() else: content = "" # Check if locale is already uncommented locale_line = f"{locale} UTF-8" if locale_line in content and not f"#{locale_line}" in content: info(f"{locale} already enabled") else: # Uncomment or append the locale if f"#{locale_line}" in content: content = content.replace(f"#{locale_line}", locale_line) elif f"# {locale_line}" in content: content = content.replace(f"# {locale_line}", locale_line) else: content += f"\n{locale_line}\n" locale_gen.write_text(content) success(f"Enabled {locale} in /etc/locale.gen") # Generate locales info("Running locale-gen...") run("locale-gen") # Set system locale env_locale = Path("/etc/env.d/02locale") env_locale.write_text(f'LANG="{locale}"\nLC_COLLATE="C.UTF-8"\n') success(f"Set LANG={locale} in /etc/env.d/02locale") # Update environment run("env-update", check=False) def setup_hostname(hostname: str = DEFAULT_HOSTNAME) -> None: """Set system hostname.""" info(f"=== Setting Hostname: {hostname} ===") # /etc/hostname hostname_file = Path("/etc/hostname") # Check if already set correctly (idempotency) if hostname_file.exists() and hostname_file.read_text().strip() == hostname: info(f"Hostname already set to {hostname}") return hostname_file.write_text(f"{hostname}\n") success(f"Set /etc/hostname to {hostname}") # /etc/conf.d/hostname (OpenRC) conf_hostname = Path("/etc/conf.d/hostname") conf_hostname.write_text(f'hostname="{hostname}"\n') success("Set /etc/conf.d/hostname") # Update /etc/hosts hosts_file = Path("/etc/hosts") if hosts_file.exists(): content = hosts_file.read_text() # Check if we need to add the hostname if hostname not in content: # Add hostname to localhost line lines = content.split("\n") new_lines = [] for line in lines: if line.startswith("127.0.0.1"): if hostname not in line: line = f"{line} {hostname}" new_lines.append(line) hosts_file.write_text("\n".join(new_lines)) success(f"Added {hostname} to /etc/hosts") def initial_sync() -> None: """Full initial sync workflow.""" activate_swap() print() emerge_webrsync() print() select_profile() print() setup_repositories() print() setup_timezone() print() setup_locale() print() setup_hostname() print() success("=== Initial Portage Setup Complete ===") print() info("Swap is active for compilation. Next steps:") print(" 1. Review /etc/portage/make.conf") print(" 2. emerge -avuDN @world") print(" 3. Install kernel and bootloader") print() info("When done, deactivate swap before exiting chroot:") print(" python setup.py swap-off")