325 lines
9.3 KiB
Python
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")
|