2026-02-05 18:05:06 -05:00

325 lines
9.3 KiB
Python

"""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")