- Interactive TUI: distro/init/libc/arch/package selection, ISO config - Root vs fakeroot mode detection with warnings - Disk space check before build - Confirmation prompts with build summary - Fakeroot support via set_mode() in base.py
168 lines
5.4 KiB
Python
168 lines
5.4 KiB
Python
"""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 <config> \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")
|
|
def interactive(mode: str | None, output: Path | None, keep_rootfs: bool, yes: bool) -> None:
|
|
"""Launch interactive TUI — select distro, packages, and build."""
|
|
from .tui import run_interactive, banner as tui_banner
|
|
|
|
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
|
|
import shutil as _shutil
|
|
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
|
|
args = [sys.executable, "-m", "euri_mklive", "interactive", "--root"]
|
|
if keep_rootfs:
|
|
args.append("--keep-rootfs")
|
|
if output:
|
|
args.extend(["-o", str(output)])
|
|
_sp.run(["sudo"] + args, 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")
|