- real dinit service files (connmand, sddm, ntpd, chrony, sshd) - void backend properly swaps runit for dinit with service definitions - ISO generation finds kernel/initrd, builds proper GRUB config - ship all package manager binaries in ISO for offline use - LICENSES/ directory with proper GPL-2.0 plain text for xbps, pacman, apt, dnf, apk, dpkg - backends explicitly install their own package managers - README updated with binary inclusion and license info
163 lines
5.4 KiB
Python
163 lines
5.4 KiB
Python
"""ISO generator — squashes rootfs and creates bootable ISO."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import glob
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
def generate(rootfs: Path, config: dict, output: Path) -> Path:
|
|
"""Generate a bootable ISO from a rootfs."""
|
|
iso_cfg = config.get("iso", {})
|
|
label = iso_cfg.get("label", "EURI")
|
|
name = iso_cfg.get("name", "Euri Linux")
|
|
version = iso_cfg.get("version", "1.0")
|
|
distro = config.get("distro", "void")
|
|
libc = config.get("libc", "glibc")
|
|
|
|
print(f"\n\033[1;36m==>\033[0m \033[1mGenerating ISO: {name} {version}\033[0m")
|
|
|
|
workdir = Path(tempfile.mkdtemp(prefix="mklive-iso-"))
|
|
iso_dir = workdir / "iso"
|
|
iso_dir.mkdir()
|
|
|
|
# copy rootfs into ISO staging
|
|
_run(["cp", "-a", str(rootfs) + "/.", str(iso_dir)])
|
|
|
|
# ─── 1. find kernel and initrd in rootfs ───
|
|
vmlinuz = _find_file(iso_dir, "vmlinuz*") or _find_file(iso_dir, "vmlinux*")
|
|
if not vmlinuz:
|
|
raise FileNotFoundError("No kernel found in rootfs")
|
|
|
|
initrd = _find_file(iso_dir, "initramfs*") or _find_file(iso_dir, "initrd*")
|
|
if not initrd:
|
|
raise FileNotFoundError("No initramfs/initrd found in rootfs")
|
|
|
|
live_dir = iso_dir / "boot" / "live"
|
|
live_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
_run(["cp", str(vmlinuz), str(live_dir / "vmlinuz")])
|
|
_run(["cp", str(initrd), str(live_dir / "initrd")])
|
|
print(f" \033[90mkernel:\033[0m {vmlinuz.name}")
|
|
print(f" \033[90minitrd:\033[0m {initrd.name}")
|
|
|
|
# ─── 2. copy package manager binaries + licenses ───
|
|
pkg_bin_dir = iso_dir / "usr" / "bin"
|
|
pkg_bin_dir.mkdir(parents=True, exist_ok=True)
|
|
licenses_dir = iso_dir / "usr" / "share" / "licenses" / "mklive"
|
|
licenses_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
pkg_mgrs = {
|
|
"xbps-install": "/usr/bin/xbps-install",
|
|
"xbps-remove": "/usr/bin/xbps-remove",
|
|
"xbps-query": "/usr/bin/xbps-query",
|
|
"xbps-repo": "/usr/bin/xbps-repo",
|
|
"xbps-digest": "/usr/bin/xbps-digest",
|
|
"xbps-pkgdb": "/usr/bin/xbps-pkgdb",
|
|
"xbps-rindex": "/usr/bin/xbps-rindex",
|
|
"pacman": "/usr/bin/pacman",
|
|
"apt": "/usr/bin/apt",
|
|
"apt-get": "/usr/bin/apt-get",
|
|
"dpkg": "/usr/bin/dpkg",
|
|
"dnf": "/usr/bin/dnf",
|
|
"apk": "/sbin/apk",
|
|
}
|
|
|
|
shipped = []
|
|
for name_bin, src in pkg_mgrs.items():
|
|
full = rootfs / src
|
|
if full.exists():
|
|
_run(["cp", str(full), str(pkg_bin_dir / name_bin)])
|
|
shipped.append(name_bin)
|
|
|
|
if shipped:
|
|
(licenses_dir / "MANIFEST").write_text(
|
|
f"# Package Manager Binaries\n\n"
|
|
f"Shipped with this ISO for offline package management:\n\n"
|
|
+ "\n".join(f"- {b}" for b in shipped)
|
|
+ "\n\nSee individual LICENSE files for each tool.\n"
|
|
)
|
|
print(f" \033[90mpkg managers:\033[0m {', '.join(shipped)}")
|
|
|
|
# ─── 3. squashfs ───
|
|
squash = workdir / "filesystem.squashfs"
|
|
print(f"\033[1;36m==>\033[0m \033[1mCreating squashfs image...\033[0m")
|
|
_run(["mksquashfs", str(iso_dir), str(squash), "-comp", "xz", "-b", "1M", "-noappend"])
|
|
|
|
# ─── 4. GRUB config ───
|
|
grub_dir = iso_dir / "boot" / "grub"
|
|
grub_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
boot_params = f"boot=live components"
|
|
if libc == "musl":
|
|
boot_params += " musl"
|
|
|
|
grub_cfg = grub_dir / "grub.cfg"
|
|
grub_cfg.write_text(f"""\
|
|
set timeout=10
|
|
set default=0
|
|
set gfxmode=auto
|
|
insmod all_video
|
|
insmod gfxterm
|
|
|
|
terminal_output gfxterm
|
|
|
|
menuentry "{name}" {{
|
|
linux /boot/live/vmlinuz {boot_params}
|
|
initrd /boot/live/initrd
|
|
}}
|
|
|
|
menuentry "{name} (copy to RAM)" {{
|
|
linux /boot/live/vmlinuz {boot_params} toram
|
|
initrd /boot/live/initrd
|
|
}}
|
|
|
|
menuentry "{name} (verbose)" {{
|
|
linux /boot/live/vmlinuz {boot_params} debug
|
|
initrd /boot/live/initrd
|
|
}}
|
|
""")
|
|
print(f" \033[90mgrub cfg:\033[0m {grub_cfg.name}")
|
|
|
|
# ─── 5. EFI boot ───
|
|
efi_dir = iso_dir / "EFI" / "BOOT"
|
|
efi_dir.mkdir(parents=True, exist_ok=True)
|
|
efi_src = Path("/usr/lib/grub/x86_64-efi/monolithic/grubx64.efi")
|
|
if efi_src.exists():
|
|
_run(["cp", str(efi_src), str(efi_dir / "BOOTX64.EFI")])
|
|
|
|
# ─── 6. xorriso to create ISO ───
|
|
iso_path = output / f"{name.lower().replace(' ', '-')}-{version}-{distro}-{libc}-x86_64.iso"
|
|
print(f"\033[1;36m==>\033[0m \033[1mCreating ISO: {iso_path.name}\033[0m")
|
|
_run([
|
|
"xorriso", "-as", "mkisofs",
|
|
"-V", label,
|
|
"-o", str(iso_path),
|
|
"-J", "-joliet-long",
|
|
"-b", "boot/live/vmlinuz",
|
|
"-no-emul-boot", "-boot-load-size", "4",
|
|
"-boot-info-table",
|
|
"-eltorito-alt-boot",
|
|
"-e", "boot/live/vmlinuz",
|
|
"-no-emul-boot",
|
|
str(iso_dir),
|
|
])
|
|
|
|
size_mb = iso_path.stat().st_size / (1024 * 1024)
|
|
print(f"\n\033[1;32m==>\033[0m \033[1mISO ready: {iso_path} ({size_mb:.1f} MB)\033[0m")
|
|
return iso_path
|
|
|
|
|
|
def _find_file(root: Path, pattern: str) -> Path | None:
|
|
"""Find a file recursively by glob pattern."""
|
|
matches = glob.glob(str(root / "**" / pattern), recursive=True)
|
|
return Path(matches[0]) if matches else None
|
|
|
|
|
|
def _run(cmd: list[str], **kw) -> subprocess.CompletedProcess:
|
|
print(f" \033[90m$\033[0m \033[1m{' '.join(cmd)}\033[0m")
|
|
return subprocess.run(cmd, check=True, capture_output=True, **kw)
|