"""euri-mklive — Universal CLI ISO builder.""" from __future__ import annotations import os import sys import tempfile from pathlib import Path import click from . import __version__ from .config import load from .rootfs import build, cleanup from .iso import generate from .distros import REGISTRY BANNER = """\033[1;36m ________ ______ ____ / ____/ / / / __ \\/ _/ / __/ / / / / /_/ // / / /___/ /_/ / _, _// / /_____/\\____/_/ |_/___/\033[0m \033[1m universal iso builder\033[0m \033[90m any distro, from any distro\033[0m """ SUPPORTED = ", ".join(sorted(REGISTRY.keys())) @click.group(invoke_without_command=True) @click.version_option(__version__, prog_name="euri-mklive") @click.pass_context def cli(ctx: click.Context) -> None: """\033[1mBuild bootable ISOs for any Linux distribution.\033[0m""" if ctx.invoked_subcommand is None: click.echo(BANNER) click.echo(f" \033[90msupported distros:\033[0m {SUPPORTED}") click.echo() click.echo(" \033[1;36meuri-mklive build-iso \033[0m build from YAML config") click.echo(" \033[1;36meuri-mklive interactive \033[0m launch interactive TUI") click.echo() click.echo(ctx.get_help()) @cli.command() @click.argument("config", type=click.Path(exists=True, path_type=Path)) @click.option("-o", "--output", type=click.Path(path_type=Path), default=".", help="Output directory") @click.option("--keep-rootfs", is_flag=True, help="Don't remove rootfs after ISO generation") def build_iso(config: Path, output: Path, keep_rootfs: bool) -> None: """Build an ISO from a YAML config file.""" click.echo(BANNER) cfg = load(config) output.mkdir(parents=True, exist_ok=True) distro = cfg["distro"] arch = cfg["arch"] init = cfg.get("init", "dinit") pkgs = len(cfg.get("packages", [])) click.echo(f" \033[36mdistro:\033[0m {distro}") click.echo(f" \033[36march:\033[0m {arch}") click.echo(f" \033[36minit:\033[0m {init}") click.echo(f" \033[36mpackages:\033[0m {pkgs}") click.echo() try: rootfs = build(cfg, output) iso = generate(rootfs, cfg, output) if not keep_rootfs: cleanup(rootfs) click.echo(f"\n\033[90mRootfs cleaned up.\033[0m") click.echo(f"\n\033[1;32mDone! ISO ready at {iso}\033[0m") except Exception as e: click.echo(f"\n\033[1;31mError: {e}\033[0m", err=True) sys.exit(1) @cli.command() @click.option("--fake", "mode", flag_value="fake", help="Run in fakeroot mode (no root needed)") @click.option("--root", "mode", flag_value="root", default=True, help="Run as root (default)") @click.option("-o", "--output", type=click.Path(path_type=Path), default=None, help="Output directory") @click.option("--keep-rootfs", is_flag=True, help="Don't remove rootfs after ISO generation") @click.option("-y", "--yes", is_flag=True, help="Skip confirmation prompts") @click.option("--config", "config_file", type=click.Path(path_type=Path), default=None, help="Load config from file (used by sudo re-exec)") def interactive(mode: str | None, output: Path | None, keep_rootfs: bool, yes: bool, config_file: Path | None) -> None: """Launch interactive TUI — select distro, packages, and build.""" from .tui import run_interactive, banner as tui_banner # load config from file (sudo re-exec) or run TUI if config_file and config_file.exists(): import json cfg = json.loads(config_file.read_text()) config_file.unlink(missing_ok=True) else: cfg = run_interactive() # override mode if flag given if mode: cfg["_mode"] = mode # override output out = output or Path(".") out.mkdir(parents=True, exist_ok=True) # inject skip confirmations if yes: cfg["_skip_confirm"] = True tui_banner() from rich.console import Console from rich.panel import Panel console = Console() # check mode and re-exec if needed if cfg["_mode"] == "root" and os.geteuid() != 0: console.print("[bold yellow]Root mode selected but not root. Re-executing with sudo...[/]") import subprocess as _sp import site import json # save config to temp file for sudo re-exec cfg_file = Path(tempfile.mktemp(suffix=".json", prefix="euri-cfg-")) cfg_file.write_text(json.dumps(cfg)) args = [sys.executable, "-m", "euri_mklive", "interactive", "--root", "--config", str(cfg_file)] if keep_rootfs: args.append("--keep-rootfs") if output: args.extend(["-o", str(output)]) # use 'sudo env PYTHONPATH=...' so root finds user-installed packages user_site = site.getusersitepackages() sudo_cmd = ["sudo", "env", f"PYTHONPATH={user_site}"] + args _sp.run(sudo_cmd, check=True) return if cfg["_mode"] == "fakeroot": import shutil as _shutil if not _shutil.which("fakeroot"): console.print("[bold red]Error:[/] fakeroot not found. Install it first.") sys.exit(1) console.print(Panel( "[bold yellow]Fakeroot mode[/]\n" "Performance degraded. Some post-install scripts may fail.\n" "For full functionality, run with sudo.", border_style="yellow", )) # build try: from .rootfs import build as _build from .iso import generate as _generate rootfs = _build(cfg, out) iso = _generate(rootfs, cfg, out) if not keep_rootfs: cleanup(rootfs) console.print(Panel( f"[bold green]ISO ready![/]\n{iso}", border_style="green", title="Success", )) except Exception as e: console.print(Panel(f"[bold red]Build failed:[/] {e}", border_style="red")) sys.exit(1) @cli.command() def supported() -> None: """List all supported distros.""" click.echo(BANNER) click.echo(" \033[1mSupported distros:\033[0m") for name, cls in sorted(REGISTRY.items()): click.echo(f" \033[36m{name:<15}\033[0m {cls.__doc__ or ''}") click.echo() click.echo(" \033[90mEach distro can be built FROM any other distro.\033[0m")