From 06350c6299e93896c2ecbc851c2ae99ac519de82 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 2 Apr 2026 23:59:19 +0200 Subject: [PATCH 001/143] captain: fix .isEnabledFor(logging.DEBUG) calls Signed-off-by: Ricardo Pardini --- captain/cli/_release.py | 2 +- captain/cli/_stages.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/captain/cli/_release.py b/captain/cli/_release.py index 41f4700b..1511ab6d 100644 --- a/captain/cli/_release.py +++ b/captain/cli/_release.py @@ -168,7 +168,7 @@ def _cmd_release(cfg: Config, extra_args: list[str], args: object = None) -> Non "--entrypoint", "/usr/bin/uv", docker.RELEASE_IMAGE, - *(["--verbose"] if logging.getLogger().isEnabledFor(logging.DEBUG) else []), + *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), "run", *inner_cmd, ) diff --git a/captain/cli/_stages.py b/captain/cli/_stages.py index d15284e7..2f7fd180 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/_stages.py @@ -50,7 +50,7 @@ def _build_kernel_stage(cfg: Config) -> None: "--entrypoint", "/usr/bin/uv", cfg.builder_image, - *(["--verbose"] if logging.getLogger().isEnabledFor(logging.DEBUG) else []), + *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), "run", "/work/build.py", "kernel", @@ -86,7 +86,7 @@ def _build_tools_stage(cfg: Config) -> None: "--entrypoint", "/usr/bin/uv", cfg.builder_image, - *(["--verbose"] if logging.getLogger().isEnabledFor(logging.DEBUG) else []), + *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), "run", "/work/build.py", "tools", @@ -188,7 +188,7 @@ def _build_iso_stage(cfg: Config) -> None: "--entrypoint", "/usr/bin/uv", cfg.builder_image, - *(["--verbose"] if logging.getLogger().isEnabledFor(logging.DEBUG) else []), + *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), "run", "/work/build.py", "iso", From af0ed1806c2451643fe2303fe4ce185bd7b7f908 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 2 Apr 2026 23:59:47 +0200 Subject: [PATCH 002/143] captain: force type console in __init__.py Signed-off-by: Ricardo Pardini --- captain/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/captain/__init__.py b/captain/__init__.py index 0c2b4290..e4082eeb 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -16,9 +16,9 @@ # Rich console — writes to stderr so log output never pollutes piped stdout. # If running under GHA, force colors. if os.environ.get("GITHUB_ACTIONS", "") == "": - console = Console(stderr=True) + console: Console = Console(stderr=True) else: - console = Console(stderr=True, color_system="standard", width=160, highlight=False) + console: Console = Console(stderr=True, color_system="standard", width=160, highlight=False) # Install Rich traceback handler globally (once, at import time). _install_rich_traceback(console=console, show_locals=True, width=None) From a71a35a8739510c84a02f84fac0116b0a98ea0fc Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 00:00:51 +0200 Subject: [PATCH 003/143] captain: catch required arg and loosen type (WiP: other way around.) Signed-off-by: Ricardo Pardini --- captain/config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/captain/config.py b/captain/config.py index a8ad0619..67bdf174 100644 --- a/captain/config.py +++ b/captain/config.py @@ -88,7 +88,7 @@ def needs_docker(self) -> bool: ) @classmethod - def from_args(cls, args: argparse.Namespace, project_dir: Path) -> Config: + def from_args(cls, args: argparse.Namespace, project_dir: Path | None) -> Config: """Create a Config from a parsed :class:`argparse.Namespace`. The *args* namespace is produced by :mod:`configargparse` which @@ -98,6 +98,10 @@ def from_args(cls, args: argparse.Namespace, project_dir: Path) -> Config: ``getattr`` with fallbacks is used because per-subcommand parsers only define the flags relevant to that subcommand. """ + + if project_dir is None: + raise ValueError("project_dir must be provided to Config.from_args") + return cls( project_dir=project_dir, output_dir=project_dir / "out", From 735200a23d081b299f7a6c6c12409025aedbecb5 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 00:01:18 +0200 Subject: [PATCH 004/143] captain: util: Rich rule's separating run() output Signed-off-by: Ricardo Pardini --- captain/util.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/captain/util.py b/captain/util.py index 9876813b..5a325252 100644 --- a/captain/util.py +++ b/captain/util.py @@ -10,6 +10,10 @@ from dataclasses import dataclass from pathlib import Path +from rich.rule import Rule + +import captain + log = logging.getLogger(__name__) @@ -75,7 +79,12 @@ def run( run_env: dict[str, str] | None = None if env is not None: run_env = {**os.environ, **env} - return subprocess.run( + + # If not capturing, and debugging, emit a Rich separator line, for visual clarity. + if not capture and log.isEnabledFor(logging.DEBUG): + captain.console.print(Rule(f"⮕ Starting subprocess: {cmd} ⮕", style="green")) + + proc = subprocess.run( cmd, check=check, capture_output=capture, @@ -84,6 +93,14 @@ def run( cwd=cwd, ) + if capture: + return proc + + if log.isEnabledFor(logging.DEBUG): + captain.console.print(Rule(f"⮕ Finished subprocess: {cmd} ⮕", style="green")) + + return proc + def ensure_dir(path: Path) -> Path: """Create a directory (and parents) if it doesn't exist, return the path.""" From 11a95fab2aa4658e3aefa34096319e6bb17254a2 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 00:56:16 +0200 Subject: [PATCH 005/143] captain: better logging; `in-docker: ` prefix Signed-off-by: Ricardo Pardini --- captain/__init__.py | 9 ++++++--- captain/docker.py | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/captain/__init__.py b/captain/__init__.py index e4082eeb..d4e1a277 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -31,6 +31,9 @@ def format(self, record: logging.LogRecord) -> str: name = record.name stage = name.split(".", 1)[1] if name.startswith("captain.") else name record.__dict__["stage"] = stage + if os.environ.get("CAPTAIN_IN_DOCKER", "") == "docker": + # Running on host: show stage names in green for visual clarity. + record.__dict__["stage"] = f"[bold][blue]in-docker[/bold]: [/blue]{stage}" return super().format(record) @@ -42,12 +45,12 @@ def format(self, record: logging.LogRecord) -> str: console=console, show_time=False, show_level=True, - show_path=False, - markup=False, + show_path=True, + markup=True, rich_tracebacks=True, tracebacks_show_locals=True, ) - _handler.setFormatter(_StageFormatter("[%(stage)s] %(message)s")) + _handler.setFormatter(_StageFormatter("%(stage)s: %(message)s")) _root.addHandler(_handler) _root.setLevel(logging.DEBUG) _root.propagate = False diff --git a/captain/docker.py b/captain/docker.py index 0aaf9211..7fa747ea 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -116,6 +116,8 @@ def run_in_release(cfg: Config, *extra_args: str) -> None: "-w", "/work", "-e", + "CAPTAIN_IN_DOCKER=docker", + "-e", f"ARCH={cfg.arch}", "-e", "RELEASE_MODE=native", @@ -157,6 +159,8 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "-w", "/work", "-e", + "CAPTAIN_IN_DOCKER=docker", + "-e", f"ARCH={cfg.arch}", "-e", f"KERNEL_VERSION={cfg.kernel_version}", @@ -223,7 +227,6 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: docker_args.extend(["-e", "KERNEL_CONFIG=/work/kernel-config"]) docker_args.extend(extra_args) - log.debug("Docker args (builder): %s", docker_args) run(docker_args) From b94ca8899d76a79d24c1cf0ca3f4cb3dc8e8cba8 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 14:14:47 +0200 Subject: [PATCH 006/143] captain: add .editorconfig matching ruff - .editorconfig for shell scripts and shell templates Signed-off-by: Ricardo Pardini --- .editorconfig | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d6a2b591 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# Match Ruff style in .editorconfig format +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +indent_style = space +indent_size = 4 +tab_width = 4 +max_line_length = 100 + +[{*.sh,*.sh.j2}] +indent_style = space +indent_size = 4 +tab_width = 4 +max_line_length = 100 \ No newline at end of file From 3056c0911fde8bae2622d1a4324539a73393e142 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 14:15:59 +0200 Subject: [PATCH 007/143] docker/mkosi: mount a Docker Volume at /work, so mkosi can cache the tools tree across invocations Signed-off-by: Ricardo Pardini --- captain/docker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/captain/docker.py b/captain/docker.py index 7fa747ea..565369ec 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -188,6 +188,8 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: f"GITHUB_ACTIONS={os.environ.get('GITHUB_ACTIONS', '')}", ] + docker_args += ["--mount", "type=volume,source=captain-workdir,target=/work"] + docker_args += ["-v", f"{cfg.project_dir}/mkosi.output:/work/mkosi.output"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.extra:/work/mkosi.extra"] docker_args += ["-v", f"{cfg.project_dir}/out:/work/out"] From fa95ce5e20a414c40c68f0df2a543e69ecc3bb25 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 14:16:34 +0200 Subject: [PATCH 008/143] util: use a Panel to log the full subprocess command line Signed-off-by: Ricardo Pardini --- captain/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/captain/util.py b/captain/util.py index 5a325252..e1c150e7 100644 --- a/captain/util.py +++ b/captain/util.py @@ -10,6 +10,7 @@ from dataclasses import dataclass from pathlib import Path +from rich.panel import Panel from rich.rule import Rule import captain @@ -82,6 +83,7 @@ def run( # If not capturing, and debugging, emit a Rich separator line, for visual clarity. if not capture and log.isEnabledFor(logging.DEBUG): + captain.console.print(Panel(f"Running command: {' '.join(cmd)}", style="green")) captain.console.print(Rule(f"⮕ Starting subprocess: {cmd} ⮕", style="green")) proc = subprocess.run( From 41637e2abb514e6910e2e4b671cbfabaa0bf0e78 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 14:17:45 +0200 Subject: [PATCH 009/143] captain: introduce Config build_kernel boolean - this will allow using a generic/distro kernel instead of building one Signed-off-by: Ricardo Pardini --- captain/artifacts.py | 11 +++++++++++ captain/cli/_commands.py | 5 +++-- captain/cli/_stages.py | 5 +++-- captain/config.py | 3 +++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/captain/artifacts.py b/captain/artifacts.py index 1a1c769d..9fb7ee94 100644 --- a/captain/artifacts.py +++ b/captain/artifacts.py @@ -43,6 +43,17 @@ def collect_kernel(cfg: Config) -> None: log.info("kernel: %s (%s)", vmlinuz_dst, _human_size(vmlinuz_dst.stat().st_size)) else: log.warning("No kernel image found in %s", cfg.kernel_output) + # In this case, mkosi might have collected the kernel from a installed linux-image package. + # Find it and use it instead. + log.debug("Looking for kernel image produced by mkosi in %s", cfg.initramfs_output) + vmlinu_files = sorted(cfg.initramfs_output.glob("*.vmlinu*")) + if vmlinu_files: + vmlinuz_src = vmlinu_files[0] + vmlinuz_dst = out / f"vmlinuz-{cfg.kernel_version}-{cfg.arch_info.output_arch}" + shutil.copy2(vmlinuz_src, vmlinuz_dst) + log.info("kernel: %s (%s)", vmlinuz_dst, _human_size(vmlinuz_dst.stat().st_size)) + else: + log.warning("No kernel image found in %s either", cfg.initramfs_output) def collect_initramfs(cfg: Config) -> None: diff --git a/captain/cli/_commands.py b/captain/cli/_commands.py index 576b51f2..16214740 100644 --- a/captain/cli/_commands.py +++ b/captain/cli/_commands.py @@ -81,10 +81,11 @@ def _cmd_iso(cfg: Config, _extra_args: list[str]) -> None: def _cmd_build(cfg: Config, extra_args: list[str]) -> None: """Full build: kernel → tools → initramfs → iso → artifacts.""" - _build_kernel_stage(cfg) + if cfg.build_kernel: + _build_kernel_stage(cfg) _build_tools_stage(cfg) _build_mkosi_stage(cfg, extra_args) - _build_iso_stage(cfg) + _build_iso_stage(cfg) # TODO also conditional... artifacts.collect(cfg) log.info("Build complete!") diff --git a/captain/cli/_stages.py b/captain/cli/_stages.py index 2f7fd180..0ffdff8f 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/_stages.py @@ -23,6 +23,7 @@ def _build_kernel_stage(cfg: Config) -> None: modules_dir = cfg.modules_output / "usr" / "lib" / "modules" vmlinuz_dir = cfg.kernel_output has_vmlinuz = vmlinuz_dir.is_dir() and any(vmlinuz_dir.glob("vmlinuz-*")) + log.debug(f"Checking kernel build idempotency: modules_dir={modules_dir}, has_vmlinuz={has_vmlinuz}") if modules_dir.is_dir() and has_vmlinuz and not cfg.force_kernel: log.info("Kernel already built (use --force-kernel to rebuild)") @@ -127,7 +128,7 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: "mkosi", f"--architecture={cfg.arch_info.mkosi_arch}", f"--extra-tree={tools_tree}", - f"--extra-tree={modules_tree}", + *([f"--extra-tree={modules_tree}"] if cfg.build_kernel else []), f"--output-dir={output_dir}", "build", *mkosi_args, @@ -145,7 +146,7 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: docker.run_mkosi( cfg, f"--extra-tree={tools_tree}", - f"--extra-tree={modules_tree}", + *([f"--extra-tree={modules_tree}"] if cfg.build_kernel else []), f"--output-dir={output_dir}", "--package-cache-dir=/cache/packages", "build", diff --git a/captain/config.py b/captain/config.py index 67bdf174..da39bb60 100644 --- a/captain/config.py +++ b/captain/config.py @@ -31,6 +31,7 @@ class Config: # Target arch: str = "amd64" + build_kernel: bool = False kernel_version: str = DEFAULT_KERNEL_VERSION kernel_config: str | None = None kernel_src: str | None = None @@ -106,6 +107,7 @@ def from_args(cls, args: argparse.Namespace, project_dir: Path | None) -> Config project_dir=project_dir, output_dir=project_dir / "out", arch=getattr(args, "arch", "amd64"), + build_kernel=getattr(args, "build_kernel", False), kernel_version=getattr(args, "kernel_version", DEFAULT_KERNEL_VERSION), kernel_config=getattr(args, "kernel_config", None) or None, kernel_src=getattr(args, "kernel_src", None) or None, @@ -136,6 +138,7 @@ def from_env(cls, project_dir: Path) -> Config: project_dir=project_dir, output_dir=project_dir / "out", arch=os.environ.get("ARCH", "amd64"), + build_kernel=os.environ.get("BUILD_KERNEL") == "1", kernel_version=os.environ.get("KERNEL_VERSION", DEFAULT_KERNEL_VERSION), kernel_config=os.environ.get("KERNEL_CONFIG") or None, kernel_src=os.environ.get("KERNEL_SRC") or None, From 493bfc0f9763ca9851586febf4ed24813bb699f9 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 14:18:10 +0200 Subject: [PATCH 010/143] WiP: use linux-image-generic distro kernel - hack until it uses the mkosi-supplied kernel for everything - artifacts: look for mkosi-supplied vmlinuz first Signed-off-by: Ricardo Pardini --- captain/__init__.py | 4 ++-- captain/artifacts.py | 34 ++++++++++++++++++++-------------- captain/cli/_commands.py | 4 ++-- captain/cli/_stages.py | 6 +++++- captain/iso.py | 10 ++++------ mkosi.conf | 7 +++++++ mkosi.finalize | 18 +++++++++++++++++- 7 files changed, 57 insertions(+), 26 deletions(-) diff --git a/captain/__init__.py b/captain/__init__.py index d4e1a277..03cc81e3 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -16,9 +16,9 @@ # Rich console — writes to stderr so log output never pollutes piped stdout. # If running under GHA, force colors. if os.environ.get("GITHUB_ACTIONS", "") == "": - console: Console = Console(stderr=True) + console: Console = Console(stderr=True) else: - console: Console = Console(stderr=True, color_system="standard", width=160, highlight=False) + console: Console = Console(stderr=True, color_system="standard", width=160, highlight=False) # Install Rich traceback handler globally (once, at import time). _install_rich_traceback(console=console, show_locals=True, width=None) diff --git a/captain/artifacts.py b/captain/artifacts.py index 9fb7ee94..26bb61c5 100644 --- a/captain/artifacts.py +++ b/captain/artifacts.py @@ -34,26 +34,32 @@ def _human_size(size: int) -> str: def collect_kernel(cfg: Config) -> None: """Copy the kernel image from mkosi.output/kernel/{version}/{arch}/ to out/.""" out = ensure_dir(cfg.output_dir) - vmlinuz_dir = cfg.kernel_output - vmlinuz_files = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] - if vmlinuz_files: - vmlinuz_src = vmlinuz_files[0] + # In this case, mkosi might have collected the kernel from a installed linux-image package. + # Find it and use it instead. + log.debug("Looking for kernel image produced by mkosi in %s", cfg.initramfs_output) + vmlinu_files = sorted(cfg.initramfs_output.glob("*.vmlinu*")) + if vmlinu_files: + vmlinuz_src = vmlinu_files[0] vmlinuz_dst = out / f"vmlinuz-{cfg.kernel_version}-{cfg.arch_info.output_arch}" shutil.copy2(vmlinuz_src, vmlinuz_dst) - log.info("kernel: %s (%s)", vmlinuz_dst, _human_size(vmlinuz_dst.stat().st_size)) + log.info( + "mkosi supplied kernel: %s (%s)", vmlinuz_dst, _human_size(vmlinuz_dst.stat().st_size) + ) else: - log.warning("No kernel image found in %s", cfg.kernel_output) - # In this case, mkosi might have collected the kernel from a installed linux-image package. - # Find it and use it instead. - log.debug("Looking for kernel image produced by mkosi in %s", cfg.initramfs_output) - vmlinu_files = sorted(cfg.initramfs_output.glob("*.vmlinu*")) - if vmlinu_files: - vmlinuz_src = vmlinu_files[0] + log.warning("No kernel image produced by mkosi in %s", cfg.initramfs_output) + vmlinuz_dir = cfg.kernel_output + vmlinuz_files = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] + if vmlinuz_files: + vmlinuz_src = vmlinuz_files[0] vmlinuz_dst = out / f"vmlinuz-{cfg.kernel_version}-{cfg.arch_info.output_arch}" shutil.copy2(vmlinuz_src, vmlinuz_dst) - log.info("kernel: %s (%s)", vmlinuz_dst, _human_size(vmlinuz_dst.stat().st_size)) + log.info( + "kernel built from source: %s (%s)", + vmlinuz_dst, + _human_size(vmlinuz_dst.stat().st_size), + ) else: - log.warning("No kernel image found in %s either", cfg.initramfs_output) + log.warning("No kernel image found in %s", cfg.kernel_output) def collect_initramfs(cfg: Config) -> None: diff --git a/captain/cli/_commands.py b/captain/cli/_commands.py index 16214740..6f2e7ca5 100644 --- a/captain/cli/_commands.py +++ b/captain/cli/_commands.py @@ -65,7 +65,7 @@ def _check_kernel_modules(cfg: Config) -> None: def _cmd_initramfs(cfg: Config, extra_args: list[str]) -> None: """Build only the initramfs via mkosi, then collect artifacts.""" - _check_kernel_modules(cfg) + # _check_kernel_modules(cfg) _build_mkosi_stage(cfg, extra_args) artifacts.collect_initramfs(cfg) artifacts.collect_kernel(cfg) @@ -85,7 +85,7 @@ def _cmd_build(cfg: Config, extra_args: list[str]) -> None: _build_kernel_stage(cfg) _build_tools_stage(cfg) _build_mkosi_stage(cfg, extra_args) - _build_iso_stage(cfg) # TODO also conditional... + _build_iso_stage(cfg) # TODO also conditional... artifacts.collect(cfg) log.info("Build complete!") diff --git a/captain/cli/_stages.py b/captain/cli/_stages.py index 0ffdff8f..e549444e 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/_stages.py @@ -23,7 +23,11 @@ def _build_kernel_stage(cfg: Config) -> None: modules_dir = cfg.modules_output / "usr" / "lib" / "modules" vmlinuz_dir = cfg.kernel_output has_vmlinuz = vmlinuz_dir.is_dir() and any(vmlinuz_dir.glob("vmlinuz-*")) - log.debug(f"Checking kernel build idempotency: modules_dir={modules_dir}, has_vmlinuz={has_vmlinuz}") + log.debug( + "Checking kernel build idempotency: modules_dir=%s, has_vmlinuz=%s", + modules_dir, + has_vmlinuz, + ) if modules_dir.is_dir() and has_vmlinuz and not cfg.force_kernel: log.info("Kernel already built (use --force-kernel to rebuild)") diff --git a/captain/iso.py b/captain/iso.py index 5aa20445..8640e5c6 100644 --- a/captain/iso.py +++ b/captain/iso.py @@ -41,13 +41,11 @@ def _grub_cfg(arch: str) -> str: def _find_vmlinuz(cfg: Config) -> Path: """Locate the vmlinuz kernel image.""" - vmlinuz_dir = cfg.kernel_output - candidates = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] - if not candidates: - log.error("No vmlinuz found in %s", vmlinuz_dir) - log.error("Build the kernel first: ./build.py kernel") + vmlinuz_files = sorted(cfg.initramfs_output.glob("*.vmlinuz*")) + if not vmlinuz_files: + log.error("No vmlinuz found in %s", cfg.initramfs_output) raise SystemExit(1) - return candidates[0] + return vmlinuz_files[0] def _find_initramfs(cfg: Config) -> Path: diff --git a/mkosi.conf b/mkosi.conf index 10a57027..11e73eee 100644 --- a/mkosi.conf +++ b/mkosi.conf @@ -34,6 +34,13 @@ WithDocs=no # can coexist under mkosi.output/. Packages= + linux-image-generic + # don't install an initrd builder -- we're it + initramfs-tools- + initramfs-tools-core- + dracut- + dracut-core- + dracut-install- # systemd and core systemd systemd-sysv # Provides poweroff, shutdown, reboot, halt, etc. diff --git a/mkosi.finalize b/mkosi.finalize index 1057bcdc..0607377d 100755 --- a/mkosi.finalize +++ b/mkosi.finalize @@ -54,6 +54,9 @@ done # is available as a cacheable artifact. We strip unneeded modules here so # the initramfs stays small without requiring a kernel recompile. # --------------------------------------------------------------------------- +echo "==> Listing all installed kernel modules: ${BUILDROOT}" +find "${BUILDROOT}/" -type f -name "*.ko*" + MODDIR="$BUILDROOT/usr/lib/modules" if [[ -d "$MODDIR" ]]; then echo "==> Trimming unnecessary kernel modules..." @@ -81,7 +84,8 @@ if [[ -d "$MODDIR" ]]; then ) for pattern in "${EXCLUDE_PATTERNS[@]}"; do - find "$MODDIR" -path "$pattern" -type d -exec rm -rf {} + 2>/dev/null || true + echo "--> Removing modules matching pattern: '${pattern}'" + find "$MODDIR" -path "$pattern" -type d -exec rm -rfv {} + || true done AFTER=$(du -sb "$MODDIR" | awk '{print $1}') @@ -97,4 +101,16 @@ if [[ -d "$MODDIR" ]]; then done fi +echo "==> Listing remaining kernel modules: ${BUILDROOT}" +find "${BUILDROOT}/" -type f -name "*.ko*" + +echo "==> Listing contents of /boot" +ls -lah "${BUILDROOT}"/boot + +echo "==> Showing full contents of environment variables for debugging:" +env + +echo "==> Removing /boot from the image to save space" +rm -rfv "$BUILDROOT"/boot + echo "==> CaptainOS finalize complete." From e861d9d9eb20dcbaa465f8956b6e582b1e45c5b3 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 15:40:00 +0200 Subject: [PATCH 011/143] gha: skip all kernel stuff - self-built kernel from source will come back at a later stage, as a .deb package Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 76 ++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 522dfab0..69a560c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v6 - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - name: Install lint tools run: make lint-install @@ -37,6 +37,7 @@ jobs: # This is the workaround. # ------------------------------------------------------------------- check-kernel-cache: + if: false # disabled runs-on: ubuntu-latest outputs: amd64-cache-hit: ${{ steps.amd64-cache.outputs.cache-hit }} @@ -67,6 +68,7 @@ jobs: # Build kernel (vmlinuz + modules) inside Docker # ------------------------------------------------------------------- build-kernel: + if: false # disabled needs: [lint, check-kernel-cache] # Forks / cache-hit → cheap default runner; mainline cache-miss → fast oracle runner runs-on: > @@ -96,7 +98,7 @@ jobs: run: cat .github/config.env >> "$GITHUB_ENV" - name: Log in to GHCR - uses: docker/login-action@v4 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -141,7 +143,7 @@ jobs: key: ${{ steps.kernel-cache-key.outputs.key }} - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - name: Build kernel run: uv run ./build.py kernel @@ -158,7 +160,7 @@ jobs: key: ${{ steps.kernel-cache-key.outputs.key }} - name: Upload kernel artifacts - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v6 with: name: kernel-${{ matrix.arch }} path: | @@ -194,7 +196,7 @@ jobs: key: tools-${{ matrix.arch }}-${{ hashFiles('captain/tools.py') }} - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - name: Download tools run: uv run ./build.py tools @@ -209,7 +211,7 @@ jobs: key: tools-${{ matrix.arch }}-${{ hashFiles('captain/tools.py') }} - name: Upload tools artifacts - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v6 with: name: tools-${{ matrix.arch }} path: | @@ -222,7 +224,7 @@ jobs: # ------------------------------------------------------------------- build-initramfs: runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [build-kernel, download-tools] + needs: [download-tools] # build-kernel strategy: fail-fast: false matrix: @@ -236,13 +238,14 @@ jobs: uses: actions/checkout@v6 - name: Download kernel artifacts - uses: actions/download-artifact@v8 + if: false # disabled + uses: actions/download-artifact@v6 with: name: kernel-${{ matrix.arch }} path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} - name: Download tools artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v6 with: name: tools-${{ matrix.arch }} path: mkosi.output/tools/${{ matrix.arch }} @@ -258,10 +261,7 @@ jobs: run: sudo apt-get -o "Dpkg::Use-Pty=0" update - name: setup-mkosi - # Pinned post-v26 to pick up systemd/mkosi@1f811f05 ("tools: move grub-pc-bin - # to arch-specific drop-in"), which fixes arm64 builds failing on the - # default tools-tree pulling in grub-pc-bin (BIOS GRUB, x86-only). - uses: systemd/mkosi@1f811f0524be3096872e79161c8e6ab3e7c2bb1f + uses: systemd/mkosi@v26 - name: Install bubblewrap run: | @@ -269,13 +269,13 @@ jobs: sudo apt-get -o "Dpkg::Use-Pty=0" install -y bubblewrap - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - name: Build initramfs run: uv run ./build.py initramfs - name: Upload initramfs artifacts - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v6 with: name: initramfs-${{ matrix.arch }} path: out/ @@ -309,7 +309,7 @@ jobs: run: cat .github/config.env >> "$GITHUB_ENV" - name: Log in to GHCR - uses: docker/login-action@v4 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -342,31 +342,34 @@ jobs: docker push "${REMOTE}:${HASH}-${{ matrix.arch }}" - name: Download kernel artifacts - uses: actions/download-artifact@v8 + if: false # don't + uses: actions/download-artifact@v6 with: name: kernel-${{ matrix.arch }} path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} - name: Download initramfs artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v6 with: name: initramfs-${{ matrix.arch }} path: out - - name: Stage initramfs for ISO build + - name: Stage initramfs and kernel for ISO build run: | mkdir -p "mkosi.output/initramfs/${KERNEL_VERSION}/${{ matrix.arch }}" - cp "out/initramfs-${KERNEL_VERSION}-${{ matrix.output_arch }}" \ + cp -v "out/initramfs-${KERNEL_VERSION}-${{ matrix.output_arch }}" \ "mkosi.output/initramfs/${KERNEL_VERSION}/${{ matrix.arch }}/image.cpio.zst" + cp -v "out/vmlinuz-${KERNEL_VERSION}-${{ matrix.output_arch }}" \ + "mkosi.output/initramfs/${KERNEL_VERSION}/${{ matrix.arch }}/image.vmlinuz" - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - name: Build ISO run: uv run ./build.py iso - name: Upload ISO artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v6 with: name: iso-${{ matrix.arch }} path: out/captainos-${{ env.KERNEL_VERSION }}-${{ matrix.output_arch }}.iso @@ -396,28 +399,29 @@ jobs: run: cat .github/config.env >> "$GITHUB_ENV" - name: Download kernel artifacts - uses: actions/download-artifact@v8 + if: false # disabled + uses: actions/download-artifact@v6 with: name: kernel-${{ matrix.target }} path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.target }} - name: Download initramfs artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v6 with: name: initramfs-${{ matrix.target }} path: out - name: Download ISO artifact - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v6 with: name: iso-${{ matrix.target }} path: out - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - name: Log in to GHCR - uses: docker/login-action@v4 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -446,46 +450,48 @@ jobs: run: cat .github/config.env >> "$GITHUB_ENV" - name: Download kernel artifacts (amd64) - uses: actions/download-artifact@v8 + if: false # no + uses: actions/download-artifact@v6 with: name: kernel-amd64 path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/amd64 - name: Download initramfs artifacts (amd64) - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v6 with: name: initramfs-amd64 path: out - name: Download ISO artifact (amd64) - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v6 with: name: iso-amd64 path: out - name: Download kernel artifacts (arm64) - uses: actions/download-artifact@v8 + if: false # no + uses: actions/download-artifact@v6 with: name: kernel-arm64 path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/arm64 - name: Download initramfs artifacts (arm64) - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v6 with: name: initramfs-arm64 path: out - name: Download ISO artifact (arm64) - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v6 with: name: iso-arm64 path: out - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - name: Log in to GHCR - uses: docker/login-action@v4 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} From 6a846075c9a3eda3b6e730d87e99eeca346c1e21 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 16:54:01 +0200 Subject: [PATCH 012/143] gha: single matrix except for combined amd64/arm64 image at the end - just do everything at once Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 328 +++------------------------------------ 1 file changed, 22 insertions(+), 306 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69a560c8..36058289 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,152 +31,16 @@ jobs: - name: Lint run: make lint - # ------------------------------------------------------------------- - # Fast cache lookup – decides whether the OCI runner is needed - # There are consistent issues with OCI runners not getting scheduled. - # This is the workaround. - # ------------------------------------------------------------------- - check-kernel-cache: - if: false # disabled - runs-on: ubuntu-latest - outputs: - amd64-cache-hit: ${{ steps.amd64-cache.outputs.cache-hit }} - arm64-cache-hit: ${{ steps.arm64-cache.outputs.cache-hit }} - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Check amd64 kernel cache - id: amd64-cache - uses: actions/cache/restore@v5 - with: - path: | - mkosi.output/kernel/${{ env.KERNEL_VERSION }}/amd64 - key: kernel-amd64-${{ env.KERNEL_VERSION }}-${{ hashFiles('kernel.configs/*', 'Dockerfile') }} - lookup-only: true - - - name: Check arm64 kernel cache - id: arm64-cache - uses: actions/cache/restore@v5 - with: - path: | - mkosi.output/kernel/${{ env.KERNEL_VERSION }}/arm64 - key: kernel-arm64-${{ env.KERNEL_VERSION }}-${{ hashFiles('kernel.configs/*', 'Dockerfile') }} - lookup-only: true - - # ------------------------------------------------------------------- - # Build kernel (vmlinuz + modules) inside Docker - # ------------------------------------------------------------------- - build-kernel: - if: false # disabled - needs: [lint, check-kernel-cache] - # Forks / cache-hit → cheap default runner; mainline cache-miss → fast oracle runner - runs-on: > - ${{ github.repository != 'tinkerbell/captain' && matrix.fork_runner - || needs.check-kernel-cache.outputs[format('{0}-cache-hit', matrix.arch)] == 'true' && matrix.fork_runner - || fromJSON(matrix.mainline_runner) }} - strategy: - fail-fast: false - matrix: - arch: [amd64, arm64] - include: - - arch: amd64 - fork_runner: ubuntu-latest - mainline_runner: '{"group":"Default","labels":["oracle-vm-16cpu-64gb-x86-64"]}' - - arch: arm64 - fork_runner: ubuntu-24.04-arm - mainline_runner: '{"group":"Default","labels":["oracle-vm-16cpu-64gb-arm64"]}' - env: - ARCH: ${{ matrix.arch }} - KERNEL_MODE: docker - MKOSI_MODE: skip - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Load shared config - run: cat .github/config.env >> "$GITHUB_ENV" - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Compute Dockerfile hash - id: dockerfile-hash - run: echo "hash=$(sha256sum Dockerfile | awk '{print $1}')" >> "$GITHUB_OUTPUT" - - - name: Pull or build builder image - id: builder - run: | - HASH="${{ steps.dockerfile-hash.outputs.hash }}" - REMOTE="ghcr.io/${{ github.repository }}/${{ env.BUILDER_IMAGE }}" - if docker pull "${REMOTE}:${HASH}-${{ matrix.arch }}"; then - docker tag "${REMOTE}:${HASH}-${{ matrix.arch }}" "${{ env.BUILDER_IMAGE }}:${HASH}" - docker tag "${REMOTE}:${HASH}-${{ matrix.arch }}" "${{ env.BUILDER_IMAGE }}" - echo "built=false" >> "$GITHUB_OUTPUT" - else - docker buildx build --progress=plain -t "${{ env.BUILDER_IMAGE }}:${HASH}" -t "${{ env.BUILDER_IMAGE }}" . - echo "built=true" >> "$GITHUB_OUTPUT" - fi - - - name: Push builder image to GHCR - if: github.ref == 'refs/heads/main' && steps.builder.outputs.built == 'true' - run: | - HASH="${{ steps.dockerfile-hash.outputs.hash }}" - REMOTE="ghcr.io/${{ github.repository }}/${{ env.BUILDER_IMAGE }}" - docker tag "${{ env.BUILDER_IMAGE }}:${HASH}" "${REMOTE}:${HASH}-${{ matrix.arch }}" - docker push "${REMOTE}:${HASH}-${{ matrix.arch }}" - - - name: Compute kernel cache key - id: kernel-cache-key - run: echo "key=kernel-${{ matrix.arch }}-${{ env.KERNEL_VERSION }}-${{ hashFiles('kernel.configs/*', 'Dockerfile') }}" >> "$GITHUB_OUTPUT" - - - name: Restore kernel cache - id: kernel-cache - uses: actions/cache/restore@v5 - with: - path: | - mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} - key: ${{ steps.kernel-cache-key.outputs.key }} - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Build kernel - run: uv run ./build.py kernel - - - name: Fix output file ownership - run: sudo chown -R "$(id -u):$(id -g)" mkosi.output/ - - - name: Save kernel cache - if: github.ref == 'refs/heads/main' && steps.kernel-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v5 - with: - path: | - mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} - key: ${{ steps.kernel-cache-key.outputs.key }} - - - name: Upload kernel artifacts - uses: actions/upload-artifact@v6 - with: - name: kernel-${{ matrix.arch }} - path: | - mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} - retention-days: 1 - # ------------------------------------------------------------------- # Download tools (containerd, runc, nerdctl, CNI plugins) # ------------------------------------------------------------------- download-tools: - needs: [lint] + needs: [ lint ] runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} strategy: fail-fast: false matrix: - arch: [amd64, arm64] + arch: [ amd64, arm64 ] env: ARCH: ${{ matrix.arch }} KERNEL_MODE: skip @@ -222,28 +86,24 @@ jobs: # ------------------------------------------------------------------- # Build initramfs via mkosi (depends on kernel + tools) # ------------------------------------------------------------------- - build-initramfs: + build-all: runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [download-tools] # build-kernel + needs: [ download-tools ] # build-kernel strategy: fail-fast: false matrix: - arch: [amd64, arm64] + include: + - { arch: amd64, output_arch: x86_64, iso: true } + - { arch: arm64, output_arch: aarch64, iso: true } env: ARCH: ${{ matrix.arch }} KERNEL_MODE: skip - MKOSI_MODE: native + MKOSI_MODE: docker + ISO_MODE: docker steps: - name: Checkout code uses: actions/checkout@v6 - - name: Download kernel artifacts - if: false # disabled - uses: actions/download-artifact@v6 - with: - name: kernel-${{ matrix.arch }} - path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} - - name: Download tools artifacts uses: actions/download-artifact@v6 with: @@ -257,22 +117,11 @@ jobs: chmod +x mkosi.output/tools/${{ matrix.arch }}/usr/local/bin/* chmod +x mkosi.output/tools/${{ matrix.arch }}/opt/cni/bin/* - - name: Refresh apt cache - run: sudo apt-get -o "Dpkg::Use-Pty=0" update - - - name: setup-mkosi - uses: systemd/mkosi@v26 - - - name: Install bubblewrap - run: | - sudo apt-get -o "Dpkg::Use-Pty=0" update - sudo apt-get -o "Dpkg::Use-Pty=0" install -y bubblewrap - - name: Install uv uses: astral-sh/setup-uv@v7 - name: Build initramfs - run: uv run ./build.py initramfs + run: uv run ./build.py build # full build, incl initramfs and iso - name: Upload initramfs artifacts uses: actions/upload-artifact@v6 @@ -281,146 +130,24 @@ jobs: path: out/ retention-days: 1 - # ------------------------------------------------------------------- - # Build UEFI-bootable ISO (depends on initramfs) - # ------------------------------------------------------------------- - build-iso: - runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [build-initramfs] - strategy: - fail-fast: false - matrix: - arch: [amd64, arm64] - include: - - arch: amd64 - output_arch: x86_64 - - arch: arm64 - output_arch: aarch64 - env: - ARCH: ${{ matrix.arch }} - KERNEL_MODE: skip - MKOSI_MODE: skip - ISO_MODE: docker - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Load shared config - run: cat .github/config.env >> "$GITHUB_ENV" - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Compute Dockerfile hash - id: dockerfile-hash - run: echo "hash=$(sha256sum Dockerfile | awk '{print $1}')" >> "$GITHUB_OUTPUT" - - - name: Pull or build builder image - id: builder - run: | - HASH="${{ steps.dockerfile-hash.outputs.hash }}" - REMOTE="ghcr.io/${{ github.repository }}/${{ env.BUILDER_IMAGE }}" - if docker pull "${REMOTE}:${HASH}-${{ matrix.arch }}"; then - docker tag "${REMOTE}:${HASH}-${{ matrix.arch }}" "${{ env.BUILDER_IMAGE }}:${HASH}" - docker tag "${REMOTE}:${HASH}-${{ matrix.arch }}" "${{ env.BUILDER_IMAGE }}" - echo "built=false" >> "$GITHUB_OUTPUT" - else - docker build -t "${{ env.BUILDER_IMAGE }}:${HASH}" -t "${{ env.BUILDER_IMAGE }}" . - echo "built=true" >> "$GITHUB_OUTPUT" - fi - - - name: Push builder image to GHCR - if: github.ref == 'refs/heads/main' && steps.builder.outputs.built == 'true' - run: | - HASH="${{ steps.dockerfile-hash.outputs.hash }}" - REMOTE="ghcr.io/${{ github.repository }}/${{ env.BUILDER_IMAGE }}" - docker tag "${{ env.BUILDER_IMAGE }}:${HASH}" "${REMOTE}:${HASH}-${{ matrix.arch }}" - docker push "${REMOTE}:${HASH}-${{ matrix.arch }}" - - - name: Download kernel artifacts - if: false # don't - uses: actions/download-artifact@v6 - with: - name: kernel-${{ matrix.arch }} - path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} - - - name: Download initramfs artifacts - uses: actions/download-artifact@v6 - with: - name: initramfs-${{ matrix.arch }} - path: out - - - name: Stage initramfs and kernel for ISO build - run: | - mkdir -p "mkosi.output/initramfs/${KERNEL_VERSION}/${{ matrix.arch }}" - cp -v "out/initramfs-${KERNEL_VERSION}-${{ matrix.output_arch }}" \ - "mkosi.output/initramfs/${KERNEL_VERSION}/${{ matrix.arch }}/image.cpio.zst" - cp -v "out/vmlinuz-${KERNEL_VERSION}-${{ matrix.output_arch }}" \ - "mkosi.output/initramfs/${KERNEL_VERSION}/${{ matrix.arch }}/image.vmlinuz" - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Build ISO - run: uv run ./build.py iso + # ------------------------------------------------------------------- + # Build UEFI-bootable ISO (depends on initramfs) + # ------------------------------------------------------------------- - name: Upload ISO artifact + if: ${{ matrix.iso }} # only if matrix entry had iso: true uses: actions/upload-artifact@v6 with: name: iso-${{ matrix.arch }} path: out/captainos-${{ env.KERNEL_VERSION }}-${{ matrix.output_arch }}.iso retention-days: 1 - # ------------------------------------------------------------------- - # Publish per-arch artifacts and compute checksums - # ------------------------------------------------------------------- - publish-per-arch: - if: github.ref == 'refs/heads/main' - runs-on: ${{ matrix.target == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [build-iso] - strategy: - fail-fast: false - matrix: - target: [amd64, arm64] - env: - ARCH: ${{ matrix.target }} - TARGET: ${{ matrix.target }} - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Load shared config - run: cat .github/config.env >> "$GITHUB_ENV" - - - name: Download kernel artifacts - if: false # disabled - uses: actions/download-artifact@v6 - with: - name: kernel-${{ matrix.target }} - path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.target }} - - - name: Download initramfs artifacts - uses: actions/download-artifact@v6 - with: - name: initramfs-${{ matrix.target }} - path: out - - - name: Download ISO artifact - uses: actions/download-artifact@v6 - with: - name: iso-${{ matrix.target }} - path: out - - - name: Install uv - uses: astral-sh/setup-uv@v7 + # ------------------------------------------------------------------- + # Publish per-arch artifacts and compute checksums + # ------------------------------------------------------------------- - name: Log in to GHCR + if: github.ref == 'refs/heads/main' uses: docker/login-action@v3 with: registry: ghcr.io @@ -428,6 +155,9 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Publish artifacts to GHCR + if: github.ref == 'refs/heads/main' + env: + TARGET: ${{ matrix.arch }} run: uv run ./build.py release publish # ------------------------------------------------------------------- @@ -436,7 +166,7 @@ jobs: publish-combined: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest - needs: [publish-per-arch] + needs: [ build-all ] env: ARCH: amd64 TARGET: combined @@ -449,13 +179,6 @@ jobs: - name: Load shared config run: cat .github/config.env >> "$GITHUB_ENV" - - name: Download kernel artifacts (amd64) - if: false # no - uses: actions/download-artifact@v6 - with: - name: kernel-amd64 - path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/amd64 - - name: Download initramfs artifacts (amd64) uses: actions/download-artifact@v6 with: @@ -468,13 +191,6 @@ jobs: name: iso-amd64 path: out - - name: Download kernel artifacts (arm64) - if: false # no - uses: actions/download-artifact@v6 - with: - name: kernel-arm64 - path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/arm64 - - name: Download initramfs artifacts (arm64) uses: actions/download-artifact@v6 with: From 4488b788a716383d19a7555826c55ad1baa8edd7 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 18:01:21 +0200 Subject: [PATCH 013/143] mkosi.conf: just use tiny-initramfs instead of blocking the others (dracut/initramfs-tools) Signed-off-by: Ricardo Pardini --- mkosi.conf | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/mkosi.conf b/mkosi.conf index 11e73eee..fceb8382 100644 --- a/mkosi.conf +++ b/mkosi.conf @@ -29,18 +29,12 @@ WithDocs=no # Pre-built kernel modules and tools are injected via separate --extra-tree= # flags on the CLI: # --extra-tree=mkosi.output/tools/{arch}/ (tools) -# --extra-tree=mkosi.output/kernel/{ver}/{arch}/modules (kernel modules) # This keeps mkosi.conf architecture-neutral so both amd64 and arm64 builds # can coexist under mkosi.output/. Packages= linux-image-generic - # don't install an initrd builder -- we're it - initramfs-tools- - initramfs-tools-core- - dracut- - dracut-core- - dracut-install- + tiny-initramfs # a tiny initrd builder to account for the linux-image dependency # systemd and core systemd systemd-sysv # Provides poweroff, shutdown, reboot, halt, etc. From 15307b3e934b3335b3a08a784fada018b19c55ba Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 18:02:07 +0200 Subject: [PATCH 014/143] mkosi.finalize: export DTBs for arches that have them - also: show info about modules and full rootfs Signed-off-by: Ricardo Pardini --- mkosi.finalize | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/mkosi.finalize b/mkosi.finalize index 0607377d..32309dec 100755 --- a/mkosi.finalize +++ b/mkosi.finalize @@ -3,6 +3,9 @@ # The root filesystem is writable here. set -euo pipefail +echo "==> Showing full contents of environment variables for debugging:" +env + echo "==> CaptainOS finalize: unlocking root account..." # Unlock root with empty password @@ -54,10 +57,11 @@ done # is available as a cacheable artifact. We strip unneeded modules here so # the initramfs stays small without requiring a kernel recompile. # --------------------------------------------------------------------------- -echo "==> Listing all installed kernel modules: ${BUILDROOT}" -find "${BUILDROOT}/" -type f -name "*.ko*" - MODDIR="$BUILDROOT/usr/lib/modules" + +echo "==> Listing all installed kernel modules: ${MODDIR}" +du -h -x -d 5 "$MODDIR" | sort -h || true + if [[ -d "$MODDIR" ]]; then echo "==> Trimming unnecessary kernel modules..." BEFORE=$(du -sb "$MODDIR" | awk '{print $1}') @@ -102,15 +106,30 @@ if [[ -d "$MODDIR" ]]; then fi echo "==> Listing remaining kernel modules: ${BUILDROOT}" -find "${BUILDROOT}/" -type f -name "*.ko*" - -echo "==> Listing contents of /boot" -ls -lah "${BUILDROOT}"/boot - -echo "==> Showing full contents of environment variables for debugging:" -env +du -h -x -d 5 "$MODDIR" | sort -h || true echo "==> Removing /boot from the image to save space" -rm -rfv "$BUILDROOT"/boot +rm -rfv "${BUILDROOT}"/boot + +DTB_SRC_DIR=$(echo -n "${BUILDROOT}"/usr/lib/linux-image-*) +if [[ -d "$DTB_SRC_DIR" ]]; then + echo "==> Exporting kernel DTBs; DTB_DIR: ${DTB_SRC_DIR}" + mkdir -pv "${BUILDROOT}/root/dtb" + # Move every .dtb* file in the DTB_SRC_DIR to "${BUILDROOT}/root/dtb" -- maintain the directory structure + find "${DTB_SRC_DIR}" -type f -name "*.dtb*" -exec sh -c 'for f; do mkdir -p "${0}/$(dirname "${f#'"${DTB_SRC_DIR}"'/}")" && mv "$f" "${0}/${f#'"${DTB_SRC_DIR}"'/}" ; done' "${BUILDROOT}/root/dtb" {} + || true + + echo "--> Leftover files in ${DTB_SRC_DIR}:" + find "${DTB_SRC_DIR}" -type f + + # Now simply output the dtb directory to mkosi's OUTPUTDIR + echo "==> Copying DTBs to mkosi output directory: ${OUTPUTDIR}/dtb" + mv -v "${BUILDROOT}/root/dtb" "${OUTPUTDIR}/dtb" +else + echo "==> No DTB source directory found at ${DTB_SRC_DIR}; skipping DTB export" +fi + +# Show a 4-deep overview of the full rootfs contents +echo "==> Final contents of root filesystem (up to depth 4):" +du -h -x -d 4 "$BUILDROOT" | sort -h || true echo "==> CaptainOS finalize complete." From 5bcb47b03c30778ac4c1591f51138c6bb344f0ff Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 18:25:28 +0200 Subject: [PATCH 015/143] artifacts: collect `dtb` `mkosi.output` folder into `out/` Signed-off-by: Ricardo Pardini --- captain/artifacts.py | 17 +++++++++++++++++ captain/cli/_commands.py | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/captain/artifacts.py b/captain/artifacts.py index 26bb61c5..180d2853 100644 --- a/captain/artifacts.py +++ b/captain/artifacts.py @@ -131,3 +131,20 @@ def collect(cfg: Config) -> None: collect_initramfs(cfg) collect_kernel(cfg) collect_iso(cfg) + + +def collect_dtbs(cfg): + """Copy the 'dtb' dir from mkosi.output/initramfs/{arch}/ to out/ if it exists.""" + indir = ensure_dir(cfg.initramfs_output) + dtb_dir: Path = indir / "dtb" + if dtb_dir.exists(): + log.info("Found dtb directory in %s, copying to output...", dtb_dir) + out = ensure_dir(cfg.output_dir) + target_dtb_dir = out / f"dtb-{cfg.kernel_version}-{cfg.arch_info.output_arch}" + # copy the directory incl subdirs and files + if target_dtb_dir.exists(): + shutil.rmtree(target_dtb_dir) + shutil.copytree(dtb_dir, target_dtb_dir) + log.info("Copied dtb directory: %s", target_dtb_dir) + else: + log.warning("No dtb directory found in %s", dtb_dir) diff --git a/captain/cli/_commands.py b/captain/cli/_commands.py index 6f2e7ca5..43577363 100644 --- a/captain/cli/_commands.py +++ b/captain/cli/_commands.py @@ -69,6 +69,7 @@ def _cmd_initramfs(cfg: Config, extra_args: list[str]) -> None: _build_mkosi_stage(cfg, extra_args) artifacts.collect_initramfs(cfg) artifacts.collect_kernel(cfg) + artifacts.collect_dtbs(cfg) log.info("Initramfs build complete!") @@ -84,7 +85,7 @@ def _cmd_build(cfg: Config, extra_args: list[str]) -> None: if cfg.build_kernel: _build_kernel_stage(cfg) _build_tools_stage(cfg) - _build_mkosi_stage(cfg, extra_args) + _cmd_initramfs(cfg, extra_args) # delegate, so it also collects _build_iso_stage(cfg) # TODO also conditional... artifacts.collect(cfg) log.info("Build complete!") From 20ade48f380695c3242cb137447a6fb7609ad392 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 18:51:56 +0200 Subject: [PATCH 016/143] publish: publish DTBs in OCI image as a directory Signed-off-by: Ricardo Pardini --- captain/cli/_commands.py | 2 +- captain/oci/_publish.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/captain/cli/_commands.py b/captain/cli/_commands.py index 43577363..55fe6c11 100644 --- a/captain/cli/_commands.py +++ b/captain/cli/_commands.py @@ -85,7 +85,7 @@ def _cmd_build(cfg: Config, extra_args: list[str]) -> None: if cfg.build_kernel: _build_kernel_stage(cfg) _build_tools_stage(cfg) - _cmd_initramfs(cfg, extra_args) # delegate, so it also collects + _cmd_initramfs(cfg, extra_args) # delegate, so it also collects _build_iso_stage(cfg) # TODO also conditional... artifacts.collect(cfg) log.info("Build complete!") diff --git a/captain/oci/_publish.py b/captain/oci/_publish.py index 0bf79417..b67ced6e 100644 --- a/captain/oci/_publish.py +++ b/captain/oci/_publish.py @@ -4,13 +4,14 @@ import contextlib import logging +import tarfile from datetime import datetime, timezone from pathlib import Path from uuid import uuid4 from captain import buildah, skopeo from captain.config import Config -from captain.util import ensure_dir +from captain.util import ensure_dir, get_arch_info from ._build import _build_platform_image, _collect_arch_artifacts, _deterministic_tar from ._common import _ARCHES, _image_ref @@ -199,8 +200,22 @@ def publish( # Create deterministic layer tars (shared across manifest pushes). arch_layer_tars: dict[str, list[Path]] = {} for arch, files in arch_files.items(): + log.info("Creating layer tars for %s... files: %s", arch, files) arch_layer_tars[arch] = [_deterministic_tar(f, out) for f in files] + # A single layer for all DTBs, if any; those are highly compressible together. + dtb_dir_in = out / f"dtb-{cfg.kernel_version}-{get_arch_info(arch).output_arch}" + if not dtb_dir_in.is_dir(): + log.warning("No dtbs directory found for %s: %s", arch, dtb_dir_in) + else: + log.info(f"Found DTB directory for {arch}: {dtb_dir_in}") + all_dtb_files: list[Path] = sorted(dtb_dir_in.glob("**/*.dtb*")) + dtb_tar_path = out / f"dtbs-{cfg.kernel_version}-{arch}.tar" + with tarfile.open(dtb_tar_path, "w") as tar: + for f in all_dtb_files: + tar.add(f, arcname=f.relative_to(out)) + arch_layer_tars[arch].append(dtb_tar_path) + pushed = True try: if target == "combined": From 54147557b9d7a60f752440b271597345731985f9 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 20:24:40 +0200 Subject: [PATCH 017/143] kernel: drop all kernel-build related code - later to be reborn as standard apt package Signed-off-by: Ricardo Pardini --- captain/artifacts.py | 52 +- captain/cli/_commands.py | 48 +- captain/cli/_main.py | 9 +- captain/cli/_parser.py | 31 +- captain/cli/_stages.py | 66 +-- captain/config.py | 40 +- captain/docker.py | 28 - captain/kernel.py | 278 ---------- captain/oci/_build.py | 21 +- captain/qemu.py | 1 - captain/util.py | 15 - kernel.configs/6.18.y.amd64 | 1032 ----------------------------------- kernel.configs/6.18.y.arm64 | 515 ----------------- kernel.configs/6.19.y.amd64 | 1032 ----------------------------------- kernel.configs/6.19.y.arm64 | 515 ----------------- 15 files changed, 36 insertions(+), 3647 deletions(-) delete mode 100644 captain/kernel.py delete mode 100644 kernel.configs/6.18.y.amd64 delete mode 100644 kernel.configs/6.18.y.arm64 delete mode 100644 kernel.configs/6.19.y.amd64 delete mode 100644 kernel.configs/6.19.y.arm64 diff --git a/captain/artifacts.py b/captain/artifacts.py index 180d2853..3b919d30 100644 --- a/captain/artifacts.py +++ b/captain/artifacts.py @@ -32,10 +32,8 @@ def _human_size(size: int) -> str: def collect_kernel(cfg: Config) -> None: - """Copy the kernel image from mkosi.output/kernel/{version}/{arch}/ to out/.""" + """Copy the kernel image produced by mkosi.""" out = ensure_dir(cfg.output_dir) - # In this case, mkosi might have collected the kernel from a installed linux-image package. - # Find it and use it instead. log.debug("Looking for kernel image produced by mkosi in %s", cfg.initramfs_output) vmlinu_files = sorted(cfg.initramfs_output.glob("*.vmlinu*")) if vmlinu_files: @@ -46,20 +44,7 @@ def collect_kernel(cfg: Config) -> None: "mkosi supplied kernel: %s (%s)", vmlinuz_dst, _human_size(vmlinuz_dst.stat().st_size) ) else: - log.warning("No kernel image produced by mkosi in %s", cfg.initramfs_output) - vmlinuz_dir = cfg.kernel_output - vmlinuz_files = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] - if vmlinuz_files: - vmlinuz_src = vmlinuz_files[0] - vmlinuz_dst = out / f"vmlinuz-{cfg.kernel_version}-{cfg.arch_info.output_arch}" - shutil.copy2(vmlinuz_src, vmlinuz_dst) - log.info( - "kernel built from source: %s (%s)", - vmlinuz_dst, - _human_size(vmlinuz_dst.stat().st_size), - ) - else: - log.warning("No kernel image found in %s", cfg.kernel_output) + log.error("No kernel image produced by mkosi in %s", cfg.initramfs_output) def collect_initramfs(cfg: Config) -> None: @@ -75,6 +60,22 @@ def collect_initramfs(cfg: Config) -> None: log.warning("No initramfs CPIO found in %s", cfg.initramfs_output) +def collect_dtbs(cfg): + """Collect the dtb directory produced by mkosi's finalize script.""" + indir = ensure_dir(cfg.initramfs_output) + dtb_dir: Path = indir / "dtb" + if dtb_dir.exists(): + log.info("Found dtb directory in %s, copying to output...", dtb_dir) + out = ensure_dir(cfg.output_dir) + target_dtb_dir = out / f"dtb-{cfg.kernel_version}-{cfg.arch_info.output_arch}" + if target_dtb_dir.exists(): + shutil.rmtree(target_dtb_dir) + shutil.copytree(dtb_dir, target_dtb_dir) + log.info("Copied dtb directory: %s", target_dtb_dir) + else: + log.warning("No dtb directory found in %s", dtb_dir) + + def collect_iso(cfg: Config) -> None: """Copy the ISO image from mkosi.output/iso/{arch}/ to out/.""" out = ensure_dir(cfg.output_dir) @@ -131,20 +132,3 @@ def collect(cfg: Config) -> None: collect_initramfs(cfg) collect_kernel(cfg) collect_iso(cfg) - - -def collect_dtbs(cfg): - """Copy the 'dtb' dir from mkosi.output/initramfs/{arch}/ to out/ if it exists.""" - indir = ensure_dir(cfg.initramfs_output) - dtb_dir: Path = indir / "dtb" - if dtb_dir.exists(): - log.info("Found dtb directory in %s, copying to output...", dtb_dir) - out = ensure_dir(cfg.output_dir) - target_dtb_dir = out / f"dtb-{cfg.kernel_version}-{cfg.arch_info.output_arch}" - # copy the directory incl subdirs and files - if target_dtb_dir.exists(): - shutil.rmtree(target_dtb_dir) - shutil.copytree(dtb_dir, target_dtb_dir) - log.info("Copied dtb directory: %s", target_dtb_dir) - else: - log.warning("No dtb directory found in %s", dtb_dir) diff --git a/captain/cli/_commands.py b/captain/cli/_commands.py index 55fe6c11..c131224a 100644 --- a/captain/cli/_commands.py +++ b/captain/cli/_commands.py @@ -13,7 +13,6 @@ from ._stages import ( _build_iso_stage, - _build_kernel_stage, _build_mkosi_stage, _build_tools_stage, ) @@ -21,51 +20,14 @@ log = logging.getLogger(__name__) -def _cmd_kernel(cfg: Config, _extra_args: list[str]) -> None: - """Build only the kernel (no tools, no mkosi).""" - _build_kernel_stage(cfg) - artifacts.collect_kernel(cfg) - log.info("Kernel build stage complete!") - - def _cmd_tools(cfg: Config, _extra_args: list[str]) -> None: """Download tools (containerd, runc, nerdctl, CNI plugins).""" _build_tools_stage(cfg) log.info("Tools stage complete!") -def _check_kernel_modules(cfg: Config) -> None: - """Verify kernel modules exist before building the initramfs. - - The initramfs depends on pre-built kernel modules in the ExtraTrees - directory. If they are missing (e.g. due to an artifact download - issue) the build should fail immediately rather than silently - producing an initramfs without modules. - """ - modules_dir = cfg.modules_output / "usr" / "lib" / "modules" - if not modules_dir.is_dir(): - log.error("Kernel modules directory not found: %s", modules_dir) - log.error("Ensure the kernel build artifacts are downloaded correctly.") - raise SystemExit(1) - # Check that at least one module version directory exists with modules - version_dirs = [d for d in modules_dir.iterdir() if d.is_dir()] - if not version_dirs: - log.error("No kernel version directories found in %s", modules_dir) - raise SystemExit(1) - # Search all version directories for at least one kernel module - for version_dir in version_dirs: - if any(version_dir.rglob("*.ko*")): - log.info("Kernel modules found in %s (version: %s)", version_dir, version_dir.name) - return - searched = ", ".join(str(d) for d in version_dirs) - log.error("No kernel modules (.ko/.ko.zst) found in any kernel version directory.") - log.error("Searched directories: %s", searched) - raise SystemExit(1) - - def _cmd_initramfs(cfg: Config, extra_args: list[str]) -> None: """Build only the initramfs via mkosi, then collect artifacts.""" - # _check_kernel_modules(cfg) _build_mkosi_stage(cfg, extra_args) artifacts.collect_initramfs(cfg) artifacts.collect_kernel(cfg) @@ -81,12 +43,10 @@ def _cmd_iso(cfg: Config, _extra_args: list[str]) -> None: def _cmd_build(cfg: Config, extra_args: list[str]) -> None: - """Full build: kernel → tools → initramfs → iso → artifacts.""" - if cfg.build_kernel: - _build_kernel_stage(cfg) + """Full build: tools → initramfs → iso → artifacts.""" _build_tools_stage(cfg) _cmd_initramfs(cfg, extra_args) # delegate, so it also collects - _build_iso_stage(cfg) # TODO also conditional... + _build_iso_stage(cfg) # TODO also conditional... / and/or include dtb's for arm64 artifacts.collect(cfg) log.info("Build complete!") @@ -218,18 +178,15 @@ def _clean_all(cfg: Config) -> None: def _cmd_summary(cfg: Config, _extra_args: list[str]) -> None: """Print mkosi configuration summary.""" tools_tree = str(cfg.tools_output) - modules_tree = str(cfg.modules_output) output_dir = str(cfg.initramfs_output) match cfg.mkosi_mode: case "docker": docker.build_builder(cfg) container_tree = f"/work/mkosi.output/tools/{cfg.arch}" - container_modules = f"/work/mkosi.output/kernel/{cfg.kernel_version}/{cfg.arch}/modules" container_outdir = f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}" docker.run_mkosi( cfg, f"--extra-tree={container_tree}", - f"--extra-tree={container_modules}", f"--output-dir={container_outdir}", "summary", ) @@ -239,7 +196,6 @@ def _cmd_summary(cfg: Config, _extra_args: list[str]) -> None: "mkosi", f"--architecture={cfg.arch_info.mkosi_arch}", f"--extra-tree={tools_tree}", - f"--extra-tree={modules_tree}", f"--output-dir={output_dir}", "summary", ], diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 33ded5d0..b299d83a 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -29,7 +29,6 @@ _cmd_clean, _cmd_initramfs, _cmd_iso, - _cmd_kernel, _cmd_qemu_test, _cmd_shell, _cmd_summary, @@ -86,7 +85,6 @@ def main(project_dir: Path | None = None) -> None: # 7. Dispatch. dispatch: dict[str, object] = { "build": _cmd_build, - "kernel": _cmd_kernel, "tools": _cmd_tools, "initramfs": _cmd_initramfs, "iso": _cmd_iso, @@ -105,23 +103,19 @@ def main(project_dir: Path | None = None) -> None: else: handler(cfg, extra) # type: ignore[operator] else: + log.error("You hit ungolden path; passing stuff to mkosi directly is not reproducible.") # Pass through to mkosi (shouldn't happen with _extract_command # but kept as a safety net). tools_tree = str(cfg.tools_output) - modules_tree = str(cfg.modules_output) output_dir = str(cfg.initramfs_output) match cfg.mkosi_mode: case "docker": docker.build_builder(cfg) container_tree = f"/work/mkosi.output/tools/{cfg.arch}" - container_modules = ( - f"/work/mkosi.output/kernel/{cfg.kernel_version}/{cfg.arch}/modules" - ) container_outdir = f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}" docker.run_mkosi( cfg, f"--extra-tree={container_tree}", - f"--extra-tree={container_modules}", f"--output-dir={container_outdir}", command, *extra, @@ -132,7 +126,6 @@ def main(project_dir: Path | None = None) -> None: "mkosi", f"--architecture={cfg.arch_info.mkosi_arch}", f"--extra-tree={tools_tree}", - f"--extra-tree={modules_tree}", f"--output-dir={output_dir}", command, *extra, diff --git a/captain/cli/_parser.py b/captain/cli/_parser.py index 4e6d1297..0b685de9 100644 --- a/captain/cli/_parser.py +++ b/captain/cli/_parser.py @@ -198,7 +198,7 @@ def _add_common_flags(parser: configargparse.ArgParser) -> None: def _add_kernel_flags(parser: configargparse.ArgParser) -> None: - """--kernel-version, --kernel-src, --kernel-mode, --force-kernel""" + """--kernel-version""" g = parser.add_argument_group("kernel") g.add_argument( "--kernel-version", @@ -207,34 +207,6 @@ def _add_kernel_flags(parser: configargparse.ArgParser) -> None: default=DEFAULT_KERNEL_VERSION, help="kernel version to build", ) - g.add_argument( - "--kernel-config", - env_var="KERNEL_CONFIG", - metavar="PATH", - default=None, - help="path to kernel config file (overrides auto-detection from kernel.configs/)", - ) - g.add_argument( - "--kernel-src", - env_var="KERNEL_SRC", - metavar="PATH", - default=None, - help="path to local kernel source tree", - ) - g.add_argument( - "--kernel-mode", - env_var="KERNEL_MODE", - default="docker", - choices=list(VALID_MODES), - help="kernel stage execution mode", - ) - g.add_argument( - "--force-kernel", - env_var="FORCE_KERNEL", - action="store_true", - default=False, - help="force kernel rebuild even if outputs exist", - ) def _add_tools_flags(parser: configargparse.ArgParser) -> None: @@ -570,7 +542,6 @@ def _add_tink_flags(parser: configargparse.ArgParser) -> None: _add_mkosi_flags, _add_iso_flags, ], - "kernel": [_add_common_flags, _add_kernel_flags], "tools": [_add_common_flags, _add_tools_flags], "initramfs": [_add_common_flags, _add_kernel_flags, _add_mkosi_flags], "iso": [_add_common_flags, _add_kernel_flags, _add_iso_flags], diff --git a/captain/cli/_stages.py b/captain/cli/_stages.py index e549444e..e59ce308 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/_stages.py @@ -4,71 +4,13 @@ import logging -from captain import docker, iso, kernel, tools +from captain import docker, iso, tools from captain.config import Config -from captain.util import check_kernel_dependencies, check_mkosi_dependencies, run +from captain.util import check_mkosi_dependencies, run log = logging.getLogger(__name__) -def _build_kernel_stage(cfg: Config) -> None: - """Run the kernel build stage according to *cfg.kernel_mode*.""" - - # --- skip --------------------------------------------------------- - if cfg.kernel_mode == "skip": - log.info("KERNEL_MODE=skip — skipping kernel build") - return - - # --- idempotency -------------------------------------------------- - modules_dir = cfg.modules_output / "usr" / "lib" / "modules" - vmlinuz_dir = cfg.kernel_output - has_vmlinuz = vmlinuz_dir.is_dir() and any(vmlinuz_dir.glob("vmlinuz-*")) - log.debug( - "Checking kernel build idempotency: modules_dir=%s, has_vmlinuz=%s", - modules_dir, - has_vmlinuz, - ) - - if modules_dir.is_dir() and has_vmlinuz and not cfg.force_kernel: - log.info("Kernel already built (use --force-kernel to rebuild)") - return - - if modules_dir.is_dir() and not has_vmlinuz: - log.warning("Modules exist but vmlinuz is missing — rebuilding kernel") - - # --- native ------------------------------------------------------- - if cfg.kernel_mode == "native": - missing = check_kernel_dependencies(cfg.arch) - if missing: - log.error("Missing kernel build tools: %s", ", ".join(missing)) - log.error("Install them or set --kernel-mode=docker.") - raise SystemExit(1) - log.info("Building kernel (native)...") - kernel.build(cfg) - return - - # --- docker ------------------------------------------------------- - docker.build_builder(cfg) - log.info("Building kernel (docker)...") - docker.run_in_builder( - cfg, - "--entrypoint", - "/usr/bin/uv", - cfg.builder_image, - *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), - "run", - "/work/build.py", - "kernel", - ) - docker.fix_docker_ownership( - cfg, - [ - f"/work/mkosi.output/kernel/{cfg.kernel_version}/{cfg.arch}", - "/work/out", - ], - ) - - def _build_tools_stage(cfg: Config) -> None: """Run the tools download stage according to *cfg.tools_mode*.""" @@ -125,14 +67,12 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: raise SystemExit(1) log.info("Building initrd with mkosi (native)...") tools_tree = str(cfg.tools_output) - modules_tree = str(cfg.modules_output) output_dir = str(cfg.initramfs_output) run( [ "mkosi", f"--architecture={cfg.arch_info.mkosi_arch}", f"--extra-tree={tools_tree}", - *([f"--extra-tree={modules_tree}"] if cfg.build_kernel else []), f"--output-dir={output_dir}", "build", *mkosi_args, @@ -145,12 +85,10 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: docker.build_builder(cfg) log.info("Building initrd with mkosi (docker)...") tools_tree = f"/work/mkosi.output/tools/{cfg.arch}" - modules_tree = f"/work/mkosi.output/kernel/{cfg.kernel_version}/{cfg.arch}/modules" output_dir = f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}" docker.run_mkosi( cfg, f"--extra-tree={tools_tree}", - *([f"--extra-tree={modules_tree}"] if cfg.build_kernel else []), f"--output-dir={output_dir}", "--package-cache-dir=/cache/packages", "build", diff --git a/captain/config.py b/captain/config.py index da39bb60..523eaf29 100644 --- a/captain/config.py +++ b/captain/config.py @@ -31,24 +31,19 @@ class Config: # Target arch: str = "amd64" - build_kernel: bool = False kernel_version: str = DEFAULT_KERNEL_VERSION - kernel_config: str | None = None - kernel_src: str | None = None # Docker builder_image: str = "captainos-builder" no_cache: bool = False # Per-stage mode: "docker" | "native" | "skip" - kernel_mode: str = "docker" tools_mode: str = "docker" mkosi_mode: str = "docker" iso_mode: str = "docker" release_mode: str = "docker" # Force flags - force_kernel: bool = False force_tools: bool = False force_iso: bool = False @@ -67,7 +62,6 @@ def __post_init__(self) -> None: self.arch_info = get_arch_info(self.arch) self.arch = self.arch_info.arch # normalise aliases (x86_64 → amd64, etc.) for name, value in ( - ("KERNEL_MODE", self.kernel_mode), ("TOOLS_MODE", self.tools_mode), ("MKOSI_MODE", self.mkosi_mode), ("ISO_MODE", self.iso_mode), @@ -81,8 +75,7 @@ def __post_init__(self) -> None: def needs_docker(self) -> bool: """True if any stage requires Docker.""" return ( - self.kernel_mode == "docker" - or self.tools_mode == "docker" + self.tools_mode == "docker" or self.mkosi_mode == "docker" or self.iso_mode == "docker" or self.release_mode == "docker" @@ -107,18 +100,13 @@ def from_args(cls, args: argparse.Namespace, project_dir: Path | None) -> Config project_dir=project_dir, output_dir=project_dir / "out", arch=getattr(args, "arch", "amd64"), - build_kernel=getattr(args, "build_kernel", False), kernel_version=getattr(args, "kernel_version", DEFAULT_KERNEL_VERSION), - kernel_config=getattr(args, "kernel_config", None) or None, - kernel_src=getattr(args, "kernel_src", None) or None, builder_image=getattr(args, "builder_image", "captainos-builder"), no_cache=getattr(args, "no_cache", False), - kernel_mode=getattr(args, "kernel_mode", "docker"), tools_mode=getattr(args, "tools_mode", "docker"), mkosi_mode=getattr(args, "mkosi_mode", "docker"), iso_mode=getattr(args, "iso_mode", "docker"), release_mode=getattr(args, "release_mode", "docker"), - force_kernel=getattr(args, "force_kernel", False), force_tools=getattr(args, "force_tools", False), force_iso=getattr(args, "force_iso", False), qemu_append=getattr(args, "qemu_append", ""), @@ -138,18 +126,13 @@ def from_env(cls, project_dir: Path) -> Config: project_dir=project_dir, output_dir=project_dir / "out", arch=os.environ.get("ARCH", "amd64"), - build_kernel=os.environ.get("BUILD_KERNEL") == "1", kernel_version=os.environ.get("KERNEL_VERSION", DEFAULT_KERNEL_VERSION), - kernel_config=os.environ.get("KERNEL_CONFIG") or None, - kernel_src=os.environ.get("KERNEL_SRC") or None, builder_image=os.environ.get("BUILDER_IMAGE", "captainos-builder"), no_cache=os.environ.get("NO_CACHE") == "1", - kernel_mode=os.environ.get("KERNEL_MODE", "docker"), tools_mode=os.environ.get("TOOLS_MODE", "docker"), mkosi_mode=os.environ.get("MKOSI_MODE", "docker"), iso_mode=os.environ.get("ISO_MODE", "docker"), release_mode=os.environ.get("RELEASE_MODE", "docker"), - force_kernel=os.environ.get("FORCE_KERNEL") == "1", force_tools=os.environ.get("FORCE_TOOLS") == "1", force_iso=os.environ.get("FORCE_ISO") == "1", qemu_append=os.environ.get("QEMU_APPEND", ""), @@ -167,27 +150,6 @@ def tools_output(self) -> Path: """ return self.project_dir / "mkosi.output" / "tools" / self.arch - @property - def kernel_output(self) -> Path: - """Per-version, per-arch directory for all kernel build artifacts. - - Contains the vmlinuz image (loaded separately by iPXE) and - a ``modules/`` subtree that mirrors a root filesystem layout - (``usr/lib/modules/{kver}/``) so it can be passed directly - as an ``--extra-tree=`` to mkosi. - """ - return self.project_dir / "mkosi.output" / "kernel" / self.kernel_version / self.arch - - @property - def modules_output(self) -> Path: - """Per-version, per-arch root for kernel modules. - - Returns ``kernel/{version}/{arch}/modules`` which contains a - merged-usr tree (``usr/lib/modules/{kver}/``) suitable for - passing as ``--extra-tree=`` to mkosi. - """ - return self.kernel_output / "modules" - @property def mkosi_output(self) -> Path: return self.project_dir / "mkosi.output" diff --git a/captain/docker.py b/captain/docker.py index 565369ec..d9741f1f 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -167,12 +167,8 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "-e", f"FORCE_TOOLS={int(cfg.force_tools)}", "-e", - f"FORCE_KERNEL={int(cfg.force_kernel)}", - "-e", f"FORCE_ISO={int(cfg.force_iso)}", "-e", - "KERNEL_MODE=native", - "-e", "TOOLS_MODE=native", "-e", "MKOSI_MODE=native", @@ -202,32 +198,8 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: docker_args += ["-v", f"{cfg.project_dir}/pyproject.toml:/work/pyproject.toml"] docker_args += ["-v", f"{cfg.project_dir}/build.py:/work/build.py"] - docker_args += ["-v", f"{cfg.project_dir}/kernel.configs:/work/kernel.configs"] - docker_args += ["--mount", "type=volume,source=captain-cache-packages,target=/cache/packages"] - # Mount kernel source if provided - if cfg.kernel_src is not None: - kernel_src_path = Path(cfg.kernel_src).resolve() - if not kernel_src_path.is_dir(): - log.error("KERNEL_SRC=%s does not exist", cfg.kernel_src) - raise SystemExit(1) - docker_args.extend(["-v", f"{kernel_src_path}:/work/kernel-src:ro"]) - docker_args.extend(["-e", "KERNEL_SRC=/work/kernel-src"]) - - # Mount kernel config override and point KERNEL_CONFIG to the container path - if cfg.kernel_config is not None: - kernel_cfg_path = Path(cfg.kernel_config) - if not kernel_cfg_path.is_absolute(): - kernel_cfg_path = (cfg.project_dir / kernel_cfg_path).resolve() - else: - kernel_cfg_path = kernel_cfg_path.resolve() - if not kernel_cfg_path.is_file(): - log.error("KERNEL_CONFIG=%s does not exist", cfg.kernel_config) - raise SystemExit(1) - docker_args.extend(["-v", f"{kernel_cfg_path}:/work/kernel-config:ro"]) - docker_args.extend(["-e", "KERNEL_CONFIG=/work/kernel-config"]) - docker_args.extend(extra_args) run(docker_args) diff --git a/captain/kernel.py b/captain/kernel.py deleted file mode 100644 index a528b0c3..00000000 --- a/captain/kernel.py +++ /dev/null @@ -1,278 +0,0 @@ -"""Kernel download, configuration, compilation, and installation. - -Heavy lifting (make, strip) is still done via subprocess — only the -orchestration is in Python. Called directly by ``cli._build_kernel_stage`` -in both native and Docker modes (inside the container ``build.py kernel`` -re-enters via the CLI with all modes forced to native). -""" - -from __future__ import annotations - -import logging -import os -import re -import shutil -import tarfile -import urllib.error -import urllib.request -from pathlib import Path - -from rich.progress import ( - BarColumn, - DownloadColumn, - Progress, - TextColumn, - TimeRemainingColumn, - TransferSpeedColumn, -) - -from captain import console -from captain.config import Config -from captain.util import ensure_dir, run, safe_extractall - -log = logging.getLogger(__name__) - -_DOWNLOAD_TIMEOUT = 60 # seconds - - -def _download_with_progress(url: str, filename: Path) -> None: - """Download *url* to *filename* with a Rich progress bar.""" - req = urllib.request.Request(url) - with urllib.request.urlopen(req, timeout=_DOWNLOAD_TIMEOUT) as resp: - total = int(resp.headers.get("Content-Length", 0)) or None - with Progress( - TextColumn("[progress.description]{task.description}"), - BarColumn(), - DownloadColumn(), - TransferSpeedColumn(), - TimeRemainingColumn(), - console=console, - ) as progress: - task = progress.add_task(" Downloading", total=total) - with open(filename, "wb") as out: - while True: - buf = resp.read(8192) - if not buf: - break - out.write(buf) - progress.update(task, advance=len(buf)) - - -def download_kernel(version: str, dest_dir: Path) -> Path: - """Download and extract a kernel tarball. Returns the source directory.""" - src_dir = dest_dir / f"linux-{version}" - if src_dir.is_dir(): - log.info("Using cached kernel source at %s", src_dir) - return src_dir - - major = version.split(".")[0] - url = f"https://cdn.kernel.org/pub/linux/kernel/v{major}.x/linux-{version}.tar.xz" - tarball = dest_dir / f"linux-{version}.tar.xz" - - log.info("Downloading kernel %s...", version) - log.info(" URL: %s", url) - ensure_dir(dest_dir) - try: - _download_with_progress(url, tarball) - except urllib.error.HTTPError as exc: - log.error("Download failed: %s — %s", exc, url) - raise SystemExit(1) from None - except urllib.error.URLError as exc: - log.error("Download failed: %s — %s", exc.reason, url) - raise SystemExit(1) from None - - log.info("Extracting kernel source...") - with tarfile.open(tarball, "r:xz") as tf: - safe_extractall(tf, path=dest_dir) - tarball.unlink() - - return src_dir - - -def _kernel_branch(version: str) -> str: - """Derive the stable branch prefix from a full kernel version.""" - parts = version.split(".") - if len(parts) < 2: - log.error("Invalid kernel version format: %s", version) - raise SystemExit(1) - return f"{parts[0]}.{parts[1]}.y" - - -def _find_defconfig(cfg: Config) -> Path: - """Locate the defconfig for the current kernel version and architecture.""" - if cfg.kernel_config: - explicit = Path(cfg.kernel_config) - if not explicit.is_absolute(): - explicit = cfg.project_dir / explicit - if explicit.is_file(): - return explicit - log.error("Kernel config not found: %s", explicit) - raise SystemExit(1) - - ai = cfg.arch_info - branch = _kernel_branch(cfg.kernel_version) - defconfig = cfg.project_dir / "kernel.configs" / f"{branch}.{ai.arch}" - if defconfig.is_file(): - return defconfig - - configs_dir = cfg.project_dir / "kernel.configs" - available = sorted( - { - p.name.rsplit(".", 1)[0] - for p in configs_dir.glob(f"*.{ai.arch}") - if not p.name.startswith(".") - } - ) - avail_str = ", ".join(available) if available else "(none)" - log.error( - "No kernel config found for %s on %s\n Expected: %s\n Available branches for %s: %s", - branch, - ai.arch, - defconfig, - ai.arch, - avail_str, - ) - raise SystemExit(1) - - -def configure_kernel(cfg: Config, src_dir: Path) -> None: - """Apply defconfig and run olddefconfig.""" - ai = cfg.arch_info - defconfig = _find_defconfig(cfg) - - make_env = {"ARCH": ai.kernel_arch} - if ai.cross_compile: - make_env["CROSS_COMPILE"] = ai.cross_compile - - log.info("Using defconfig: %s", defconfig) - shutil.copy2(defconfig, src_dir / ".config") - run(["make", "olddefconfig"], env=make_env, cwd=src_dir) - branch = _kernel_branch(cfg.kernel_version) - resolved = cfg.project_dir / "kernel.configs" / f".config.resolved.{branch}.{ai.arch}" - shutil.copy2(src_dir / ".config", resolved) - log.info("Resolved config saved to kernel.configs/.config.resolved.%s.%s", branch, ai.arch) - - if ai.kernel_arch == "x86_64": - log.info("Increasing COMMAND_LINE_SIZE to 4096 (x86_64)...") - setup_h = src_dir / "arch" / "x86" / "include" / "asm" / "setup.h" - text = setup_h.read_text() - new_text = re.sub( - r"#define COMMAND_LINE_SIZE\s+2048", - "#define COMMAND_LINE_SIZE 4096", - text, - ) - if new_text == text: - log.warning( - "COMMAND_LINE_SIZE patch did not match — the kernel default may have changed" - ) - setup_h.write_text(new_text) - - -def build_kernel(cfg: Config, src_dir: Path) -> str: - """Compile the kernel image and modules. Returns the built kernel version string.""" - ai = cfg.arch_info - nproc = os.cpu_count() or 1 - - make_env = {"ARCH": ai.kernel_arch} - if ai.cross_compile: - make_env["CROSS_COMPILE"] = ai.cross_compile - - log.info("Building kernel with %d jobs...", nproc) - run( - ["make", f"-j{nproc}", ai.image_target, "modules"], - env=make_env, - cwd=src_dir, - ) - - result = run( - ["make", "-s", "kernelrelease"], - env={"ARCH": ai.kernel_arch}, - capture=True, - cwd=src_dir, - ) - built_kver = result.stdout.strip() - log.info("Built kernel version: %s", built_kver) - return built_kver - - -def install_kernel(cfg: Config, src_dir: Path, built_kver: str) -> None: - """Install modules and vmlinuz into mkosi.output/kernel/{version}/{arch}/.""" - ai = cfg.arch_info - modules_root = cfg.modules_output - - make_env = {"ARCH": ai.kernel_arch} - if ai.cross_compile: - make_env["CROSS_COMPILE"] = ai.cross_compile - - log.info("Installing modules...") - run( - ["make", f"INSTALL_MOD_PATH={modules_root}", "modules_install"], - env=make_env, - cwd=src_dir, - ) - - log.info("Stripping debug symbols from modules...") - strip_cmd = f"{ai.strip_prefix}strip" - for ko in modules_root.rglob("*.ko"): - run([strip_cmd, "--strip-unneeded", str(ko)], check=False) - - log.info("Compressing kernel modules with zstd...") - for ko in modules_root.rglob("*.ko"): - run(["zstd", "--rm", "-q", "-19", str(ko)], check=True) - - mod_base = modules_root / "lib" / "modules" / built_kver - (mod_base / "build").unlink(missing_ok=True) - (mod_base / "source").unlink(missing_ok=True) - - usr_moddir = ensure_dir(modules_root / "usr" / "lib" / "modules" / built_kver) - if mod_base.is_dir(): - for item in mod_base.iterdir(): - dest = usr_moddir / item.name - if dest.exists(): - if dest.is_dir(): - shutil.rmtree(dest) - else: - dest.unlink() - shutil.move(str(item), str(dest)) - shutil.rmtree(modules_root / "lib", ignore_errors=True) - - log.info("Running depmod for compressed modules...") - run( - ["depmod", "-a", "-b", str(modules_root / "usr"), built_kver], - check=True, - ) - - kernel_image = src_dir / ai.kernel_image_path - vmlinuz_dir = ensure_dir(cfg.kernel_output) - - for old in vmlinuz_dir.glob("vmlinuz-*"): - old.unlink(missing_ok=True) - - shutil.copy2(kernel_image, vmlinuz_dir / f"vmlinuz-{built_kver}") - - log.info("Kernel build complete:") - vmlinuz = vmlinuz_dir / f"vmlinuz-{built_kver}" - vmlinuz_size = vmlinuz.stat().st_size / (1024 * 1024) - log.info(" Image: %s (%.1fM)", vmlinuz, vmlinuz_size) - log.info(" Modules: %s/", usr_moddir) - log.info(" Version: %s", built_kver) - log.info(" Output: %s", cfg.kernel_output) - - -def build(cfg: Config) -> None: - """Full kernel build pipeline — download, configure, build, install.""" - if cfg.kernel_output.exists(): - shutil.rmtree(cfg.kernel_output) - ensure_dir(cfg.kernel_output) - - build_dir = Path("/var/tmp/kernel-build") - - if cfg.kernel_src and Path(cfg.kernel_src).is_dir(): - log.info("Using provided kernel source at %s", cfg.kernel_src) - src_dir = Path(cfg.kernel_src) - else: - src_dir = download_kernel(cfg.kernel_version, build_dir) - - configure_kernel(cfg, src_dir) - built_kver = build_kernel(cfg, src_dir) - install_kernel(cfg, src_dir, built_kver) diff --git a/captain/oci/_build.py b/captain/oci/_build.py index 4b0a22ba..23776ee5 100644 --- a/captain/oci/_build.py +++ b/captain/oci/_build.py @@ -4,7 +4,6 @@ import contextlib import logging -import shutil import tarfile from datetime import datetime from pathlib import Path @@ -46,16 +45,18 @@ def _collect_arch_artifacts( Returns [vmlinuz, initramfs, iso, checksums] paths in *out*. """ - # Collect kernel - vmlinuz_dir = project_dir / "mkosi.output" / "kernel" / kernel_version / arch - vmlinuz_files = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] oarch = get_arch_info(arch).output_arch - vmlinuz_dst = out / f"vmlinuz-{kernel_version}-{oarch}" - if vmlinuz_files: - shutil.copy2(vmlinuz_files[0], vmlinuz_dst) - log.info("kernel: %s", vmlinuz_dst) - else: - log.warning("No kernel image found for %s", arch) + + ## # Collect kernel @TODO probably fix or remove? + ## vmlinuz_dir = project_dir / "mkosi.output" / "kernel" / kernel_version / arch + ## vmlinuz_files = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] + ## + ## vmlinuz_dst = out / f"vmlinuz-{kernel_version}-{oarch}" + ## if vmlinuz_files: + ## shutil.copy2(vmlinuz_files[0], vmlinuz_dst) + ## log.info("kernel: %s", vmlinuz_dst) + ## else: + ## log.warning("No kernel image found for %s", arch) arch_files = [ out / f"vmlinuz-{kernel_version}-{oarch}", diff --git a/captain/qemu.py b/captain/qemu.py index d8af60f7..bbbd8d5c 100644 --- a/captain/qemu.py +++ b/captain/qemu.py @@ -77,7 +77,6 @@ def run_qemu(cfg: Config, args: argparse.Namespace | None = None) -> None: log.error("Build artifacts not found:") for m in missing: log.error(" %s", m) - log.error("Run './build.py --kernel-version %s' first.", cfg.kernel_version) sys.exit(1) tink = _tink_cmdline(args) if args is not None else "" diff --git a/captain/util.py b/captain/util.py index e1c150e7..797f8fa3 100644 --- a/captain/util.py +++ b/captain/util.py @@ -24,14 +24,9 @@ class ArchInfo: arch: str # canonical name: amd64 | arm64 output_arch: str # user-facing name in artifact filenames: x86_64 | aarch64 - kernel_arch: str # kernel ARCH value - cross_compile: str # CROSS_COMPILE prefix (empty for native) - image_target: str # kernel image make target - kernel_image_path: str # relative path to built kernel image dl_arch: str # architecture name in download URLs mkosi_arch: str # mkosi --architecture value qemu_binary: str # QEMU system emulator binary - strip_prefix: str # prefix for strip command def get_arch_info(arch: str) -> ArchInfo: @@ -41,27 +36,17 @@ def get_arch_info(arch: str) -> ArchInfo: return ArchInfo( arch="amd64", output_arch="x86_64", - kernel_arch="x86_64", - cross_compile="", - image_target="bzImage", - kernel_image_path="arch/x86/boot/bzImage", dl_arch="amd64", mkosi_arch="x86-64", qemu_binary="qemu-system-x86_64", - strip_prefix="", ) case "arm64" | "aarch64": return ArchInfo( arch="arm64", output_arch="aarch64", - kernel_arch="arm64", - cross_compile="aarch64-linux-gnu-", - image_target="Image", - kernel_image_path="arch/arm64/boot/Image", dl_arch="arm64", mkosi_arch="arm64", qemu_binary="qemu-system-aarch64", - strip_prefix="aarch64-linux-gnu-", ) case _: log.error("Unsupported architecture: %s", arch) diff --git a/kernel.configs/6.18.y.amd64 b/kernel.configs/6.18.y.amd64 deleted file mode 100644 index c6d243cc..00000000 --- a/kernel.configs/6.18.y.amd64 +++ /dev/null @@ -1,1032 +0,0 @@ -# CaptainOS kernel defconfig for x86_64 -# Comprehensive config for bare-metal provisioning with container/network support. -# Derived from HookOS generic-6.6.y-x86_64 proven config, adapted for kernel 6.18. -# -# Generate a full .config from this: -# cp 6.18.y.amd64 .config && make olddefconfig - -# --- Architecture --- -CONFIG_64BIT=y -CONFIG_X86_64=y -CONFIG_SMP=y -CONFIG_NR_CPUS=128 -# CONFIG_X86_EXTENDED_PLATFORM is not set -CONFIG_X86_INTEL_LPSS=y -CONFIG_HYPERVISOR_GUEST=y -CONFIG_PARAVIRT_SPINLOCKS=y -CONFIG_XEN=y -CONFIG_XEN_PVH=y -CONFIG_X86_REROUTE_FOR_BROKEN_BOOT_IRQS=y -# CONFIG_X86_MCE is not set -CONFIG_X86_MSR=y -CONFIG_X86_CPUID=y -# CONFIG_X86_5LEVEL is not set -CONFIG_HZ_1000=y -CONFIG_PHYSICAL_ALIGN=0x1000000 -CONFIG_LEGACY_VSYSCALL_NONE=y -# CONFIG_MODIFY_LDT_SYSCALL is not set -CONFIG_IA32_EMULATION=y - -# --- General --- -CONFIG_LOCALVERSION="-captainos" -# CONFIG_LOCALVERSION_AUTO is not set -CONFIG_DEFAULT_HOSTNAME="captainos" -CONFIG_SYSVIPC=y -CONFIG_POSIX_MQUEUE=y -CONFIG_AUDIT=y -CONFIG_NO_HZ_IDLE=y -CONFIG_HIGH_RES_TIMERS=y -CONFIG_PREEMPT_VOLUNTARY=y -CONFIG_BSD_PROCESS_ACCT=y -CONFIG_BSD_PROCESS_ACCT_V3=y -CONFIG_IKCONFIG=y -CONFIG_IKCONFIG_PROC=y -CONFIG_CC_OPTIMIZE_FOR_SIZE=y -CONFIG_EXPERT=y -CONFIG_KEXEC=y -CONFIG_KEXEC_FILE=y -CONFIG_SCHED_AUTOGROUP=y -CONFIG_CHECKPOINT_RESTORE=y -CONFIG_KPROBES=y -CONFIG_JUMP_LABEL=y -CONFIG_BINFMT_MISC=y - -# --- Cgroups --- -CONFIG_CGROUPS=y -CONFIG_CGROUP_FREEZER=y -CONFIG_CGROUP_PIDS=y -CONFIG_CGROUP_RDMA=y -CONFIG_CGROUP_DEVICE=y -CONFIG_CGROUP_CPUACCT=y -CONFIG_CGROUP_PERF=y -CONFIG_CGROUP_BPF=y -CONFIG_CGROUP_MISC=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CGROUP_NET_PRIO=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_CFS_BANDWIDTH=y -CONFIG_RT_GROUP_SCHED=y -CONFIG_CGROUP_SCHED=y -CONFIG_CPUSETS=y - -# --- Namespaces --- -CONFIG_NAMESPACES=y -CONFIG_USER_NS=y -CONFIG_PID_NS=y -CONFIG_NET_NS=y -CONFIG_UTS_NS=y -CONFIG_IPC_NS=y - -# --- Init / Initramfs --- -CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="" -# CONFIG_RD_BZIP2 is not set -# CONFIG_RD_LZMA is not set -CONFIG_RD_GZIP=y -CONFIG_RD_XZ=y -# CONFIG_RD_LZO is not set -# CONFIG_RD_LZ4 is not set -CONFIG_RD_ZSTD=y - -# --- Memory management --- -CONFIG_SLAB_FREELIST_RANDOM=y -# CONFIG_COMPAT_BRK is not set -CONFIG_MEMORY_HOTPLUG=y -CONFIG_MEMORY_HOTREMOVE=y -CONFIG_KSM=y -CONFIG_DEFAULT_MMAP_MIN_ADDR=65536 -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_ZONE_DEVICE=y -CONFIG_HUGETLBFS=y - -# --- Power / Suspend --- -# CONFIG_SUSPEND is not set - -# --- ACPI --- -CONFIG_ACPI=y -# CONFIG_ACPI_REV_OVERRIDE_POSSIBLE is not set -CONFIG_ACPI_DOCK=y -CONFIG_ACPI_IPMI=y -CONFIG_ACPI_PROCESSOR_AGGREGATOR=y -CONFIG_ACPI_SBS=y -CONFIG_ACPI_NFIT=y -CONFIG_ACPI_APEI=y -CONFIG_ACPI_APEI_GHES=y - -# --- CPU frequency --- -CONFIG_CPU_FREQ_STAT=y -CONFIG_CPU_FREQ_GOV_POWERSAVE=y -CONFIG_CPU_FREQ_GOV_USERSPACE=y -CONFIG_CPU_FREQ_GOV_ONDEMAND=y -CONFIG_CPU_FREQ_GOV_CONSERVATIVE=y -CONFIG_X86_PCC_CPUFREQ=y -CONFIG_X86_ACPI_CPUFREQ=y -CONFIG_X86_POWERNOW_K8=y -CONFIG_X86_P4_CLOCKMOD=y -CONFIG_CPU_IDLE_GOV_LADDER=y -CONFIG_INTEL_IDLE=y - -# --- KVM --- -CONFIG_KVM=m -CONFIG_KVM_INTEL=m -CONFIG_KVM_AMD=m - -# --- Networking --- -CONFIG_NET=y -CONFIG_PACKET=y -CONFIG_PACKET_DIAG=y -CONFIG_UNIX=y -CONFIG_UNIX_DIAG=y -CONFIG_XFRM_USER=m -CONFIG_XFRM_SUB_POLICY=y -CONFIG_XFRM_STATISTICS=y -CONFIG_NET_KEY=m -CONFIG_NET_KEY_MIGRATE=y -CONFIG_XDP_SOCKETS=y -CONFIG_INET=y -CONFIG_IP_MULTICAST=y -CONFIG_IP_ADVANCED_ROUTER=y -CONFIG_IP_FIB_TRIE_STATS=y -CONFIG_IP_MULTIPLE_TABLES=y -CONFIG_IP_ROUTE_MULTIPATH=y -CONFIG_IP_ROUTE_VERBOSE=y -CONFIG_IP_PNP=y -CONFIG_IP_PNP_DHCP=y -CONFIG_IP_PNP_BOOTP=y -CONFIG_NET_IPIP=y -CONFIG_NET_IPGRE_DEMUX=y -CONFIG_NET_IPGRE=m -CONFIG_NET_IPGRE_BROADCAST=y -CONFIG_IP_MROUTE=y -CONFIG_IP_MROUTE_MULTIPLE_TABLES=y -CONFIG_IP_PIMSM_V1=y -CONFIG_IP_PIMSM_V2=y -CONFIG_NET_IPVTI=m -CONFIG_NET_FOU_IP_TUNNELS=y -CONFIG_INET_AH=m -CONFIG_INET_ESP=m -CONFIG_INET_IPCOMP=m -CONFIG_INET_UDP_DIAG=y -CONFIG_TCP_MD5SIG=y -CONFIG_SYN_COOKIES=y -CONFIG_IPV6=y -CONFIG_IPV6_ROUTER_PREF=y -CONFIG_INET6_AH=m -CONFIG_INET6_ESP=m -CONFIG_INET6_IPCOMP=m -CONFIG_IPV6_MIP6=m -CONFIG_IPV6_ILA=m -CONFIG_IPV6_VTI=m -CONFIG_IPV6_SIT=m -CONFIG_IPV6_SIT_6RD=y -CONFIG_IPV6_GRE=m -CONFIG_IPV6_MULTIPLE_TABLES=y -CONFIG_IPV6_SUBTREES=y -CONFIG_NETLABEL=y -CONFIG_NETWORK_SECMARK=y -CONFIG_BRIDGE=y -CONFIG_BRIDGE_VLAN_FILTERING=y -CONFIG_VLAN_8021Q=y -CONFIG_BONDING=m -CONFIG_TUN=y -CONFIG_VETH=y -CONFIG_MACVLAN=y -CONFIG_MACVTAP=y -CONFIG_IPVLAN=y -CONFIG_VXLAN=y -CONFIG_GENEVE=m -CONFIG_NETCONSOLE=y -CONFIG_TAP=y -CONFIG_DUMMY=m -CONFIG_NLMON=y -CONFIG_IP_SCTP=m -CONFIG_L2TP=m -CONFIG_NET_TEAM=m -CONFIG_NET_TEAM_MODE_BROADCAST=m -CONFIG_NET_TEAM_MODE_ROUNDROBIN=m -CONFIG_NET_TEAM_MODE_RANDOM=m -CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=m -CONFIG_NET_TEAM_MODE_LOADBALANCE=m -CONFIG_OPENVSWITCH=m -CONFIG_VSOCKETS=m -CONFIG_VIRTIO_VSOCKETS=m -CONFIG_HYPERV_VSOCKETS=m -CONFIG_NETLINK_DIAG=y -CONFIG_MPLS_ROUTING=m -CONFIG_MPLS_IPTUNNEL=m -CONFIG_NET_SWITCHDEV=y -CONFIG_NET_9P=y -CONFIG_NET_9P_VIRTIO=y - -# --- Traffic control --- -CONFIG_NET_SCHED=y -CONFIG_NET_SCH_HTB=m -CONFIG_NET_SCH_HFSC=m -CONFIG_NET_SCH_PRIO=m -CONFIG_NET_SCH_MULTIQ=m -CONFIG_NET_SCH_RED=m -CONFIG_NET_SCH_SFB=m -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_SCH_TEQL=m -CONFIG_NET_SCH_TBF=m -CONFIG_NET_SCH_GRED=m -CONFIG_NET_SCH_NETEM=m -CONFIG_NET_SCH_DRR=m -CONFIG_NET_SCH_MQPRIO=m -CONFIG_NET_SCH_CHOKE=m -CONFIG_NET_SCH_QFQ=m -CONFIG_NET_SCH_INGRESS=m -CONFIG_NET_CLS_BASIC=y -CONFIG_NET_CLS_ROUTE4=y -CONFIG_NET_CLS_FW=y -CONFIG_NET_CLS_U32=y -CONFIG_CLS_U32_PERF=y -CONFIG_CLS_U32_MARK=y -CONFIG_NET_CLS_FLOW=y -CONFIG_NET_CLS_CGROUP=y -CONFIG_NET_CLS_BPF=y -CONFIG_NET_CLS_MATCHALL=y -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_CMP=y -CONFIG_NET_EMATCH_NBYTE=y -CONFIG_NET_EMATCH_U32=y -CONFIG_NET_EMATCH_META=y -CONFIG_NET_EMATCH_TEXT=y -CONFIG_NET_EMATCH_IPSET=y -CONFIG_NET_CLS_ACT=y -CONFIG_NET_ACT_POLICE=y -CONFIG_NET_ACT_GACT=y -CONFIG_GACT_PROB=y -CONFIG_NET_ACT_MIRRED=y -CONFIG_NET_ACT_IPT=y -CONFIG_NET_ACT_NAT=y -CONFIG_NET_ACT_PEDIT=y -CONFIG_NET_ACT_SIMP=y -CONFIG_NET_ACT_SKBEDIT=y -CONFIG_NET_ACT_CSUM=y -CONFIG_NET_ACT_BPF=y - -# --- Netfilter --- -CONFIG_NETFILTER=y -CONFIG_NETFILTER_ADVANCED=y -CONFIG_BRIDGE_NETFILTER=y -CONFIG_NF_CONNTRACK=y -CONFIG_NF_CONNTRACK_ZONES=y -CONFIG_NF_CONNTRACK_PROCFS=y -CONFIG_NF_CONNTRACK_EVENTS=y -CONFIG_NF_CONNTRACK_TIMEOUT=y -CONFIG_NF_CONNTRACK_TIMESTAMP=y -CONFIG_NF_CONNTRACK_AMANDA=y -CONFIG_NF_CONNTRACK_FTP=y -CONFIG_NF_CONNTRACK_H323=y -CONFIG_NF_CONNTRACK_IRC=y -CONFIG_NF_CONNTRACK_NETBIOS_NS=y -CONFIG_NF_CONNTRACK_SNMP=y -CONFIG_NF_CONNTRACK_PPTP=y -CONFIG_NF_CONNTRACK_SANE=y -CONFIG_NF_CONNTRACK_SIP=y -CONFIG_NF_CONNTRACK_TFTP=y -CONFIG_NF_CT_NETLINK=y -CONFIG_NF_CT_NETLINK_TIMEOUT=y -CONFIG_NF_CT_NETLINK_HELPER=y -CONFIG_NETFILTER_NETLINK_GLUE_CT=y -CONFIG_NF_NAT=y -CONFIG_NF_TABLES=y -CONFIG_NF_TABLES_INET=y -CONFIG_NF_TABLES_NETDEV=y -CONFIG_NFT_CT=y -CONFIG_NFT_CONNLIMIT=y -CONFIG_NFT_LOG=y -CONFIG_NFT_LIMIT=y -CONFIG_NFT_MASQ=y -CONFIG_NFT_REDIR=y -CONFIG_NFT_NAT=y -CONFIG_NFT_TUNNEL=y -CONFIG_NFT_QUEUE=y -CONFIG_NFT_REJECT=y -CONFIG_NFT_COMPAT=y -CONFIG_NFT_HASH=y -CONFIG_NFT_OSF=y -CONFIG_NFT_TPROXY=y -CONFIG_NFT_DUP_NETDEV=y -CONFIG_NFT_FWD_NETDEV=y -CONFIG_NETFILTER_XT_SET=y -CONFIG_NETFILTER_XT_TARGET_CHECKSUM=y -CONFIG_NETFILTER_XT_TARGET_CLASSIFY=y -CONFIG_NETFILTER_XT_TARGET_CONNMARK=y -CONFIG_NETFILTER_XT_TARGET_DSCP=y -CONFIG_NETFILTER_XT_TARGET_HMARK=y -CONFIG_NETFILTER_XT_TARGET_IDLETIMER=y -CONFIG_NETFILTER_XT_TARGET_LOG=y -CONFIG_NETFILTER_XT_TARGET_MARK=y -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y -CONFIG_NETFILTER_XT_TARGET_NFLOG=y -CONFIG_NETFILTER_XT_TARGET_NFQUEUE=y -CONFIG_NETFILTER_XT_TARGET_NOTRACK=y -CONFIG_NETFILTER_XT_TARGET_TEE=y -CONFIG_NETFILTER_XT_TARGET_TPROXY=y -CONFIG_NETFILTER_XT_TARGET_TRACE=y -CONFIG_NETFILTER_XT_TARGET_TCPMSS=y -CONFIG_NETFILTER_XT_TARGET_TCPOPTSTRIP=y -CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y -CONFIG_NETFILTER_XT_MATCH_BPF=y -CONFIG_NETFILTER_XT_MATCH_CGROUP=y -CONFIG_NETFILTER_XT_MATCH_CLUSTER=y -CONFIG_NETFILTER_XT_MATCH_COMMENT=y -CONFIG_NETFILTER_XT_MATCH_CONNBYTES=y -CONFIG_NETFILTER_XT_MATCH_CONNLABEL=y -CONFIG_NETFILTER_XT_MATCH_CONNLIMIT=y -CONFIG_NETFILTER_XT_MATCH_CONNMARK=y -CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y -CONFIG_NETFILTER_XT_MATCH_CPU=y -CONFIG_NETFILTER_XT_MATCH_DCCP=y -CONFIG_NETFILTER_XT_MATCH_DEVGROUP=y -CONFIG_NETFILTER_XT_MATCH_DSCP=y -CONFIG_NETFILTER_XT_MATCH_ESP=y -CONFIG_NETFILTER_XT_MATCH_HASHLIMIT=y -CONFIG_NETFILTER_XT_MATCH_HELPER=y -CONFIG_NETFILTER_XT_MATCH_IPCOMP=y -CONFIG_NETFILTER_XT_MATCH_IPRANGE=y -CONFIG_NETFILTER_XT_MATCH_IPVS=y -CONFIG_NETFILTER_XT_MATCH_L2TP=y -CONFIG_NETFILTER_XT_MATCH_LENGTH=y -CONFIG_NETFILTER_XT_MATCH_LIMIT=y -CONFIG_NETFILTER_XT_MATCH_MAC=y -CONFIG_NETFILTER_XT_MATCH_MARK=y -CONFIG_NETFILTER_XT_MATCH_MULTIPORT=y -CONFIG_NETFILTER_XT_MATCH_NFACCT=y -CONFIG_NETFILTER_XT_MATCH_OSF=y -CONFIG_NETFILTER_XT_MATCH_OWNER=y -CONFIG_NETFILTER_XT_MATCH_POLICY=y -CONFIG_NETFILTER_XT_MATCH_PHYSDEV=y -CONFIG_NETFILTER_XT_MATCH_PKTTYPE=y -CONFIG_NETFILTER_XT_MATCH_QUOTA=y -CONFIG_NETFILTER_XT_MATCH_RATEEST=y -CONFIG_NETFILTER_XT_MATCH_REALM=y -CONFIG_NETFILTER_XT_MATCH_RECENT=y -CONFIG_NETFILTER_XT_MATCH_SCTP=y -CONFIG_NETFILTER_XT_MATCH_SOCKET=y -CONFIG_NETFILTER_XT_MATCH_STATE=y -CONFIG_NETFILTER_XT_MATCH_STATISTIC=y -CONFIG_NETFILTER_XT_MATCH_STRING=y -CONFIG_NETFILTER_XT_MATCH_TCPMSS=y -CONFIG_NETFILTER_XT_MATCH_TIME=y -CONFIG_NETFILTER_XT_MATCH_U32=y -CONFIG_IP_SET=y -CONFIG_IP_SET_BITMAP_IP=y -CONFIG_IP_SET_BITMAP_IPMAC=y -CONFIG_IP_SET_BITMAP_PORT=y -CONFIG_IP_SET_HASH_IP=y -CONFIG_IP_SET_HASH_IPPORT=y -CONFIG_IP_SET_HASH_IPPORTIP=y -CONFIG_IP_SET_HASH_IPPORTNET=y -CONFIG_IP_SET_HASH_NET=y -CONFIG_IP_SET_HASH_NETPORT=y -CONFIG_IP_SET_HASH_NETIFACE=y -CONFIG_IP_SET_LIST_SET=y -CONFIG_IP_VS=y -CONFIG_IP_VS_IPV6=y -CONFIG_IP_VS_PROTO_TCP=y -CONFIG_IP_VS_PROTO_UDP=y -CONFIG_IP_VS_PROTO_ESP=y -CONFIG_IP_VS_PROTO_AH=y -CONFIG_IP_VS_PROTO_SCTP=y -CONFIG_IP_VS_RR=y -CONFIG_IP_VS_WRR=y -CONFIG_IP_VS_LC=y -CONFIG_IP_VS_WLC=y -CONFIG_IP_VS_FO=y -CONFIG_IP_VS_OVF=y -CONFIG_IP_VS_LBLC=y -CONFIG_IP_VS_LBLCR=y -CONFIG_IP_VS_DH=y -CONFIG_IP_VS_SH=y -CONFIG_IP_VS_MH=y -CONFIG_IP_VS_SED=y -CONFIG_IP_VS_NQ=y -CONFIG_IP_VS_FTP=y -CONFIG_NFT_DUP_IPV4=y -CONFIG_NF_TABLES_ARP=y -CONFIG_NF_LOG_ARP=y -CONFIG_IP_NF_IPTABLES=y -CONFIG_IP_NF_MATCH_AH=y -CONFIG_IP_NF_MATCH_ECN=y -CONFIG_IP_NF_MATCH_RPFILTER=y -CONFIG_IP_NF_MATCH_TTL=y -CONFIG_IP_NF_FILTER=y -CONFIG_IP_NF_TARGET_REJECT=y -CONFIG_IP_NF_TARGET_SYNPROXY=y -CONFIG_IP_NF_NAT=y -CONFIG_IP_NF_TARGET_MASQUERADE=y -CONFIG_IP_NF_TARGET_NETMAP=y -CONFIG_IP_NF_TARGET_REDIRECT=y -CONFIG_IP_NF_MANGLE=y -CONFIG_IP_NF_TARGET_ECN=y -CONFIG_IP_NF_TARGET_TTL=y -CONFIG_IP_NF_RAW=y -CONFIG_IP_NF_SECURITY=y -CONFIG_IP_NF_ARPTABLES=y -CONFIG_IP_NF_ARPFILTER=y -CONFIG_IP_NF_ARP_MANGLE=y -CONFIG_NFT_DUP_IPV6=y -CONFIG_IP6_NF_IPTABLES=y -CONFIG_IP6_NF_MATCH_AH=y -CONFIG_IP6_NF_MATCH_EUI64=y -CONFIG_IP6_NF_MATCH_FRAG=y -CONFIG_IP6_NF_MATCH_OPTS=y -CONFIG_IP6_NF_MATCH_HL=y -CONFIG_IP6_NF_MATCH_IPV6HEADER=y -CONFIG_IP6_NF_MATCH_MH=y -CONFIG_IP6_NF_MATCH_RPFILTER=y -CONFIG_IP6_NF_MATCH_RT=y -CONFIG_IP6_NF_TARGET_HL=y -CONFIG_IP6_NF_FILTER=y -CONFIG_IP6_NF_TARGET_REJECT=y -CONFIG_IP6_NF_TARGET_SYNPROXY=y -CONFIG_IP6_NF_MANGLE=y -CONFIG_IP6_NF_RAW=y -CONFIG_IP6_NF_SECURITY=y -CONFIG_IP6_NF_NAT=y -CONFIG_IP6_NF_TARGET_MASQUERADE=y -CONFIG_IP6_NF_TARGET_NPT=y -CONFIG_NF_TABLES_BRIDGE=y -CONFIG_NFT_BRIDGE_REJECT=y -CONFIG_BRIDGE_NF_EBTABLES=y -CONFIG_BRIDGE_EBT_BROUTE=y -CONFIG_BRIDGE_EBT_T_FILTER=y -CONFIG_BRIDGE_EBT_T_NAT=y -CONFIG_BRIDGE_EBT_802_3=y -CONFIG_BRIDGE_EBT_AMONG=y -CONFIG_BRIDGE_EBT_ARP=y -CONFIG_BRIDGE_EBT_IP=y -CONFIG_BRIDGE_EBT_IP6=y -CONFIG_BRIDGE_EBT_LIMIT=y -CONFIG_BRIDGE_EBT_MARK=y -CONFIG_BRIDGE_EBT_PKTTYPE=y -CONFIG_BRIDGE_EBT_STP=y -CONFIG_BRIDGE_EBT_VLAN=y -CONFIG_BRIDGE_EBT_ARPREPLY=y -CONFIG_BRIDGE_EBT_DNAT=y -CONFIG_BRIDGE_EBT_MARK_T=y -CONFIG_BRIDGE_EBT_REDIRECT=y -CONFIG_BRIDGE_EBT_SNAT=y -CONFIG_BRIDGE_EBT_LOG=y -CONFIG_BRIDGE_EBT_NFLOG=y - -# --- Block devices --- -CONFIG_BLK_DEV=y -CONFIG_BLK_DEV_INTEGRITY=y -CONFIG_BLK_DEV_THROTTLING=y -CONFIG_BLK_CGROUP_IOLATENCY=y -CONFIG_PARTITION_ADVANCED=y -CONFIG_LDM_PARTITION=y -CONFIG_CMDLINE_PARTITION=y -CONFIG_BLK_DEV_LOOP=y -CONFIG_BLK_DEV_DRBD=m -CONFIG_BLK_DEV_NBD=y -CONFIG_BLK_DEV_RAM=y -CONFIG_BLK_DEV_RAM_SIZE=65536 -CONFIG_ATA_OVER_ETH=m -CONFIG_BLK_DEV_NVME=y - -# --- SCSI --- -CONFIG_SCSI=y -# CONFIG_SCSI_PROC_FS is not set -CONFIG_BLK_DEV_SD=y -CONFIG_BLK_DEV_SR=y -CONFIG_CHR_DEV_SG=y -CONFIG_ISCSI_TCP=m -CONFIG_SCSI_HPSA=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=y -CONFIG_SCSI_MPI3MR=m -CONFIG_SCSI_SMARTPQI=m -CONFIG_VMWARE_PVSCSI=y -CONFIG_XEN_SCSI_FRONTEND=y -CONFIG_SCSI_VIRTIO=y - -# --- ATA / SATA --- -CONFIG_ATA=y -# CONFIG_ATA_VERBOSE_ERROR is not set -# CONFIG_SATA_PMP is not set -CONFIG_SATA_AHCI=y -CONFIG_ATA_PIIX=y -CONFIG_SATA_MV=y -CONFIG_SATA_NV=y -CONFIG_SATA_PROMISE=y -CONFIG_SATA_SIL=y -CONFIG_SATA_SIS=y -CONFIG_SATA_SVW=y -CONFIG_SATA_ULI=y -CONFIG_SATA_VIA=y -CONFIG_SATA_VITESSE=y -CONFIG_ATA_GENERIC=y - -# --- Device mapper / MD --- -CONFIG_MD=y -CONFIG_BLK_DEV_MD=y -CONFIG_MD_LINEAR=y -CONFIG_MD_RAID0=y -CONFIG_MD_RAID1=y -CONFIG_MD_RAID10=y -CONFIG_MD_RAID456=y -CONFIG_MD_MULTIPATH=y -CONFIG_BLK_DEV_DM=y -CONFIG_DM_CRYPT=y -CONFIG_DM_SNAPSHOT=y -CONFIG_DM_THIN_PROVISIONING=y -CONFIG_DM_MULTIPATH=m -CONFIG_DM_MULTIPATH_QL=m -CONFIG_DM_MULTIPATH_ST=m - -# --- Fusion --- -CONFIG_FUSION=y -CONFIG_FUSION_SPI=y - -# --- Network drivers (built-in for reliable boot) --- -CONFIG_NETDEVICES=y -# CONFIG_NET_VENDOR_3COM is not set -# CONFIG_NET_VENDOR_ADAPTEC is not set -# CONFIG_NET_VENDOR_AGERE is not set -# CONFIG_NET_VENDOR_ALACRITECH is not set -# CONFIG_NET_VENDOR_ALTEON is not set -# CONFIG_NET_VENDOR_AMD is not set -# CONFIG_NET_VENDOR_AQUANTIA is not set -# CONFIG_NET_VENDOR_ARC is not set -# CONFIG_NET_VENDOR_ATHEROS is not set -# CONFIG_NET_VENDOR_CADENCE is not set -# CONFIG_NET_VENDOR_CAVIUM is not set -# CONFIG_NET_VENDOR_CHELSIO is not set -# CONFIG_NET_VENDOR_CORTINA is not set -# CONFIG_NET_VENDOR_DEC is not set -# CONFIG_NET_VENDOR_DLINK is not set -# CONFIG_NET_VENDOR_EMULEX is not set -# CONFIG_NET_VENDOR_EZCHIP is not set -# CONFIG_NET_VENDOR_HUAWEI is not set -# CONFIG_NET_VENDOR_I825XX is not set -# CONFIG_NET_VENDOR_MARVELL is not set -# CONFIG_NET_VENDOR_MICREL is not set -# CONFIG_NET_VENDOR_MICROCHIP is not set -# CONFIG_NET_VENDOR_MYRI is not set -# CONFIG_NET_VENDOR_NI is not set -# CONFIG_NET_VENDOR_NATSEMI is not set -# CONFIG_NET_VENDOR_NETERION is not set -# CONFIG_NET_VENDOR_NVIDIA is not set -# CONFIG_NET_VENDOR_OKI is not set -# CONFIG_NET_VENDOR_PACKET_ENGINES is not set -# CONFIG_NET_VENDOR_QLOGIC is not set -# CONFIG_NET_VENDOR_BROCADE is not set -# CONFIG_NET_VENDOR_QUALCOMM is not set -# CONFIG_NET_VENDOR_RDC is not set -# CONFIG_NET_VENDOR_RENESAS is not set -# CONFIG_NET_VENDOR_ROCKER is not set -# CONFIG_NET_VENDOR_SAMSUNG is not set -# CONFIG_NET_VENDOR_SEEQ is not set -# CONFIG_NET_VENDOR_SILAN is not set -# CONFIG_NET_VENDOR_SIS is not set -# CONFIG_NET_VENDOR_SMSC is not set -# CONFIG_NET_VENDOR_SOCIONEXT is not set -# CONFIG_NET_VENDOR_STMICRO is not set -# CONFIG_NET_VENDOR_SUN is not set -# CONFIG_NET_VENDOR_SYNOPSYS is not set -# CONFIG_NET_VENDOR_TEHUTI is not set -# CONFIG_NET_VENDOR_TI is not set -# CONFIG_NET_VENDOR_VIA is not set -# CONFIG_NET_VENDOR_WIZNET is not set -CONFIG_E1000=y -CONFIG_E1000E=y -CONFIG_IGB=y -CONFIG_IGBVF=y -CONFIG_IXGBE=y -CONFIG_IXGBEVF=y -CONFIG_I40E=y -CONFIG_I40EVF=y -CONFIG_ICE=y -CONFIG_IGC=y -CONFIG_MLX4_EN=y -CONFIG_MLX5_CORE=y -CONFIG_MLX5_CORE_EN=y -CONFIG_BNXT=y -CONFIG_CNIC=m -CONFIG_TIGON3=y -CONFIG_BNX2X=y -CONFIG_R8169=y -CONFIG_8139CP=y -CONFIG_8139TOO=y -CONFIG_VMXNET3=y -CONFIG_HYPERV_NET=y -CONFIG_ENA_ETHERNET=m -CONFIG_GVE=m -CONFIG_NFP=m -CONFIG_IONIC=y -CONFIG_VIRTIO_NET=y - -# --- USB network --- -CONFIG_USB_CATC=m -CONFIG_USB_KAWETH=m -CONFIG_USB_PEGASUS=m -CONFIG_USB_RTL8150=y -CONFIG_USB_RTL8152=y -CONFIG_USB_LAN78XX=m -CONFIG_USB_USBNET=y -CONFIG_USB_NET_AX8817X=m -CONFIG_USB_NET_AX88179_178A=m -CONFIG_USB_NET_CDC_EEM=m -CONFIG_USB_NET_CDC_NCM=m -CONFIG_USB_NET_NET1080=m -CONFIG_USB_NET_CDC_SUBSET=m -CONFIG_USB_NET_ZAURUS=m - -# --- WAN --- -CONFIG_WAN=y -CONFIG_HDLC=m -CONFIG_HDLC_CISCO=m - -# --- PPP --- -CONFIG_PPP=m -CONFIG_PPP_BSDCOMP=m -CONFIG_PPP_DEFLATE=m -CONFIG_PPP_FILTER=y -CONFIG_PPP_MPPE=m -CONFIG_PPP_MULTILINK=y -CONFIG_PPPOE=m -CONFIG_PPTP=m -CONFIG_PPPOL2TP=m -CONFIG_PPP_ASYNC=m -CONFIG_PPP_SYNC_TTY=m - -# --- USB --- -CONFIG_USB_SUPPORT=y -CONFIG_USB=y -CONFIG_USB_XHCI_HCD=y -CONFIG_USB_EHCI_HCD=y -CONFIG_USB_OHCI_HCD=y -CONFIG_USB_UHCI_HCD=y -CONFIG_USB_XHCI_PCI=y -CONFIG_USB_UAS=y -CONFIG_USB_STORAGE=y - -# --- USB serial (BMC/console access) --- -CONFIG_USB_SERIAL=y -CONFIG_USB_SERIAL_CONSOLE=y -CONFIG_USB_SERIAL_GENERIC=y -CONFIG_USB_SERIAL_CH341=y -CONFIG_USB_SERIAL_CP210X=y -CONFIG_USB_SERIAL_FTDI_SIO=y -CONFIG_USB_SERIAL_PL2303=y -CONFIG_USB_SERIAL_TI=y -CONFIG_USB_SERIAL_OPTION=y - -# --- Input --- -CONFIG_INPUT_FF_MEMLESS=y -CONFIG_INPUT_SPARSEKMAP=y -CONFIG_INPUT_MOUSEDEV=y -CONFIG_INPUT_MOUSEDEV_PSAUX=y -CONFIG_INPUT_EVDEV=y -# CONFIG_INPUT_MOUSE is not set -CONFIG_INPUT_MISC=y -CONFIG_INPUT_PCSPKR=y -CONFIG_INPUT_ATLAS_BTNS=y -CONFIG_INPUT_UINPUT=y -CONFIG_SERIO_PCIPS2=y -CONFIG_SERIO_RAW=y -# CONFIG_LEGACY_PTYS is not set -# CONFIG_INPUT_JOYSTICK is not set -# CONFIG_INPUT_TOUCHSCREEN is not set - -# --- MMC --- -CONFIG_MMC=y -CONFIG_MMC_SDHCI=y -CONFIG_MMC_SDHCI_PCI=y -# CONFIG_MMC_RICOH_MMC is not set -CONFIG_MMC_SDHCI_ACPI=y -CONFIG_MMC_SDHCI_PLTFM=y - -# --- Filesystems --- -CONFIG_EXT4_FS=y -CONFIG_EXT4_FS_POSIX_ACL=y -CONFIG_EXT4_FS_SECURITY=y -CONFIG_XFS_FS=y -CONFIG_XFS_QUOTA=y -CONFIG_XFS_POSIX_ACL=y -CONFIG_BTRFS_FS=m -CONFIG_BTRFS_FS_POSIX_ACL=y -CONFIG_FS_DAX=y -CONFIG_FS_ENCRYPTION=y -CONFIG_FANOTIFY=y -CONFIG_QUOTA=y -CONFIG_QUOTA_NETLINK_INTERFACE=y -CONFIG_VFAT_FS=y -CONFIG_MSDOS_FS=y -CONFIG_FAT_DEFAULT_IOCHARSET="utf8" -CONFIG_NTFS_FS=m -CONFIG_TMPFS=y -CONFIG_TMPFS_POSIX_ACL=y -CONFIG_TMPFS_XATTR=y -CONFIG_PROC_FS=y -CONFIG_PROC_KCORE=y -CONFIG_SYSFS=y -CONFIG_EFIVAR_FS=y -CONFIG_OVERLAY_FS=y -CONFIG_SQUASHFS=y -CONFIG_SQUASHFS_XATTR=y -CONFIG_SQUASHFS_LZ4=y -CONFIG_SQUASHFS_LZO=y -CONFIG_SQUASHFS_XZ=y -CONFIG_FUSE_FS=y -CONFIG_CUSE=y -CONFIG_ISO9660_FS=y -CONFIG_JOLIET=y -CONFIG_ZISOFS=y -CONFIG_UDF_FS=y -CONFIG_FSCACHE=y -CONFIG_FSCACHE_STATS=y -CONFIG_CACHEFILES=y -CONFIG_NFS_FS=m -# CONFIG_NFS_V2 is not set -CONFIG_NFS_V4=m -CONFIG_NFS_V4_1=y -CONFIG_NFS_V4_2=y -CONFIG_NFS_FSCACHE=y -CONFIG_NFSD=m -CONFIG_NFSD_V4=y -CONFIG_CEPH_FS=m -CONFIG_CEPH_FSCACHE=y -CONFIG_CEPH_FS_POSIX_ACL=y -CONFIG_CIFS=y -# CONFIG_CIFS_ALLOW_INSECURE_LEGACY is not set -CONFIG_CIFS_XATTR=y -CONFIG_CIFS_DFS_UPCALL=y -CONFIG_CIFS_FSCACHE=y -CONFIG_9P_FS=y -CONFIG_9P_FSCACHE=y -CONFIG_9P_FS_POSIX_ACL=y -CONFIG_9P_FS_SECURITY=y - -# --- NLS --- -CONFIG_NLS_CODEPAGE_437=y -CONFIG_NLS_ASCII=y -CONFIG_NLS_ISO8859_1=y -CONFIG_NLS_UTF8=y - -# --- Virtio (built-in for reliable boot) --- -CONFIG_VIRTIO=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BLK=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_INPUT=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y -CONFIG_HW_RANDOM_VIRTIO=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m - -# --- Console / Serial --- -CONFIG_VT=y -CONFIG_VT_CONSOLE=y -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_SERIAL_8250_NR_UARTS=32 -# CONFIG_SERIAL_8250_MID is not set -CONFIG_SERIAL_DEV_BUS=y -CONFIG_TTY_PRINTK=y -CONFIG_HW_CONSOLE=y -CONFIG_VGA_CONSOLE=y -CONFIG_DUMMY_CONSOLE=y - -# --- IPMI --- -CONFIG_IPMI_HANDLER=y -CONFIG_IPMI_DMI_DECODE=y -CONFIG_IPMI_DEVICE_INTERFACE=y -CONFIG_IPMI_SI=y -CONFIG_IPMI_POWEROFF=y - -# --- Hardware RNG --- -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_TIMERIOMEM=y - -# --- I2C --- -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MUX=y - -# --- TPM --- -CONFIG_TCG_TIS_I2C_ATMEL=m -CONFIG_TCG_TIS_I2C_INFINEON=m -CONFIG_TCG_TIS_I2C_NUVOTON=m -CONFIG_TCG_NSC=m -CONFIG_TCG_ATMEL=m -CONFIG_TCG_INFINEON=m -CONFIG_TCG_XEN=m -CONFIG_TCG_VTPM_PROXY=m -CONFIG_TCG_TIS_ST33ZP24_I2C=m - -# --- Platform / MFD --- -CONFIG_NVRAM=y -CONFIG_HPET=y -CONFIG_HANGCHECK_TIMER=y -CONFIG_LPC_ICH=y -CONFIG_LPC_SCH=y -CONFIG_MFD_INTEL_LPSS_ACPI=y -CONFIG_MFD_INTEL_LPSS_PCI=y -CONFIG_MFD_SM501=y -CONFIG_MFD_VX855=y -# CONFIG_THERMAL_HWMON is not set - -# --- RTC --- -CONFIG_RTC_CLASS=y - -# --- DMA --- -CONFIG_DMADEVICES=y - -# --- Security --- -CONFIG_SECCOMP=y -CONFIG_SECCOMP_FILTER=y -CONFIG_SECURITY=y -CONFIG_SECURITY_NETWORK=y -CONFIG_SECURITY_NETWORK_XFRM=y -CONFIG_SECURITY_PATH=y -CONFIG_SECURITY_DMESG_RESTRICT=y -CONFIG_KEYS=y -CONFIG_PERSISTENT_KEYRINGS=y -CONFIG_TRUSTED_KEYS=y -CONFIG_KEY_DH_OPERATIONS=y -CONFIG_HARDENED_USERCOPY=y -CONFIG_FORTIFY_SOURCE=y -CONFIG_STATIC_USERMODEHELPER=y -CONFIG_SECURITY_YAMA=y -CONFIG_INTEGRITY_SIGNATURE=y -CONFIG_INTEGRITY_ASYMMETRIC_KEYS=y -CONFIG_IMA=y -CONFIG_IMA_DEFAULT_HASH_SHA256=y -CONFIG_IMA_READ_POLICY=y -CONFIG_IMA_APPRAISE=y -CONFIG_EVM=y -CONFIG_LSM="yama,loadpin,safesetid,integrity" - -# --- Crypto --- -CONFIG_CRYPTO=y -CONFIG_CRYPTO_USER=y -CONFIG_CRYPTO_AES=y -CONFIG_CRYPTO_SHA256=y -CONFIG_CRYPTO_SHA512=y -CONFIG_CRYPTO_XTS=y -CONFIG_CRYPTO_ANUBIS=y -CONFIG_CRYPTO_BLOWFISH=y -CONFIG_CRYPTO_CAMELLIA=y -CONFIG_CRYPTO_DES=y -CONFIG_CRYPTO_FCRYPT=y -CONFIG_CRYPTO_KHAZAD=y -CONFIG_CRYPTO_SEED=y -CONFIG_CRYPTO_TEA=y -CONFIG_CRYPTO_TWOFISH=y -CONFIG_CRYPTO_ARC4=y -CONFIG_CRYPTO_KEYWRAP=y -CONFIG_CRYPTO_LRW=y -CONFIG_CRYPTO_PCBC=y -CONFIG_CRYPTO_CHACHA20POLY1305=y -CONFIG_CRYPTO_SEQIV=y -CONFIG_CRYPTO_ECHAINIV=y -CONFIG_CRYPTO_MICHAEL_MIC=y -CONFIG_CRYPTO_RMD160=y -CONFIG_CRYPTO_VMAC=y -CONFIG_CRYPTO_WP512=y -CONFIG_CRYPTO_XCBC=y -CONFIG_CRYPTO_CRC32=y -CONFIG_CRYPTO_LZO=y -CONFIG_CRYPTO_842=y -CONFIG_CRYPTO_LZ4=y -CONFIG_CRYPTO_LZ4HC=y -CONFIG_CRYPTO_ANSI_CPRNG=y -CONFIG_CRYPTO_USER_API_HASH=y -CONFIG_CRYPTO_USER_API_SKCIPHER=y -CONFIG_CRYPTO_USER_API_RNG=y -CONFIG_CRYPTO_USER_API_AEAD=y - -# --- x86_64 hardware crypto acceleration --- -CONFIG_CRYPTO_AES_NI_INTEL=y -CONFIG_CRYPTO_BLOWFISH_X86_64=y -CONFIG_CRYPTO_CAMELLIA_AESNI_AVX2_X86_64=y -CONFIG_CRYPTO_CAST5_AVX_X86_64=y -CONFIG_CRYPTO_CAST6_AVX_X86_64=y -CONFIG_CRYPTO_DES3_EDE_X86_64=y -CONFIG_CRYPTO_SERPENT_SSE2_X86_64=y -CONFIG_CRYPTO_SERPENT_AVX2_X86_64=y -CONFIG_CRYPTO_TWOFISH_AVX_X86_64=y -CONFIG_CRYPTO_CHACHA20_X86_64=y -CONFIG_CRYPTO_POLY1305_X86_64=y -CONFIG_CRYPTO_SHA1_SSSE3=y -CONFIG_CRYPTO_SHA256_SSSE3=y -CONFIG_CRYPTO_SHA512_SSSE3=y -CONFIG_CRYPTO_GHASH_CLMUL_NI_INTEL=y -CONFIG_CRYPTO_CRC32C_INTEL=y -CONFIG_CRYPTO_CRC32_PCLMUL=y -CONFIG_CRYPTO_DEV_PADLOCK=y -CONFIG_CRYPTO_DEV_PADLOCK_AES=y -CONFIG_CRYPTO_DEV_PADLOCK_SHA=y -CONFIG_CRYPTO_DEV_VIRTIO=m -CONFIG_PKCS7_MESSAGE_PARSER=y - -# --- BPF --- -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y - -# --- PCI --- -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_HOTPLUG_PCI_PCIE=y -CONFIG_PCI_STUB=y -CONFIG_PCI_IOV=y -# CONFIG_VGA_ARB is not set -CONFIG_HOTPLUG_PCI=y -CONFIG_HOTPLUG_PCI_ACPI=y -CONFIG_HOTPLUG_PCI_SHPC=y -CONFIG_PCI_HYPERV_INTERFACE=m - -# --- IOMMU --- -CONFIG_AMD_IOMMU=y -CONFIG_INTEL_IOMMU=y -CONFIG_IRQ_REMAP=y - -# --- Xen --- -# CONFIG_XEN_BACKEND is not set -CONFIG_XEN_GNTDEV=y -CONFIG_XEN_GRANT_DEV_ALLOC=y -CONFIG_XEN_PVCALLS_FRONTEND=y -CONFIG_XEN_ACPI_PROCESSOR=y -# CONFIG_XEN_SYMS is not set - -# --- Intel platform --- -CONFIG_INTEL_IPS=y - -# --- DEVFREQ --- -CONFIG_PM_DEVFREQ=y -CONFIG_DEVFREQ_GOV_SIMPLE_ONDEMAND=y - -# --- Reset --- -CONFIG_RESET_CONTROLLER=y - -# --- DAX --- -CONFIG_DEV_DAX=y -CONFIG_DEV_DAX_PMEM=m - -# --- Misc --- -CONFIG_FW_LOADER=y -CONFIG_PRINTK=y -CONFIG_PRINTK_TIME=y -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_EARLY_PRINTK=y -CONFIG_PANIC_TIMEOUT=10 -CONFIG_PANIC_ON_OOPS=y -CONFIG_MAGIC_SYSRQ=y -CONFIG_UEVENT_HELPER=y -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_CONNECTOR=y -CONFIG_DMI_SYSFS=y -CONFIG_SYSFB_SIMPLEFB=y -CONFIG_RESET_ATTACK_MITIGATION=y -# CONFIG_PNP_DEBUG_MESSAGES is not set -CONFIG_HYPERV=y -CONFIG_HYPERV_UTILS=y -CONFIG_HYPERV_BALLOON=y - -# --- Module compression --- -CONFIG_MODULE_COMPRESS_ZSTD=y -CONFIG_MODULE_DECOMPRESS=y - -# --- EFI boot support --- -CONFIG_EFI=y -CONFIG_EFI_STUB=y -CONFIG_EFI_MIXED=y - -# --- Framebuffer/Console (required for EFI boot video output) --- -CONFIG_DRM=y -CONFIG_DRM_I915=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_FRAMEBUFFER_CONSOLE=y - -# --- Debugging / Diagnostics --- -CONFIG_FRAME_WARN=1024 -CONFIG_BUG_ON_DATA_CORRUPTION=y -CONFIG_HARDLOCKUP_DETECTOR=y -CONFIG_WQ_WATCHDOG=y -CONFIG_DEBUG_NOTIFIERS=y -CONFIG_RCU_CPU_STALL_TIMEOUT=60 -# CONFIG_RCU_TRACE is not set -CONFIG_IO_STRICT_DEVMEM=y - -# --- Disable unnecessary subsystems --- -# CONFIG_SOUND is not set -# CONFIG_WIRELESS is not set -# CONFIG_WLAN is not set -# CONFIG_BLUETOOTH is not set -# CONFIG_NFC is not set -# CONFIG_INFINIBAND is not set -# CONFIG_MEDIA_SUPPORT is not set diff --git a/kernel.configs/6.18.y.arm64 b/kernel.configs/6.18.y.arm64 deleted file mode 100644 index 795c91e6..00000000 --- a/kernel.configs/6.18.y.arm64 +++ /dev/null @@ -1,515 +0,0 @@ -# CaptainOS kernel defconfig for arm64 (aarch64) -# Minimal config for bare-metal provisioning with container/network support. -# Based on 6.12.y config with targeted additions for CaptainOS use case, adapted for kernel 6.18. -# -# Generate a full .config from this: -# cp 6.18.y.arm64 .config && make ARCH=arm64 olddefconfig - -# --- Architecture --- -CONFIG_ARM64=y -CONFIG_SMP=y -CONFIG_NR_CPUS=128 -CONFIG_NUMA=y -CONFIG_ARM64_VA_BITS_48=y -CONFIG_SCHED_MC=y -CONFIG_SCHED_SMT=y -CONFIG_HZ_1000=y -CONFIG_XEN=y -CONFIG_COMPAT=y -CONFIG_RANDOMIZE_BASE=y -CONFIG_ARM64_ACPI_PARKING_PROTOCOL=y - -# --- General --- -CONFIG_LOCALVERSION="-captainos" -CONFIG_DEFAULT_HOSTNAME="captainos" -CONFIG_SYSVIPC=y -CONFIG_POSIX_MQUEUE=y -CONFIG_AUDIT=y -CONFIG_NO_HZ_IDLE=y -CONFIG_HIGH_RES_TIMERS=y -CONFIG_PREEMPT_VOLUNTARY=y -CONFIG_BSD_PROCESS_ACCT=y -CONFIG_KEXEC=y -CONFIG_KEXEC_FILE=y -CONFIG_CC_OPTIMIZE_FOR_SIZE=y -CONFIG_CGROUPS=y -CONFIG_CGROUP_FREEZER=y -CONFIG_CGROUP_PIDS=y -CONFIG_CGROUP_DEVICE=y -CONFIG_CGROUP_CPUACCT=y -CONFIG_CGROUP_PERF=y -CONFIG_CGROUP_BPF=y -CONFIG_CGROUP_MISC=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CGROUP_RDMA=y -CONFIG_CPUSETS=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_BLK_CGROUP_IOLATENCY=y -CONFIG_BLK_DEV_THROTTLING=y -CONFIG_CFS_BANDWIDTH=y -CONFIG_RT_GROUP_SCHED=y -CONFIG_CGROUP_SCHED=y -CONFIG_CGROUP_NET_PRIO=y -CONFIG_NAMESPACES=y -CONFIG_USER_NS=y -CONFIG_PID_NS=y -CONFIG_NET_NS=y -CONFIG_UTS_NS=y -CONFIG_IPC_NS=y -CONFIG_CHECKPOINT_RESTORE=y -# Memory management -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_KSM=y -CONFIG_MEMORY_HOTPLUG=y -CONFIG_MEMORY_HOTREMOVE=y -CONFIG_HUGETLBFS=y - -# --- Init / Initramfs --- -CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="" - -# --- Networking --- -CONFIG_NET=y -CONFIG_PACKET=y -CONFIG_UNIX=y -CONFIG_INET=y -CONFIG_IP_MULTICAST=y -CONFIG_IP_ADVANCED_ROUTER=y -CONFIG_IP_MULTIPLE_TABLES=y -CONFIG_IP_ROUTE_MULTIPATH=y -CONFIG_IP_PNP=y -CONFIG_IP_PNP_DHCP=y -CONFIG_IP_PNP_BOOTP=y -CONFIG_NET_IPIP=m -CONFIG_NET_IPGRE_DEMUX=m -CONFIG_NET_IPGRE=m -CONFIG_SYN_COOKIES=y -CONFIG_IPV6=y -CONFIG_BRIDGE=y -CONFIG_VLAN_8021Q=m -CONFIG_BONDING=m -CONFIG_TUN=y -CONFIG_VETH=y -CONFIG_MACVLAN=y -CONFIG_MACVTAP=y -CONFIG_IPVLAN=y -CONFIG_TAP=y -CONFIG_DUMMY=m -CONFIG_VXLAN=y -CONFIG_GENEVE=m -CONFIG_NLMON=y -# Traffic control / QoS (needed by CNI plugins) -CONFIG_NET_SCHED=y -CONFIG_NET_SCH_HTB=m -CONFIG_NET_SCH_PRIO=m -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_SCH_TBF=m -CONFIG_NET_SCH_NETEM=m -CONFIG_NET_SCH_INGRESS=m -CONFIG_NET_CLS_BASIC=y -CONFIG_NET_CLS_FW=y -CONFIG_NET_CLS_U32=y -CONFIG_NET_CLS_CGROUP=y -CONFIG_NET_CLS_BPF=y -CONFIG_NET_CLS_MATCHALL=y -CONFIG_NET_CLS_ACT=y -CONFIG_NET_ACT_POLICE=y -CONFIG_NET_ACT_GACT=y -CONFIG_NET_ACT_MIRRED=y -CONFIG_NET_ACT_BPF=y -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_U32=y - -# --- Netfilter --- -CONFIG_NETFILTER=y -CONFIG_NETFILTER_ADVANCED=y -CONFIG_NF_CONNTRACK=y -CONFIG_NF_NAT=y -CONFIG_NF_TABLES=y -CONFIG_NF_TABLES_INET=y -CONFIG_NF_TABLES_IPV4=y -CONFIG_NF_TABLES_IPV6=y -CONFIG_NFT_NAT=y -CONFIG_NFT_MASQ=y -CONFIG_NFT_CT=y -CONFIG_NFT_COMPAT=y -CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y -CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y -CONFIG_NETFILTER_XT_MATCH_COMMENT=y -CONFIG_NETFILTER_XT_MATCH_MARK=y -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y -CONFIG_IP_NF_IPTABLES=y -CONFIG_IP_NF_FILTER=y -CONFIG_IP_NF_NAT=y -CONFIG_IP6_NF_IPTABLES=y -CONFIG_IP6_NF_FILTER=y -CONFIG_IP6_NF_NAT=y -CONFIG_BRIDGE_NETFILTER=y -CONFIG_BRIDGE_NF_EBTABLES=y -CONFIG_BRIDGE_EBT_BROUTE=y -CONFIG_BRIDGE_EBT_T_FILTER=y -CONFIG_BRIDGE_EBT_T_NAT=y -CONFIG_BRIDGE_EBT_ARP=y -CONFIG_BRIDGE_EBT_IP=y -CONFIG_BRIDGE_EBT_IP6=y -CONFIG_BRIDGE_EBT_MARK=y -CONFIG_BRIDGE_EBT_MARK_T=y -CONFIG_BRIDGE_EBT_VLAN=y -# IPVS (container load balancing / kube-proxy) -CONFIG_IP_VS=y -CONFIG_IP_VS_IPV6=y -CONFIG_IP_VS_PROTO_TCP=y -CONFIG_IP_VS_PROTO_UDP=y -CONFIG_IP_VS_RR=y -CONFIG_IP_VS_WRR=y -CONFIG_IP_VS_SH=y -CONFIG_IP_VS_NFCT=y -# IP sets (used by iptables/nftables for efficient matching) -CONFIG_IP_SET=y -CONFIG_IP_SET_HASH_IP=y -CONFIG_IP_SET_HASH_IPPORT=y -CONFIG_IP_SET_HASH_IPPORTNET=y -CONFIG_IP_SET_HASH_NET=y -CONFIG_IP_SET_HASH_NETPORT=y -CONFIG_IP_SET_BITMAP_IP=y -CONFIG_IP_SET_BITMAP_PORT=y -CONFIG_IP_SET_LIST_SET=y -CONFIG_NETFILTER_XT_SET=y - -# --- Block devices --- -CONFIG_BLK_DEV=y -CONFIG_BLK_DEV_LOOP=y -CONFIG_BLK_DEV_NBD=y -CONFIG_BLK_DEV_RAM=y -CONFIG_BLK_DEV_RAM_SIZE=65536 -CONFIG_BLK_DEV_NVME=y -CONFIG_ATA=y -CONFIG_SATA_AHCI=y -CONFIG_SATA_AHCI_PLATFORM=y - -# --- SCSI --- -CONFIG_SCSI=y -CONFIG_BLK_DEV_SD=y -CONFIG_CHR_DEV_SG=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=y -CONFIG_SCSI_MPI3MR=m -CONFIG_SCSI_HPSA=y -CONFIG_SCSI_HISI_SAS=y -CONFIG_SCSI_HISI_SAS_PCI=y -CONFIG_XEN_SCSI_FRONTEND=y -CONFIG_SCSI_VIRTIO=y - -# --- Device mapper --- -CONFIG_MD=y -CONFIG_BLK_DEV_DM=y -CONFIG_DM_CRYPT=y -CONFIG_DM_THIN_PROVISIONING=y -CONFIG_DM_SNAPSHOT=y -CONFIG_DM_MULTIPATH=m -CONFIG_MD_RAID0=m -CONFIG_MD_RAID1=m - -# --- Network drivers (built-in for reliable boot) --- -CONFIG_NETDEVICES=y -CONFIG_VIRTIO_NET=y -CONFIG_E1000E=y -CONFIG_IGB=y -CONFIG_IXGBE=y -CONFIG_I40E=y -CONFIG_ICE=y -CONFIG_IGC=y -CONFIG_MLX4_EN=y -CONFIG_MLX5_CORE=y -CONFIG_MLX5_CORE_EN=y -CONFIG_BNXT=y -CONFIG_TIGON3=y -CONFIG_R8169=y -CONFIG_THUNDER_NIC_BGX=m -CONFIG_THUNDER_NIC_PF=m -CONFIG_THUNDER_NIC_VF=m -CONFIG_MACB=m -CONFIG_HNS3=m -CONFIG_NET_XGENE=y -CONFIG_MVNETA=y -CONFIG_MVPP2=y -CONFIG_STMMAC_ETH=m -CONFIG_ENA_ETHERNET=m -CONFIG_GVE=m -CONFIG_VMXNET3=y -CONFIG_AMD_XGBE=y -# Broadcom GENET (Raspberry Pi 4B built-in Ethernet) -CONFIG_BCMGENET=y -# PHY subsystem (required by GENET and other MAC drivers) -CONFIG_PHYLIB=y -CONFIG_MDIO_BUS=y -CONFIG_BROADCOM_PHY=y -CONFIG_BCM7XXX_PHY=y -CONFIG_MDIO_BCM_UNIMAC=y -CONFIG_MARVELL_PHY=y -CONFIG_MARVELL_10G_PHY=y -CONFIG_MICREL_PHY=y -CONFIG_ROCKCHIP_PHY=y -CONFIG_REALTEK_PHY=y -CONFIG_MESON_GXL_PHY=y -CONFIG_AMD_PHY=y - -# --- USB --- -CONFIG_USB_SUPPORT=y -CONFIG_USB=y -CONFIG_USB_XHCI_HCD=y -CONFIG_USB_EHCI_HCD=y -CONFIG_USB_OHCI_HCD=y -CONFIG_USB_XHCI_PCI=y -CONFIG_USB_UAS=y -CONFIG_USB_STORAGE=y -CONFIG_USB_DWC3=y -CONFIG_USB_DWC3_HOST=y -CONFIG_USB_DWC2=y -CONFIG_USB_ISP1760=y - -# --- MMC/SD (Raspberry Pi, Rockchip, and other SBCs) --- -CONFIG_MMC=y -CONFIG_MMC_BLOCK_MINORS=32 -CONFIG_MMC_SDHCI=y -CONFIG_MMC_SDHCI_PCI=y -CONFIG_MMC_SDHCI_ACPI=y -CONFIG_MMC_SDHCI_PLTFM=y -CONFIG_MMC_SDHCI_OF_ARASAN=y -CONFIG_MMC_SDHCI_OF_DWCMSHC=y -CONFIG_MMC_SDHCI_CADENCE=y -CONFIG_MMC_SDHCI_TEGRA=y -CONFIG_MMC_SDHCI_MSM=y -CONFIG_MMC_SDHCI_XENON=y -CONFIG_MMC_SDHCI_F_SDH30=y -CONFIG_MMC_ARMMMCI=y -CONFIG_MMC_DW=y -CONFIG_MMC_DW_ROCKCHIP=y -CONFIG_MMC_DW_K3=y -CONFIG_MMC_MESON_GX=y -CONFIG_MMC_SUNXI=m -CONFIG_MMC_SPI=y -CONFIG_MMC_HSQ=y - -# --- Filesystems --- -CONFIG_EXT4_FS=y -CONFIG_XFS_FS=m -CONFIG_VFAT_FS=y -CONFIG_TMPFS=y -CONFIG_TMPFS_POSIX_ACL=y -CONFIG_PROC_FS=y -CONFIG_SYSFS=y -CONFIG_OVERLAY_FS=y -CONFIG_SQUASHFS=y -CONFIG_SQUASHFS_XATTR=y -CONFIG_SQUASHFS_LZ4=y -CONFIG_SQUASHFS_LZO=y -CONFIG_SQUASHFS_XZ=y -CONFIG_NFS_FS=m -CONFIG_NFS_V4=m -CONFIG_NFS_V4_1=y -CONFIG_NFS_V4_2=y -CONFIG_FUSE_FS=y -CONFIG_CUSE=y -CONFIG_EFIVAR_FS=y -CONFIG_ISO9660_FS=y -CONFIG_JOLIET=y -CONFIG_UDF_FS=y -CONFIG_MSDOS_FS=y -CONFIG_FAT_DEFAULT_IOCHARSET="utf8" -CONFIG_NTFS_FS=m -CONFIG_TMPFS_XATTR=y -CONFIG_NLS_CODEPAGE_437=y -CONFIG_NLS_ASCII=y -CONFIG_NLS_ISO8859_1=y -CONFIG_NLS_UTF8=y - -# --- Virtio (built-in for reliable boot) --- -CONFIG_VIRTIO=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BLK=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_INPUT=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y -CONFIG_HW_RANDOM_VIRTIO=y - -# --- Console / Serial --- -CONFIG_SERIAL_AMBA_PL011=y -CONFIG_SERIAL_AMBA_PL011_CONSOLE=y -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_VT=y -CONFIG_VT_CONSOLE=y -CONFIG_HW_CONSOLE=y -CONFIG_DUMMY_CONSOLE=y - -# --- Security --- -CONFIG_SECCOMP=y -CONFIG_SECCOMP_FILTER=y -CONFIG_SECURITY=y -CONFIG_KEYS=y - -# --- Crypto --- -CONFIG_CRYPTO=y -CONFIG_CRYPTO_AES=y -CONFIG_CRYPTO_SHA256=y -CONFIG_CRYPTO_SHA512=y -CONFIG_CRYPTO_XTS=y -CONFIG_ARM64_CRYPTO=y -CONFIG_CRYPTO_AES_ARM64_CE=y -CONFIG_CRYPTO_SHA2_ARM64_CE=y - -# --- BPF --- -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y -CONFIG_XDP_SOCKETS=y - -# --- ARM64 SoC/platform support --- -CONFIG_ARCH_BCM2835=y -CONFIG_ARCH_ROCKCHIP=y -CONFIG_ARCH_MESON=y -CONFIG_ARCH_SUNXI=y -CONFIG_ARCH_TEGRA=y -CONFIG_ARCH_MVEBU=y -CONFIG_ARCH_HISI=y -CONFIG_ARCH_QCOM=y -CONFIG_ARCH_EXYNOS=y -CONFIG_ARCH_VEXPRESS=y -CONFIG_ARCH_XGENE=y -CONFIG_ARCH_THUNDER=y -CONFIG_ARCH_THUNDER2=y -CONFIG_ARCH_SEATTLE=y -CONFIG_ARCH_SYNQUACER=y -CONFIG_ARCH_UNIPHIER=y -# ACPI / UEFI -CONFIG_ACPI=y -CONFIG_ACPI_DOCK=y -CONFIG_ACPI_IPMI=y -CONFIG_ACPI_APEI=y -CONFIG_ACPI_APEI_GHES=y -CONFIG_IPMI_HANDLER=m -CONFIG_IPMI_DMI_DECODE=y -CONFIG_IPMI_DEVICE_INTERFACE=m -CONFIG_IPMI_SI=m -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_HOTPLUG_PCI=y -CONFIG_PCI_HOST_GENERIC=y -CONFIG_PCIE_ROCKCHIP_HOST=y -CONFIG_PCIE_ARMADA_8K=y -CONFIG_PCIE_TEGRA194_HOST=y -# Broadcom STB PCIe controller (Raspberry Pi 4B — needed for VL805 USB 3.0) -CONFIG_PCIE_BRCMSTB=y -CONFIG_PCIE_ALTERA=y -CONFIG_PCI_AARDVARK=y -CONFIG_PCI_TEGRA=y -CONFIG_PCI_XGENE=y -CONFIG_PCI_HOST_THUNDER_PEM=y -CONFIG_PCI_HOST_THUNDER_ECAM=y -CONFIG_PCIE_QCOM=y -CONFIG_PCIE_AL=y -CONFIG_PCI_MESON=y -CONFIG_PCI_HISI=y -CONFIG_PCIE_KIRIN=y -CONFIG_PCIE_HISI_STB=y -CONFIG_PCI_IOV=y -CONFIG_PCI_PASID=y -CONFIG_HOTPLUG_PCI_ACPI=y -# IOMMU -CONFIG_ARM_SMMU=y -CONFIG_ARM_SMMU_V3=y -CONFIG_FW_LOADER=y -# Raspberry Pi firmware mailbox (required for GENET MAC address, clocks, etc.) -CONFIG_RASPBERRYPI_FIRMWARE=y -CONFIG_RASPBERRYPI_POWER=y -CONFIG_ARM_SCMI_PROTOCOL=y -CONFIG_ARM_SCPI_PROTOCOL=y -CONFIG_OF=y -CONFIG_USE_OF=y -# RTC -CONFIG_RTC_CLASS=y -CONFIG_RTC_DRV_EFI=y -# DMA -CONFIG_DMADEVICES=y -CONFIG_PL330_DMA=y -CONFIG_PRINTK=y -CONFIG_PRINTK_TIME=y -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_PANIC_TIMEOUT=10 -CONFIG_PANIC_ON_OOPS=y -CONFIG_MAGIC_SYSRQ=y -CONFIG_RD_GZIP=y -CONFIG_RD_XZ=y -CONFIG_RD_ZSTD=y -CONFIG_UEVENT_HELPER=y -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_CONNECTOR=y -CONFIG_DMI_SYSFS=y -CONFIG_EFI=y -CONFIG_EFI_STUB=y - -# --- Framebuffer/Console (required for EFI boot video output) --- -CONFIG_DRM=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_DRM_VIRTIO_GPU=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_FRAMEBUFFER_CONSOLE=y -CONFIG_SYSFB_SIMPLEFB=y -# Virtualization -CONFIG_VIRTUALIZATION=y -CONFIG_KVM=y -CONFIG_VFIO=y -CONFIG_VFIO_PCI=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m - -# --- Module compression --- -CONFIG_MODULE_COMPRESS_ZSTD=y -CONFIG_MODULE_DECOMPRESS=y - -# --- I2C (SBC board management, EEPROM, sensors) --- -CONFIG_I2C=y -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MV64XXX=y -CONFIG_I2C_RK3X=y -CONFIG_I2C_TEGRA=y -CONFIG_I2C_DESIGNWARE_PLATFORM=y -# SPI -CONFIG_SPI=y -CONFIG_SPI_ROCKCHIP=y -CONFIG_SPI_PL022=y -CONFIG_SPI_ORION=y -# GPIO -CONFIG_GPIOLIB=y -# Hardware watchdog -CONFIG_WATCHDOG=y -CONFIG_SOFTLOCKUP_DETECTOR=y -CONFIG_WQ_WATCHDOG=y -# Hardware RNG -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_TIMERIOMEM=y -# TPM -CONFIG_TCG_TIS=y -CONFIG_TCG_FTPM_TEE=y -CONFIG_TEE=y -CONFIG_OPTEE=y - -# --- Disable unnecessary subsystems --- -# CONFIG_SOUND is not set -# CONFIG_WIRELESS is not set -# CONFIG_WLAN is not set -# CONFIG_BLUETOOTH is not set -# CONFIG_NFC is not set -# CONFIG_INFINIBAND is not set -# CONFIG_MEDIA_SUPPORT is not set -CONFIG_INPUT_EVDEV=y -# CONFIG_INPUT_JOYSTICK is not set -# CONFIG_INPUT_TOUCHSCREEN is not set diff --git a/kernel.configs/6.19.y.amd64 b/kernel.configs/6.19.y.amd64 deleted file mode 100644 index c6d243cc..00000000 --- a/kernel.configs/6.19.y.amd64 +++ /dev/null @@ -1,1032 +0,0 @@ -# CaptainOS kernel defconfig for x86_64 -# Comprehensive config for bare-metal provisioning with container/network support. -# Derived from HookOS generic-6.6.y-x86_64 proven config, adapted for kernel 6.18. -# -# Generate a full .config from this: -# cp 6.18.y.amd64 .config && make olddefconfig - -# --- Architecture --- -CONFIG_64BIT=y -CONFIG_X86_64=y -CONFIG_SMP=y -CONFIG_NR_CPUS=128 -# CONFIG_X86_EXTENDED_PLATFORM is not set -CONFIG_X86_INTEL_LPSS=y -CONFIG_HYPERVISOR_GUEST=y -CONFIG_PARAVIRT_SPINLOCKS=y -CONFIG_XEN=y -CONFIG_XEN_PVH=y -CONFIG_X86_REROUTE_FOR_BROKEN_BOOT_IRQS=y -# CONFIG_X86_MCE is not set -CONFIG_X86_MSR=y -CONFIG_X86_CPUID=y -# CONFIG_X86_5LEVEL is not set -CONFIG_HZ_1000=y -CONFIG_PHYSICAL_ALIGN=0x1000000 -CONFIG_LEGACY_VSYSCALL_NONE=y -# CONFIG_MODIFY_LDT_SYSCALL is not set -CONFIG_IA32_EMULATION=y - -# --- General --- -CONFIG_LOCALVERSION="-captainos" -# CONFIG_LOCALVERSION_AUTO is not set -CONFIG_DEFAULT_HOSTNAME="captainos" -CONFIG_SYSVIPC=y -CONFIG_POSIX_MQUEUE=y -CONFIG_AUDIT=y -CONFIG_NO_HZ_IDLE=y -CONFIG_HIGH_RES_TIMERS=y -CONFIG_PREEMPT_VOLUNTARY=y -CONFIG_BSD_PROCESS_ACCT=y -CONFIG_BSD_PROCESS_ACCT_V3=y -CONFIG_IKCONFIG=y -CONFIG_IKCONFIG_PROC=y -CONFIG_CC_OPTIMIZE_FOR_SIZE=y -CONFIG_EXPERT=y -CONFIG_KEXEC=y -CONFIG_KEXEC_FILE=y -CONFIG_SCHED_AUTOGROUP=y -CONFIG_CHECKPOINT_RESTORE=y -CONFIG_KPROBES=y -CONFIG_JUMP_LABEL=y -CONFIG_BINFMT_MISC=y - -# --- Cgroups --- -CONFIG_CGROUPS=y -CONFIG_CGROUP_FREEZER=y -CONFIG_CGROUP_PIDS=y -CONFIG_CGROUP_RDMA=y -CONFIG_CGROUP_DEVICE=y -CONFIG_CGROUP_CPUACCT=y -CONFIG_CGROUP_PERF=y -CONFIG_CGROUP_BPF=y -CONFIG_CGROUP_MISC=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CGROUP_NET_PRIO=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_CFS_BANDWIDTH=y -CONFIG_RT_GROUP_SCHED=y -CONFIG_CGROUP_SCHED=y -CONFIG_CPUSETS=y - -# --- Namespaces --- -CONFIG_NAMESPACES=y -CONFIG_USER_NS=y -CONFIG_PID_NS=y -CONFIG_NET_NS=y -CONFIG_UTS_NS=y -CONFIG_IPC_NS=y - -# --- Init / Initramfs --- -CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="" -# CONFIG_RD_BZIP2 is not set -# CONFIG_RD_LZMA is not set -CONFIG_RD_GZIP=y -CONFIG_RD_XZ=y -# CONFIG_RD_LZO is not set -# CONFIG_RD_LZ4 is not set -CONFIG_RD_ZSTD=y - -# --- Memory management --- -CONFIG_SLAB_FREELIST_RANDOM=y -# CONFIG_COMPAT_BRK is not set -CONFIG_MEMORY_HOTPLUG=y -CONFIG_MEMORY_HOTREMOVE=y -CONFIG_KSM=y -CONFIG_DEFAULT_MMAP_MIN_ADDR=65536 -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_ZONE_DEVICE=y -CONFIG_HUGETLBFS=y - -# --- Power / Suspend --- -# CONFIG_SUSPEND is not set - -# --- ACPI --- -CONFIG_ACPI=y -# CONFIG_ACPI_REV_OVERRIDE_POSSIBLE is not set -CONFIG_ACPI_DOCK=y -CONFIG_ACPI_IPMI=y -CONFIG_ACPI_PROCESSOR_AGGREGATOR=y -CONFIG_ACPI_SBS=y -CONFIG_ACPI_NFIT=y -CONFIG_ACPI_APEI=y -CONFIG_ACPI_APEI_GHES=y - -# --- CPU frequency --- -CONFIG_CPU_FREQ_STAT=y -CONFIG_CPU_FREQ_GOV_POWERSAVE=y -CONFIG_CPU_FREQ_GOV_USERSPACE=y -CONFIG_CPU_FREQ_GOV_ONDEMAND=y -CONFIG_CPU_FREQ_GOV_CONSERVATIVE=y -CONFIG_X86_PCC_CPUFREQ=y -CONFIG_X86_ACPI_CPUFREQ=y -CONFIG_X86_POWERNOW_K8=y -CONFIG_X86_P4_CLOCKMOD=y -CONFIG_CPU_IDLE_GOV_LADDER=y -CONFIG_INTEL_IDLE=y - -# --- KVM --- -CONFIG_KVM=m -CONFIG_KVM_INTEL=m -CONFIG_KVM_AMD=m - -# --- Networking --- -CONFIG_NET=y -CONFIG_PACKET=y -CONFIG_PACKET_DIAG=y -CONFIG_UNIX=y -CONFIG_UNIX_DIAG=y -CONFIG_XFRM_USER=m -CONFIG_XFRM_SUB_POLICY=y -CONFIG_XFRM_STATISTICS=y -CONFIG_NET_KEY=m -CONFIG_NET_KEY_MIGRATE=y -CONFIG_XDP_SOCKETS=y -CONFIG_INET=y -CONFIG_IP_MULTICAST=y -CONFIG_IP_ADVANCED_ROUTER=y -CONFIG_IP_FIB_TRIE_STATS=y -CONFIG_IP_MULTIPLE_TABLES=y -CONFIG_IP_ROUTE_MULTIPATH=y -CONFIG_IP_ROUTE_VERBOSE=y -CONFIG_IP_PNP=y -CONFIG_IP_PNP_DHCP=y -CONFIG_IP_PNP_BOOTP=y -CONFIG_NET_IPIP=y -CONFIG_NET_IPGRE_DEMUX=y -CONFIG_NET_IPGRE=m -CONFIG_NET_IPGRE_BROADCAST=y -CONFIG_IP_MROUTE=y -CONFIG_IP_MROUTE_MULTIPLE_TABLES=y -CONFIG_IP_PIMSM_V1=y -CONFIG_IP_PIMSM_V2=y -CONFIG_NET_IPVTI=m -CONFIG_NET_FOU_IP_TUNNELS=y -CONFIG_INET_AH=m -CONFIG_INET_ESP=m -CONFIG_INET_IPCOMP=m -CONFIG_INET_UDP_DIAG=y -CONFIG_TCP_MD5SIG=y -CONFIG_SYN_COOKIES=y -CONFIG_IPV6=y -CONFIG_IPV6_ROUTER_PREF=y -CONFIG_INET6_AH=m -CONFIG_INET6_ESP=m -CONFIG_INET6_IPCOMP=m -CONFIG_IPV6_MIP6=m -CONFIG_IPV6_ILA=m -CONFIG_IPV6_VTI=m -CONFIG_IPV6_SIT=m -CONFIG_IPV6_SIT_6RD=y -CONFIG_IPV6_GRE=m -CONFIG_IPV6_MULTIPLE_TABLES=y -CONFIG_IPV6_SUBTREES=y -CONFIG_NETLABEL=y -CONFIG_NETWORK_SECMARK=y -CONFIG_BRIDGE=y -CONFIG_BRIDGE_VLAN_FILTERING=y -CONFIG_VLAN_8021Q=y -CONFIG_BONDING=m -CONFIG_TUN=y -CONFIG_VETH=y -CONFIG_MACVLAN=y -CONFIG_MACVTAP=y -CONFIG_IPVLAN=y -CONFIG_VXLAN=y -CONFIG_GENEVE=m -CONFIG_NETCONSOLE=y -CONFIG_TAP=y -CONFIG_DUMMY=m -CONFIG_NLMON=y -CONFIG_IP_SCTP=m -CONFIG_L2TP=m -CONFIG_NET_TEAM=m -CONFIG_NET_TEAM_MODE_BROADCAST=m -CONFIG_NET_TEAM_MODE_ROUNDROBIN=m -CONFIG_NET_TEAM_MODE_RANDOM=m -CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=m -CONFIG_NET_TEAM_MODE_LOADBALANCE=m -CONFIG_OPENVSWITCH=m -CONFIG_VSOCKETS=m -CONFIG_VIRTIO_VSOCKETS=m -CONFIG_HYPERV_VSOCKETS=m -CONFIG_NETLINK_DIAG=y -CONFIG_MPLS_ROUTING=m -CONFIG_MPLS_IPTUNNEL=m -CONFIG_NET_SWITCHDEV=y -CONFIG_NET_9P=y -CONFIG_NET_9P_VIRTIO=y - -# --- Traffic control --- -CONFIG_NET_SCHED=y -CONFIG_NET_SCH_HTB=m -CONFIG_NET_SCH_HFSC=m -CONFIG_NET_SCH_PRIO=m -CONFIG_NET_SCH_MULTIQ=m -CONFIG_NET_SCH_RED=m -CONFIG_NET_SCH_SFB=m -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_SCH_TEQL=m -CONFIG_NET_SCH_TBF=m -CONFIG_NET_SCH_GRED=m -CONFIG_NET_SCH_NETEM=m -CONFIG_NET_SCH_DRR=m -CONFIG_NET_SCH_MQPRIO=m -CONFIG_NET_SCH_CHOKE=m -CONFIG_NET_SCH_QFQ=m -CONFIG_NET_SCH_INGRESS=m -CONFIG_NET_CLS_BASIC=y -CONFIG_NET_CLS_ROUTE4=y -CONFIG_NET_CLS_FW=y -CONFIG_NET_CLS_U32=y -CONFIG_CLS_U32_PERF=y -CONFIG_CLS_U32_MARK=y -CONFIG_NET_CLS_FLOW=y -CONFIG_NET_CLS_CGROUP=y -CONFIG_NET_CLS_BPF=y -CONFIG_NET_CLS_MATCHALL=y -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_CMP=y -CONFIG_NET_EMATCH_NBYTE=y -CONFIG_NET_EMATCH_U32=y -CONFIG_NET_EMATCH_META=y -CONFIG_NET_EMATCH_TEXT=y -CONFIG_NET_EMATCH_IPSET=y -CONFIG_NET_CLS_ACT=y -CONFIG_NET_ACT_POLICE=y -CONFIG_NET_ACT_GACT=y -CONFIG_GACT_PROB=y -CONFIG_NET_ACT_MIRRED=y -CONFIG_NET_ACT_IPT=y -CONFIG_NET_ACT_NAT=y -CONFIG_NET_ACT_PEDIT=y -CONFIG_NET_ACT_SIMP=y -CONFIG_NET_ACT_SKBEDIT=y -CONFIG_NET_ACT_CSUM=y -CONFIG_NET_ACT_BPF=y - -# --- Netfilter --- -CONFIG_NETFILTER=y -CONFIG_NETFILTER_ADVANCED=y -CONFIG_BRIDGE_NETFILTER=y -CONFIG_NF_CONNTRACK=y -CONFIG_NF_CONNTRACK_ZONES=y -CONFIG_NF_CONNTRACK_PROCFS=y -CONFIG_NF_CONNTRACK_EVENTS=y -CONFIG_NF_CONNTRACK_TIMEOUT=y -CONFIG_NF_CONNTRACK_TIMESTAMP=y -CONFIG_NF_CONNTRACK_AMANDA=y -CONFIG_NF_CONNTRACK_FTP=y -CONFIG_NF_CONNTRACK_H323=y -CONFIG_NF_CONNTRACK_IRC=y -CONFIG_NF_CONNTRACK_NETBIOS_NS=y -CONFIG_NF_CONNTRACK_SNMP=y -CONFIG_NF_CONNTRACK_PPTP=y -CONFIG_NF_CONNTRACK_SANE=y -CONFIG_NF_CONNTRACK_SIP=y -CONFIG_NF_CONNTRACK_TFTP=y -CONFIG_NF_CT_NETLINK=y -CONFIG_NF_CT_NETLINK_TIMEOUT=y -CONFIG_NF_CT_NETLINK_HELPER=y -CONFIG_NETFILTER_NETLINK_GLUE_CT=y -CONFIG_NF_NAT=y -CONFIG_NF_TABLES=y -CONFIG_NF_TABLES_INET=y -CONFIG_NF_TABLES_NETDEV=y -CONFIG_NFT_CT=y -CONFIG_NFT_CONNLIMIT=y -CONFIG_NFT_LOG=y -CONFIG_NFT_LIMIT=y -CONFIG_NFT_MASQ=y -CONFIG_NFT_REDIR=y -CONFIG_NFT_NAT=y -CONFIG_NFT_TUNNEL=y -CONFIG_NFT_QUEUE=y -CONFIG_NFT_REJECT=y -CONFIG_NFT_COMPAT=y -CONFIG_NFT_HASH=y -CONFIG_NFT_OSF=y -CONFIG_NFT_TPROXY=y -CONFIG_NFT_DUP_NETDEV=y -CONFIG_NFT_FWD_NETDEV=y -CONFIG_NETFILTER_XT_SET=y -CONFIG_NETFILTER_XT_TARGET_CHECKSUM=y -CONFIG_NETFILTER_XT_TARGET_CLASSIFY=y -CONFIG_NETFILTER_XT_TARGET_CONNMARK=y -CONFIG_NETFILTER_XT_TARGET_DSCP=y -CONFIG_NETFILTER_XT_TARGET_HMARK=y -CONFIG_NETFILTER_XT_TARGET_IDLETIMER=y -CONFIG_NETFILTER_XT_TARGET_LOG=y -CONFIG_NETFILTER_XT_TARGET_MARK=y -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y -CONFIG_NETFILTER_XT_TARGET_NFLOG=y -CONFIG_NETFILTER_XT_TARGET_NFQUEUE=y -CONFIG_NETFILTER_XT_TARGET_NOTRACK=y -CONFIG_NETFILTER_XT_TARGET_TEE=y -CONFIG_NETFILTER_XT_TARGET_TPROXY=y -CONFIG_NETFILTER_XT_TARGET_TRACE=y -CONFIG_NETFILTER_XT_TARGET_TCPMSS=y -CONFIG_NETFILTER_XT_TARGET_TCPOPTSTRIP=y -CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y -CONFIG_NETFILTER_XT_MATCH_BPF=y -CONFIG_NETFILTER_XT_MATCH_CGROUP=y -CONFIG_NETFILTER_XT_MATCH_CLUSTER=y -CONFIG_NETFILTER_XT_MATCH_COMMENT=y -CONFIG_NETFILTER_XT_MATCH_CONNBYTES=y -CONFIG_NETFILTER_XT_MATCH_CONNLABEL=y -CONFIG_NETFILTER_XT_MATCH_CONNLIMIT=y -CONFIG_NETFILTER_XT_MATCH_CONNMARK=y -CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y -CONFIG_NETFILTER_XT_MATCH_CPU=y -CONFIG_NETFILTER_XT_MATCH_DCCP=y -CONFIG_NETFILTER_XT_MATCH_DEVGROUP=y -CONFIG_NETFILTER_XT_MATCH_DSCP=y -CONFIG_NETFILTER_XT_MATCH_ESP=y -CONFIG_NETFILTER_XT_MATCH_HASHLIMIT=y -CONFIG_NETFILTER_XT_MATCH_HELPER=y -CONFIG_NETFILTER_XT_MATCH_IPCOMP=y -CONFIG_NETFILTER_XT_MATCH_IPRANGE=y -CONFIG_NETFILTER_XT_MATCH_IPVS=y -CONFIG_NETFILTER_XT_MATCH_L2TP=y -CONFIG_NETFILTER_XT_MATCH_LENGTH=y -CONFIG_NETFILTER_XT_MATCH_LIMIT=y -CONFIG_NETFILTER_XT_MATCH_MAC=y -CONFIG_NETFILTER_XT_MATCH_MARK=y -CONFIG_NETFILTER_XT_MATCH_MULTIPORT=y -CONFIG_NETFILTER_XT_MATCH_NFACCT=y -CONFIG_NETFILTER_XT_MATCH_OSF=y -CONFIG_NETFILTER_XT_MATCH_OWNER=y -CONFIG_NETFILTER_XT_MATCH_POLICY=y -CONFIG_NETFILTER_XT_MATCH_PHYSDEV=y -CONFIG_NETFILTER_XT_MATCH_PKTTYPE=y -CONFIG_NETFILTER_XT_MATCH_QUOTA=y -CONFIG_NETFILTER_XT_MATCH_RATEEST=y -CONFIG_NETFILTER_XT_MATCH_REALM=y -CONFIG_NETFILTER_XT_MATCH_RECENT=y -CONFIG_NETFILTER_XT_MATCH_SCTP=y -CONFIG_NETFILTER_XT_MATCH_SOCKET=y -CONFIG_NETFILTER_XT_MATCH_STATE=y -CONFIG_NETFILTER_XT_MATCH_STATISTIC=y -CONFIG_NETFILTER_XT_MATCH_STRING=y -CONFIG_NETFILTER_XT_MATCH_TCPMSS=y -CONFIG_NETFILTER_XT_MATCH_TIME=y -CONFIG_NETFILTER_XT_MATCH_U32=y -CONFIG_IP_SET=y -CONFIG_IP_SET_BITMAP_IP=y -CONFIG_IP_SET_BITMAP_IPMAC=y -CONFIG_IP_SET_BITMAP_PORT=y -CONFIG_IP_SET_HASH_IP=y -CONFIG_IP_SET_HASH_IPPORT=y -CONFIG_IP_SET_HASH_IPPORTIP=y -CONFIG_IP_SET_HASH_IPPORTNET=y -CONFIG_IP_SET_HASH_NET=y -CONFIG_IP_SET_HASH_NETPORT=y -CONFIG_IP_SET_HASH_NETIFACE=y -CONFIG_IP_SET_LIST_SET=y -CONFIG_IP_VS=y -CONFIG_IP_VS_IPV6=y -CONFIG_IP_VS_PROTO_TCP=y -CONFIG_IP_VS_PROTO_UDP=y -CONFIG_IP_VS_PROTO_ESP=y -CONFIG_IP_VS_PROTO_AH=y -CONFIG_IP_VS_PROTO_SCTP=y -CONFIG_IP_VS_RR=y -CONFIG_IP_VS_WRR=y -CONFIG_IP_VS_LC=y -CONFIG_IP_VS_WLC=y -CONFIG_IP_VS_FO=y -CONFIG_IP_VS_OVF=y -CONFIG_IP_VS_LBLC=y -CONFIG_IP_VS_LBLCR=y -CONFIG_IP_VS_DH=y -CONFIG_IP_VS_SH=y -CONFIG_IP_VS_MH=y -CONFIG_IP_VS_SED=y -CONFIG_IP_VS_NQ=y -CONFIG_IP_VS_FTP=y -CONFIG_NFT_DUP_IPV4=y -CONFIG_NF_TABLES_ARP=y -CONFIG_NF_LOG_ARP=y -CONFIG_IP_NF_IPTABLES=y -CONFIG_IP_NF_MATCH_AH=y -CONFIG_IP_NF_MATCH_ECN=y -CONFIG_IP_NF_MATCH_RPFILTER=y -CONFIG_IP_NF_MATCH_TTL=y -CONFIG_IP_NF_FILTER=y -CONFIG_IP_NF_TARGET_REJECT=y -CONFIG_IP_NF_TARGET_SYNPROXY=y -CONFIG_IP_NF_NAT=y -CONFIG_IP_NF_TARGET_MASQUERADE=y -CONFIG_IP_NF_TARGET_NETMAP=y -CONFIG_IP_NF_TARGET_REDIRECT=y -CONFIG_IP_NF_MANGLE=y -CONFIG_IP_NF_TARGET_ECN=y -CONFIG_IP_NF_TARGET_TTL=y -CONFIG_IP_NF_RAW=y -CONFIG_IP_NF_SECURITY=y -CONFIG_IP_NF_ARPTABLES=y -CONFIG_IP_NF_ARPFILTER=y -CONFIG_IP_NF_ARP_MANGLE=y -CONFIG_NFT_DUP_IPV6=y -CONFIG_IP6_NF_IPTABLES=y -CONFIG_IP6_NF_MATCH_AH=y -CONFIG_IP6_NF_MATCH_EUI64=y -CONFIG_IP6_NF_MATCH_FRAG=y -CONFIG_IP6_NF_MATCH_OPTS=y -CONFIG_IP6_NF_MATCH_HL=y -CONFIG_IP6_NF_MATCH_IPV6HEADER=y -CONFIG_IP6_NF_MATCH_MH=y -CONFIG_IP6_NF_MATCH_RPFILTER=y -CONFIG_IP6_NF_MATCH_RT=y -CONFIG_IP6_NF_TARGET_HL=y -CONFIG_IP6_NF_FILTER=y -CONFIG_IP6_NF_TARGET_REJECT=y -CONFIG_IP6_NF_TARGET_SYNPROXY=y -CONFIG_IP6_NF_MANGLE=y -CONFIG_IP6_NF_RAW=y -CONFIG_IP6_NF_SECURITY=y -CONFIG_IP6_NF_NAT=y -CONFIG_IP6_NF_TARGET_MASQUERADE=y -CONFIG_IP6_NF_TARGET_NPT=y -CONFIG_NF_TABLES_BRIDGE=y -CONFIG_NFT_BRIDGE_REJECT=y -CONFIG_BRIDGE_NF_EBTABLES=y -CONFIG_BRIDGE_EBT_BROUTE=y -CONFIG_BRIDGE_EBT_T_FILTER=y -CONFIG_BRIDGE_EBT_T_NAT=y -CONFIG_BRIDGE_EBT_802_3=y -CONFIG_BRIDGE_EBT_AMONG=y -CONFIG_BRIDGE_EBT_ARP=y -CONFIG_BRIDGE_EBT_IP=y -CONFIG_BRIDGE_EBT_IP6=y -CONFIG_BRIDGE_EBT_LIMIT=y -CONFIG_BRIDGE_EBT_MARK=y -CONFIG_BRIDGE_EBT_PKTTYPE=y -CONFIG_BRIDGE_EBT_STP=y -CONFIG_BRIDGE_EBT_VLAN=y -CONFIG_BRIDGE_EBT_ARPREPLY=y -CONFIG_BRIDGE_EBT_DNAT=y -CONFIG_BRIDGE_EBT_MARK_T=y -CONFIG_BRIDGE_EBT_REDIRECT=y -CONFIG_BRIDGE_EBT_SNAT=y -CONFIG_BRIDGE_EBT_LOG=y -CONFIG_BRIDGE_EBT_NFLOG=y - -# --- Block devices --- -CONFIG_BLK_DEV=y -CONFIG_BLK_DEV_INTEGRITY=y -CONFIG_BLK_DEV_THROTTLING=y -CONFIG_BLK_CGROUP_IOLATENCY=y -CONFIG_PARTITION_ADVANCED=y -CONFIG_LDM_PARTITION=y -CONFIG_CMDLINE_PARTITION=y -CONFIG_BLK_DEV_LOOP=y -CONFIG_BLK_DEV_DRBD=m -CONFIG_BLK_DEV_NBD=y -CONFIG_BLK_DEV_RAM=y -CONFIG_BLK_DEV_RAM_SIZE=65536 -CONFIG_ATA_OVER_ETH=m -CONFIG_BLK_DEV_NVME=y - -# --- SCSI --- -CONFIG_SCSI=y -# CONFIG_SCSI_PROC_FS is not set -CONFIG_BLK_DEV_SD=y -CONFIG_BLK_DEV_SR=y -CONFIG_CHR_DEV_SG=y -CONFIG_ISCSI_TCP=m -CONFIG_SCSI_HPSA=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=y -CONFIG_SCSI_MPI3MR=m -CONFIG_SCSI_SMARTPQI=m -CONFIG_VMWARE_PVSCSI=y -CONFIG_XEN_SCSI_FRONTEND=y -CONFIG_SCSI_VIRTIO=y - -# --- ATA / SATA --- -CONFIG_ATA=y -# CONFIG_ATA_VERBOSE_ERROR is not set -# CONFIG_SATA_PMP is not set -CONFIG_SATA_AHCI=y -CONFIG_ATA_PIIX=y -CONFIG_SATA_MV=y -CONFIG_SATA_NV=y -CONFIG_SATA_PROMISE=y -CONFIG_SATA_SIL=y -CONFIG_SATA_SIS=y -CONFIG_SATA_SVW=y -CONFIG_SATA_ULI=y -CONFIG_SATA_VIA=y -CONFIG_SATA_VITESSE=y -CONFIG_ATA_GENERIC=y - -# --- Device mapper / MD --- -CONFIG_MD=y -CONFIG_BLK_DEV_MD=y -CONFIG_MD_LINEAR=y -CONFIG_MD_RAID0=y -CONFIG_MD_RAID1=y -CONFIG_MD_RAID10=y -CONFIG_MD_RAID456=y -CONFIG_MD_MULTIPATH=y -CONFIG_BLK_DEV_DM=y -CONFIG_DM_CRYPT=y -CONFIG_DM_SNAPSHOT=y -CONFIG_DM_THIN_PROVISIONING=y -CONFIG_DM_MULTIPATH=m -CONFIG_DM_MULTIPATH_QL=m -CONFIG_DM_MULTIPATH_ST=m - -# --- Fusion --- -CONFIG_FUSION=y -CONFIG_FUSION_SPI=y - -# --- Network drivers (built-in for reliable boot) --- -CONFIG_NETDEVICES=y -# CONFIG_NET_VENDOR_3COM is not set -# CONFIG_NET_VENDOR_ADAPTEC is not set -# CONFIG_NET_VENDOR_AGERE is not set -# CONFIG_NET_VENDOR_ALACRITECH is not set -# CONFIG_NET_VENDOR_ALTEON is not set -# CONFIG_NET_VENDOR_AMD is not set -# CONFIG_NET_VENDOR_AQUANTIA is not set -# CONFIG_NET_VENDOR_ARC is not set -# CONFIG_NET_VENDOR_ATHEROS is not set -# CONFIG_NET_VENDOR_CADENCE is not set -# CONFIG_NET_VENDOR_CAVIUM is not set -# CONFIG_NET_VENDOR_CHELSIO is not set -# CONFIG_NET_VENDOR_CORTINA is not set -# CONFIG_NET_VENDOR_DEC is not set -# CONFIG_NET_VENDOR_DLINK is not set -# CONFIG_NET_VENDOR_EMULEX is not set -# CONFIG_NET_VENDOR_EZCHIP is not set -# CONFIG_NET_VENDOR_HUAWEI is not set -# CONFIG_NET_VENDOR_I825XX is not set -# CONFIG_NET_VENDOR_MARVELL is not set -# CONFIG_NET_VENDOR_MICREL is not set -# CONFIG_NET_VENDOR_MICROCHIP is not set -# CONFIG_NET_VENDOR_MYRI is not set -# CONFIG_NET_VENDOR_NI is not set -# CONFIG_NET_VENDOR_NATSEMI is not set -# CONFIG_NET_VENDOR_NETERION is not set -# CONFIG_NET_VENDOR_NVIDIA is not set -# CONFIG_NET_VENDOR_OKI is not set -# CONFIG_NET_VENDOR_PACKET_ENGINES is not set -# CONFIG_NET_VENDOR_QLOGIC is not set -# CONFIG_NET_VENDOR_BROCADE is not set -# CONFIG_NET_VENDOR_QUALCOMM is not set -# CONFIG_NET_VENDOR_RDC is not set -# CONFIG_NET_VENDOR_RENESAS is not set -# CONFIG_NET_VENDOR_ROCKER is not set -# CONFIG_NET_VENDOR_SAMSUNG is not set -# CONFIG_NET_VENDOR_SEEQ is not set -# CONFIG_NET_VENDOR_SILAN is not set -# CONFIG_NET_VENDOR_SIS is not set -# CONFIG_NET_VENDOR_SMSC is not set -# CONFIG_NET_VENDOR_SOCIONEXT is not set -# CONFIG_NET_VENDOR_STMICRO is not set -# CONFIG_NET_VENDOR_SUN is not set -# CONFIG_NET_VENDOR_SYNOPSYS is not set -# CONFIG_NET_VENDOR_TEHUTI is not set -# CONFIG_NET_VENDOR_TI is not set -# CONFIG_NET_VENDOR_VIA is not set -# CONFIG_NET_VENDOR_WIZNET is not set -CONFIG_E1000=y -CONFIG_E1000E=y -CONFIG_IGB=y -CONFIG_IGBVF=y -CONFIG_IXGBE=y -CONFIG_IXGBEVF=y -CONFIG_I40E=y -CONFIG_I40EVF=y -CONFIG_ICE=y -CONFIG_IGC=y -CONFIG_MLX4_EN=y -CONFIG_MLX5_CORE=y -CONFIG_MLX5_CORE_EN=y -CONFIG_BNXT=y -CONFIG_CNIC=m -CONFIG_TIGON3=y -CONFIG_BNX2X=y -CONFIG_R8169=y -CONFIG_8139CP=y -CONFIG_8139TOO=y -CONFIG_VMXNET3=y -CONFIG_HYPERV_NET=y -CONFIG_ENA_ETHERNET=m -CONFIG_GVE=m -CONFIG_NFP=m -CONFIG_IONIC=y -CONFIG_VIRTIO_NET=y - -# --- USB network --- -CONFIG_USB_CATC=m -CONFIG_USB_KAWETH=m -CONFIG_USB_PEGASUS=m -CONFIG_USB_RTL8150=y -CONFIG_USB_RTL8152=y -CONFIG_USB_LAN78XX=m -CONFIG_USB_USBNET=y -CONFIG_USB_NET_AX8817X=m -CONFIG_USB_NET_AX88179_178A=m -CONFIG_USB_NET_CDC_EEM=m -CONFIG_USB_NET_CDC_NCM=m -CONFIG_USB_NET_NET1080=m -CONFIG_USB_NET_CDC_SUBSET=m -CONFIG_USB_NET_ZAURUS=m - -# --- WAN --- -CONFIG_WAN=y -CONFIG_HDLC=m -CONFIG_HDLC_CISCO=m - -# --- PPP --- -CONFIG_PPP=m -CONFIG_PPP_BSDCOMP=m -CONFIG_PPP_DEFLATE=m -CONFIG_PPP_FILTER=y -CONFIG_PPP_MPPE=m -CONFIG_PPP_MULTILINK=y -CONFIG_PPPOE=m -CONFIG_PPTP=m -CONFIG_PPPOL2TP=m -CONFIG_PPP_ASYNC=m -CONFIG_PPP_SYNC_TTY=m - -# --- USB --- -CONFIG_USB_SUPPORT=y -CONFIG_USB=y -CONFIG_USB_XHCI_HCD=y -CONFIG_USB_EHCI_HCD=y -CONFIG_USB_OHCI_HCD=y -CONFIG_USB_UHCI_HCD=y -CONFIG_USB_XHCI_PCI=y -CONFIG_USB_UAS=y -CONFIG_USB_STORAGE=y - -# --- USB serial (BMC/console access) --- -CONFIG_USB_SERIAL=y -CONFIG_USB_SERIAL_CONSOLE=y -CONFIG_USB_SERIAL_GENERIC=y -CONFIG_USB_SERIAL_CH341=y -CONFIG_USB_SERIAL_CP210X=y -CONFIG_USB_SERIAL_FTDI_SIO=y -CONFIG_USB_SERIAL_PL2303=y -CONFIG_USB_SERIAL_TI=y -CONFIG_USB_SERIAL_OPTION=y - -# --- Input --- -CONFIG_INPUT_FF_MEMLESS=y -CONFIG_INPUT_SPARSEKMAP=y -CONFIG_INPUT_MOUSEDEV=y -CONFIG_INPUT_MOUSEDEV_PSAUX=y -CONFIG_INPUT_EVDEV=y -# CONFIG_INPUT_MOUSE is not set -CONFIG_INPUT_MISC=y -CONFIG_INPUT_PCSPKR=y -CONFIG_INPUT_ATLAS_BTNS=y -CONFIG_INPUT_UINPUT=y -CONFIG_SERIO_PCIPS2=y -CONFIG_SERIO_RAW=y -# CONFIG_LEGACY_PTYS is not set -# CONFIG_INPUT_JOYSTICK is not set -# CONFIG_INPUT_TOUCHSCREEN is not set - -# --- MMC --- -CONFIG_MMC=y -CONFIG_MMC_SDHCI=y -CONFIG_MMC_SDHCI_PCI=y -# CONFIG_MMC_RICOH_MMC is not set -CONFIG_MMC_SDHCI_ACPI=y -CONFIG_MMC_SDHCI_PLTFM=y - -# --- Filesystems --- -CONFIG_EXT4_FS=y -CONFIG_EXT4_FS_POSIX_ACL=y -CONFIG_EXT4_FS_SECURITY=y -CONFIG_XFS_FS=y -CONFIG_XFS_QUOTA=y -CONFIG_XFS_POSIX_ACL=y -CONFIG_BTRFS_FS=m -CONFIG_BTRFS_FS_POSIX_ACL=y -CONFIG_FS_DAX=y -CONFIG_FS_ENCRYPTION=y -CONFIG_FANOTIFY=y -CONFIG_QUOTA=y -CONFIG_QUOTA_NETLINK_INTERFACE=y -CONFIG_VFAT_FS=y -CONFIG_MSDOS_FS=y -CONFIG_FAT_DEFAULT_IOCHARSET="utf8" -CONFIG_NTFS_FS=m -CONFIG_TMPFS=y -CONFIG_TMPFS_POSIX_ACL=y -CONFIG_TMPFS_XATTR=y -CONFIG_PROC_FS=y -CONFIG_PROC_KCORE=y -CONFIG_SYSFS=y -CONFIG_EFIVAR_FS=y -CONFIG_OVERLAY_FS=y -CONFIG_SQUASHFS=y -CONFIG_SQUASHFS_XATTR=y -CONFIG_SQUASHFS_LZ4=y -CONFIG_SQUASHFS_LZO=y -CONFIG_SQUASHFS_XZ=y -CONFIG_FUSE_FS=y -CONFIG_CUSE=y -CONFIG_ISO9660_FS=y -CONFIG_JOLIET=y -CONFIG_ZISOFS=y -CONFIG_UDF_FS=y -CONFIG_FSCACHE=y -CONFIG_FSCACHE_STATS=y -CONFIG_CACHEFILES=y -CONFIG_NFS_FS=m -# CONFIG_NFS_V2 is not set -CONFIG_NFS_V4=m -CONFIG_NFS_V4_1=y -CONFIG_NFS_V4_2=y -CONFIG_NFS_FSCACHE=y -CONFIG_NFSD=m -CONFIG_NFSD_V4=y -CONFIG_CEPH_FS=m -CONFIG_CEPH_FSCACHE=y -CONFIG_CEPH_FS_POSIX_ACL=y -CONFIG_CIFS=y -# CONFIG_CIFS_ALLOW_INSECURE_LEGACY is not set -CONFIG_CIFS_XATTR=y -CONFIG_CIFS_DFS_UPCALL=y -CONFIG_CIFS_FSCACHE=y -CONFIG_9P_FS=y -CONFIG_9P_FSCACHE=y -CONFIG_9P_FS_POSIX_ACL=y -CONFIG_9P_FS_SECURITY=y - -# --- NLS --- -CONFIG_NLS_CODEPAGE_437=y -CONFIG_NLS_ASCII=y -CONFIG_NLS_ISO8859_1=y -CONFIG_NLS_UTF8=y - -# --- Virtio (built-in for reliable boot) --- -CONFIG_VIRTIO=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BLK=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_INPUT=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y -CONFIG_HW_RANDOM_VIRTIO=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m - -# --- Console / Serial --- -CONFIG_VT=y -CONFIG_VT_CONSOLE=y -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_SERIAL_8250_NR_UARTS=32 -# CONFIG_SERIAL_8250_MID is not set -CONFIG_SERIAL_DEV_BUS=y -CONFIG_TTY_PRINTK=y -CONFIG_HW_CONSOLE=y -CONFIG_VGA_CONSOLE=y -CONFIG_DUMMY_CONSOLE=y - -# --- IPMI --- -CONFIG_IPMI_HANDLER=y -CONFIG_IPMI_DMI_DECODE=y -CONFIG_IPMI_DEVICE_INTERFACE=y -CONFIG_IPMI_SI=y -CONFIG_IPMI_POWEROFF=y - -# --- Hardware RNG --- -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_TIMERIOMEM=y - -# --- I2C --- -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MUX=y - -# --- TPM --- -CONFIG_TCG_TIS_I2C_ATMEL=m -CONFIG_TCG_TIS_I2C_INFINEON=m -CONFIG_TCG_TIS_I2C_NUVOTON=m -CONFIG_TCG_NSC=m -CONFIG_TCG_ATMEL=m -CONFIG_TCG_INFINEON=m -CONFIG_TCG_XEN=m -CONFIG_TCG_VTPM_PROXY=m -CONFIG_TCG_TIS_ST33ZP24_I2C=m - -# --- Platform / MFD --- -CONFIG_NVRAM=y -CONFIG_HPET=y -CONFIG_HANGCHECK_TIMER=y -CONFIG_LPC_ICH=y -CONFIG_LPC_SCH=y -CONFIG_MFD_INTEL_LPSS_ACPI=y -CONFIG_MFD_INTEL_LPSS_PCI=y -CONFIG_MFD_SM501=y -CONFIG_MFD_VX855=y -# CONFIG_THERMAL_HWMON is not set - -# --- RTC --- -CONFIG_RTC_CLASS=y - -# --- DMA --- -CONFIG_DMADEVICES=y - -# --- Security --- -CONFIG_SECCOMP=y -CONFIG_SECCOMP_FILTER=y -CONFIG_SECURITY=y -CONFIG_SECURITY_NETWORK=y -CONFIG_SECURITY_NETWORK_XFRM=y -CONFIG_SECURITY_PATH=y -CONFIG_SECURITY_DMESG_RESTRICT=y -CONFIG_KEYS=y -CONFIG_PERSISTENT_KEYRINGS=y -CONFIG_TRUSTED_KEYS=y -CONFIG_KEY_DH_OPERATIONS=y -CONFIG_HARDENED_USERCOPY=y -CONFIG_FORTIFY_SOURCE=y -CONFIG_STATIC_USERMODEHELPER=y -CONFIG_SECURITY_YAMA=y -CONFIG_INTEGRITY_SIGNATURE=y -CONFIG_INTEGRITY_ASYMMETRIC_KEYS=y -CONFIG_IMA=y -CONFIG_IMA_DEFAULT_HASH_SHA256=y -CONFIG_IMA_READ_POLICY=y -CONFIG_IMA_APPRAISE=y -CONFIG_EVM=y -CONFIG_LSM="yama,loadpin,safesetid,integrity" - -# --- Crypto --- -CONFIG_CRYPTO=y -CONFIG_CRYPTO_USER=y -CONFIG_CRYPTO_AES=y -CONFIG_CRYPTO_SHA256=y -CONFIG_CRYPTO_SHA512=y -CONFIG_CRYPTO_XTS=y -CONFIG_CRYPTO_ANUBIS=y -CONFIG_CRYPTO_BLOWFISH=y -CONFIG_CRYPTO_CAMELLIA=y -CONFIG_CRYPTO_DES=y -CONFIG_CRYPTO_FCRYPT=y -CONFIG_CRYPTO_KHAZAD=y -CONFIG_CRYPTO_SEED=y -CONFIG_CRYPTO_TEA=y -CONFIG_CRYPTO_TWOFISH=y -CONFIG_CRYPTO_ARC4=y -CONFIG_CRYPTO_KEYWRAP=y -CONFIG_CRYPTO_LRW=y -CONFIG_CRYPTO_PCBC=y -CONFIG_CRYPTO_CHACHA20POLY1305=y -CONFIG_CRYPTO_SEQIV=y -CONFIG_CRYPTO_ECHAINIV=y -CONFIG_CRYPTO_MICHAEL_MIC=y -CONFIG_CRYPTO_RMD160=y -CONFIG_CRYPTO_VMAC=y -CONFIG_CRYPTO_WP512=y -CONFIG_CRYPTO_XCBC=y -CONFIG_CRYPTO_CRC32=y -CONFIG_CRYPTO_LZO=y -CONFIG_CRYPTO_842=y -CONFIG_CRYPTO_LZ4=y -CONFIG_CRYPTO_LZ4HC=y -CONFIG_CRYPTO_ANSI_CPRNG=y -CONFIG_CRYPTO_USER_API_HASH=y -CONFIG_CRYPTO_USER_API_SKCIPHER=y -CONFIG_CRYPTO_USER_API_RNG=y -CONFIG_CRYPTO_USER_API_AEAD=y - -# --- x86_64 hardware crypto acceleration --- -CONFIG_CRYPTO_AES_NI_INTEL=y -CONFIG_CRYPTO_BLOWFISH_X86_64=y -CONFIG_CRYPTO_CAMELLIA_AESNI_AVX2_X86_64=y -CONFIG_CRYPTO_CAST5_AVX_X86_64=y -CONFIG_CRYPTO_CAST6_AVX_X86_64=y -CONFIG_CRYPTO_DES3_EDE_X86_64=y -CONFIG_CRYPTO_SERPENT_SSE2_X86_64=y -CONFIG_CRYPTO_SERPENT_AVX2_X86_64=y -CONFIG_CRYPTO_TWOFISH_AVX_X86_64=y -CONFIG_CRYPTO_CHACHA20_X86_64=y -CONFIG_CRYPTO_POLY1305_X86_64=y -CONFIG_CRYPTO_SHA1_SSSE3=y -CONFIG_CRYPTO_SHA256_SSSE3=y -CONFIG_CRYPTO_SHA512_SSSE3=y -CONFIG_CRYPTO_GHASH_CLMUL_NI_INTEL=y -CONFIG_CRYPTO_CRC32C_INTEL=y -CONFIG_CRYPTO_CRC32_PCLMUL=y -CONFIG_CRYPTO_DEV_PADLOCK=y -CONFIG_CRYPTO_DEV_PADLOCK_AES=y -CONFIG_CRYPTO_DEV_PADLOCK_SHA=y -CONFIG_CRYPTO_DEV_VIRTIO=m -CONFIG_PKCS7_MESSAGE_PARSER=y - -# --- BPF --- -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y - -# --- PCI --- -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_HOTPLUG_PCI_PCIE=y -CONFIG_PCI_STUB=y -CONFIG_PCI_IOV=y -# CONFIG_VGA_ARB is not set -CONFIG_HOTPLUG_PCI=y -CONFIG_HOTPLUG_PCI_ACPI=y -CONFIG_HOTPLUG_PCI_SHPC=y -CONFIG_PCI_HYPERV_INTERFACE=m - -# --- IOMMU --- -CONFIG_AMD_IOMMU=y -CONFIG_INTEL_IOMMU=y -CONFIG_IRQ_REMAP=y - -# --- Xen --- -# CONFIG_XEN_BACKEND is not set -CONFIG_XEN_GNTDEV=y -CONFIG_XEN_GRANT_DEV_ALLOC=y -CONFIG_XEN_PVCALLS_FRONTEND=y -CONFIG_XEN_ACPI_PROCESSOR=y -# CONFIG_XEN_SYMS is not set - -# --- Intel platform --- -CONFIG_INTEL_IPS=y - -# --- DEVFREQ --- -CONFIG_PM_DEVFREQ=y -CONFIG_DEVFREQ_GOV_SIMPLE_ONDEMAND=y - -# --- Reset --- -CONFIG_RESET_CONTROLLER=y - -# --- DAX --- -CONFIG_DEV_DAX=y -CONFIG_DEV_DAX_PMEM=m - -# --- Misc --- -CONFIG_FW_LOADER=y -CONFIG_PRINTK=y -CONFIG_PRINTK_TIME=y -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_EARLY_PRINTK=y -CONFIG_PANIC_TIMEOUT=10 -CONFIG_PANIC_ON_OOPS=y -CONFIG_MAGIC_SYSRQ=y -CONFIG_UEVENT_HELPER=y -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_CONNECTOR=y -CONFIG_DMI_SYSFS=y -CONFIG_SYSFB_SIMPLEFB=y -CONFIG_RESET_ATTACK_MITIGATION=y -# CONFIG_PNP_DEBUG_MESSAGES is not set -CONFIG_HYPERV=y -CONFIG_HYPERV_UTILS=y -CONFIG_HYPERV_BALLOON=y - -# --- Module compression --- -CONFIG_MODULE_COMPRESS_ZSTD=y -CONFIG_MODULE_DECOMPRESS=y - -# --- EFI boot support --- -CONFIG_EFI=y -CONFIG_EFI_STUB=y -CONFIG_EFI_MIXED=y - -# --- Framebuffer/Console (required for EFI boot video output) --- -CONFIG_DRM=y -CONFIG_DRM_I915=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_FRAMEBUFFER_CONSOLE=y - -# --- Debugging / Diagnostics --- -CONFIG_FRAME_WARN=1024 -CONFIG_BUG_ON_DATA_CORRUPTION=y -CONFIG_HARDLOCKUP_DETECTOR=y -CONFIG_WQ_WATCHDOG=y -CONFIG_DEBUG_NOTIFIERS=y -CONFIG_RCU_CPU_STALL_TIMEOUT=60 -# CONFIG_RCU_TRACE is not set -CONFIG_IO_STRICT_DEVMEM=y - -# --- Disable unnecessary subsystems --- -# CONFIG_SOUND is not set -# CONFIG_WIRELESS is not set -# CONFIG_WLAN is not set -# CONFIG_BLUETOOTH is not set -# CONFIG_NFC is not set -# CONFIG_INFINIBAND is not set -# CONFIG_MEDIA_SUPPORT is not set diff --git a/kernel.configs/6.19.y.arm64 b/kernel.configs/6.19.y.arm64 deleted file mode 100644 index 795c91e6..00000000 --- a/kernel.configs/6.19.y.arm64 +++ /dev/null @@ -1,515 +0,0 @@ -# CaptainOS kernel defconfig for arm64 (aarch64) -# Minimal config for bare-metal provisioning with container/network support. -# Based on 6.12.y config with targeted additions for CaptainOS use case, adapted for kernel 6.18. -# -# Generate a full .config from this: -# cp 6.18.y.arm64 .config && make ARCH=arm64 olddefconfig - -# --- Architecture --- -CONFIG_ARM64=y -CONFIG_SMP=y -CONFIG_NR_CPUS=128 -CONFIG_NUMA=y -CONFIG_ARM64_VA_BITS_48=y -CONFIG_SCHED_MC=y -CONFIG_SCHED_SMT=y -CONFIG_HZ_1000=y -CONFIG_XEN=y -CONFIG_COMPAT=y -CONFIG_RANDOMIZE_BASE=y -CONFIG_ARM64_ACPI_PARKING_PROTOCOL=y - -# --- General --- -CONFIG_LOCALVERSION="-captainos" -CONFIG_DEFAULT_HOSTNAME="captainos" -CONFIG_SYSVIPC=y -CONFIG_POSIX_MQUEUE=y -CONFIG_AUDIT=y -CONFIG_NO_HZ_IDLE=y -CONFIG_HIGH_RES_TIMERS=y -CONFIG_PREEMPT_VOLUNTARY=y -CONFIG_BSD_PROCESS_ACCT=y -CONFIG_KEXEC=y -CONFIG_KEXEC_FILE=y -CONFIG_CC_OPTIMIZE_FOR_SIZE=y -CONFIG_CGROUPS=y -CONFIG_CGROUP_FREEZER=y -CONFIG_CGROUP_PIDS=y -CONFIG_CGROUP_DEVICE=y -CONFIG_CGROUP_CPUACCT=y -CONFIG_CGROUP_PERF=y -CONFIG_CGROUP_BPF=y -CONFIG_CGROUP_MISC=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CGROUP_RDMA=y -CONFIG_CPUSETS=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_BLK_CGROUP_IOLATENCY=y -CONFIG_BLK_DEV_THROTTLING=y -CONFIG_CFS_BANDWIDTH=y -CONFIG_RT_GROUP_SCHED=y -CONFIG_CGROUP_SCHED=y -CONFIG_CGROUP_NET_PRIO=y -CONFIG_NAMESPACES=y -CONFIG_USER_NS=y -CONFIG_PID_NS=y -CONFIG_NET_NS=y -CONFIG_UTS_NS=y -CONFIG_IPC_NS=y -CONFIG_CHECKPOINT_RESTORE=y -# Memory management -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_KSM=y -CONFIG_MEMORY_HOTPLUG=y -CONFIG_MEMORY_HOTREMOVE=y -CONFIG_HUGETLBFS=y - -# --- Init / Initramfs --- -CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="" - -# --- Networking --- -CONFIG_NET=y -CONFIG_PACKET=y -CONFIG_UNIX=y -CONFIG_INET=y -CONFIG_IP_MULTICAST=y -CONFIG_IP_ADVANCED_ROUTER=y -CONFIG_IP_MULTIPLE_TABLES=y -CONFIG_IP_ROUTE_MULTIPATH=y -CONFIG_IP_PNP=y -CONFIG_IP_PNP_DHCP=y -CONFIG_IP_PNP_BOOTP=y -CONFIG_NET_IPIP=m -CONFIG_NET_IPGRE_DEMUX=m -CONFIG_NET_IPGRE=m -CONFIG_SYN_COOKIES=y -CONFIG_IPV6=y -CONFIG_BRIDGE=y -CONFIG_VLAN_8021Q=m -CONFIG_BONDING=m -CONFIG_TUN=y -CONFIG_VETH=y -CONFIG_MACVLAN=y -CONFIG_MACVTAP=y -CONFIG_IPVLAN=y -CONFIG_TAP=y -CONFIG_DUMMY=m -CONFIG_VXLAN=y -CONFIG_GENEVE=m -CONFIG_NLMON=y -# Traffic control / QoS (needed by CNI plugins) -CONFIG_NET_SCHED=y -CONFIG_NET_SCH_HTB=m -CONFIG_NET_SCH_PRIO=m -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_SCH_TBF=m -CONFIG_NET_SCH_NETEM=m -CONFIG_NET_SCH_INGRESS=m -CONFIG_NET_CLS_BASIC=y -CONFIG_NET_CLS_FW=y -CONFIG_NET_CLS_U32=y -CONFIG_NET_CLS_CGROUP=y -CONFIG_NET_CLS_BPF=y -CONFIG_NET_CLS_MATCHALL=y -CONFIG_NET_CLS_ACT=y -CONFIG_NET_ACT_POLICE=y -CONFIG_NET_ACT_GACT=y -CONFIG_NET_ACT_MIRRED=y -CONFIG_NET_ACT_BPF=y -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_U32=y - -# --- Netfilter --- -CONFIG_NETFILTER=y -CONFIG_NETFILTER_ADVANCED=y -CONFIG_NF_CONNTRACK=y -CONFIG_NF_NAT=y -CONFIG_NF_TABLES=y -CONFIG_NF_TABLES_INET=y -CONFIG_NF_TABLES_IPV4=y -CONFIG_NF_TABLES_IPV6=y -CONFIG_NFT_NAT=y -CONFIG_NFT_MASQ=y -CONFIG_NFT_CT=y -CONFIG_NFT_COMPAT=y -CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y -CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y -CONFIG_NETFILTER_XT_MATCH_COMMENT=y -CONFIG_NETFILTER_XT_MATCH_MARK=y -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y -CONFIG_IP_NF_IPTABLES=y -CONFIG_IP_NF_FILTER=y -CONFIG_IP_NF_NAT=y -CONFIG_IP6_NF_IPTABLES=y -CONFIG_IP6_NF_FILTER=y -CONFIG_IP6_NF_NAT=y -CONFIG_BRIDGE_NETFILTER=y -CONFIG_BRIDGE_NF_EBTABLES=y -CONFIG_BRIDGE_EBT_BROUTE=y -CONFIG_BRIDGE_EBT_T_FILTER=y -CONFIG_BRIDGE_EBT_T_NAT=y -CONFIG_BRIDGE_EBT_ARP=y -CONFIG_BRIDGE_EBT_IP=y -CONFIG_BRIDGE_EBT_IP6=y -CONFIG_BRIDGE_EBT_MARK=y -CONFIG_BRIDGE_EBT_MARK_T=y -CONFIG_BRIDGE_EBT_VLAN=y -# IPVS (container load balancing / kube-proxy) -CONFIG_IP_VS=y -CONFIG_IP_VS_IPV6=y -CONFIG_IP_VS_PROTO_TCP=y -CONFIG_IP_VS_PROTO_UDP=y -CONFIG_IP_VS_RR=y -CONFIG_IP_VS_WRR=y -CONFIG_IP_VS_SH=y -CONFIG_IP_VS_NFCT=y -# IP sets (used by iptables/nftables for efficient matching) -CONFIG_IP_SET=y -CONFIG_IP_SET_HASH_IP=y -CONFIG_IP_SET_HASH_IPPORT=y -CONFIG_IP_SET_HASH_IPPORTNET=y -CONFIG_IP_SET_HASH_NET=y -CONFIG_IP_SET_HASH_NETPORT=y -CONFIG_IP_SET_BITMAP_IP=y -CONFIG_IP_SET_BITMAP_PORT=y -CONFIG_IP_SET_LIST_SET=y -CONFIG_NETFILTER_XT_SET=y - -# --- Block devices --- -CONFIG_BLK_DEV=y -CONFIG_BLK_DEV_LOOP=y -CONFIG_BLK_DEV_NBD=y -CONFIG_BLK_DEV_RAM=y -CONFIG_BLK_DEV_RAM_SIZE=65536 -CONFIG_BLK_DEV_NVME=y -CONFIG_ATA=y -CONFIG_SATA_AHCI=y -CONFIG_SATA_AHCI_PLATFORM=y - -# --- SCSI --- -CONFIG_SCSI=y -CONFIG_BLK_DEV_SD=y -CONFIG_CHR_DEV_SG=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=y -CONFIG_SCSI_MPI3MR=m -CONFIG_SCSI_HPSA=y -CONFIG_SCSI_HISI_SAS=y -CONFIG_SCSI_HISI_SAS_PCI=y -CONFIG_XEN_SCSI_FRONTEND=y -CONFIG_SCSI_VIRTIO=y - -# --- Device mapper --- -CONFIG_MD=y -CONFIG_BLK_DEV_DM=y -CONFIG_DM_CRYPT=y -CONFIG_DM_THIN_PROVISIONING=y -CONFIG_DM_SNAPSHOT=y -CONFIG_DM_MULTIPATH=m -CONFIG_MD_RAID0=m -CONFIG_MD_RAID1=m - -# --- Network drivers (built-in for reliable boot) --- -CONFIG_NETDEVICES=y -CONFIG_VIRTIO_NET=y -CONFIG_E1000E=y -CONFIG_IGB=y -CONFIG_IXGBE=y -CONFIG_I40E=y -CONFIG_ICE=y -CONFIG_IGC=y -CONFIG_MLX4_EN=y -CONFIG_MLX5_CORE=y -CONFIG_MLX5_CORE_EN=y -CONFIG_BNXT=y -CONFIG_TIGON3=y -CONFIG_R8169=y -CONFIG_THUNDER_NIC_BGX=m -CONFIG_THUNDER_NIC_PF=m -CONFIG_THUNDER_NIC_VF=m -CONFIG_MACB=m -CONFIG_HNS3=m -CONFIG_NET_XGENE=y -CONFIG_MVNETA=y -CONFIG_MVPP2=y -CONFIG_STMMAC_ETH=m -CONFIG_ENA_ETHERNET=m -CONFIG_GVE=m -CONFIG_VMXNET3=y -CONFIG_AMD_XGBE=y -# Broadcom GENET (Raspberry Pi 4B built-in Ethernet) -CONFIG_BCMGENET=y -# PHY subsystem (required by GENET and other MAC drivers) -CONFIG_PHYLIB=y -CONFIG_MDIO_BUS=y -CONFIG_BROADCOM_PHY=y -CONFIG_BCM7XXX_PHY=y -CONFIG_MDIO_BCM_UNIMAC=y -CONFIG_MARVELL_PHY=y -CONFIG_MARVELL_10G_PHY=y -CONFIG_MICREL_PHY=y -CONFIG_ROCKCHIP_PHY=y -CONFIG_REALTEK_PHY=y -CONFIG_MESON_GXL_PHY=y -CONFIG_AMD_PHY=y - -# --- USB --- -CONFIG_USB_SUPPORT=y -CONFIG_USB=y -CONFIG_USB_XHCI_HCD=y -CONFIG_USB_EHCI_HCD=y -CONFIG_USB_OHCI_HCD=y -CONFIG_USB_XHCI_PCI=y -CONFIG_USB_UAS=y -CONFIG_USB_STORAGE=y -CONFIG_USB_DWC3=y -CONFIG_USB_DWC3_HOST=y -CONFIG_USB_DWC2=y -CONFIG_USB_ISP1760=y - -# --- MMC/SD (Raspberry Pi, Rockchip, and other SBCs) --- -CONFIG_MMC=y -CONFIG_MMC_BLOCK_MINORS=32 -CONFIG_MMC_SDHCI=y -CONFIG_MMC_SDHCI_PCI=y -CONFIG_MMC_SDHCI_ACPI=y -CONFIG_MMC_SDHCI_PLTFM=y -CONFIG_MMC_SDHCI_OF_ARASAN=y -CONFIG_MMC_SDHCI_OF_DWCMSHC=y -CONFIG_MMC_SDHCI_CADENCE=y -CONFIG_MMC_SDHCI_TEGRA=y -CONFIG_MMC_SDHCI_MSM=y -CONFIG_MMC_SDHCI_XENON=y -CONFIG_MMC_SDHCI_F_SDH30=y -CONFIG_MMC_ARMMMCI=y -CONFIG_MMC_DW=y -CONFIG_MMC_DW_ROCKCHIP=y -CONFIG_MMC_DW_K3=y -CONFIG_MMC_MESON_GX=y -CONFIG_MMC_SUNXI=m -CONFIG_MMC_SPI=y -CONFIG_MMC_HSQ=y - -# --- Filesystems --- -CONFIG_EXT4_FS=y -CONFIG_XFS_FS=m -CONFIG_VFAT_FS=y -CONFIG_TMPFS=y -CONFIG_TMPFS_POSIX_ACL=y -CONFIG_PROC_FS=y -CONFIG_SYSFS=y -CONFIG_OVERLAY_FS=y -CONFIG_SQUASHFS=y -CONFIG_SQUASHFS_XATTR=y -CONFIG_SQUASHFS_LZ4=y -CONFIG_SQUASHFS_LZO=y -CONFIG_SQUASHFS_XZ=y -CONFIG_NFS_FS=m -CONFIG_NFS_V4=m -CONFIG_NFS_V4_1=y -CONFIG_NFS_V4_2=y -CONFIG_FUSE_FS=y -CONFIG_CUSE=y -CONFIG_EFIVAR_FS=y -CONFIG_ISO9660_FS=y -CONFIG_JOLIET=y -CONFIG_UDF_FS=y -CONFIG_MSDOS_FS=y -CONFIG_FAT_DEFAULT_IOCHARSET="utf8" -CONFIG_NTFS_FS=m -CONFIG_TMPFS_XATTR=y -CONFIG_NLS_CODEPAGE_437=y -CONFIG_NLS_ASCII=y -CONFIG_NLS_ISO8859_1=y -CONFIG_NLS_UTF8=y - -# --- Virtio (built-in for reliable boot) --- -CONFIG_VIRTIO=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BLK=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_INPUT=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y -CONFIG_HW_RANDOM_VIRTIO=y - -# --- Console / Serial --- -CONFIG_SERIAL_AMBA_PL011=y -CONFIG_SERIAL_AMBA_PL011_CONSOLE=y -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_VT=y -CONFIG_VT_CONSOLE=y -CONFIG_HW_CONSOLE=y -CONFIG_DUMMY_CONSOLE=y - -# --- Security --- -CONFIG_SECCOMP=y -CONFIG_SECCOMP_FILTER=y -CONFIG_SECURITY=y -CONFIG_KEYS=y - -# --- Crypto --- -CONFIG_CRYPTO=y -CONFIG_CRYPTO_AES=y -CONFIG_CRYPTO_SHA256=y -CONFIG_CRYPTO_SHA512=y -CONFIG_CRYPTO_XTS=y -CONFIG_ARM64_CRYPTO=y -CONFIG_CRYPTO_AES_ARM64_CE=y -CONFIG_CRYPTO_SHA2_ARM64_CE=y - -# --- BPF --- -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y -CONFIG_XDP_SOCKETS=y - -# --- ARM64 SoC/platform support --- -CONFIG_ARCH_BCM2835=y -CONFIG_ARCH_ROCKCHIP=y -CONFIG_ARCH_MESON=y -CONFIG_ARCH_SUNXI=y -CONFIG_ARCH_TEGRA=y -CONFIG_ARCH_MVEBU=y -CONFIG_ARCH_HISI=y -CONFIG_ARCH_QCOM=y -CONFIG_ARCH_EXYNOS=y -CONFIG_ARCH_VEXPRESS=y -CONFIG_ARCH_XGENE=y -CONFIG_ARCH_THUNDER=y -CONFIG_ARCH_THUNDER2=y -CONFIG_ARCH_SEATTLE=y -CONFIG_ARCH_SYNQUACER=y -CONFIG_ARCH_UNIPHIER=y -# ACPI / UEFI -CONFIG_ACPI=y -CONFIG_ACPI_DOCK=y -CONFIG_ACPI_IPMI=y -CONFIG_ACPI_APEI=y -CONFIG_ACPI_APEI_GHES=y -CONFIG_IPMI_HANDLER=m -CONFIG_IPMI_DMI_DECODE=y -CONFIG_IPMI_DEVICE_INTERFACE=m -CONFIG_IPMI_SI=m -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_HOTPLUG_PCI=y -CONFIG_PCI_HOST_GENERIC=y -CONFIG_PCIE_ROCKCHIP_HOST=y -CONFIG_PCIE_ARMADA_8K=y -CONFIG_PCIE_TEGRA194_HOST=y -# Broadcom STB PCIe controller (Raspberry Pi 4B — needed for VL805 USB 3.0) -CONFIG_PCIE_BRCMSTB=y -CONFIG_PCIE_ALTERA=y -CONFIG_PCI_AARDVARK=y -CONFIG_PCI_TEGRA=y -CONFIG_PCI_XGENE=y -CONFIG_PCI_HOST_THUNDER_PEM=y -CONFIG_PCI_HOST_THUNDER_ECAM=y -CONFIG_PCIE_QCOM=y -CONFIG_PCIE_AL=y -CONFIG_PCI_MESON=y -CONFIG_PCI_HISI=y -CONFIG_PCIE_KIRIN=y -CONFIG_PCIE_HISI_STB=y -CONFIG_PCI_IOV=y -CONFIG_PCI_PASID=y -CONFIG_HOTPLUG_PCI_ACPI=y -# IOMMU -CONFIG_ARM_SMMU=y -CONFIG_ARM_SMMU_V3=y -CONFIG_FW_LOADER=y -# Raspberry Pi firmware mailbox (required for GENET MAC address, clocks, etc.) -CONFIG_RASPBERRYPI_FIRMWARE=y -CONFIG_RASPBERRYPI_POWER=y -CONFIG_ARM_SCMI_PROTOCOL=y -CONFIG_ARM_SCPI_PROTOCOL=y -CONFIG_OF=y -CONFIG_USE_OF=y -# RTC -CONFIG_RTC_CLASS=y -CONFIG_RTC_DRV_EFI=y -# DMA -CONFIG_DMADEVICES=y -CONFIG_PL330_DMA=y -CONFIG_PRINTK=y -CONFIG_PRINTK_TIME=y -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_PANIC_TIMEOUT=10 -CONFIG_PANIC_ON_OOPS=y -CONFIG_MAGIC_SYSRQ=y -CONFIG_RD_GZIP=y -CONFIG_RD_XZ=y -CONFIG_RD_ZSTD=y -CONFIG_UEVENT_HELPER=y -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_CONNECTOR=y -CONFIG_DMI_SYSFS=y -CONFIG_EFI=y -CONFIG_EFI_STUB=y - -# --- Framebuffer/Console (required for EFI boot video output) --- -CONFIG_DRM=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_DRM_VIRTIO_GPU=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_FRAMEBUFFER_CONSOLE=y -CONFIG_SYSFB_SIMPLEFB=y -# Virtualization -CONFIG_VIRTUALIZATION=y -CONFIG_KVM=y -CONFIG_VFIO=y -CONFIG_VFIO_PCI=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m - -# --- Module compression --- -CONFIG_MODULE_COMPRESS_ZSTD=y -CONFIG_MODULE_DECOMPRESS=y - -# --- I2C (SBC board management, EEPROM, sensors) --- -CONFIG_I2C=y -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MV64XXX=y -CONFIG_I2C_RK3X=y -CONFIG_I2C_TEGRA=y -CONFIG_I2C_DESIGNWARE_PLATFORM=y -# SPI -CONFIG_SPI=y -CONFIG_SPI_ROCKCHIP=y -CONFIG_SPI_PL022=y -CONFIG_SPI_ORION=y -# GPIO -CONFIG_GPIOLIB=y -# Hardware watchdog -CONFIG_WATCHDOG=y -CONFIG_SOFTLOCKUP_DETECTOR=y -CONFIG_WQ_WATCHDOG=y -# Hardware RNG -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_TIMERIOMEM=y -# TPM -CONFIG_TCG_TIS=y -CONFIG_TCG_FTPM_TEE=y -CONFIG_TEE=y -CONFIG_OPTEE=y - -# --- Disable unnecessary subsystems --- -# CONFIG_SOUND is not set -# CONFIG_WIRELESS is not set -# CONFIG_WLAN is not set -# CONFIG_BLUETOOTH is not set -# CONFIG_NFC is not set -# CONFIG_INFINIBAND is not set -# CONFIG_MEDIA_SUPPORT is not set -CONFIG_INPUT_EVDEV=y -# CONFIG_INPUT_JOYSTICK is not set -# CONFIG_INPUT_TOUCHSCREEN is not set From 52d97d9c024a3d0bfef6c2940e54f56c46fe847d Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 20:41:35 +0200 Subject: [PATCH 018/143] mkosi.finalize: drop kernel sound/media/net-wireless modules - mkosi.finalize: clean up logging a bit - mkosi.postinst: debugs Signed-off-by: Ricardo Pardini --- mkosi.finalize | 26 +++++++++++++------------- mkosi.postinst | 9 +++++++++ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/mkosi.finalize b/mkosi.finalize index 32309dec..f97b7425 100755 --- a/mkosi.finalize +++ b/mkosi.finalize @@ -59,13 +59,11 @@ done # --------------------------------------------------------------------------- MODDIR="$BUILDROOT/usr/lib/modules" -echo "==> Listing all installed kernel modules: ${MODDIR}" -du -h -x -d 5 "$MODDIR" | sort -h || true - if [[ -d "$MODDIR" ]]; then echo "==> Trimming unnecessary kernel modules..." BEFORE=$(du -sb "$MODDIR" | awk '{print $1}') + # @TODO: this has to be templated, for flavors like fat or full we should keep most modules. # Module directory patterns to remove (relative to kernel/ inside the # modules tree). Each entry is passed to 'find -path'. EXCLUDE_PATTERNS=( @@ -85,6 +83,12 @@ if [[ -d "$MODDIR" ]]; then "*/kernel/virt" # Niche NIC driver "*/kernel/drivers/net/ethernet/netronome" + # Sound stuff + "*/kernel/sound" + # Media stuff + "*/kernel/drivers/media" + # Wireless net drivers + "*/kernel/drivers/net/wireless" ) for pattern in "${EXCLUDE_PATTERNS[@]}"; do @@ -105,11 +109,11 @@ if [[ -d "$MODDIR" ]]; then done fi -echo "==> Listing remaining kernel modules: ${BUILDROOT}" -du -h -x -d 5 "$MODDIR" | sort -h || true +echo "==> Listing 20 biggest remaining kernel modules du: ${MODDIR}" +du -h -x -d 5 "$MODDIR" | sort -h | tail -n20 || true echo "==> Removing /boot from the image to save space" -rm -rfv "${BUILDROOT}"/boot +rm -rf "${BUILDROOT:?}"/boot DTB_SRC_DIR=$(echo -n "${BUILDROOT}"/usr/lib/linux-image-*) if [[ -d "$DTB_SRC_DIR" ]]; then @@ -118,18 +122,14 @@ if [[ -d "$DTB_SRC_DIR" ]]; then # Move every .dtb* file in the DTB_SRC_DIR to "${BUILDROOT}/root/dtb" -- maintain the directory structure find "${DTB_SRC_DIR}" -type f -name "*.dtb*" -exec sh -c 'for f; do mkdir -p "${0}/$(dirname "${f#'"${DTB_SRC_DIR}"'/}")" && mv "$f" "${0}/${f#'"${DTB_SRC_DIR}"'/}" ; done' "${BUILDROOT}/root/dtb" {} + || true - echo "--> Leftover files in ${DTB_SRC_DIR}:" - find "${DTB_SRC_DIR}" -type f - # Now simply output the dtb directory to mkosi's OUTPUTDIR echo "==> Copying DTBs to mkosi output directory: ${OUTPUTDIR}/dtb" - mv -v "${BUILDROOT}/root/dtb" "${OUTPUTDIR}/dtb" + mv "${BUILDROOT}/root/dtb" "${OUTPUTDIR}/dtb" else echo "==> No DTB source directory found at ${DTB_SRC_DIR}; skipping DTB export" fi -# Show a 4-deep overview of the full rootfs contents -echo "==> Final contents of root filesystem (up to depth 4):" -du -h -x -d 4 "$BUILDROOT" | sort -h || true +echo "==> Final contents of root filesystem (up to depth 4, top 30):" +du -h -x -d 4 "$BUILDROOT" | sort -h | tail -n30 || true echo "==> CaptainOS finalize complete." diff --git a/mkosi.postinst b/mkosi.postinst index 6ce6f597..bafdb9b8 100755 --- a/mkosi.postinst +++ b/mkosi.postinst @@ -5,6 +5,12 @@ # creating directories and dropping config files. set -euo pipefail +echo "==> CaptainOS post-install: dump env at $(hostname)..." +env + +echo "==> CaptainOS post-install: show mounts at $(hostname)..." +mount || true + echo "==> CaptainOS post-install: configuring services..." # Ensure CA certificate bundle is generated (dpkg triggers may not @@ -37,4 +43,7 @@ done # Set default target to multi-user (no graphical) systemctl set-default multi-user.target 2>/dev/null || true +echo "==> List installed packages..." +dpkg -l --no-pager || true + echo "==> CaptainOS post-install complete." From 3117a14efcdc9d220e580ec31f1ab2a8d0cfcae6 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 20:49:02 +0200 Subject: [PATCH 019/143] gha: introduce FLAVOR_ID (nee KERNEL_VERSION) gha: fix Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36058289..3a77dfef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ permissions: packages: write env: - KERNEL_VERSION: 6.18.16 + DEFAULT_FLAVOR_ID: "trixie-full" jobs: # ------------------------------------------------------------------- @@ -93,13 +93,14 @@ jobs: fail-fast: false matrix: include: - - { arch: amd64, output_arch: x86_64, iso: true } - - { arch: arm64, output_arch: aarch64, iso: true } + - { arch: amd64, output_arch: x86_64, iso: true, FLAVOR_ID: "trixie-full" } + - { arch: arm64, output_arch: aarch64, iso: true, FLAVOR_ID: "trixie-full" } env: ARCH: ${{ matrix.arch }} KERNEL_MODE: skip MKOSI_MODE: docker ISO_MODE: docker + KERNEL_VERSION: ${{ matrix.FLAVOR_ID }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -126,7 +127,7 @@ jobs: - name: Upload initramfs artifacts uses: actions/upload-artifact@v6 with: - name: initramfs-${{ matrix.arch }} + name: initramfs-${{ matrix.FLAVOR_ID }}-${{ matrix.arch }} path: out/ retention-days: 1 @@ -138,8 +139,8 @@ jobs: if: ${{ matrix.iso }} # only if matrix entry had iso: true uses: actions/upload-artifact@v6 with: - name: iso-${{ matrix.arch }} - path: out/captainos-${{ env.KERNEL_VERSION }}-${{ matrix.output_arch }}.iso + name: iso-${{ matrix.FLAVOR_ID }}-${{ matrix.arch }} + path: out/captainos-${{ matrix.FLAVOR_ID }}-${{ matrix.output_arch }}.iso retention-days: 1 # ------------------------------------------------------------------- @@ -182,25 +183,25 @@ jobs: - name: Download initramfs artifacts (amd64) uses: actions/download-artifact@v6 with: - name: initramfs-amd64 + name: initramfs-${{ env.DEFAULT_FLAVOR_ID }}-amd64 path: out - name: Download ISO artifact (amd64) uses: actions/download-artifact@v6 with: - name: iso-amd64 + name: iso-${{ env.DEFAULT_FLAVOR_ID }}-amd64 path: out - name: Download initramfs artifacts (arm64) uses: actions/download-artifact@v6 with: - name: initramfs-arm64 + name: initramfs-${{ env.DEFAULT_FLAVOR_ID }}-arm64 path: out - name: Download ISO artifact (arm64) uses: actions/download-artifact@v6 with: - name: iso-arm64 + name: iso-${{ env.DEFAULT_FLAVOR_ID }}-arm64 path: out - name: Install uv From 1d653658d1b594bb339ae7abc4ac3c1a7c9aa8cc Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 21:06:42 +0200 Subject: [PATCH 020/143] release: include cfg.kernel_version as suffix to all tags Signed-off-by: Ricardo Pardini --- captain/cli/_release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/captain/cli/_release.py b/captain/cli/_release.py index 1511ab6d..a7dc34a4 100644 --- a/captain/cli/_release.py +++ b/captain/cli/_release.py @@ -195,6 +195,7 @@ def _cmd_release(cfg: Config, extra_args: list[str], args: object = None) -> Non exclude = getattr(args, "version_exclude", None) sha = _resolve_git_sha(args, cfg.project_dir) tag = oci.compute_version_tag(cfg.project_dir, sha, exclude=exclude) + tag = f"{tag}-{cfg.kernel_version}" if sub == "publish": target = getattr(args, "target", None) or cfg.arch From 408f4355e80c09a3009d20bd4d45254fd1bf5636 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 21:10:40 +0200 Subject: [PATCH 021/143] gha: bump external actions so GHA stops complaining about Node 24 Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a77dfef..3a7f9f6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,7 +106,7 @@ jobs: uses: actions/checkout@v6 - name: Download tools artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v8 with: name: tools-${{ matrix.arch }} path: mkosi.output/tools/${{ matrix.arch }} @@ -149,7 +149,7 @@ jobs: - name: Log in to GHCR if: github.ref == 'refs/heads/main' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -181,25 +181,25 @@ jobs: run: cat .github/config.env >> "$GITHUB_ENV" - name: Download initramfs artifacts (amd64) - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v8 with: name: initramfs-${{ env.DEFAULT_FLAVOR_ID }}-amd64 path: out - name: Download ISO artifact (amd64) - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v8 with: name: iso-${{ env.DEFAULT_FLAVOR_ID }}-amd64 path: out - name: Download initramfs artifacts (arm64) - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v8 with: name: initramfs-${{ env.DEFAULT_FLAVOR_ID }}-arm64 path: out - name: Download ISO artifact (arm64) - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v8 with: name: iso-${{ env.DEFAULT_FLAVOR_ID }}-arm64 path: out @@ -208,7 +208,7 @@ jobs: uses: astral-sh/setup-uv@v7 - name: Log in to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} From 63c7321abe237ca54f84a2aa36d08a0972e78696 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 21:23:14 +0200 Subject: [PATCH 022/143] gha: pass DEFAULT_FLAVOR_ID as KERNEL_VERSION for publish-combined gha: pass DEFAULT_FLAVOR_ID as KERNEL_VERSION for publish-combined - retry Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 3 +++ captain/config.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a7f9f6b..5ab165a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,6 +166,7 @@ jobs: # ------------------------------------------------------------------- publish-combined: if: github.ref == 'refs/heads/main' + name: "publish-combined" runs-on: ubuntu-latest needs: [ build-all ] env: @@ -215,4 +216,6 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Publish combined image to GHCR + env: + KERNEL_VERSION: "${{ env.DEFAULT_FLAVOR_ID }}" run: uv run ./build.py release publish diff --git a/captain/config.py b/captain/config.py index 523eaf29..44831389 100644 --- a/captain/config.py +++ b/captain/config.py @@ -13,7 +13,7 @@ log = logging.getLogger(__name__) -# Valid values for KERNEL_MODE and MKOSI_MODE. +# Valid values for ISO_MODE and MKOSI_MODE. VALID_MODES = ("docker", "native", "skip") # The single source of truth for the default kernel version. From edbd2fa5688af47244758acefe52b33ec1253a4c Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 22:00:37 +0200 Subject: [PATCH 023/143] captain/gha: KERNEL_VERSION (et al) is now FLAVOR_ID - `--flavor-id` Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 10 ++++------ captain/artifacts.py | 8 ++++---- captain/cli/__init__.py | 8 ++++---- captain/cli/_commands.py | 12 ++++++------ captain/cli/_main.py | 2 +- captain/cli/_parser.py | 28 ++++++++++++++-------------- captain/cli/_release.py | 12 ++++++------ captain/cli/_stages.py | 8 ++++---- captain/config.py | 16 ++++++++-------- captain/docker.py | 2 +- captain/iso.py | 2 +- captain/oci/_build.py | 14 +++++++------- captain/oci/_publish.py | 6 +++--- captain/qemu.py | 4 ++-- captain/util.py | 13 +------------ 15 files changed, 66 insertions(+), 79 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ab165a5..5b28fee3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,6 @@ jobs: arch: [ amd64, arm64 ] env: ARCH: ${{ matrix.arch }} - KERNEL_MODE: skip MKOSI_MODE: skip TOOLS_MODE: native steps: @@ -84,11 +83,11 @@ jobs: retention-days: 1 # ------------------------------------------------------------------- - # Build initramfs via mkosi (depends on kernel + tools) + # Build initramfs via mkosi (depends on tools) # ------------------------------------------------------------------- build-all: runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [ download-tools ] # build-kernel + needs: [ download-tools ] strategy: fail-fast: false matrix: @@ -97,10 +96,9 @@ jobs: - { arch: arm64, output_arch: aarch64, iso: true, FLAVOR_ID: "trixie-full" } env: ARCH: ${{ matrix.arch }} - KERNEL_MODE: skip MKOSI_MODE: docker ISO_MODE: docker - KERNEL_VERSION: ${{ matrix.FLAVOR_ID }} + FLAVOR_ID: ${{ matrix.FLAVOR_ID }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -217,5 +215,5 @@ jobs: - name: Publish combined image to GHCR env: - KERNEL_VERSION: "${{ env.DEFAULT_FLAVOR_ID }}" + FLAVOR_ID: "${{ env.DEFAULT_FLAVOR_ID }}" run: uv run ./build.py release publish diff --git a/captain/artifacts.py b/captain/artifacts.py index 3b919d30..82b07abe 100644 --- a/captain/artifacts.py +++ b/captain/artifacts.py @@ -38,7 +38,7 @@ def collect_kernel(cfg: Config) -> None: vmlinu_files = sorted(cfg.initramfs_output.glob("*.vmlinu*")) if vmlinu_files: vmlinuz_src = vmlinu_files[0] - vmlinuz_dst = out / f"vmlinuz-{cfg.kernel_version}-{cfg.arch_info.output_arch}" + vmlinuz_dst = out / f"vmlinuz-{cfg.flavor_id}-{cfg.arch_info.output_arch}" shutil.copy2(vmlinuz_src, vmlinuz_dst) log.info( "mkosi supplied kernel: %s (%s)", vmlinuz_dst, _human_size(vmlinuz_dst.stat().st_size) @@ -53,7 +53,7 @@ def collect_initramfs(cfg: Config) -> None: cpio_files = sorted(cfg.initramfs_output.glob("*.cpio*")) if cpio_files: initrd_src = cpio_files[0] - initrd_dst = out / f"initramfs-{cfg.kernel_version}-{cfg.arch_info.output_arch}" + initrd_dst = out / f"initramfs-{cfg.flavor_id}-{cfg.arch_info.output_arch}" shutil.copy2(initrd_src, initrd_dst) log.info("initramfs: %s (%s)", initrd_dst, _human_size(initrd_dst.stat().st_size)) else: @@ -67,7 +67,7 @@ def collect_dtbs(cfg): if dtb_dir.exists(): log.info("Found dtb directory in %s, copying to output...", dtb_dir) out = ensure_dir(cfg.output_dir) - target_dtb_dir = out / f"dtb-{cfg.kernel_version}-{cfg.arch_info.output_arch}" + target_dtb_dir = out / f"dtb-{cfg.flavor_id}-{cfg.arch_info.output_arch}" if target_dtb_dir.exists(): shutil.rmtree(target_dtb_dir) shutil.copytree(dtb_dir, target_dtb_dir) @@ -83,7 +83,7 @@ def collect_iso(cfg: Config) -> None: iso_files = sorted(iso_dir.glob("*.iso")) if iso_dir.is_dir() else [] if iso_files: iso_src = iso_files[0] - iso_dst = out / f"captainos-{cfg.kernel_version}-{cfg.arch_info.output_arch}.iso" + iso_dst = out / f"captainos-{cfg.flavor_id}-{cfg.arch_info.output_arch}.iso" shutil.copy2(iso_src, iso_dst) log.info("iso: %s (%s)", iso_dst, _human_size(iso_dst.stat().st_size)) diff --git a/captain/cli/__init__.py b/captain/cli/__init__.py index fa440c15..77481c1c 100644 --- a/captain/cli/__init__.py +++ b/captain/cli/__init__.py @@ -5,12 +5,12 @@ CLI args > environment variables > defaults -The subcommand (``build``, ``kernel``, ``tools``, …) is extracted from +The subcommand (``build``, ``initramfs``, ``tools``, …) is extracted from ``sys.argv`` *before* parsing so that flags work in any position:: - ./build.py --arch=arm64 kernel # works - ./build.py kernel --arch=arm64 # also works - ARCH=arm64 ./build.py kernel # also works + ./build.py --arch=arm64 initramfs # works + ./build.py initramfs --arch=arm64 # also works + ARCH=arm64 ./build.py initramfs # also works """ from captain.cli._main import main diff --git a/captain/cli/_commands.py b/captain/cli/_commands.py index c131224a..54e9df13 100644 --- a/captain/cli/_commands.py +++ b/captain/cli/_commands.py @@ -65,7 +65,7 @@ def _cmd_shell(cfg: Config, _extra_args: list[str]) -> None: def _cmd_clean(cfg: Config, _extra_args: list[str], args: object = None) -> None: - """Remove build artifacts for the selected kernel version, or all.""" + """Remove build artifacts for the selected flavor, or all.""" clean_all = getattr(args, "clean_all", False) if clean_all: @@ -75,8 +75,8 @@ def _cmd_clean(cfg: Config, _extra_args: list[str], args: object = None) -> None def _clean_version(cfg: Config) -> None: - """Remove build artifacts for a single kernel version.""" - kver = cfg.kernel_version + """Remove build artifacts for a single flavor.""" + kver = cfg.flavor_id log.info("Cleaning build artifacts for kernel %s (%s)...", kver, cfg.arch) mkosi_output = cfg.mkosi_output @@ -130,7 +130,7 @@ def _clean_version(cfg: Config) -> None: def _clean_all(cfg: Config) -> None: - """Remove all build artifacts (all kernel versions).""" + """Remove all build artifacts (all flavors).""" log.info("Cleaning ALL build artifacts...") mkosi_output = cfg.mkosi_output mkosi_cache = cfg.project_dir / "mkosi.cache" @@ -183,7 +183,7 @@ def _cmd_summary(cfg: Config, _extra_args: list[str]) -> None: case "docker": docker.build_builder(cfg) container_tree = f"/work/mkosi.output/tools/{cfg.arch}" - container_outdir = f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}" + container_outdir = f"/work/mkosi.output/initramfs/{cfg.flavor_id}/{cfg.arch}" docker.run_mkosi( cfg, f"--extra-tree={container_tree}", @@ -224,7 +224,7 @@ def _cmd_checksums(cfg: Config, _extra_args: list[str], args: object = None) -> # Default mode: produce checksums for the selected architecture. out = cfg.output_dir oarch = cfg.arch_info.output_arch - kver = cfg.kernel_version + kver = cfg.flavor_id arch_files = [ out / f"vmlinuz-{kver}-{oarch}", out / f"initramfs-{kver}-{oarch}", diff --git a/captain/cli/_main.py b/captain/cli/_main.py index b299d83a..286c237f 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -112,7 +112,7 @@ def main(project_dir: Path | None = None) -> None: case "docker": docker.build_builder(cfg) container_tree = f"/work/mkosi.output/tools/{cfg.arch}" - container_outdir = f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}" + container_outdir = f"/work/mkosi.output/initramfs/{cfg.flavor_id}/{cfg.arch}" docker.run_mkosi( cfg, f"--extra-tree={container_tree}", diff --git a/captain/cli/_parser.py b/captain/cli/_parser.py index 0b685de9..46229c48 100644 --- a/captain/cli/_parser.py +++ b/captain/cli/_parser.py @@ -9,7 +9,7 @@ import configargparse -from captain.config import DEFAULT_KERNEL_VERSION +from captain.config import DEFAULT_FLAVOR_ID # --------------------------------------------------------------------------- # Known subcommands (order matters for help text) @@ -197,14 +197,14 @@ def _add_common_flags(parser: configargparse.ArgParser) -> None: ) -def _add_kernel_flags(parser: configargparse.ArgParser) -> None: - """--kernel-version""" +def _add_flavor_flags(parser: configargparse.ArgParser) -> None: + """--flavor-id""" g = parser.add_argument_group("kernel") g.add_argument( - "--kernel-version", - env_var="KERNEL_VERSION", + "--flavor-id", + env_var="FLAVOR_ID", metavar="VER", - default=DEFAULT_KERNEL_VERSION, + default=DEFAULT_FLAVOR_ID, help="kernel version to build", ) @@ -537,18 +537,18 @@ def _add_tink_flags(parser: configargparse.ArgParser) -> None: _COMMAND_FLAGS: dict[str, list[Callable[..., None]]] = { "build": [ _add_common_flags, - _add_kernel_flags, + _add_flavor_flags, _add_tools_flags, _add_mkosi_flags, _add_iso_flags, ], "tools": [_add_common_flags, _add_tools_flags], - "initramfs": [_add_common_flags, _add_kernel_flags, _add_mkosi_flags], - "iso": [_add_common_flags, _add_kernel_flags, _add_iso_flags], - "checksums": [_add_common_flags, _add_kernel_flags, _add_checksums_flags], - "release": [_add_common_flags, _add_kernel_flags, _add_release_flags], + "initramfs": [_add_common_flags, _add_flavor_flags, _add_mkosi_flags], + "iso": [_add_common_flags, _add_flavor_flags, _add_iso_flags], + "checksums": [_add_common_flags, _add_flavor_flags, _add_checksums_flags], + "release": [_add_common_flags, _add_flavor_flags, _add_release_flags], "shell": [_add_common_flags], - "clean": [_add_common_flags, _add_kernel_flags, _add_clean_flags], - "summary": [_add_common_flags, _add_kernel_flags, _add_summary_flags], - "qemu-test": [_add_common_flags, _add_kernel_flags, _add_qemu_flags, _add_tink_flags], + "clean": [_add_common_flags, _add_flavor_flags, _add_clean_flags], + "summary": [_add_common_flags, _add_flavor_flags, _add_summary_flags], + "qemu-test": [_add_common_flags, _add_flavor_flags, _add_qemu_flags, _add_tink_flags], } diff --git a/captain/cli/_release.py b/captain/cli/_release.py index a7dc34a4..5b6fa496 100644 --- a/captain/cli/_release.py +++ b/captain/cli/_release.py @@ -15,7 +15,7 @@ from ._parser import ( _add_common_flags, - _add_kernel_flags, + _add_flavor_flags, _add_release_base_flags, _add_release_pull_output, _add_release_tag_version, @@ -30,13 +30,13 @@ _RELEASE_SUBCMD_INFO: dict[str, tuple[str, list]] = { "publish": ( "Publish artifacts as a multi-arch OCI image", - [_add_common_flags, _add_kernel_flags, _add_release_base_flags, _add_release_target_flag], + [_add_common_flags, _add_flavor_flags, _add_release_base_flags, _add_release_target_flag], ), "pull": ( "Pull and extract artifacts (amd64, arm64, or combined)", [ _add_common_flags, - _add_kernel_flags, + _add_flavor_flags, _add_release_base_flags, _add_release_target_flag, _add_release_pull_output, @@ -44,7 +44,7 @@ ), "tag": ( "Tag all artifact images with a version", - [_add_common_flags, _add_kernel_flags, _add_release_base_flags, _add_release_tag_version], + [_add_common_flags, _add_flavor_flags, _add_release_base_flags, _add_release_tag_version], ), } @@ -135,7 +135,7 @@ def _cmd_release(cfg: Config, extra_args: list[str], args: object = None) -> Non sha = _resolve_git_sha(args, cfg.project_dir) env_args: list[str] = [ "-e", - f"KERNEL_VERSION={cfg.kernel_version}", + f"FLAVOR_ID={cfg.flavor_id}", "-e", f"REGISTRY={registry}", "-e", @@ -195,7 +195,7 @@ def _cmd_release(cfg: Config, extra_args: list[str], args: object = None) -> Non exclude = getattr(args, "version_exclude", None) sha = _resolve_git_sha(args, cfg.project_dir) tag = oci.compute_version_tag(cfg.project_dir, sha, exclude=exclude) - tag = f"{tag}-{cfg.kernel_version}" + tag = f"{tag}-{cfg.flavor_id}" if sub == "publish": target = getattr(args, "target", None) or cfg.arch diff --git a/captain/cli/_stages.py b/captain/cli/_stages.py index e59ce308..92cc2796 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/_stages.py @@ -1,4 +1,4 @@ -"""Build stage orchestration — kernel, tools, mkosi, ISO.""" +"""Build stage orchestration — tools, mkosi, ISO.""" from __future__ import annotations @@ -85,7 +85,7 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: docker.build_builder(cfg) log.info("Building initrd with mkosi (docker)...") tools_tree = f"/work/mkosi.output/tools/{cfg.arch}" - output_dir = f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}" + output_dir = f"/work/mkosi.output/initramfs/{cfg.flavor_id}/{cfg.arch}" docker.run_mkosi( cfg, f"--extra-tree={tools_tree}", @@ -97,7 +97,7 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: docker.fix_docker_ownership( cfg, [ - f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}", + f"/work/mkosi.output/initramfs/{cfg.flavor_id}/{cfg.arch}", "/work/out", ], ) @@ -112,7 +112,7 @@ def _build_iso_stage(cfg: Config) -> None: return # --- idempotency -------------------------------------------------- - iso_path = cfg.iso_output / f"captainos-{cfg.kernel_version}-{cfg.arch_info.output_arch}.iso" + iso_path = cfg.iso_output / f"captainos-{cfg.flavor_id}-{cfg.arch_info.output_arch}.iso" if iso_path.is_file() and not cfg.force_iso: log.info("ISO already built: %s (use --force-iso to rebuild)", iso_path) return diff --git a/captain/config.py b/captain/config.py index 44831389..c87a2f39 100644 --- a/captain/config.py +++ b/captain/config.py @@ -16,9 +16,9 @@ # Valid values for ISO_MODE and MKOSI_MODE. VALID_MODES = ("docker", "native", "skip") -# The single source of truth for the default kernel version. -# Override at runtime via --kernel-version or KERNEL_VERSION env var. -DEFAULT_KERNEL_VERSION = "6.18.16" +# The single source of truth for the default flavor. +# Override at runtime via --flavor-id or FLAVOR_ID env var. +DEFAULT_FLAVOR_ID = "6.18.16" @dataclass(slots=True) @@ -31,7 +31,7 @@ class Config: # Target arch: str = "amd64" - kernel_version: str = DEFAULT_KERNEL_VERSION + flavor_id: str = DEFAULT_FLAVOR_ID # Docker builder_image: str = "captainos-builder" @@ -100,7 +100,7 @@ def from_args(cls, args: argparse.Namespace, project_dir: Path | None) -> Config project_dir=project_dir, output_dir=project_dir / "out", arch=getattr(args, "arch", "amd64"), - kernel_version=getattr(args, "kernel_version", DEFAULT_KERNEL_VERSION), + flavor_id=getattr(args, "flavor_id", DEFAULT_FLAVOR_ID), builder_image=getattr(args, "builder_image", "captainos-builder"), no_cache=getattr(args, "no_cache", False), tools_mode=getattr(args, "tools_mode", "docker"), @@ -126,7 +126,7 @@ def from_env(cls, project_dir: Path) -> Config: project_dir=project_dir, output_dir=project_dir / "out", arch=os.environ.get("ARCH", "amd64"), - kernel_version=os.environ.get("KERNEL_VERSION", DEFAULT_KERNEL_VERSION), + flavor_id=os.environ.get("FLAVOR_ID", DEFAULT_FLAVOR_ID), builder_image=os.environ.get("BUILDER_IMAGE", "captainos-builder"), no_cache=os.environ.get("NO_CACHE") == "1", tools_mode=os.environ.get("TOOLS_MODE", "docker"), @@ -157,12 +157,12 @@ def mkosi_output(self) -> Path: @property def initramfs_output(self) -> Path: """Per-version, per-arch directory for mkosi initramfs output.""" - return self.project_dir / "mkosi.output" / "initramfs" / self.kernel_version / self.arch + return self.project_dir / "mkosi.output" / "initramfs" / self.flavor_id / self.arch @property def iso_output(self) -> Path: """Per-version, per-arch directory for the built ISO image.""" - return self.project_dir / "mkosi.output" / "iso" / self.kernel_version / self.arch + return self.project_dir / "mkosi.output" / "iso" / self.flavor_id / self.arch @property def iso_staging(self) -> Path: diff --git a/captain/docker.py b/captain/docker.py index d9741f1f..ef3d8f57 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -163,7 +163,7 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "-e", f"ARCH={cfg.arch}", "-e", - f"KERNEL_VERSION={cfg.kernel_version}", + f"FLAVOR_ID={cfg.flavor_id}", "-e", f"FORCE_TOOLS={int(cfg.force_tools)}", "-e", diff --git a/captain/iso.py b/captain/iso.py index 8640e5c6..fb577778 100644 --- a/captain/iso.py +++ b/captain/iso.py @@ -96,7 +96,7 @@ def build(cfg: Config) -> None: (grub_dir / "grub.cfg").write_text(_grub_cfg(cfg.arch)) iso_dir = ensure_dir(cfg.iso_output) - iso_path = iso_dir / f"captainos-{cfg.kernel_version}-{cfg.arch_info.output_arch}.iso" + iso_path = iso_dir / f"captainos-{cfg.flavor_id}-{cfg.arch_info.output_arch}.iso" log.info("Building ISO with grub-mkrescue (%s)...", grub_platform) grub_mkrescue = shutil.which("grub-mkrescue") diff --git a/captain/oci/_build.py b/captain/oci/_build.py index 23776ee5..0fc28338 100644 --- a/captain/oci/_build.py +++ b/captain/oci/_build.py @@ -39,7 +39,7 @@ def _collect_arch_artifacts( project_dir: Path, out: Path, arch: str, - kernel_version: str, + flavor_id: str, ) -> list[Path]: """Collect and return the artifact files for a single architecture. @@ -48,10 +48,10 @@ def _collect_arch_artifacts( oarch = get_arch_info(arch).output_arch ## # Collect kernel @TODO probably fix or remove? - ## vmlinuz_dir = project_dir / "mkosi.output" / "kernel" / kernel_version / arch + ## vmlinuz_dir = project_dir / "mkosi.output" / "kernel" / flavor_id / arch ## vmlinuz_files = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] ## - ## vmlinuz_dst = out / f"vmlinuz-{kernel_version}-{oarch}" + ## vmlinuz_dst = out / f"vmlinuz-{flavor_id}-{oarch}" ## if vmlinuz_files: ## shutil.copy2(vmlinuz_files[0], vmlinuz_dst) ## log.info("kernel: %s", vmlinuz_dst) @@ -59,11 +59,11 @@ def _collect_arch_artifacts( ## log.warning("No kernel image found for %s", arch) arch_files = [ - out / f"vmlinuz-{kernel_version}-{oarch}", - out / f"initramfs-{kernel_version}-{oarch}", - out / f"captainos-{kernel_version}-{oarch}.iso", + out / f"vmlinuz-{flavor_id}-{oarch}", + out / f"initramfs-{flavor_id}-{oarch}", + out / f"captainos-{flavor_id}-{oarch}.iso", ] - checksums_path = out / f"sha256sums-{kernel_version}-{oarch}.txt" + checksums_path = out / f"sha256sums-{flavor_id}-{oarch}.txt" artifacts.collect_checksums(arch_files, checksums_path) push_files = [*arch_files, checksums_path] diff --git a/captain/oci/_publish.py b/captain/oci/_publish.py index b67ced6e..e757eda8 100644 --- a/captain/oci/_publish.py +++ b/captain/oci/_publish.py @@ -194,7 +194,7 @@ def publish( cfg.project_dir, out, arch, - cfg.kernel_version, + cfg.flavor_id, ) # Create deterministic layer tars (shared across manifest pushes). @@ -204,13 +204,13 @@ def publish( arch_layer_tars[arch] = [_deterministic_tar(f, out) for f in files] # A single layer for all DTBs, if any; those are highly compressible together. - dtb_dir_in = out / f"dtb-{cfg.kernel_version}-{get_arch_info(arch).output_arch}" + dtb_dir_in = out / f"dtb-{cfg.flavor_id}-{get_arch_info(arch).output_arch}" if not dtb_dir_in.is_dir(): log.warning("No dtbs directory found for %s: %s", arch, dtb_dir_in) else: log.info(f"Found DTB directory for {arch}: {dtb_dir_in}") all_dtb_files: list[Path] = sorted(dtb_dir_in.glob("**/*.dtb*")) - dtb_tar_path = out / f"dtbs-{cfg.kernel_version}-{arch}.tar" + dtb_tar_path = out / f"dtbs-{cfg.flavor_id}-{arch}.tar" with tarfile.open(dtb_tar_path, "w") as tar: for f in all_dtb_files: tar.add(f, arcname=f.relative_to(out)) diff --git a/captain/qemu.py b/captain/qemu.py index bbbd8d5c..ae0da4a0 100644 --- a/captain/qemu.py +++ b/captain/qemu.py @@ -65,8 +65,8 @@ def run_qemu(cfg: Config, args: argparse.Namespace | None = None) -> None: :mod:`configargparse`. When provided, Tinkerbell kernel cmdline parameters are drawn from it instead of the environment. """ - kernel = cfg.output_dir / f"vmlinuz-{cfg.kernel_version}-{cfg.arch_info.output_arch}" - initrd = cfg.output_dir / f"initramfs-{cfg.kernel_version}-{cfg.arch_info.output_arch}" + kernel = cfg.output_dir / f"vmlinuz-{cfg.flavor_id}-{cfg.arch_info.output_arch}" + initrd = cfg.output_dir / f"initramfs-{cfg.flavor_id}-{cfg.arch_info.output_arch}" missing: list[str] = [] if not kernel.is_file(): diff --git a/captain/util.py b/captain/util.py index 797f8fa3..61736fad 100644 --- a/captain/util.py +++ b/captain/util.py @@ -142,17 +142,6 @@ def _missing(cmds: list[str]) -> list[str]: return [cmd for cmd in cmds if _shutil.which(cmd) is None] -def check_kernel_dependencies(arch: str) -> list[str]: - """Check host tools required for a native kernel build. - - Returns a list of missing command names (empty if all found). - """ - required = ["make", "gcc", "flex", "bison", "bc", "rsync", "strip", "zstd", "depmod"] - if arch in ("arm64", "aarch64"): - required += ["aarch64-linux-gnu-gcc", "aarch64-linux-gnu-strip"] - return _missing(required) - - def check_mkosi_dependencies() -> list[str]: """Check host tools required for a native mkosi image build. @@ -183,4 +172,4 @@ def check_dependencies(arch: str) -> list[str]: Returns a list of missing command names (empty if all found). """ - return check_kernel_dependencies(arch) + check_mkosi_dependencies() + return check_mkosi_dependencies() From 2afad9fe8917596fca7aa7526971c2b98cbdd0c9 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 22:02:35 +0200 Subject: [PATCH 024/143] captain: cleanups / doc updates WiP (drop kernel build-related) Signed-off-by: Ricardo Pardini --- .gitignore | 1 - README.md | 5 +- captain/oci/_build.py | 11 --- docs/kernel-build.md | 177 +----------------------------------------- mkosi.conf | 6 -- 5 files changed, 3 insertions(+), 197 deletions(-) diff --git a/.gitignore b/.gitignore index 6781065c..b09d4542 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ mkosi.cache/ mkosi.tools/ mkosi.tools.manifest mkosi.builddir/ -kernel.configs/.config.resolved.* out/ # Editor diff --git a/README.md b/README.md index 00e2a5b5..799ab842 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,7 @@ build configuration: --no-cache rebuild builder image without Docker cache kernel: - --kernel-version VER kernel version to build (default: 6.18.16) - --kernel-src PATH path to local kernel source tree - --kernel-mode {docker,native,skip} kernel stage execution mode (default: docker) - --force-kernel force kernel rebuild even if outputs exist + --flavor-id VER kernel version to build (default: 6.18.16) tools: --tools-mode {docker,native,skip} tools stage execution mode (default: docker) diff --git a/captain/oci/_build.py b/captain/oci/_build.py index 0fc28338..0f39b69f 100644 --- a/captain/oci/_build.py +++ b/captain/oci/_build.py @@ -47,17 +47,6 @@ def _collect_arch_artifacts( """ oarch = get_arch_info(arch).output_arch - ## # Collect kernel @TODO probably fix or remove? - ## vmlinuz_dir = project_dir / "mkosi.output" / "kernel" / flavor_id / arch - ## vmlinuz_files = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] - ## - ## vmlinuz_dst = out / f"vmlinuz-{flavor_id}-{oarch}" - ## if vmlinuz_files: - ## shutil.copy2(vmlinuz_files[0], vmlinuz_dst) - ## log.info("kernel: %s", vmlinuz_dst) - ## else: - ## log.warning("No kernel image found for %s", arch) - arch_files = [ out / f"vmlinuz-{flavor_id}-{oarch}", out / f"initramfs-{flavor_id}-{oarch}", diff --git a/docs/kernel-build.md b/docs/kernel-build.md index 0130ef39..0f5fb217 100644 --- a/docs/kernel-build.md +++ b/docs/kernel-build.md @@ -1,178 +1,5 @@ # Kernel Build Process -CaptainOS builds a custom Linux kernel from upstream source with -project-specific defconfigs. The build is orchestrated by the `captain.kernel` -module and driven through the CLI (`./build.py kernel`). +CaptainOS uses standard or custom Debian-packaged kernels. -## Quick Start - -```bash -# Build the kernel (defaults to Docker mode, amd64, kernel 6.18.16) -./build.py kernel - -# Build for arm64 -./build.py kernel --arch=arm64 - -# Use a local kernel source tree instead of downloading -./build.py kernel --kernel-src=/path/to/linux - -# Force a rebuild even if outputs already exist -./build.py kernel --force-kernel - -# Build natively (no Docker) -./build.py kernel --kernel-mode=native -``` - -Every flag also has an environment variable form (e.g. `ARCH`, `KERNEL_VERSION`, -`KERNEL_MODE`, `KERNEL_SRC`, `FORCE_KERNEL`). - -## Execution Modes - -The `--kernel-mode` flag controls how the kernel is built: - -| Mode | Description | -|----------|------------------------------------------------------------------------| -| `docker` | (default) Builds inside the `captainos-builder` Docker container. | -| `native` | Builds directly on the host; requires kernel build tools to be installed. | -| `skip` | Skips the kernel stage entirely. | - -In Docker mode, the builder container is based on Debian Trixie and includes -all kernel build dependencies (gcc, make, flex, bison, bc, libelf, libssl, -dwarves/pahole, etc.) plus cross-compilation toolchains for arm64. Inside -the container, the mode is forced to `native` so Docker is never invoked -recursively. - -## Build Pipeline - -The `kernel.build()` function runs four stages sequentially: - -### 1. Download - -`kernel.download_kernel()` fetches the upstream tarball from -`cdn.kernel.org`: - -``` -https://cdn.kernel.org/pub/linux/kernel/v{major}.x/linux-{version}.tar.xz -``` - -The tarball is extracted into `/var/tmp/kernel-build/linux-{version}/`. -If the extracted source directory already exists, it is reused. - -When `--kernel-src` is set, this step is skipped and the local source tree -is used as-is. - -### 2. Configure - -`kernel.configure_kernel()` applies the project defconfig for the target -architecture: - -1. Copies `kernel.configs/{major}.{minor}.y.{arch}` into the source tree as `.config`. -2. Runs `make olddefconfig` to resolve any new symbols against defaults. -3. Saves the fully resolved config to `kernel.configs/.config.resolved.{branch}.{arch}` for - debugging. - -If no defconfig file exists for the target kernel version and arch, the build -exits with an error listing the available kernel branches. - -For `x86_64` builds, the `COMMAND_LINE_SIZE` is patched from 2048 to 4096 -in `arch/x86/include/asm/setup.h` because Tinkerbell passes large kernel -command lines. - -Cross-compilation is set up automatically via the `ARCH` and -`CROSS_COMPILE` environment variables (e.g. `CROSS_COMPILE=aarch64-linux-gnu-` -for arm64). - -### 3. Compile - -`kernel.build_kernel()` runs the parallel make: - -```bash -make -j$(nproc) {image_target} modules -``` - -The image target is architecture-dependent: - -| Architecture | `ARCH` | Image target | Output path | -|-------------|-----------|-------------|-------------------------------| -| amd64 | `x86_64` | `bzImage` | `arch/x86/boot/bzImage` | -| arm64 | `arm64` | `Image` | `arch/arm64/boot/Image` | - -After compilation, `make -s kernelrelease` is invoked to determine the -exact built kernel version string (e.g. `6.18.16-captainos`). - -### 4. Install - -`kernel.install_kernel()` places the built artifacts into the output tree: - -1. **Module installation** — `make INSTALL_MOD_PATH=... modules_install` - installs modules to `mkosi.output/kernel/{version}/{arch}/modules/`. -2. **Strip** — Debug symbols are stripped from every `.ko` file with - `strip --strip-unneeded`. -3. **Compress** — Modules are compressed with `zstd --rm -q -19` producing - `.ko.zst` files. The defconfig enables `CONFIG_MODULE_COMPRESS_ZSTD` - and `CONFIG_MODULE_DECOMPRESS` so the kernel loads compressed modules at - runtime. -4. **Clean up** — The `build` and `source` symlinks are removed from the - modules directory. -5. **Merged-usr layout** — Modules are relocated from `lib/modules/` to - `usr/lib/modules/` to follow the merged-usr filesystem convention. -6. **depmod** — `depmod -a` regenerates module dependency metadata for - the compressed `.ko.zst` files. -7. **Kernel image** — The kernel image (`vmlinuz-{version}`) is copied to - `mkosi.output/kernel/{kernel_version}/{arch}/`. It is kept separate from - the extra-tree so it is **not** included in the initramfs CPIO — iPXE loads - the kernel independently. - -## Output Layout - -After a successful build, the kernel stage produces: - -``` -mkosi.output/ -├── tools/{arch}/ # tools only (containerd, runc, etc.) -│ ├── usr/local/bin/ -│ └── opt/cni/bin/ -└── kernel/{kernel_version}/{arch}/ - ├── vmlinuz-{version} # kernel image (bzImage or Image) - └── modules/ # passed as --extra-tree to mkosi - └── usr/lib/modules/{version}/ - ├── kernel/... # compressed .ko.zst module files - ├── modules.dep - ├── modules.dep.bin - └── ... -``` - -The tools and modules subtrees are both passed to mkosi via -separate `--extra-tree=` flags and merged into the initramfs. The vmlinuz -image is collected into `out/` by `artifacts.collect_kernel()` as -`out/vmlinuz-{kernel_version}-{arch}`. - -## Idempotency - -The CLI checks for existing outputs before starting: - -- If both `mkosi.output/kernel/{kernel_version}/{arch}/modules/usr/lib/modules/` and - `mkosi.output/kernel/{kernel_version}/{arch}/vmlinuz-*` exist, the build is skipped. -- Use `--force-kernel` (or `FORCE_KERNEL=1`) to force a rebuild. -- If modules exist but the vmlinuz is missing, the kernel is rebuilt - automatically. - -## Defconfigs - -Architecture-specific defconfigs live in the `kernel.configs/` directory, -named by stable branch: `{major}.{minor}.y.{arch}`. This allows -multiple kernel versions to coexist — each stable branch (e.g. 6.18.y, -6.19.y) has its own config per architecture. - -- `kernel.configs/6.18.y.amd64` — x86_64 config adapted for kernel 6.18. -- `kernel.configs/6.18.y.arm64` — arm64 config adapted for kernel 6.18. -- `kernel.configs/6.19.y.amd64` — x86_64 config adapted for kernel 6.19. -- `kernel.configs/6.19.y.arm64` — arm64 config adapted for kernel 6.19. - -The default kernel version is defined as `DEFAULT_KERNEL_VERSION` in -`captain/config.py` and can be overridden via `--kernel-version` or -the `KERNEL_VERSION` environment variable. - -Both configs include support for bare-metal provisioning, container -runtimes (cgroups v2, namespaces, overlayfs), and broad hardware/network -driver coverage. The local version suffix is set to `-captainos`. +The default flavor, `trixie-full`, uses `linux-image-generic` default Debian kernel. diff --git a/mkosi.conf b/mkosi.conf index fceb8382..50d24b3a 100644 --- a/mkosi.conf +++ b/mkosi.conf @@ -26,12 +26,6 @@ WithDocs=no # rootfs → tmpfs via switch_root before exec'ing systemd. This makes # pivot_root(2) work for container runtimes (runc). -# Pre-built kernel modules and tools are injected via separate --extra-tree= -# flags on the CLI: -# --extra-tree=mkosi.output/tools/{arch}/ (tools) -# This keeps mkosi.conf architecture-neutral so both amd64 and arm64 builds -# can coexist under mkosi.output/. - Packages= linux-image-generic tiny-initramfs # a tiny initrd builder to account for the linux-image dependency From 3ffa6253cbb8b179bb23a1cf986e786cc2dfb304 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 3 Apr 2026 22:27:50 +0200 Subject: [PATCH 025/143] captain: DEFAULT_FLAVOR_ID = "trixie-full" Signed-off-by: Ricardo Pardini --- captain/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/captain/config.py b/captain/config.py index c87a2f39..63781cdf 100644 --- a/captain/config.py +++ b/captain/config.py @@ -18,7 +18,7 @@ # The single source of truth for the default flavor. # Override at runtime via --flavor-id or FLAVOR_ID env var. -DEFAULT_FLAVOR_ID = "6.18.16" +DEFAULT_FLAVOR_ID = "trixie-full" @dataclass(slots=True) From f529db91df6c76724c09dc69eb3c12300fdb7a4c Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sat, 4 Apr 2026 23:37:50 +0200 Subject: [PATCH 026/143] flavors: introduce flavors but logging sucks and no dataclass better, logging still sucks for Full -> Common much better in-package this sucks less cosmetics some static and template rendering and hardcoded cleanup kinda-works kinda works Signed-off-by: Ricardo Pardini --- .gitignore | 11 ++ captain/cli/_main.py | 13 +- captain/config.py | 7 + captain/docker.py | 1 + captain/flavor.py | 166 ++++++++++++++++++ captain/flavors/common_armbian/__init__.py | 36 ++++ .../etc/apt/sources.list.d/armbian-next.list | 2 + .../usr/share/keyrings/armbian-next.gpg | Bin 0 -> 1754 bytes captain/flavors/common_debian/__init__.py | 39 ++++ .../flavors/common_debian/mkosi.conf.j2 | 3 +- .../mkosi.extra}/etc/acpi/events/powerbtn | 0 .../mkosi.extra}/etc/acpi/powerbtn.sh | 0 .../etc/cni/net.d/10-bridge.conflist | 0 .../mkosi.extra}/etc/containerd/config.toml | 0 .../mkosi.extra}/etc/locale.conf | 0 .../common_debian/mkosi.extra}/etc/machine-id | 0 .../mkosi.extra}/etc/modules-load.d/ipmi.conf | 0 .../common_debian/mkosi.extra}/etc/motd | 0 .../mkosi.extra}/etc/nerdctl/nerdctl.toml | 0 .../common_debian/mkosi.extra}/etc/os-release | 0 .../mkosi.extra}/etc/rsyslog.conf | 0 .../etc/sysctl.d/99-ip-forward.conf | 0 .../etc/sysctl.d/99-quiet-audit.conf | 0 .../etc/systemd/network/80-dhcp.network | 0 .../etc/systemd/resolved.conf.d/no-stub.conf | 0 .../systemd/system/captainos-banner.service | 0 .../system/captainos-static-network.service | 0 .../etc/systemd/system/containerd.service | 0 .../etc/systemd/system/default.target | 0 .../etc/systemd/system/initrd-cleanup.service | 0 .../systemd/system/initrd-parse-etc.service | 0 .../systemd/system/initrd-switch-root.service | 0 .../systemd/system/initrd-switch-root.target | 0 .../system/initrd-udevadm-cleanup-db.service | 0 .../etc/systemd/system/initrd.target | 0 .../system/rsyslog-hostname-reload.path | 0 .../system/rsyslog-hostname-reload.service | 0 .../etc/systemd/system/rsyslog.service | 0 .../serial-getty@.service.d/autologin.conf | 0 .../systemd/system/systemd-firstboot.service | 0 .../systemd/system/tink-agent-setup.service | 0 .../etc/systemd/system/tink-agent.service | 0 .../mkosi.extra}/etc/systemd/timesyncd.conf | 0 .../common_debian/mkosi.extra}/etc/timezone | 0 .../flavors/common_debian/mkosi.extra}/init | 0 .../common_debian/mkosi.extra}/root/.bashrc | 0 .../common_debian/mkosi.extra}/root/.profile | 0 .../usr/local/bin/captainos-banner | 0 .../usr/local/bin/captainos-static-network | 0 .../mkosi.extra}/usr/local/bin/rsyslog-start | 0 .../usr/local/bin/tink-agent-setup | 0 .../usr/local/bin/tink-agent-start | 0 .../common_debian/mkosi.finalize.sh.j2 | 0 .../common_debian/mkosi.postinst.sh.j2 | 0 captain/flavors/trixie_full/__init__.py | 32 ++++ captain/flavors/trixie_rockchip64/__init__.py | 32 ++++ pyproject.toml | 8 +- 57 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 captain/flavor.py create mode 100644 captain/flavors/common_armbian/__init__.py create mode 100644 captain/flavors/common_armbian/mkosi.sandbox/etc/apt/sources.list.d/armbian-next.list create mode 100644 captain/flavors/common_armbian/mkosi.sandbox/usr/share/keyrings/armbian-next.gpg create mode 100644 captain/flavors/common_debian/__init__.py rename mkosi.conf => captain/flavors/common_debian/mkosi.conf.j2 (95%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/acpi/events/powerbtn (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/acpi/powerbtn.sh (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/cni/net.d/10-bridge.conflist (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/containerd/config.toml (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/locale.conf (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/machine-id (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/modules-load.d/ipmi.conf (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/motd (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/nerdctl/nerdctl.toml (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/os-release (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/rsyslog.conf (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/sysctl.d/99-ip-forward.conf (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/sysctl.d/99-quiet-audit.conf (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/network/80-dhcp.network (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/resolved.conf.d/no-stub.conf (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/captainos-banner.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/captainos-static-network.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/containerd.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/default.target (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/initrd-cleanup.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/initrd-parse-etc.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/initrd-switch-root.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/initrd-switch-root.target (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/initrd-udevadm-cleanup-db.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/initrd.target (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/rsyslog-hostname-reload.path (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/rsyslog-hostname-reload.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/rsyslog.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/serial-getty@.service.d/autologin.conf (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/systemd-firstboot.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/tink-agent-setup.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/system/tink-agent.service (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/systemd/timesyncd.conf (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/etc/timezone (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/init (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/root/.bashrc (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/root/.profile (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/usr/local/bin/captainos-banner (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/usr/local/bin/captainos-static-network (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/usr/local/bin/rsyslog-start (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/usr/local/bin/tink-agent-setup (100%) rename {mkosi.extra => captain/flavors/common_debian/mkosi.extra}/usr/local/bin/tink-agent-start (100%) rename mkosi.finalize => captain/flavors/common_debian/mkosi.finalize.sh.j2 (100%) rename mkosi.postinst => captain/flavors/common_debian/mkosi.postinst.sh.j2 (100%) create mode 100644 captain/flavors/trixie_full/__init__.py create mode 100644 captain/flavors/trixie_rockchip64/__init__.py diff --git a/.gitignore b/.gitignore index b09d4542..9af69344 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,19 @@ mkosi.tools.manifest mkosi.builddir/ out/ +# Generated artifacts +/mkosi.conf +/mkosi.finalize +/mkosi.postinst +/mkosi.extra +/mkosi.sandbox + # Editor *.swp *.swo *~ .vscode/ +.idea/ # OS .DS_Store @@ -28,3 +36,6 @@ dist/ .mypy_cache/ .pyright/ .pytest_cache/ + +# Ignore uv.lock for now; we should always use the latest version of packages etc +uv.lock diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 286c237f..f21a3ad0 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -19,10 +19,12 @@ import sys from pathlib import Path +import captain.flavor from captain import docker from captain.config import Config from captain.util import run +from ..flavor import BaseFlavor from ._commands import ( _cmd_build, _cmd_checksums, @@ -82,7 +84,10 @@ def main(project_dir: Path | None = None) -> None: cfg = Config.from_args(args, project_dir) cfg.mkosi_args = mkosi_args - # 7. Dispatch. + # 7. Instantiate the flavor (from cfg.flavor_id) + flavor: BaseFlavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + + # 8. Dispatch. dispatch: dict[str, object] = { "build": _cmd_build, "tools": _cmd_tools, @@ -96,8 +101,14 @@ def main(project_dir: Path | None = None) -> None: "qemu-test": _cmd_qemu_test, } + log.debug("Dispatching command '%s', extra args: %s", command, extra) handler = dispatch.get(command) if handler is not None: + # If building, or doing initramfs (mkosi), + # generate the flavor first so the files are in place. + if command in ("build", "initramfs"): + flavor.generate() + if command in ("qemu-test", "checksums", "release", "clean"): handler(cfg, extra, args=args) # type: ignore[operator] else: diff --git a/captain/config.py b/captain/config.py index 63781cdf..1baeff49 100644 --- a/captain/config.py +++ b/captain/config.py @@ -96,6 +96,10 @@ def from_args(cls, args: argparse.Namespace, project_dir: Path | None) -> Config if project_dir is None: raise ValueError("project_dir must be provided to Config.from_args") + log.debug( + "Creating Config from args: %s (env flavor: %s)", args, os.environ.get("FLAVOR_ID") + ) + return cls( project_dir=project_dir, output_dir=project_dir / "out", @@ -122,6 +126,9 @@ def from_env(cls, project_dir: Path) -> Config: for any non-CLI callers (e.g. tests, scripts) that need a ``Config`` without going through argparse. """ + + log.debug("Creating Config from env: %s", os.environ) + return cls( project_dir=project_dir, output_dir=project_dir / "out", diff --git a/captain/docker.py b/captain/docker.py index ef3d8f57..388ccd7f 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -188,6 +188,7 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: docker_args += ["-v", f"{cfg.project_dir}/mkosi.output:/work/mkosi.output"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.extra:/work/mkosi.extra"] + docker_args += ["-v", f"{cfg.project_dir}/mkosi.sandbox:/work/mkosi.sandbox"] docker_args += ["-v", f"{cfg.project_dir}/out:/work/out"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.conf:/work/mkosi.conf"] diff --git a/captain/flavor.py b/captain/flavor.py new file mode 100644 index 00000000..e56e472b --- /dev/null +++ b/captain/flavor.py @@ -0,0 +1,166 @@ +"""Flavor-specific configuration.""" + +from __future__ import annotations + +import logging +import shutil +from pathlib import Path +from typing import Protocol, runtime_checkable + +import jinja2 + +from captain.config import Config + +log = logging.getLogger(__name__) + + +@runtime_checkable +class BaseFlavor(Protocol): + cfg: Config + id: str + name: str + description: str + flavor_dir: Path + supported_architectures: frozenset[str] + template_map: dict[str, list[Path]] + static_map: dict[str, Path] + + def setup(self, cfg: Config, flavor_dir: Path) -> None: + if cfg is None: + raise ValueError("cfg (Config) cannot be None") + self.cfg = cfg + + if flavor_dir is None: + raise ValueError("flavor_dir (Path) cannot be None") + if not flavor_dir.is_dir(): + raise ValueError(f"flavor_dir {flavor_dir} does not exist or is not a directory") + self.flavor_dir = flavor_dir + + self.template_map = {} + self.static_map = {} + log.debug("Called BaseFlavor.setup()...") + pass + + def generate(self): + log.debug("Called BaseFlavor.generate()...") + # Before generating, cleanup known targets. @TODO make dir disposable instead + log.debug("Cleaning up old generated files in %s", self.cfg.project_dir) + shutil.rmtree(self.cfg.project_dir / "mkosi.conf", ignore_errors=True) + shutil.rmtree(self.cfg.project_dir / "mkosi.postinst", ignore_errors=True) + shutil.rmtree(self.cfg.project_dir / "mkosi.finalize", ignore_errors=True) + shutil.rmtree(self.cfg.project_dir / "mkosi.extra", ignore_errors=True) + shutil.rmtree(self.cfg.project_dir / "mkosi.sandbox", ignore_errors=True) + + self.copy_static_files(self.cfg.project_dir) + self.render_templates(self.cfg.project_dir) # For compatibility + pass + + def specific_flavor_dir(self, flavor_id: str) -> Path: + flavor_id_underscore = flavor_id.replace("-", "_") + flavor_dir = self.cfg.project_dir / "captain" / "flavors" / flavor_id_underscore + + if not flavor_dir.is_dir(): + log.error( + "Specific Flavor dir '%s' not found. Expected to find directory %s", + flavor_id, + flavor_dir, + ) + raise SystemExit(1) + return flavor_dir + + def render_templates(self, output_dir: Path): + log.debug("Called BaseFlavor.render_templates() with output_dir: %s", output_dir) + # Use jinja2 to render all templates in self.template_map, writing output to output_dir + # The keys of self.template_map are the relative output paths (e.g. "mkosi.conf"), and the + # values are lists of Path objects pointing to Jinja2 template files. + # If more than one template is provided for a given output path, they should be rendered + # in order and concatenated together to produce the final output file. + for relative_output_path, template_paths in self.template_map.items(): + log.debug( + "Rendering templates for output path '%s': %s", + relative_output_path, + template_paths, + ) + rendered_content = "" + for template_path in template_paths: + log.debug("Rendering template %s", template_path) + # Here you would load the template file, render it with the appropriate context + # (e.g. using Jinja2), and append the rendered content to rendered_content. + # For example: + template = jinja2.Environment( + loader=jinja2.FileSystemLoader(template_path.parent), + undefined=jinja2.StrictUndefined, + ).get_template(template_path.name) + rendered_content += template.render(cfg=self.cfg, flavor=self) + + output_file_path = output_dir / relative_output_path + log.debug("Writing rendered content to %s", output_file_path) + output_file_path.parent.mkdir(parents=True, exist_ok=True) + output_file_path.write_text(rendered_content) + + # Make output_file executable @TODO: we will need a way to tell + output_file_path.chmod(output_file_path.stat().st_mode | 0o111) + + def copy_static_files(self, project_dir): + # Do a plain copy of all files in self.static_map to project_dir / relative_path, where + # relative_path is the key in self.static_map + for relative_path, source_path in self.static_map.items(): + destination_path = project_dir / relative_path + log.debug("Copying static file from '%s' to '%s'", source_path, destination_path) + destination_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, destination_path) + + +def create_and_setup_flavor_for_id(flavor_id: str, cfg: Config) -> BaseFlavor: + log.debug("Creating and setting up flavor for id '%s'", flavor_id) + flavor_id_underscore = flavor_id.replace("-", "_") + flavor_dir = cfg.project_dir / "captain" / "flavors" / flavor_id_underscore + + if not flavor_dir.is_dir(): + log.error( + "Flavor '%s' not found. Expected to find directory %s", + flavor_id, + flavor_dir, + ) + raise SystemExit(1) + + wanted_module = f"captain.flavors.{flavor_id_underscore}" + log.debug("Attempting to import flavor module %s from directory %s", wanted_module, flavor_dir) + + try: + module = __import__(wanted_module, fromlist=["create_flavor"]) + except ImportError as e: + log.error( + "Failed to import flavor module %s from directory %s: %s", + wanted_module, + flavor_dir, + e, + ) + raise e + + # Validate API explicitly + if not hasattr(module, "create_flavor"): + log.error("Flavor module %s does not define create_flavor()", wanted_module) + raise SystemExit(1) + + log.debug("Executing %s.create_flavor()", wanted_module) + flavor: BaseFlavor = module.create_flavor() + + if not isinstance(flavor, BaseFlavor): + log.error( + "create_flavor() in %s did not return BaseFlavor (got %r)", + wanted_module, + type(flavor), + ) + raise SystemExit(1) + + log.debug("Calling setup() on flavor %s with config: %s", flavor, cfg) + flavor.setup(cfg, flavor_dir) + + log.debug( + "Flavor is setup; description: %s; supported_architectures: %s", + flavor.description, + flavor.supported_architectures, + ) + + return flavor diff --git a/captain/flavors/common_armbian/__init__.py b/captain/flavors/common_armbian/__init__.py new file mode 100644 index 00000000..70ecb4b9 --- /dev/null +++ b/captain/flavors/common_armbian/__init__.py @@ -0,0 +1,36 @@ +import logging +from dataclasses import dataclass +from pathlib import Path + +from captain.config import Config +from captain.flavors.common_debian import DebianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +@dataclass +class ArmbianCommonFlavor(DebianCommonFlavor): + id = "common-armbian" + name = "Armbian Common" + description = "Base flavor for Armbian-based distros" + + def setup(self, cfg: Config, flavor_dir: Path) -> None: + super().setup(cfg, flavor_dir) + log.warning( + "ArmbianCommonFlavor setting up; mkosi arch: %s; flavor_dir: %s", + cfg.arch_info.mkosi_arch, + flavor_dir, + ) + this_flavor_dir = self.specific_flavor_dir("common-armbian") + + # Now, lets enumerate and add all the static files this flavor's mkosi.sandbox directory + # and add them to self.static_map with the key being the relative path from the flavor dir + extra_dir = this_flavor_dir / "mkosi.sandbox" + if extra_dir.exists() and extra_dir.is_dir(): + for extra_file in extra_dir.rglob("*"): + if extra_file.is_file(): + relative_path = extra_file.relative_to(this_flavor_dir) + self.static_map[str(relative_path)] = extra_file + + def extra_mkosi_conf_distribution(self) -> str: + return "# frak this" diff --git a/captain/flavors/common_armbian/mkosi.sandbox/etc/apt/sources.list.d/armbian-next.list b/captain/flavors/common_armbian/mkosi.sandbox/etc/apt/sources.list.d/armbian-next.list new file mode 100644 index 00000000..0b1c08db --- /dev/null +++ b/captain/flavors/common_armbian/mkosi.sandbox/etc/apt/sources.list.d/armbian-next.list @@ -0,0 +1,2 @@ +deb [signed-by=/usr/share/keyrings/armbian-next.gpg] https://apt-test.next.armbian.com armbian main +deb [signed-by=/usr/share/keyrings/armbian-next.gpg] https://apt-test.next.armbian.com armbian-trixie main diff --git a/captain/flavors/common_armbian/mkosi.sandbox/usr/share/keyrings/armbian-next.gpg b/captain/flavors/common_armbian/mkosi.sandbox/usr/share/keyrings/armbian-next.gpg new file mode 100644 index 0000000000000000000000000000000000000000..1e1b9eed3caf617998e259f7f21768d155fecbb0 GIT binary patch literal 1754 zcmV<01||8K0gVJ@>H>fP3;@&B51Qms9@TKnz|j@>$(2);alOF%T1onuG$Q|cT|&sL zytEPkHj@sBw`EG~AeUtd{7P_3lsa6^@vHM`tvz8SBq(yHY`8JMfdy*=CZ!@~gtAt4 zhXq_NI3V4PCFTZPNjZ<>(Xej#m#phSYpLI~2=G98k^CPkL~jn2ZjxzY@wK&$hQFYP zn#pD|Wrwi#){>nrnqriz*E`_bDwL5G=OHW;c$5&oHOiYHGei80cb6Mtq*ueFwi&pH z-3SSgw4DwjI$=a+v0Lq~)~)>TR19Su=H>M8Kjn%)isxQdN;xRzXme|{h5Akh;Fr8% zO8?p~wsPBwJ{mqXa%>aZ1)z^F^?F^(pr9>4<{)8D{w=fq>UmFd^X?d{JGp0xr#TP+ zt0B`-%xl4DX*9l$b;PS6P(I2G`K}MOxStd-$Y~9-jeh0vjwrv+=DWhB*G9E9!52f~ zc}BRa&s0nP*y0SW*(79j+B z*%x>`0J&w-BhJFtUtk7DWgC_S0%hs~fC3u>1q%rX2Lc8a3JC}c0t6NU0|5da0Rk6* z0162Z*I!@;M`at9kLU~lpa!JXupu(oB;p17i_4kq>g+%>&%iA%+H1heJN00MWi3Ss zA-Z1Cp>Qwp|$Z`SFWN}adzt`p{Jez`<*`q*vU3Hjux|bxdDv?W$FTe0So}Dfs>{9 z81Jhq@I*F{Fjz)F`81JAm&fS(FlraNYR}3>M(v&bbsb=2g#)saeNHfhbx8_T8&)U{ ze&`&@B4q~$)JOvvroKd*c|(c6o5=yov1)7QE(@dz{9N14n-@0uo`R(+{7j4b`s?MOve?TpG7U_ z?gXHO4_N%1?U_KFLi=(Q(q`FRS9-68(ncBQv{p6Jjh5^{)zrxQ2hMxY{rm2)g_j!d z_s$+KGUCYHl@0Du;8yXiF=|3xvY_Sl(o-;ZFDBUMKfNaZ5U`1_Pb?L3mSvl|R<%tF zry!8H*$cM?UIb^+0`ob%`y7f(3m>PG)>vRm%V18XHfrJ4uIqrzQuaT9$@gJ5vaAxB z@&q1@Y5ah4AN}6=!?6=!%9P|5raw>@wkogOrY2hb1H0K=B9LIyVeC|Z`raZHc z-mqT>Ub%fF@NaSmX_rS@tws%^Z^Ej>`EwhkOYR;19dpu)>On9xr4ke$w9u~MrtnIV z>Do7}!8@am6)$QdKlERVQes5%u&L20cRUweR@-51D*2gsggEBcC-tukEX_$^ig*0E zqSjy{Ub1%AhFeIp-Fq8@?8rqsfCY*3=Nm;N;s5vl9Mpjq>n(@A)R_zVLs;qft^v`7 zbR^E!-^Uk?$8Xe^n^ZoqNN-O wljP_nfrIzce3ANkg4UQr0(u`g*2QUsVCVNj$KBe>D})lt*S68!hE0DbpL>r+Gynhq literal 0 HcmV?d00001 diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py new file mode 100644 index 00000000..4200e4d0 --- /dev/null +++ b/captain/flavors/common_debian/__init__.py @@ -0,0 +1,39 @@ +import logging +from dataclasses import dataclass +from pathlib import Path + +from captain.config import Config +from captain.flavor import BaseFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +@dataclass +class DebianCommonFlavor(BaseFlavor): + id = "common-debian" + name = "Debian Common" + description = "Base flavor for Debian-based distros" + + def setup(self, cfg: Config, flavor_dir: Path) -> None: + super().setup(cfg, flavor_dir) + log.warning( + "DebianCommonFlavor setting up; mkosi arch: %s; flavor_dir: %s", + cfg.arch_info.mkosi_arch, + flavor_dir, + ) + this_flavor_dir = self.specific_flavor_dir("common-debian") + self.template_map["mkosi.conf"] = [this_flavor_dir / "mkosi.conf.j2"] + self.template_map["mkosi.postinst"] = [this_flavor_dir / "mkosi.postinst.sh.j2"] + self.template_map["mkosi.finalize"] = [this_flavor_dir / "mkosi.finalize.sh.j2"] + + # Now, lets enumerate and add all the static files this flavor's mkosi.extra directory + # and add them to self.static_map with the key being the relative path from the flavor dir + extra_dir = this_flavor_dir / "mkosi.extra" + if extra_dir.exists() and extra_dir.is_dir(): + for extra_file in extra_dir.rglob("*"): + if extra_file.is_file(): + relative_path = extra_file.relative_to(this_flavor_dir) + self.static_map[str(relative_path)] = extra_file + + def extra_mkosi_conf_distribution(self) -> str: + return "" diff --git a/mkosi.conf b/captain/flavors/common_debian/mkosi.conf.j2 similarity index 95% rename from mkosi.conf rename to captain/flavors/common_debian/mkosi.conf.j2 index 50d24b3a..dae28b35 100644 --- a/mkosi.conf +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -1,6 +1,7 @@ [Distribution] Distribution=debian Release=trixie +{{ flavor.extra_mkosi_conf_distribution() }} [Output] Format=cpio @@ -27,7 +28,7 @@ WithDocs=no # pivot_root(2) work for container runtimes (runc). Packages= - linux-image-generic + {{ ' '.join(flavor.kernel_packages()) }} tiny-initramfs # a tiny initrd builder to account for the linux-image dependency # systemd and core systemd diff --git a/mkosi.extra/etc/acpi/events/powerbtn b/captain/flavors/common_debian/mkosi.extra/etc/acpi/events/powerbtn similarity index 100% rename from mkosi.extra/etc/acpi/events/powerbtn rename to captain/flavors/common_debian/mkosi.extra/etc/acpi/events/powerbtn diff --git a/mkosi.extra/etc/acpi/powerbtn.sh b/captain/flavors/common_debian/mkosi.extra/etc/acpi/powerbtn.sh similarity index 100% rename from mkosi.extra/etc/acpi/powerbtn.sh rename to captain/flavors/common_debian/mkosi.extra/etc/acpi/powerbtn.sh diff --git a/mkosi.extra/etc/cni/net.d/10-bridge.conflist b/captain/flavors/common_debian/mkosi.extra/etc/cni/net.d/10-bridge.conflist similarity index 100% rename from mkosi.extra/etc/cni/net.d/10-bridge.conflist rename to captain/flavors/common_debian/mkosi.extra/etc/cni/net.d/10-bridge.conflist diff --git a/mkosi.extra/etc/containerd/config.toml b/captain/flavors/common_debian/mkosi.extra/etc/containerd/config.toml similarity index 100% rename from mkosi.extra/etc/containerd/config.toml rename to captain/flavors/common_debian/mkosi.extra/etc/containerd/config.toml diff --git a/mkosi.extra/etc/locale.conf b/captain/flavors/common_debian/mkosi.extra/etc/locale.conf similarity index 100% rename from mkosi.extra/etc/locale.conf rename to captain/flavors/common_debian/mkosi.extra/etc/locale.conf diff --git a/mkosi.extra/etc/machine-id b/captain/flavors/common_debian/mkosi.extra/etc/machine-id similarity index 100% rename from mkosi.extra/etc/machine-id rename to captain/flavors/common_debian/mkosi.extra/etc/machine-id diff --git a/mkosi.extra/etc/modules-load.d/ipmi.conf b/captain/flavors/common_debian/mkosi.extra/etc/modules-load.d/ipmi.conf similarity index 100% rename from mkosi.extra/etc/modules-load.d/ipmi.conf rename to captain/flavors/common_debian/mkosi.extra/etc/modules-load.d/ipmi.conf diff --git a/mkosi.extra/etc/motd b/captain/flavors/common_debian/mkosi.extra/etc/motd similarity index 100% rename from mkosi.extra/etc/motd rename to captain/flavors/common_debian/mkosi.extra/etc/motd diff --git a/mkosi.extra/etc/nerdctl/nerdctl.toml b/captain/flavors/common_debian/mkosi.extra/etc/nerdctl/nerdctl.toml similarity index 100% rename from mkosi.extra/etc/nerdctl/nerdctl.toml rename to captain/flavors/common_debian/mkosi.extra/etc/nerdctl/nerdctl.toml diff --git a/mkosi.extra/etc/os-release b/captain/flavors/common_debian/mkosi.extra/etc/os-release similarity index 100% rename from mkosi.extra/etc/os-release rename to captain/flavors/common_debian/mkosi.extra/etc/os-release diff --git a/mkosi.extra/etc/rsyslog.conf b/captain/flavors/common_debian/mkosi.extra/etc/rsyslog.conf similarity index 100% rename from mkosi.extra/etc/rsyslog.conf rename to captain/flavors/common_debian/mkosi.extra/etc/rsyslog.conf diff --git a/mkosi.extra/etc/sysctl.d/99-ip-forward.conf b/captain/flavors/common_debian/mkosi.extra/etc/sysctl.d/99-ip-forward.conf similarity index 100% rename from mkosi.extra/etc/sysctl.d/99-ip-forward.conf rename to captain/flavors/common_debian/mkosi.extra/etc/sysctl.d/99-ip-forward.conf diff --git a/mkosi.extra/etc/sysctl.d/99-quiet-audit.conf b/captain/flavors/common_debian/mkosi.extra/etc/sysctl.d/99-quiet-audit.conf similarity index 100% rename from mkosi.extra/etc/sysctl.d/99-quiet-audit.conf rename to captain/flavors/common_debian/mkosi.extra/etc/sysctl.d/99-quiet-audit.conf diff --git a/mkosi.extra/etc/systemd/network/80-dhcp.network b/captain/flavors/common_debian/mkosi.extra/etc/systemd/network/80-dhcp.network similarity index 100% rename from mkosi.extra/etc/systemd/network/80-dhcp.network rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/network/80-dhcp.network diff --git a/mkosi.extra/etc/systemd/resolved.conf.d/no-stub.conf b/captain/flavors/common_debian/mkosi.extra/etc/systemd/resolved.conf.d/no-stub.conf similarity index 100% rename from mkosi.extra/etc/systemd/resolved.conf.d/no-stub.conf rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/resolved.conf.d/no-stub.conf diff --git a/mkosi.extra/etc/systemd/system/captainos-banner.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/captainos-banner.service similarity index 100% rename from mkosi.extra/etc/systemd/system/captainos-banner.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/captainos-banner.service diff --git a/mkosi.extra/etc/systemd/system/captainos-static-network.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/captainos-static-network.service similarity index 100% rename from mkosi.extra/etc/systemd/system/captainos-static-network.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/captainos-static-network.service diff --git a/mkosi.extra/etc/systemd/system/containerd.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/containerd.service similarity index 100% rename from mkosi.extra/etc/systemd/system/containerd.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/containerd.service diff --git a/mkosi.extra/etc/systemd/system/default.target b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/default.target similarity index 100% rename from mkosi.extra/etc/systemd/system/default.target rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/default.target diff --git a/mkosi.extra/etc/systemd/system/initrd-cleanup.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-cleanup.service similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd-cleanup.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-cleanup.service diff --git a/mkosi.extra/etc/systemd/system/initrd-parse-etc.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-parse-etc.service similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd-parse-etc.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-parse-etc.service diff --git a/mkosi.extra/etc/systemd/system/initrd-switch-root.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-switch-root.service similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd-switch-root.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-switch-root.service diff --git a/mkosi.extra/etc/systemd/system/initrd-switch-root.target b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-switch-root.target similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd-switch-root.target rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-switch-root.target diff --git a/mkosi.extra/etc/systemd/system/initrd-udevadm-cleanup-db.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-udevadm-cleanup-db.service similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd-udevadm-cleanup-db.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-udevadm-cleanup-db.service diff --git a/mkosi.extra/etc/systemd/system/initrd.target b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd.target similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd.target rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd.target diff --git a/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.path b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.path similarity index 100% rename from mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.path rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.path diff --git a/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.service similarity index 100% rename from mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.service diff --git a/mkosi.extra/etc/systemd/system/rsyslog.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog.service similarity index 100% rename from mkosi.extra/etc/systemd/system/rsyslog.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog.service diff --git a/mkosi.extra/etc/systemd/system/serial-getty@.service.d/autologin.conf b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/serial-getty@.service.d/autologin.conf similarity index 100% rename from mkosi.extra/etc/systemd/system/serial-getty@.service.d/autologin.conf rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/serial-getty@.service.d/autologin.conf diff --git a/mkosi.extra/etc/systemd/system/systemd-firstboot.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-firstboot.service similarity index 100% rename from mkosi.extra/etc/systemd/system/systemd-firstboot.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-firstboot.service diff --git a/mkosi.extra/etc/systemd/system/tink-agent-setup.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service similarity index 100% rename from mkosi.extra/etc/systemd/system/tink-agent-setup.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service diff --git a/mkosi.extra/etc/systemd/system/tink-agent.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent.service similarity index 100% rename from mkosi.extra/etc/systemd/system/tink-agent.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent.service diff --git a/mkosi.extra/etc/systemd/timesyncd.conf b/captain/flavors/common_debian/mkosi.extra/etc/systemd/timesyncd.conf similarity index 100% rename from mkosi.extra/etc/systemd/timesyncd.conf rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/timesyncd.conf diff --git a/mkosi.extra/etc/timezone b/captain/flavors/common_debian/mkosi.extra/etc/timezone similarity index 100% rename from mkosi.extra/etc/timezone rename to captain/flavors/common_debian/mkosi.extra/etc/timezone diff --git a/mkosi.extra/init b/captain/flavors/common_debian/mkosi.extra/init similarity index 100% rename from mkosi.extra/init rename to captain/flavors/common_debian/mkosi.extra/init diff --git a/mkosi.extra/root/.bashrc b/captain/flavors/common_debian/mkosi.extra/root/.bashrc similarity index 100% rename from mkosi.extra/root/.bashrc rename to captain/flavors/common_debian/mkosi.extra/root/.bashrc diff --git a/mkosi.extra/root/.profile b/captain/flavors/common_debian/mkosi.extra/root/.profile similarity index 100% rename from mkosi.extra/root/.profile rename to captain/flavors/common_debian/mkosi.extra/root/.profile diff --git a/mkosi.extra/usr/local/bin/captainos-banner b/captain/flavors/common_debian/mkosi.extra/usr/local/bin/captainos-banner similarity index 100% rename from mkosi.extra/usr/local/bin/captainos-banner rename to captain/flavors/common_debian/mkosi.extra/usr/local/bin/captainos-banner diff --git a/mkosi.extra/usr/local/bin/captainos-static-network b/captain/flavors/common_debian/mkosi.extra/usr/local/bin/captainos-static-network similarity index 100% rename from mkosi.extra/usr/local/bin/captainos-static-network rename to captain/flavors/common_debian/mkosi.extra/usr/local/bin/captainos-static-network diff --git a/mkosi.extra/usr/local/bin/rsyslog-start b/captain/flavors/common_debian/mkosi.extra/usr/local/bin/rsyslog-start similarity index 100% rename from mkosi.extra/usr/local/bin/rsyslog-start rename to captain/flavors/common_debian/mkosi.extra/usr/local/bin/rsyslog-start diff --git a/mkosi.extra/usr/local/bin/tink-agent-setup b/captain/flavors/common_debian/mkosi.extra/usr/local/bin/tink-agent-setup similarity index 100% rename from mkosi.extra/usr/local/bin/tink-agent-setup rename to captain/flavors/common_debian/mkosi.extra/usr/local/bin/tink-agent-setup diff --git a/mkosi.extra/usr/local/bin/tink-agent-start b/captain/flavors/common_debian/mkosi.extra/usr/local/bin/tink-agent-start similarity index 100% rename from mkosi.extra/usr/local/bin/tink-agent-start rename to captain/flavors/common_debian/mkosi.extra/usr/local/bin/tink-agent-start diff --git a/mkosi.finalize b/captain/flavors/common_debian/mkosi.finalize.sh.j2 similarity index 100% rename from mkosi.finalize rename to captain/flavors/common_debian/mkosi.finalize.sh.j2 diff --git a/mkosi.postinst b/captain/flavors/common_debian/mkosi.postinst.sh.j2 similarity index 100% rename from mkosi.postinst rename to captain/flavors/common_debian/mkosi.postinst.sh.j2 diff --git a/captain/flavors/trixie_full/__init__.py b/captain/flavors/trixie_full/__init__.py new file mode 100644 index 00000000..c3bcbf3e --- /dev/null +++ b/captain/flavors/trixie_full/__init__.py @@ -0,0 +1,32 @@ +import logging +from dataclasses import dataclass +from pathlib import Path + +from captain.config import Config +from captain.flavor import BaseFlavor +from captain.flavors.common_debian import DebianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieFullFlavor() + + +@dataclass +class TrixieFullFlavor(DebianCommonFlavor): + id = "trixie-full" + name = "Trixie Full" + description = "Debian Trixie based with linux-image-generic standard Debian kernel" + supported_architectures = frozenset(["amd64", "arm64"]) + + def setup(self, cfg: Config, flavor_dir: Path) -> None: + super().setup(cfg, flavor_dir) + log.warning( + "TrixieFullFlavor setting up; mkosi arch: %s; flavor_dir: %s", + cfg.arch_info.mkosi_arch, + flavor_dir, + ) + + def kernel_packages(self) -> set[str]: + return {"linux-image-generic"} diff --git a/captain/flavors/trixie_rockchip64/__init__.py b/captain/flavors/trixie_rockchip64/__init__.py new file mode 100644 index 00000000..da08158c --- /dev/null +++ b/captain/flavors/trixie_rockchip64/__init__.py @@ -0,0 +1,32 @@ +import logging +from dataclasses import dataclass +from pathlib import Path + +from captain.config import Config +from captain.flavor import BaseFlavor +from captain.flavors.common_armbian import ArmbianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieRockchip64Flavor() + + +@dataclass +class TrixieRockchip64Flavor(ArmbianCommonFlavor): + id = "trixie-rockchip64" + name = "Trixie for Rockchip 64-bit ARM machines" + description = "Debian Trixie based with Armbian's rockchip64-edge kernel" + supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + + def setup(self, cfg: Config, flavor_dir: Path) -> None: + super().setup(cfg, flavor_dir) + log.warning( + "TrixieRockchip64Flavor setting up; mkosi arch: %s; flavor_dir: %s", + cfg.arch_info.mkosi_arch, + flavor_dir, + ) + + def kernel_packages(self) -> set[str]: + return {"linux-image-edge-rockchip64"} diff --git a/pyproject.toml b/pyproject.toml index 17aa984a..c518bcaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,10 +2,14 @@ version = "0.0.1" name = "captain" requires-python = ">=3.13" -dependencies = ["configargparse>=1.7", "rich>=14.3.3"] +dependencies = [ + "configargparse>=1.7", + "rich>=14.3.3", + "jinja2>=3.1.6" +] [project.optional-dependencies] -dev = ["ruff>=0.9", "pyright>=1.1"] +dev = ["ruff>=0.15.9", "pyright>=1.1"] [tool.ruff] target-version = "py310" From acd4d362d3648c47f776ef151e7b901fd5a596aa Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 5 Apr 2026 13:35:16 +0200 Subject: [PATCH 027/143] gha: add trixie-rockchip64 flavor Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b28fee3..d9c07ed1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,7 @@ jobs: include: - { arch: amd64, output_arch: x86_64, iso: true, FLAVOR_ID: "trixie-full" } - { arch: arm64, output_arch: aarch64, iso: true, FLAVOR_ID: "trixie-full" } + - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-rockchip64" } env: ARCH: ${{ matrix.arch }} MKOSI_MODE: docker From dab824fc07a921bfe675f67445e033ae5fd1a188 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 5 Apr 2026 13:46:16 +0200 Subject: [PATCH 028/143] stages: show what tools_mode is running Signed-off-by: Ricardo Pardini --- captain/cli/_stages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/captain/cli/_stages.py b/captain/cli/_stages.py index 92cc2796..6198128e 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/_stages.py @@ -21,13 +21,13 @@ def _build_tools_stage(cfg: Config) -> None: # --- native ------------------------------------------------------- if cfg.tools_mode == "native": - log.info("Downloading tools (nerdctl, containerd, etc.)...") + log.info("Downloading tools (nerdctl, containerd, etc.) native...") tools.download_all(cfg) return # --- docker ------------------------------------------------------- docker.build_builder(cfg) - log.info("Downloading tools (nerdctl, containerd, etc.)...") + log.info("Downloading tools (nerdctl, containerd, etc.) docker...") docker.run_in_builder( cfg, "--entrypoint", From 9a78e6e2dabfb30234c00b8e982c13a71525dfcb Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 5 Apr 2026 13:53:18 +0200 Subject: [PATCH 029/143] gha: pass --arch to build.py build Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9c07ed1..4753b325 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,7 +121,7 @@ jobs: uses: astral-sh/setup-uv@v7 - name: Build initramfs - run: uv run ./build.py build # full build, incl initramfs and iso + run: uv run ./build.py --arch=${{ matrix.arch }} # full build, incl initramfs and iso - name: Upload initramfs artifacts uses: actions/upload-artifact@v6 From 07ad4ab0836e794decbed5f2f1b4f1bb31f5f804 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 5 Apr 2026 14:03:39 +0200 Subject: [PATCH 030/143] common_debian: add 01nopty with `Dpkg::Use-Pty "0";` to mkosi sandbox tree Signed-off-by: Ricardo Pardini --- captain/flavors/common_debian/__init__.py | 9 +++++++++ .../mkosi.sandbox/etc/apt/apt.conf.d/01nopty | 1 + 2 files changed, 10 insertions(+) create mode 100644 captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py index 4200e4d0..7803beca 100644 --- a/captain/flavors/common_debian/__init__.py +++ b/captain/flavors/common_debian/__init__.py @@ -35,5 +35,14 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: relative_path = extra_file.relative_to(this_flavor_dir) self.static_map[str(relative_path)] = extra_file + # Now, lets enumerate and add all the static files this flavor's mkosi.sandbox directory + # and add them to self.static_map with the key being the relative path from the flavor dir + extra_dir = this_flavor_dir / "mkosi.sandbox" + if extra_dir.exists() and extra_dir.is_dir(): + for extra_file in extra_dir.rglob("*"): + if extra_file.is_file(): + relative_path = extra_file.relative_to(this_flavor_dir) + self.static_map[str(relative_path)] = extra_file + def extra_mkosi_conf_distribution(self) -> str: return "" diff --git a/captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty b/captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty new file mode 100644 index 00000000..ba2d7904 --- /dev/null +++ b/captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty @@ -0,0 +1 @@ +Dpkg::Use-Pty "0"; From f7299fd1465ed4e4402718c05c9f391f375c1c0e Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 11:57:39 +0200 Subject: [PATCH 031/143] captain: add support for mkosi skeleton tree - gha: still trying to fix dpkg/apt output in gha - this went nowhere, but hindsight is 20/20 Signed-off-by: Ricardo Pardini --- .gitignore | 1 + captain/docker.py | 1 + captain/flavor.py | 1 + captain/flavors/common_debian/__init__.py | 9 +++++++++ captain/flavors/common_debian/mkosi.conf.j2 | 1 + .../mkosi.sandbox/etc/apt/apt.conf.d/01nopty | 3 +++ .../mkosi.skeleton/etc/apt/apt.conf.d/01nopty | 4 ++++ 7 files changed, 20 insertions(+) create mode 100644 captain/flavors/common_debian/mkosi.skeleton/etc/apt/apt.conf.d/01nopty diff --git a/.gitignore b/.gitignore index 9af69344..4098d42b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ out/ /mkosi.postinst /mkosi.extra /mkosi.sandbox +/mkosi.skeleton # Editor *.swp diff --git a/captain/docker.py b/captain/docker.py index 388ccd7f..c869032b 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -189,6 +189,7 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: docker_args += ["-v", f"{cfg.project_dir}/mkosi.output:/work/mkosi.output"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.extra:/work/mkosi.extra"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.sandbox:/work/mkosi.sandbox"] + docker_args += ["-v", f"{cfg.project_dir}/mkosi.skeleton:/work/mkosi.skeleton"] docker_args += ["-v", f"{cfg.project_dir}/out:/work/out"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.conf:/work/mkosi.conf"] diff --git a/captain/flavor.py b/captain/flavor.py index e56e472b..f4e75dff 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -50,6 +50,7 @@ def generate(self): shutil.rmtree(self.cfg.project_dir / "mkosi.finalize", ignore_errors=True) shutil.rmtree(self.cfg.project_dir / "mkosi.extra", ignore_errors=True) shutil.rmtree(self.cfg.project_dir / "mkosi.sandbox", ignore_errors=True) + shutil.rmtree(self.cfg.project_dir / "mkosi.skeleton", ignore_errors=True) self.copy_static_files(self.cfg.project_dir) self.render_templates(self.cfg.project_dir) # For compatibility diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py index 7803beca..292cabb4 100644 --- a/captain/flavors/common_debian/__init__.py +++ b/captain/flavors/common_debian/__init__.py @@ -44,5 +44,14 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: relative_path = extra_file.relative_to(this_flavor_dir) self.static_map[str(relative_path)] = extra_file + # Now, lets enumerate and add all the static files this flavor's mkosi.skeleton directory + # and add them to self.static_map with the key being the relative path from the flavor dir + extra_dir = this_flavor_dir / "mkosi.skeleton" + if extra_dir.exists() and extra_dir.is_dir(): + for extra_file in extra_dir.rglob("*"): + if extra_file.is_file(): + relative_path = extra_file.relative_to(this_flavor_dir) + self.static_map[str(relative_path)] = extra_file + def extra_mkosi_conf_distribution(self) -> str: return "" diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index dae28b35..98a307e2 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -10,6 +10,7 @@ CompressLevel=19 OutputDirectory=mkosi.output [Build] +Environment=TERM=dumb ToolsTree=yes Incremental=yes CacheDirectory=mkosi.cache diff --git a/captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty b/captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty index ba2d7904..551c0a65 100644 --- a/captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty +++ b/captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty @@ -1 +1,4 @@ Dpkg::Use-Pty "0"; +APT::Quiet "2"; +Dpkg::Progress-Fancy "false"; +APT::Color "true"; diff --git a/captain/flavors/common_debian/mkosi.skeleton/etc/apt/apt.conf.d/01nopty b/captain/flavors/common_debian/mkosi.skeleton/etc/apt/apt.conf.d/01nopty new file mode 100644 index 00000000..551c0a65 --- /dev/null +++ b/captain/flavors/common_debian/mkosi.skeleton/etc/apt/apt.conf.d/01nopty @@ -0,0 +1,4 @@ +Dpkg::Use-Pty "0"; +APT::Quiet "2"; +Dpkg::Progress-Fancy "false"; +APT::Color "true"; From 73f557944c515cc0a2a28118c16e858d3e2ff2ac Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 11:59:02 +0200 Subject: [PATCH 032/143] common_debian: common bash header with logging, sprinkle some dust on postinst and finalize Signed-off-by: Ricardo Pardini --- captain/flavors/common_debian/__init__.py | 10 ++- captain/flavors/common_debian/bash.header.sh | 19 +++++ .../common_debian/mkosi.finalize.sh.j2 | 50 ++++++------ .../common_debian/mkosi.postinst.sh.j2 | 76 +++++++++++++------ 4 files changed, 104 insertions(+), 51 deletions(-) create mode 100644 captain/flavors/common_debian/bash.header.sh diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py index 292cabb4..f9c22d24 100644 --- a/captain/flavors/common_debian/__init__.py +++ b/captain/flavors/common_debian/__init__.py @@ -23,8 +23,14 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: ) this_flavor_dir = self.specific_flavor_dir("common-debian") self.template_map["mkosi.conf"] = [this_flavor_dir / "mkosi.conf.j2"] - self.template_map["mkosi.postinst"] = [this_flavor_dir / "mkosi.postinst.sh.j2"] - self.template_map["mkosi.finalize"] = [this_flavor_dir / "mkosi.finalize.sh.j2"] + self.template_map["mkosi.postinst"] = [ + this_flavor_dir / "bash.header.sh", + this_flavor_dir / "mkosi.postinst.sh.j2", + ] + self.template_map["mkosi.finalize"] = [ + this_flavor_dir / "bash.header.sh", + this_flavor_dir / "mkosi.finalize.sh.j2", + ] # Now, lets enumerate and add all the static files this flavor's mkosi.extra directory # and add them to self.static_map with the key being the relative path from the flavor dir diff --git a/captain/flavors/common_debian/bash.header.sh b/captain/flavors/common_debian/bash.header.sh new file mode 100644 index 00000000..afde1faa --- /dev/null +++ b/captain/flavors/common_debian/bash.header.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euo pipefail + +# logger utility, output ANSI-colored messages to stderr; first argument is level (debug/info/warn/error), all other arguments are the message. +declare -A log_colors=(["debug"]="0;36" ["info"]="0;32" ["notice"]="1;32" ["warn"]="1;33" ["warning"]="1;33" ["error"]="1;31") +declare -A log_emoji=(["debug"]="🐛" ["info"]="🌿" ["notice"]="🌱" ["warn"]="🚸" ["warning"]="🚸" ["error"]="🚨") +function log() { + declare level="${1}" + shift + [[ "${level}" == "debug" && "${DEBUG}" != "yes" ]] && return # Skip debugs unless DEBUG=yes is set in the environment + # Normal output + declare color="\033[${log_colors[${level}]}m" + declare emoji="${log_emoji[${level}]}" + declare ansi_reset="\033[0m" + level=$(printf "%-5s" "${level}") # pad to 5 characters before printing + echo -e "${emoji} ${ansi_reset}[${color}${level}${ansi_reset}] ${color}${*}${ansi_reset}" >&2 +} + diff --git a/captain/flavors/common_debian/mkosi.finalize.sh.j2 b/captain/flavors/common_debian/mkosi.finalize.sh.j2 index f97b7425..c0c67d41 100755 --- a/captain/flavors/common_debian/mkosi.finalize.sh.j2 +++ b/captain/flavors/common_debian/mkosi.finalize.sh.j2 @@ -3,10 +3,10 @@ # The root filesystem is writable here. set -euo pipefail -echo "==> Showing full contents of environment variables for debugging:" +log info "Showing full contents of environment variables for debugging:" env -echo "==> CaptainOS finalize: unlocking root account..." +log info "CaptainOS finalize: unlocking root account..." # Unlock root with empty password # Replace the locked root entry (! or * in password field) with empty hash @@ -16,38 +16,38 @@ if [[ -f "$BUILDROOT/etc/shadow" ]]; then fi # Ensure /etc/initrd-release does not exist -rm -f "$BUILDROOT/etc/initrd-release" -echo " /etc/initrd-release removed" +log info "Ensure no /etc/initrd-release exists..." +rm -fv "$BUILDROOT/etc/initrd-release" # Remove systemd-firstboot completely -rm -f "$BUILDROOT/usr/lib/systemd/system/systemd-firstboot.service" -rm -f "$BUILDROOT/usr/lib/systemd/system/sysinit.target.wants/systemd-firstboot.service" -rm -f "$BUILDROOT/usr/bin/systemd-firstboot" -echo " systemd-firstboot removed" +log info "Completely remove systemd-firstboot..." +rm -fv "$BUILDROOT/usr/lib/systemd/system/systemd-firstboot.service" +rm -fv "$BUILDROOT/usr/lib/systemd/system/sysinit.target.wants/systemd-firstboot.service" +rm -fv "$BUILDROOT/usr/bin/systemd-firstboot" # Point /etc/resolv.conf at the non-stub resolv.conf written by # systemd-resolved. This file contains upstream (DHCP-provided) # nameservers instead of 127.0.0.53. tink-agent bind-mounts the host's # /etc/resolv.conf into action containers so they need real, routable # nameservers (the stub address is unreachable from container namespaces). -ln -sf /run/systemd/resolve/resolv.conf "$BUILDROOT/etc/resolv.conf" -echo " /etc/resolv.conf -> /run/systemd/resolve/resolv.conf" +log info "Handle resolv.conf..." +ln -sfv /run/systemd/resolve/resolv.conf "$BUILDROOT/etc/resolv.conf" # Ensure /init (switch_root script) is executable +log info "Ensure /init is executable..." if [[ -f "$BUILDROOT/init" ]]; then - chmod +x "$BUILDROOT/init" - echo " /init made executable" + chmod -v +x "$BUILDROOT/init" fi # Ensure tool binaries are executable. # GitHub Actions artifact upload/download and some archive tools strip # the execute bit. Re-apply +x to every tool directory so containerd, # runc, nerdctl, and CNI plugins can actually run. +log info "Ensure tool binaries are executable..." for dir in usr/local/bin opt/cni/bin; do target="$BUILDROOT/$dir" if [[ -d "$target" ]]; then - find "$target" -type f -exec chmod +x {} + - echo " +x restored on $dir/*" + find "$target" -type f -exec chmod -v +x {} + fi done @@ -60,7 +60,7 @@ done MODDIR="$BUILDROOT/usr/lib/modules" if [[ -d "$MODDIR" ]]; then - echo "==> Trimming unnecessary kernel modules..." + log info "Trimming unnecessary kernel modules..." BEFORE=$(du -sb "$MODDIR" | awk '{print $1}') # @TODO: this has to be templated, for flavors like fat or full we should keep most modules. @@ -92,14 +92,14 @@ if [[ -d "$MODDIR" ]]; then ) for pattern in "${EXCLUDE_PATTERNS[@]}"; do - echo "--> Removing modules matching pattern: '${pattern}'" - find "$MODDIR" -path "$pattern" -type d -exec rm -rfv {} + || true + log info " Removing modules matching pattern: '${pattern}'" + find "$MODDIR" -path "$pattern" -type d -exec rm -rf {} + || true done AFTER=$(du -sb "$MODDIR" | awk '{print $1}') SAVED=$(( (BEFORE - AFTER) / 1024 / 1024 )) [[ $SAVED -lt 0 ]] && SAVED=0 - echo " Removed ${SAVED}MB of unnecessary kernel modules" + log info " Removed ${SAVED}MB of unnecessary kernel modules" # Regenerate module dependency files after removal for all kernel versions for kdir in "$MODDIR"/*; do @@ -109,27 +109,27 @@ if [[ -d "$MODDIR" ]]; then done fi -echo "==> Listing 20 biggest remaining kernel modules du: ${MODDIR}" +log info "Listing 20 biggest remaining kernel modules du: ${MODDIR}" du -h -x -d 5 "$MODDIR" | sort -h | tail -n20 || true -echo "==> Removing /boot from the image to save space" +log info "Removing /boot from the image to save space" rm -rf "${BUILDROOT:?}"/boot DTB_SRC_DIR=$(echo -n "${BUILDROOT}"/usr/lib/linux-image-*) if [[ -d "$DTB_SRC_DIR" ]]; then - echo "==> Exporting kernel DTBs; DTB_DIR: ${DTB_SRC_DIR}" + log info "Exporting kernel DTBs; DTB_DIR: ${DTB_SRC_DIR}" mkdir -pv "${BUILDROOT}/root/dtb" # Move every .dtb* file in the DTB_SRC_DIR to "${BUILDROOT}/root/dtb" -- maintain the directory structure find "${DTB_SRC_DIR}" -type f -name "*.dtb*" -exec sh -c 'for f; do mkdir -p "${0}/$(dirname "${f#'"${DTB_SRC_DIR}"'/}")" && mv "$f" "${0}/${f#'"${DTB_SRC_DIR}"'/}" ; done' "${BUILDROOT}/root/dtb" {} + || true # Now simply output the dtb directory to mkosi's OUTPUTDIR - echo "==> Copying DTBs to mkosi output directory: ${OUTPUTDIR}/dtb" + log info "Copying DTBs to mkosi output directory: ${OUTPUTDIR}/dtb" mv "${BUILDROOT}/root/dtb" "${OUTPUTDIR}/dtb" else - echo "==> No DTB source directory found at ${DTB_SRC_DIR}; skipping DTB export" + log info "No DTB source directory found at ${DTB_SRC_DIR}; skipping DTB export" fi -echo "==> Final contents of root filesystem (up to depth 4, top 30):" +log info "Final contents of root filesystem (up to depth 4, top 30):" du -h -x -d 4 "$BUILDROOT" | sort -h | tail -n30 || true -echo "==> CaptainOS finalize complete." +log info "CaptainOS finalize complete." diff --git a/captain/flavors/common_debian/mkosi.postinst.sh.j2 b/captain/flavors/common_debian/mkosi.postinst.sh.j2 index bafdb9b8..e090e19c 100755 --- a/captain/flavors/common_debian/mkosi.postinst.sh.j2 +++ b/captain/flavors/common_debian/mkosi.postinst.sh.j2 @@ -5,45 +5,73 @@ # creating directories and dropping config files. set -euo pipefail -echo "==> CaptainOS post-install: dump env at $(hostname)..." +log info "CaptainOS post-install: dump env at $(hostname)..." env -echo "==> CaptainOS post-install: show mounts at $(hostname)..." +log info "CaptainOS post-install: show mounts at $(hostname)..." mount || true -echo "==> CaptainOS post-install: configuring services..." - # Ensure CA certificate bundle is generated (dpkg triggers may not # fire reliably in mkosi's chroot). +log info "Generating fresh CA certificate bundle..." update-ca-certificates --fresh 2>/dev/null || true +# @TODO: this is the wrong place to do any systemd enablement/disablement/masking: mkosi will +# after this still apply systemd "presets" which will change the game completely. +# This is better done in a finalize.chroot script which would run after that. +# alternatively: adopt/introduce systemd presets, but that's a can of worms. + +log info "Listing all systemd units..." +systemctl list-unit-files --no-pager || true + +log info "CaptainOS post-install: configuring services..." # Enable core services -for unit in systemd-networkd systemd-resolved systemd-timesyncd containerd captainos-banner \ - captainos-static-network tink-agent-setup tink-agent rsyslog rsyslog-hostname-reload.path; do - systemctl enable "$unit" 2>/dev/null || true +declare -a units_to_enable=( + systemd-networkd systemd-resolved systemd-timesyncd containerd captainos-banner + systemd-time-wait-sync # wait for time sync + captainos-static-network tink-agent-setup tink-agent rsyslog rsyslog-hostname-reload.path +) +declare unit +for unit in "${units_to_enable[@]}"; do + log info "Enabling systemd unit ${unit}..." + if systemctl enable "${unit}"; then + log info " ${unit} enabled successfully." + else + log warning " Failed to enable ${unit}, but continuing anyway..." + fi done # Root password is set via mkosi.conf RootPassword= setting # Disable unnecessary systemd units to speed up boot -for unit in \ - apt-daily.timer \ - apt-daily-upgrade.timer \ - e2scrub_all.timer \ - e2scrub_reap.service \ - fstrim.timer \ - logrotate.timer \ - man-db.timer \ - remote-fs.target \ - systemd-firstboot.service; do - systemctl disable "$unit" 2>/dev/null || true - systemctl mask "$unit" 2>/dev/null || true +declare -a units_to_disable=( + apt-daily.timer + apt-daily-upgrade.timer + e2scrub_all.timer + e2scrub_reap.service + fstrim.timer + logrotate.timer + man-db.timer + remote-fs.target + systemd-firstboot.service +) +for unit in "${units_to_disable[@]}"; do + log info "Disabling and masking systemd unit ${unit}..." + if systemctl disable "${unit}"; then + log info " ${unit} disabled successfully." + else + log warning " Failed to disable ${unit}, but continuing anyway..." + fi + + if systemctl mask "${unit}"; then + log info " ${unit} masked successfully." + else + log warning " Failed to mask ${unit}, but continuing anyway..." + fi done # Set default target to multi-user (no graphical) -systemctl set-default multi-user.target 2>/dev/null || true - -echo "==> List installed packages..." -dpkg -l --no-pager || true +log info "Setting default systemd target to multi-user.target (no graphical)..." +systemctl set-default multi-user.target || true -echo "==> CaptainOS post-install complete." +log info "CaptainOS post-install complete." From 5e30a23caedaf7d82592b9fa188e9a71488738d3 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 11:59:18 +0200 Subject: [PATCH 033/143] mkosi.conf: output a JSON manifest Signed-off-by: Ricardo Pardini --- captain/flavors/common_debian/mkosi.conf.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index 98a307e2..e2be9f61 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -4,6 +4,7 @@ Release=trixie {{ flavor.extra_mkosi_conf_distribution() }} [Output] +ManifestFormat=json Format=cpio CompressOutput=zstd CompressLevel=19 From 291d12edb2a8f57b30c383cb5be5e939c4282126 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 11:59:43 +0200 Subject: [PATCH 034/143] mkosi.conf: force tools tree to be Debian Trixie - mkosi defaults to 'testing' Signed-off-by: Ricardo Pardini --- captain/flavors/common_debian/mkosi.conf.j2 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index e2be9f61..fc6068b1 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -13,6 +13,8 @@ OutputDirectory=mkosi.output [Build] Environment=TERM=dumb ToolsTree=yes +ToolsTreeDistribution=debian +ToolsTreeRelease=trixie Incremental=yes CacheDirectory=mkosi.cache From ecbb0134dab4650a423588fbe4f572a7cd246272 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 12:00:02 +0200 Subject: [PATCH 035/143] mkosi.conf: use sandbox tree also for tools tree Signed-off-by: Ricardo Pardini --- captain/flavors/common_debian/mkosi.conf.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index fc6068b1..5383634d 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -15,6 +15,7 @@ Environment=TERM=dumb ToolsTree=yes ToolsTreeDistribution=debian ToolsTreeRelease=trixie +ToolsTreeSandboxTrees=mkosi.sandbox Incremental=yes CacheDirectory=mkosi.cache From c673d693d4129bc01d958856606ea7427aef5728 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 12:04:47 +0200 Subject: [PATCH 036/143] flavor: ensure flavor supports requested architecture Signed-off-by: Ricardo Pardini --- captain/flavor.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/captain/flavor.py b/captain/flavor.py index f4e75dff..c5fa5382 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -158,6 +158,22 @@ def create_and_setup_flavor_for_id(flavor_id: str, cfg: Config) -> BaseFlavor: log.debug("Calling setup() on flavor %s with config: %s", flavor, cfg) flavor.setup(cfg, flavor_dir) + # Ensure the current arch is supported by the flavor + if cfg.arch_info.arch not in flavor.supported_architectures: + log.error( + "Flavor '%s' does not support architecture '%s'. Supported architectures: %s", + flavor.id, + cfg.arch_info.arch, + flavor.supported_architectures, + ) + raise SystemExit(1) + else: + log.debug( + "Flavor '%s' supports architecture '%s'", + flavor.id, + cfg.arch_info.arch, + ) + log.debug( "Flavor is setup; description: %s; supported_architectures: %s", flavor.description, From 4fcdfcee6e0a99c82d5b076bd89e2dd35e49863e Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 12:08:11 +0200 Subject: [PATCH 037/143] flavors: cleanup a bit Signed-off-by: Ricardo Pardini --- captain/flavors/common_armbian/__init__.py | 8 -------- captain/flavors/common_debian/__init__.py | 9 +-------- captain/flavors/common_debian/mkosi.conf.j2 | 1 - captain/flavors/trixie_full/__init__.py | 10 ---------- captain/flavors/trixie_rockchip64/__init__.py | 10 ---------- 5 files changed, 1 insertion(+), 37 deletions(-) diff --git a/captain/flavors/common_armbian/__init__.py b/captain/flavors/common_armbian/__init__.py index 70ecb4b9..e79bdfd8 100644 --- a/captain/flavors/common_armbian/__init__.py +++ b/captain/flavors/common_armbian/__init__.py @@ -16,11 +16,6 @@ class ArmbianCommonFlavor(DebianCommonFlavor): def setup(self, cfg: Config, flavor_dir: Path) -> None: super().setup(cfg, flavor_dir) - log.warning( - "ArmbianCommonFlavor setting up; mkosi arch: %s; flavor_dir: %s", - cfg.arch_info.mkosi_arch, - flavor_dir, - ) this_flavor_dir = self.specific_flavor_dir("common-armbian") # Now, lets enumerate and add all the static files this flavor's mkosi.sandbox directory @@ -31,6 +26,3 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: if extra_file.is_file(): relative_path = extra_file.relative_to(this_flavor_dir) self.static_map[str(relative_path)] = extra_file - - def extra_mkosi_conf_distribution(self) -> str: - return "# frak this" diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py index f9c22d24..60c84dca 100644 --- a/captain/flavors/common_debian/__init__.py +++ b/captain/flavors/common_debian/__init__.py @@ -16,11 +16,7 @@ class DebianCommonFlavor(BaseFlavor): def setup(self, cfg: Config, flavor_dir: Path) -> None: super().setup(cfg, flavor_dir) - log.warning( - "DebianCommonFlavor setting up; mkosi arch: %s; flavor_dir: %s", - cfg.arch_info.mkosi_arch, - flavor_dir, - ) + this_flavor_dir = self.specific_flavor_dir("common-debian") self.template_map["mkosi.conf"] = [this_flavor_dir / "mkosi.conf.j2"] self.template_map["mkosi.postinst"] = [ @@ -58,6 +54,3 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: if extra_file.is_file(): relative_path = extra_file.relative_to(this_flavor_dir) self.static_map[str(relative_path)] = extra_file - - def extra_mkosi_conf_distribution(self) -> str: - return "" diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index 5383634d..5913c5ae 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -1,7 +1,6 @@ [Distribution] Distribution=debian Release=trixie -{{ flavor.extra_mkosi_conf_distribution() }} [Output] ManifestFormat=json diff --git a/captain/flavors/trixie_full/__init__.py b/captain/flavors/trixie_full/__init__.py index c3bcbf3e..409ed1e5 100644 --- a/captain/flavors/trixie_full/__init__.py +++ b/captain/flavors/trixie_full/__init__.py @@ -1,8 +1,6 @@ import logging from dataclasses import dataclass -from pathlib import Path -from captain.config import Config from captain.flavor import BaseFlavor from captain.flavors.common_debian import DebianCommonFlavor @@ -20,13 +18,5 @@ class TrixieFullFlavor(DebianCommonFlavor): description = "Debian Trixie based with linux-image-generic standard Debian kernel" supported_architectures = frozenset(["amd64", "arm64"]) - def setup(self, cfg: Config, flavor_dir: Path) -> None: - super().setup(cfg, flavor_dir) - log.warning( - "TrixieFullFlavor setting up; mkosi arch: %s; flavor_dir: %s", - cfg.arch_info.mkosi_arch, - flavor_dir, - ) - def kernel_packages(self) -> set[str]: return {"linux-image-generic"} diff --git a/captain/flavors/trixie_rockchip64/__init__.py b/captain/flavors/trixie_rockchip64/__init__.py index da08158c..c49b5ab7 100644 --- a/captain/flavors/trixie_rockchip64/__init__.py +++ b/captain/flavors/trixie_rockchip64/__init__.py @@ -1,8 +1,6 @@ import logging from dataclasses import dataclass -from pathlib import Path -from captain.config import Config from captain.flavor import BaseFlavor from captain.flavors.common_armbian import ArmbianCommonFlavor @@ -20,13 +18,5 @@ class TrixieRockchip64Flavor(ArmbianCommonFlavor): description = "Debian Trixie based with Armbian's rockchip64-edge kernel" supported_architectures = frozenset(["arm64"]) # does NOT support amd64 - def setup(self, cfg: Config, flavor_dir: Path) -> None: - super().setup(cfg, flavor_dir) - log.warning( - "TrixieRockchip64Flavor setting up; mkosi arch: %s; flavor_dir: %s", - cfg.arch_info.mkosi_arch, - flavor_dir, - ) - def kernel_packages(self) -> set[str]: return {"linux-image-edge-rockchip64"} From 71eb40a6beddd1225f0e7fe7d2da93f19bf87901 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 12:10:06 +0200 Subject: [PATCH 038/143] flavors/gha: introduce meson64 flavor Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 1 + captain/flavors/trixie_meson64/__init__.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 captain/flavors/trixie_meson64/__init__.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4753b325..23315b11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,6 +95,7 @@ jobs: - { arch: amd64, output_arch: x86_64, iso: true, FLAVOR_ID: "trixie-full" } - { arch: arm64, output_arch: aarch64, iso: true, FLAVOR_ID: "trixie-full" } - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-rockchip64" } + - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-meson64" } env: ARCH: ${{ matrix.arch }} MKOSI_MODE: docker diff --git a/captain/flavors/trixie_meson64/__init__.py b/captain/flavors/trixie_meson64/__init__.py new file mode 100644 index 00000000..f3dd09e5 --- /dev/null +++ b/captain/flavors/trixie_meson64/__init__.py @@ -0,0 +1,22 @@ +import logging +from dataclasses import dataclass + +from captain.flavor import BaseFlavor +from captain.flavors.common_armbian import ArmbianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieMeson64Flavor() + + +@dataclass +class TrixieMeson64Flavor(ArmbianCommonFlavor): + id = "trixie-meson64" + name = "Trixie for Meson (Amlogic) 64-bit ARM machines" + description = "Debian Trixie based with Armbian's meson64-edge kernel" + supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + + def kernel_packages(self) -> set[str]: + return {"linux-image-edge-meson64"} From 8ce0d98403e21ce222f1fa98298dd28edd9f82ec Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 12:23:03 +0200 Subject: [PATCH 039/143] docker: never interactive, never a terminal Signed-off-by: Ricardo Pardini --- captain/docker.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/captain/docker.py b/captain/docker.py index c869032b..f303c209 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -6,7 +6,6 @@ import logging import os import platform -import sys from pathlib import Path from captain.config import Config @@ -108,9 +107,6 @@ def run_in_release(cfg: Config, *extra_args: str) -> None: "--rm", # Buildah needs mount/remount capabilities for layer operations. "--privileged", - # interactive if running in a terminal - *(["-i"] if sys.stdout.isatty() and sys.stdin.isatty() else []), - "-t", # terminal "-v", f"{cfg.project_dir}:/work", "-w", @@ -153,9 +149,6 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "run", "--rm", "--privileged", - # interactive if running in a terminal - *(["-i"] if sys.stdout.isatty() and sys.stdin.isatty() else []), - "-t", # terminal "-w", "/work", "-e", From 0a9e051cf400aed7aba7908705daacb856e4d94f Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 12:40:32 +0200 Subject: [PATCH 040/143] cli/flavor: pass Flavor down to build and initramfs cmds; introduce BaseFlavor::has_iso() - so only the fully UEFI/ACPI flavor gets ISOs by default - they don't make sense for DeviceTree flavors Signed-off-by: Ricardo Pardini --- captain/cli/_commands.py | 14 ++++++++++---- captain/cli/_main.py | 6 +++--- captain/flavor.py | 3 +++ captain/flavors/trixie_full/__init__.py | 4 ++++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/captain/cli/_commands.py b/captain/cli/_commands.py index 54e9df13..6d319185 100644 --- a/captain/cli/_commands.py +++ b/captain/cli/_commands.py @@ -9,6 +9,7 @@ from captain import artifacts, docker, qemu from captain.config import Config +from captain.flavor import BaseFlavor from captain.util import run from ._stages import ( @@ -26,7 +27,7 @@ def _cmd_tools(cfg: Config, _extra_args: list[str]) -> None: log.info("Tools stage complete!") -def _cmd_initramfs(cfg: Config, extra_args: list[str]) -> None: +def _cmd_initramfs(cfg: Config, flavor: BaseFlavor, extra_args: list[str]) -> None: """Build only the initramfs via mkosi, then collect artifacts.""" _build_mkosi_stage(cfg, extra_args) artifacts.collect_initramfs(cfg) @@ -42,11 +43,16 @@ def _cmd_iso(cfg: Config, _extra_args: list[str]) -> None: log.info("ISO build complete!") -def _cmd_build(cfg: Config, extra_args: list[str]) -> None: +def _cmd_build(cfg: Config, flavor: BaseFlavor, extra_args: list[str]) -> None: """Full build: tools → initramfs → iso → artifacts.""" _build_tools_stage(cfg) - _cmd_initramfs(cfg, extra_args) # delegate, so it also collects - _build_iso_stage(cfg) # TODO also conditional... / and/or include dtb's for arm64 + _cmd_initramfs(cfg, flavor, extra_args) # delegate, so it also collects + + if flavor.has_iso(): + _build_iso_stage(cfg) + else: + log.info("Flavor '%s' does not have an ISO stage; skipping.", flavor.id) + artifacts.collect(cfg) log.info("Build complete!") diff --git a/captain/cli/_main.py b/captain/cli/_main.py index f21a3ad0..08702fa8 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -22,9 +22,9 @@ import captain.flavor from captain import docker from captain.config import Config +from captain.flavor import BaseFlavor from captain.util import run -from ..flavor import BaseFlavor from ._commands import ( _cmd_build, _cmd_checksums, @@ -108,8 +108,8 @@ def main(project_dir: Path | None = None) -> None: # generate the flavor first so the files are in place. if command in ("build", "initramfs"): flavor.generate() - - if command in ("qemu-test", "checksums", "release", "clean"): + handler(cfg, flavor, extra) # type: ignore[operator] + elif command in ("qemu-test", "checksums", "release", "clean"): handler(cfg, extra, args=args) # type: ignore[operator] else: handler(cfg, extra) # type: ignore[operator] diff --git a/captain/flavor.py b/captain/flavor.py index c5fa5382..c79ee2d7 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -111,6 +111,9 @@ def copy_static_files(self, project_dir): destination_path.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source_path, destination_path) + def has_iso(self) -> bool: + return False + def create_and_setup_flavor_for_id(flavor_id: str, cfg: Config) -> BaseFlavor: log.debug("Creating and setting up flavor for id '%s'", flavor_id) diff --git a/captain/flavors/trixie_full/__init__.py b/captain/flavors/trixie_full/__init__.py index 409ed1e5..dd28880b 100644 --- a/captain/flavors/trixie_full/__init__.py +++ b/captain/flavors/trixie_full/__init__.py @@ -20,3 +20,7 @@ class TrixieFullFlavor(DebianCommonFlavor): def kernel_packages(self) -> set[str]: return {"linux-image-generic"} + + # This flavor can produce working ISO images (generic UEFI/ACPI) + def has_iso(self) -> bool: + return True From 222a379271f9725e1041762ff52dfb168f8ec040 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 16:04:33 +0200 Subject: [PATCH 041/143] gha/docker: set and pass down FORCE_COLOR=1 Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 1 + captain/docker.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23315b11..428a71e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ permissions: env: DEFAULT_FLAVOR_ID: "trixie-full" + FORCE_COLOR: "1" jobs: # ------------------------------------------------------------------- diff --git a/captain/docker.py b/captain/docker.py index f303c209..7151eb92 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -124,6 +124,8 @@ def run_in_release(cfg: Config, *extra_args: str) -> None: "-e", f"TERM={os.environ.get('TERM', 'xterm-256color')}", "-e", + "FORCE_COLOR=1", + "-e", f"COLUMNS={os.environ.get('COLUMNS', '200')}", "-e", f"GITHUB_ACTIONS={os.environ.get('GITHUB_ACTIONS', '')}", @@ -172,6 +174,8 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "-e", f"TERM={os.environ.get('TERM', 'xterm-256color')}", "-e", + "FORCE_COLOR=1", + "-e", f"COLUMNS={os.environ.get('COLUMNS', '200')}", "-e", f"GITHUB_ACTIONS={os.environ.get('GITHUB_ACTIONS', '')}", From 9480df9974a158cc8618dd3327cc8d2bbf2b5d46 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 16:38:11 +0200 Subject: [PATCH 042/143] captain: don't shorten out logging record name Signed-off-by: Ricardo Pardini --- captain/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/captain/__init__.py b/captain/__init__.py index 03cc81e3..4cdd54ea 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -29,11 +29,10 @@ class _StageFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: name = record.name - stage = name.split(".", 1)[1] if name.startswith("captain.") else name - record.__dict__["stage"] = stage + record.__dict__["stage"] = name if os.environ.get("CAPTAIN_IN_DOCKER", "") == "docker": - # Running on host: show stage names in green for visual clarity. - record.__dict__["stage"] = f"[bold][blue]in-docker[/bold]: [/blue]{stage}" + # Running on host: show stage names in blue for visual clarity. + record.__dict__["stage"] = f"[bold][blue]in-docker[/bold]: [/blue]{name}" return super().format(record) @@ -46,7 +45,7 @@ def format(self, record: logging.LogRecord) -> str: show_time=False, show_level=True, show_path=True, - markup=True, + markup=True, # interprets [braket]stuff[/bracket] in log messages, beware rich_tracebacks=True, tracebacks_show_locals=True, ) From d606306d55e038d437afb0f6e9522817321b445a Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 17:15:32 +0200 Subject: [PATCH 043/143] buildah: take env BUILDAH_INSECURE=1 for `--tls-verify=false` - so one can use an HTTP local `registry:2` for testing pushes Signed-off-by: Ricardo Pardini --- captain/buildah.py | 13 ++++++++++++- captain/docker.py | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/captain/buildah.py b/captain/buildah.py index cbc3110e..e595069b 100644 --- a/captain/buildah.py +++ b/captain/buildah.py @@ -13,6 +13,7 @@ from __future__ import annotations import logging +import os from pathlib import Path from captain.util import run @@ -119,7 +120,17 @@ def manifest_push( dest: str, ) -> None: log.info("buildah manifest push → %s", dest) - run(["buildah", "manifest", "push", "--all", manifest, f"docker://{dest}"]) + run( + [ + "buildah", + "manifest", + "push", + *(("--tls-verify=false",) if os.environ.get("BUILDAH_INSECURE") == "1" else ()), + "--all", + manifest, + f"docker://{dest}", + ] + ) def rmi( diff --git a/captain/docker.py b/captain/docker.py index 7151eb92..656fd634 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -122,6 +122,8 @@ def run_in_release(cfg: Config, *extra_args: str) -> None: "-e", "BUILDAH_ISOLATION=chroot", "-e", + f"BUILDAH_INSECURE={os.environ.get('BUILDAH_INSECURE', '')}", + "-e", f"TERM={os.environ.get('TERM', 'xterm-256color')}", "-e", "FORCE_COLOR=1", From eb4766745b7a44da13bea294b7d54ddab42f0e8d Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 17:15:52 +0200 Subject: [PATCH 044/143] release: pass flavor down to release, avoid releasing iso for !has_iso Signed-off-by: Ricardo Pardini --- captain/cli/_main.py | 6 ++++-- captain/cli/_release.py | 6 +++++- captain/oci/_build.py | 6 +++++- captain/oci/_publish.py | 7 +++---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 08702fa8..a2aecfc9 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -106,10 +106,12 @@ def main(project_dir: Path | None = None) -> None: if handler is not None: # If building, or doing initramfs (mkosi), # generate the flavor first so the files are in place. - if command in ("build", "initramfs"): + if command in ("build", "initramfs", "publish"): flavor.generate() handler(cfg, flavor, extra) # type: ignore[operator] - elif command in ("qemu-test", "checksums", "release", "clean"): + elif command in ("release"): + handler(cfg, flavor, extra, args=args) # type: ignore[operator] + elif command in ("qemu-test", "checksums", "clean"): handler(cfg, extra, args=args) # type: ignore[operator] else: handler(cfg, extra) # type: ignore[operator] diff --git a/captain/cli/_release.py b/captain/cli/_release.py index 5b6fa496..4ed0c7d6 100644 --- a/captain/cli/_release.py +++ b/captain/cli/_release.py @@ -11,6 +11,7 @@ from captain import docker, oci from captain.config import Config +from captain.flavor import BaseFlavor from captain.util import check_release_dependencies from ._parser import ( @@ -85,7 +86,9 @@ def _resolve_git_sha(args: object, project_dir: Path) -> str: return result.stdout.strip() -def _cmd_release(cfg: Config, extra_args: list[str], args: object = None) -> None: +def _cmd_release( + cfg: Config, flavor: BaseFlavor, extra_args: list[str], args: object = None +) -> None: """OCI artifact operations: publish, pull, tag.""" # Peel the release subcommand from extra_args. @@ -202,6 +205,7 @@ def _cmd_release(cfg: Config, extra_args: list[str], args: object = None) -> Non force = getattr(args, "force", False) oci.publish( cfg, + flavor, target=target, registry=registry, repository=repository, diff --git a/captain/oci/_build.py b/captain/oci/_build.py index 0f39b69f..ac846d62 100644 --- a/captain/oci/_build.py +++ b/captain/oci/_build.py @@ -40,6 +40,7 @@ def _collect_arch_artifacts( out: Path, arch: str, flavor_id: str, + has_iso: bool, ) -> list[Path]: """Collect and return the artifact files for a single architecture. @@ -50,8 +51,11 @@ def _collect_arch_artifacts( arch_files = [ out / f"vmlinuz-{flavor_id}-{oarch}", out / f"initramfs-{flavor_id}-{oarch}", - out / f"captainos-{flavor_id}-{oarch}.iso", ] + + if has_iso: + arch_files += [out / f"captainos-{flavor_id}-{oarch}.iso"] + checksums_path = out / f"sha256sums-{flavor_id}-{oarch}.txt" artifacts.collect_checksums(arch_files, checksums_path) diff --git a/captain/oci/_publish.py b/captain/oci/_publish.py index e757eda8..eebf5205 100644 --- a/captain/oci/_publish.py +++ b/captain/oci/_publish.py @@ -11,6 +11,7 @@ from captain import buildah, skopeo from captain.config import Config +from captain.flavor import BaseFlavor from captain.util import ensure_dir, get_arch_info from ._build import _build_platform_image, _collect_arch_artifacts, _deterministic_tar @@ -152,6 +153,7 @@ def _publish_combined( def publish( cfg: Config, + flavor: BaseFlavor, *, target: str, registry: str, @@ -191,10 +193,7 @@ def publish( arch_files: dict[str, list[Path]] = {} for arch in arches: arch_files[arch] = _collect_arch_artifacts( - cfg.project_dir, - out, - arch, - cfg.flavor_id, + cfg.project_dir, out, arch, cfg.flavor_id, has_iso=flavor.has_iso() ) # Create deterministic layer tars (shared across manifest pushes). From ead10d653b20b67ae165835c8cd01263e7b6daa7 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 20:23:08 +0200 Subject: [PATCH 045/143] cli: new cli under `click_cli` - add to pyproject.toml, thus `uv run captain ...` Signed-off-by: Ricardo Pardini --- captain/click_cli/__init__.py | 20 +++ captain/click_cli/_build.py | 169 ++++++++++++++++++ captain/click_cli/_builder.py | 130 ++++++++++++++ captain/click_cli/_main.py | 119 +++++++++++++ captain/click_cli/_release_publish.py | 243 ++++++++++++++++++++++++++ click_cli.py | 32 ++++ pyproject.toml | 8 + 7 files changed, 721 insertions(+) create mode 100644 captain/click_cli/__init__.py create mode 100644 captain/click_cli/_build.py create mode 100644 captain/click_cli/_builder.py create mode 100644 captain/click_cli/_main.py create mode 100644 captain/click_cli/_release_publish.py create mode 100644 click_cli.py diff --git a/captain/click_cli/__init__.py b/captain/click_cli/__init__.py new file mode 100644 index 00000000..b8b35d11 --- /dev/null +++ b/captain/click_cli/__init__.py @@ -0,0 +1,20 @@ +"""Click-based CLI for CaptainOS. + +Provides the ``captain`` console script with subcommands: + +- ``builder`` — build the Docker builder image, optionally push it +- ``build`` — full build pipeline (tools → initramfs → iso → artifacts) +- ``release-publish`` — publish artifacts as OCI images via buildah + +Shell completion is available for bash and zsh:: + + # bash + eval "$(_CAPTAIN_COMPLETE=bash_source captain)" + + # zsh + eval "$(_CAPTAIN_COMPLETE=zsh_source captain)" +""" + +from captain.click_cli._main import main + +__all__ = ["main"] diff --git a/captain/click_cli/_build.py b/captain/click_cli/_build.py new file mode 100644 index 00000000..8d23edc4 --- /dev/null +++ b/captain/click_cli/_build.py @@ -0,0 +1,169 @@ +"""``captain build`` — full build pipeline (tools → initramfs → iso → artifacts).""" + +from __future__ import annotations + +import logging + +import click + +import captain.flavor +from captain.click_cli._main import cli, common_options, resolve_project_dir +from captain.config import Config + +log = logging.getLogger(__name__) + + +@cli.command( + "build", + short_help="Run the full build pipeline via mkosi.", +) +@common_options +@click.option( + "--builder-image", + envvar="BUILDER_IMAGE", + default="captainos-builder", + show_default=True, + help="Docker builder image name.", +) +@click.option( + "--no-cache", + envvar="NO_CACHE", + is_flag=True, + default=False, + help="Rebuild the builder image without Docker layer cache.", +) +@click.option( + "--mkosi-mode", + envvar="MKOSI_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Mkosi stage execution mode (docker, native, skip).", +) +@click.option( + "--tools-mode", + envvar="TOOLS_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Tools download stage execution mode (docker, native, skip).", +) +@click.option( + "--iso-mode", + envvar="ISO_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="ISO build stage execution mode (docker, native, skip).", +) +@click.option( + "--force", + "force_mkosi", + is_flag=True, + default=False, + help="Pass --force through to mkosi.", +) +@click.option( + "--force-tools", + envvar="FORCE_TOOLS", + is_flag=True, + default=False, + help="Re-download tools even if outputs already exist.", +) +@click.option( + "--force-iso", + envvar="FORCE_ISO", + is_flag=True, + default=False, + help="Force ISO rebuild even if outputs already exist.", +) +def build_cmd( + *, + arch: str, + flavor_id: str, + project_dir: str | None, + verbose: bool, + builder_image: str, + no_cache: bool, + mkosi_mode: str, + tools_mode: str, + iso_mode: str, + force_mkosi: bool, + force_tools: bool, + force_iso: bool, +) -> None: + """Run the full CaptainOS build pipeline. + + Stages executed in order: tools → initramfs (mkosi) → ISO → artifact + collection. Each stage can be independently set to run inside Docker, + natively on the host, or skipped entirely. + + \b + Examples + -------- + captain build + captain build --arch arm64 + captain build --flavor-id trixie-meson64 --arch arm64 + captain build --mkosi-mode native --tools-mode native + captain build --force --force-tools + """ + _configure_logging(verbose) + + proj = resolve_project_dir(project_dir) + + cfg = Config( + project_dir=proj, + output_dir=proj / "out", + arch=arch, + flavor_id=flavor_id, + builder_image=builder_image, + no_cache=no_cache, + tools_mode=tools_mode, + mkosi_mode=mkosi_mode, + iso_mode=iso_mode, + force_tools=force_tools, + force_iso=force_iso, + ) + + if force_mkosi: + cfg.mkosi_args = ["--force"] + + # Instantiate and generate the flavor. + flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + flavor.generate() + + # Import build commands (reuse existing stage orchestration). + from captain import artifacts + from captain.cli._stages import ( + _build_iso_stage, + _build_mkosi_stage, + _build_tools_stage, + ) + + # Tools stage. + _build_tools_stage(cfg) + + # Initramfs (mkosi) stage + artifact collection. + _build_mkosi_stage(cfg, list(cfg.mkosi_args)) + artifacts.collect_initramfs(cfg) + artifacts.collect_kernel(cfg) + artifacts.collect_dtbs(cfg) + log.info("Initramfs build complete.") + + # ISO stage (if the flavor supports it). + if flavor.has_iso(): + _build_iso_stage(cfg) + else: + log.info("Flavor '%s' does not produce an ISO — skipping.", flavor.id) + + # Final artifact collection. + artifacts.collect(cfg) + log.info("Build complete!") + + +def _configure_logging(verbose: bool) -> None: + level = logging.DEBUG if verbose else logging.INFO + logging.getLogger("captain").setLevel(level) diff --git a/captain/click_cli/_builder.py b/captain/click_cli/_builder.py new file mode 100644 index 00000000..db167b6f --- /dev/null +++ b/captain/click_cli/_builder.py @@ -0,0 +1,130 @@ +"""``captain builder`` — build (and optionally push) the Docker builder image.""" + +from __future__ import annotations + +import logging + +import click + +from captain.click_cli._main import cli, common_options, resolve_project_dir +from captain.config import Config +from captain.docker import build_builder + +log = logging.getLogger(__name__) + + +@cli.command( + "builder", + short_help="Build the Docker builder image, optionally push it.", +) +@common_options +@click.option( + "--builder-image", + envvar="BUILDER_IMAGE", + default="captainos-builder", + show_default=True, + help="Docker builder image name.", +) +@click.option( + "--no-cache", + envvar="NO_CACHE", + is_flag=True, + default=False, + help="Rebuild the builder image without Docker layer cache.", +) +@click.option( + "--push", + is_flag=True, + default=False, + help="Push the built image to a registry after building.", +) +@click.option( + "--registry", + envvar="REGISTRY", + default=None, + help="Registry hostname (e.g. ghcr.io). Required when --push is set.", +) +@click.option( + "--registry-path", + envvar="REGISTRY_PATH", + default=None, + help="Repository path within the registry (e.g. tinkerbell/captain/builder).", +) +@click.option( + "--tag", + "push_tag", + default=None, + help="Tag to apply when pushing (default: Dockerfile content hash).", +) +def builder_cmd( + *, + arch: str, + flavor_id: str, + project_dir: str | None, + verbose: bool, + builder_image: str, + no_cache: bool, + push: bool, + registry: str | None, + registry_path: str | None, + push_tag: str | None, +) -> None: + """Build the Docker builder image used by other build stages. + + By default the image is built locally only. Pass --push together + with --registry and --registry-path to also push the image to a + remote container registry. + + \b + Examples + -------- + captain builder + captain builder --no-cache + captain builder --push --registry ghcr.io --registry-path tinkerbell/captain/builder + captain builder --push --registry ghcr.io --registry-path tinkerbell/captain/builder --tag lt + """ + _configure_logging(verbose) + + proj = resolve_project_dir(project_dir) + + cfg = Config( + project_dir=proj, + output_dir=proj / "out", + arch=arch, + flavor_id=flavor_id, + builder_image=builder_image, + no_cache=no_cache, + ) + + # 1. Build the image. + build_builder(cfg) + log.info("Builder image '%s' is ready.", cfg.builder_image) + + # 2. Optionally push. + if push: + if not registry or not registry_path: + raise click.UsageError( + "--registry and --registry-path are required when --push is set." + ) + import hashlib + + from captain.util import run + + if push_tag is None: + # Use the Dockerfile content hash (same logic as docker.py) + dockerfile = cfg.project_dir / "Dockerfile" + push_tag = hashlib.sha256(dockerfile.read_bytes()).hexdigest() + + full_ref = f"{registry}/{registry_path}:{push_tag}" + log.info("Pushing builder image → %s", full_ref) + + # Tag and push. + run(["docker", "tag", cfg.builder_image, full_ref]) + run(["docker", "push", full_ref]) + log.info("Push complete: %s", full_ref) + + +def _configure_logging(verbose: bool) -> None: + """Set the captain logger level based on the --verbose flag.""" + level = logging.DEBUG if verbose else logging.INFO + logging.getLogger("captain").setLevel(level) diff --git a/captain/click_cli/_main.py b/captain/click_cli/_main.py new file mode 100644 index 00000000..fd2c3305 --- /dev/null +++ b/captain/click_cli/_main.py @@ -0,0 +1,119 @@ +"""Click CLI — main group with shared options and subcommand registration.""" + +from __future__ import annotations + +import functools +import logging +import sys +from pathlib import Path +from typing import Any + +import click + +from captain.config import DEFAULT_FLAVOR_ID + +log = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Shared option decorators +# --------------------------------------------------------------------------- + + +def common_options(fn: Any) -> Any: + """Decorate a click command with options shared by every subcommand.""" + + @click.option( + "--arch", + envvar="ARCH", + default="amd64", + show_default=True, + type=click.Choice(["amd64", "arm64"], case_sensitive=False), + metavar="ARCH", + help="Target architecture (amd64, arm64).", + ) + @click.option( + "--flavor-id", + envvar="FLAVOR_ID", + default=DEFAULT_FLAVOR_ID, + show_default=True, + help="Flavor (kernel/board config) to build.", + ) + @click.option( + "--project-dir", + envvar="CAPTAIN_PROJECT_DIR", + default=None, + type=click.Path(exists=True, file_okay=False, resolve_path=True), + help="Project root directory (auto-detected when omitted).", + ) + @click.option( + "-v", + "--verbose", + is_flag=True, + default=False, + help="Enable debug-level logging.", + ) + @functools.wraps(fn) + def wrapper(**kwargs: Any) -> Any: + return fn(**kwargs) + + return wrapper + + +# --------------------------------------------------------------------------- +# Resolve project directory +# --------------------------------------------------------------------------- + + +def resolve_project_dir(project_dir: str | None) -> Path: + """Return an absolute ``Path`` for the project root.""" + if project_dir is not None: + return Path(project_dir) + # Walk upward from this file until we find pyproject.toml. + candidate = Path(__file__).resolve().parent.parent.parent + if (candidate / "pyproject.toml").is_file(): + return candidate + click.echo("Error: cannot auto-detect project directory. Pass --project-dir.", err=True) + sys.exit(1) + + +# --------------------------------------------------------------------------- +# Top-level Click group +# --------------------------------------------------------------------------- + +CONTEXT_SETTINGS = dict( + help_option_names=["-h", "--help"], + max_content_width=120, +) + + +@click.group( + context_settings=CONTEXT_SETTINGS, + invoke_without_command=True, + help=( + "CaptainOS build system.\n\n" + "Run 'captain COMMAND --help' for details on each subcommand.\n\n" + "Shell completion (bash/zsh):\n\n" + ' eval "$(_CAPTAIN_COMPLETE=bash_source captain)" # bash\n\n' + ' eval "$(_CAPTAIN_COMPLETE=zsh_source captain)" # zsh' + ), +) +@click.version_option(package_name="captain") +@click.pass_context +def cli(ctx: click.Context) -> None: + """CaptainOS build system — click CLI.""" + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + + +# --------------------------------------------------------------------------- +# Register subcommands (imported lazily to avoid circular imports) +# --------------------------------------------------------------------------- + + +def main() -> None: + """Console-script entry point.""" + # Import subcommand modules to register them on the group. + from captain.click_cli import _build, _builder, _release_publish # noqa: F401 + + cli() diff --git a/captain/click_cli/_release_publish.py b/captain/click_cli/_release_publish.py new file mode 100644 index 00000000..3a85fb96 --- /dev/null +++ b/captain/click_cli/_release_publish.py @@ -0,0 +1,243 @@ +"""``captain release-publish`` — publish artifacts as OCI images via buildah.""" + +from __future__ import annotations + +import logging +import subprocess +from pathlib import Path + +import click + +import captain.flavor +from captain import oci +from captain.click_cli._main import cli, common_options, resolve_project_dir +from captain.config import Config +from captain.util import check_release_dependencies + +log = logging.getLogger(__name__) + + +@cli.command( + "release-publish", + short_help="Publish build artifacts as a multi-arch OCI image.", +) +@common_options +@click.option( + "--builder-image", + envvar="BUILDER_IMAGE", + default="captainos-builder", + show_default=True, + help="Docker builder image name.", +) +@click.option( + "--no-cache", + envvar="NO_CACHE", + is_flag=True, + default=False, + help="Rebuild images without Docker layer cache.", +) +@click.option( + "--release-mode", + envvar="RELEASE_MODE", + default="native", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Release stage execution mode (docker, native, skip).", +) +@click.option( + "--registry", + envvar="REGISTRY", + default="ghcr.io", + show_default=True, + help="OCI registry hostname.", +) +@click.option( + "--repository", + envvar="GITHUB_REPOSITORY", + default="tinkerbell/captain", + show_default=True, + help="Repository path (owner/name).", +) +@click.option( + "--oci-artifact-name", + envvar="OCI_ARTIFACT_NAME", + default="artifacts", + show_default=True, + help="OCI artifact image name.", +) +@click.option( + "--target", + envvar="TARGET", + default=None, + type=click.Choice(["amd64", "arm64", "combined"], case_sensitive=False), + metavar="TARGET", + help="Artifact target: amd64, arm64, or combined (default: value of --arch).", +) +@click.option( + "--git-sha", + envvar="GITHUB_SHA", + default=None, + help="Git commit SHA (default: auto-detected via git rev-parse HEAD).", +) +@click.option( + "--version-exclude", + envvar="VERSION_EXCLUDE", + default=None, + help="Tag to exclude from git-describe version lookup.", +) +@click.option( + "--force", + "force", + is_flag=True, + default=False, + help="Publish even if the image already exists in the registry.", +) +def release_publish_cmd( + *, + arch: str, + flavor_id: str, + project_dir: str | None, + verbose: bool, + builder_image: str, + no_cache: bool, + release_mode: str, + registry: str, + repository: str, + oci_artifact_name: str, + target: str | None, + git_sha: str | None, + version_exclude: str | None, + force: bool, +) -> None: + """Publish build artifacts as a multi-arch OCI image to a registry. + + Uses buildah to construct OCI images from the build artifacts (kernel, + initramfs, ISO, DTBs) and pushes them to the specified registry. + + Each artifact file becomes its own layer. Deterministic tar generation + ensures byte-identical layers across runs so that registries can + deduplicate blobs. + + \b + Examples + -------- + captain release-publish + captain release-publish --arch arm64 --target arm64 + captain release-publish --target combined --force + captain release-publish --registry ghcr.io --repository tinkerbell/captain + """ + _configure_logging(verbose) + + proj = resolve_project_dir(project_dir) + + if target is None: + target = arch + + cfg = Config( + project_dir=proj, + output_dir=proj / "out", + arch=arch, + flavor_id=flavor_id, + builder_image=builder_image, + no_cache=no_cache, + release_mode=release_mode, + ) + + # --- skip mode -------------------------------------------------------- + if cfg.release_mode == "skip": + log.info("RELEASE_MODE=skip — nothing to do.") + return + + # --- docker mode ------------------------------------------------------ + if cfg.release_mode == "docker": + from captain import docker + + docker.build_release_image(cfg) + sha = _resolve_git_sha(git_sha, proj) + + env_args: list[str] = [ + "-e", + f"FLAVOR_ID={cfg.flavor_id}", + "-e", + f"REGISTRY={registry}", + "-e", + f"GITHUB_REPOSITORY={repository}", + "-e", + f"OCI_ARTIFACT_NAME={oci_artifact_name}", + "-e", + f"GITHUB_SHA={sha}", + "-e", + f"TARGET={target}", + ] + if version_exclude: + env_args += ["-e", f"VERSION_EXCLUDE={version_exclude}"] + if force: + env_args += ["-e", "FORCE=true"] + + inner_cmd = ["/work/build.py", "release", "publish"] + + try: + docker.run_in_release( + cfg, + *env_args, + "--entrypoint", + "/usr/bin/uv", + docker.RELEASE_IMAGE, + *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), + "run", + *inner_cmd, + ) + except subprocess.CalledProcessError as exc: + raise SystemExit(exc.returncode) from None + docker.fix_docker_ownership(cfg, ["/work/out"]) + return + + # --- native mode ------------------------------------------------------ + missing = check_release_dependencies() + if missing: + log.error("Missing release tools: %s", ", ".join(missing)) + log.error("Install them or set --release-mode=docker.") + raise SystemExit(1) + + sha = _resolve_git_sha(git_sha, proj) + tag = oci.compute_version_tag(proj, sha, exclude=version_exclude) + tag = f"{tag}-{cfg.flavor_id}" + + flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + + oci.publish( + cfg, + flavor, + target=target, + registry=registry, + repository=repository, + artifact_name=oci_artifact_name, + tag=tag, + sha=sha, + force=force, + ) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _resolve_git_sha(sha: str | None, project_dir: Path) -> str: + """Return the provided SHA or auto-detect via ``git rev-parse HEAD``.""" + if sha: + return sha + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + capture_output=True, + text=True, + check=True, + cwd=project_dir, + ) + return result.stdout.strip() + + +def _configure_logging(verbose: bool) -> None: + level = logging.DEBUG if verbose else logging.INFO + logging.getLogger("captain").setLevel(level) diff --git a/click_cli.py b/click_cli.py new file mode 100644 index 00000000..e0147bc0 --- /dev/null +++ b/click_cli.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""CaptainOS build system — click-based CLI entry point. + +Requires: Python >= 3.13, click, Rich, Docker (unless all stages use native/skip). +Use Astral's ``uv`` to run:: + + uv run click_cli.py --help + uv run click_cli.py builder + uv run click_cli.py build --arch arm64 + uv run click_cli.py release-publish --target combined + +Or, after ``uv pip install -e .``:: + + captain --help +""" + +import sys + +if sys.version_info < (3, 13): + print("ERROR: Python >= 3.13 is required.", file=sys.stderr) + sys.exit(1) + +try: + from captain.click_cli import main +except ImportError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + uv_url = "https://docs.astral.sh/uv/getting-started/installation/" + print(f"Missing dependencies, use uv to run. See {uv_url}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index c518bcaf..e98295b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,21 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] version = "0.0.1" name = "captain" requires-python = ">=3.13" dependencies = [ + "click>=8.1", "configargparse>=1.7", "rich>=14.3.3", "jinja2>=3.1.6" ] +[project.scripts] +captain = "captain.click_cli:main" + [project.optional-dependencies] dev = ["ruff>=0.15.9", "pyright>=1.1"] From ff8cdc1a90e7c133fb0f5bfab16cdda70bde39ac Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 6 Apr 2026 20:45:06 +0200 Subject: [PATCH 046/143] click: add tools command - using captain.cli._stages Signed-off-by: Ricardo Pardini --- captain/click_cli/_main.py | 2 +- captain/click_cli/_tools.py | 98 +++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 captain/click_cli/_tools.py diff --git a/captain/click_cli/_main.py b/captain/click_cli/_main.py index fd2c3305..da9bbf42 100644 --- a/captain/click_cli/_main.py +++ b/captain/click_cli/_main.py @@ -114,6 +114,6 @@ def cli(ctx: click.Context) -> None: def main() -> None: """Console-script entry point.""" # Import subcommand modules to register them on the group. - from captain.click_cli import _build, _builder, _release_publish # noqa: F401 + from captain.click_cli import _build, _builder, _release_publish, _tools # noqa: F401 cli() diff --git a/captain/click_cli/_tools.py b/captain/click_cli/_tools.py new file mode 100644 index 00000000..c3267d22 --- /dev/null +++ b/captain/click_cli/_tools.py @@ -0,0 +1,98 @@ +"""``captain tools`` — download tools (containerd, runc, nerdctl, CNI plugins).""" + +from __future__ import annotations + +import logging + +import click + +from captain.click_cli._main import cli, common_options, resolve_project_dir +from captain.config import Config + +log = logging.getLogger(__name__) + + +@cli.command( + "tools", + short_help="Download tools (containerd, runc, nerdctl, CNI).", +) +@common_options +@click.option( + "--builder-image", + envvar="BUILDER_IMAGE", + default="captainos-builder", + show_default=True, + help="Docker builder image name.", +) +@click.option( + "--no-cache", + envvar="NO_CACHE", + is_flag=True, + default=False, + help="Rebuild the builder image without Docker layer cache.", +) +@click.option( + "--tools-mode", + envvar="TOOLS_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Tools download stage execution mode (docker, native, skip).", +) +@click.option( + "--force-tools", + envvar="FORCE_TOOLS", + is_flag=True, + default=False, + help="Re-download tools even if outputs already exist.", +) +def tools_cmd( + *, + arch: str, + flavor_id: str, + project_dir: str | None, + verbose: bool, + builder_image: str, + no_cache: bool, + tools_mode: str, + force_tools: bool, +) -> None: + """Download tools (containerd, runc, nerdctl, CNI plugins). + + Fetches pre-built binaries for the target architecture and stages + them under ``mkosi.output/tools/{arch}/``. The tools are later + merged into the initramfs by mkosi via ``--extra-tree``. + + \b + Examples + -------- + captain tools + captain tools --arch arm64 + captain tools --tools-mode native + captain tools --force-tools + """ + _configure_logging(verbose) + + proj = resolve_project_dir(project_dir) + + cfg = Config( + project_dir=proj, + output_dir=proj / "out", + arch=arch, + flavor_id=flavor_id, + builder_image=builder_image, + no_cache=no_cache, + tools_mode=tools_mode, + force_tools=force_tools, + ) + + from captain.cli._stages import _build_tools_stage + + _build_tools_stage(cfg) + log.info("Tools stage complete!") + + +def _configure_logging(verbose: bool) -> None: + level = logging.DEBUG if verbose else logging.INFO + logging.getLogger("captain").setLevel(level) From 324e102cdb8542e50a236bafeaf9e803371b2ab2 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 00:07:46 +0200 Subject: [PATCH 047/143] click: some sugar, list available flavors via reflection Signed-off-by: Ricardo Pardini --- captain/click_cli/_main.py | 5 ++++- captain/click_cli/_release_publish.py | 3 ++- captain/flavor.py | 25 +++++++++++++++++++++++++ captain/util.py | 11 +++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/captain/click_cli/_main.py b/captain/click_cli/_main.py index da9bbf42..fe6e6bca 100644 --- a/captain/click_cli/_main.py +++ b/captain/click_cli/_main.py @@ -11,6 +11,8 @@ import click from captain.config import DEFAULT_FLAVOR_ID +from captain.flavor import list_available_flavors +from captain.util import detect_current_machine_arch log = logging.getLogger(__name__) @@ -26,7 +28,7 @@ def common_options(fn: Any) -> Any: @click.option( "--arch", envvar="ARCH", - default="amd64", + default=(detect_current_machine_arch()), show_default=True, type=click.Choice(["amd64", "arm64"], case_sensitive=False), metavar="ARCH", @@ -37,6 +39,7 @@ def common_options(fn: Any) -> Any: envvar="FLAVOR_ID", default=DEFAULT_FLAVOR_ID, show_default=True, + type=click.Choice(list_available_flavors(), case_sensitive=False), help="Flavor (kernel/board config) to build.", ) @click.option( diff --git a/captain/click_cli/_release_publish.py b/captain/click_cli/_release_publish.py index 3a85fb96..ec1b1006 100644 --- a/captain/click_cli/_release_publish.py +++ b/captain/click_cli/_release_publish.py @@ -72,7 +72,8 @@ default=None, type=click.Choice(["amd64", "arm64", "combined"], case_sensitive=False), metavar="TARGET", - help="Artifact target: amd64, arm64, or combined (default: value of --arch).", + help="Artifact target: amd64, arm64, or combined (default: value of --arch); " + "combined requires trixie-full or equivalent flavor with both architectures' outputs present.", ) @click.option( "--git-sha", diff --git a/captain/flavor.py b/captain/flavor.py index c79ee2d7..18098da7 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -115,6 +115,31 @@ def has_iso(self) -> bool: return False +def list_available_flavors() -> list[str]: + import importlib + import pkgutil + + package = importlib.import_module("captain.flavors") + # iter_modules finds immediate children; walk_packages recurses + ret = [] + for _finder, module_name, _is_pkg in pkgutil.walk_packages( + package.__path__, prefix=f"{package.__name__}." + ): + try: + module = importlib.import_module(module_name) + except Exception as exc: + log.debug(f"Skipping {module_name}: {exc}") + continue + + fn = getattr(module, "create_flavor", None) + if fn is not None and callable(fn): + flavor_id = module_name.rsplit(".", 1)[-1].replace("_", "-") + ret.append(flavor_id) + log.debug("Discovered flavor '%s' via module %s", flavor_id, module_name) + + return sorted(ret) + + def create_and_setup_flavor_for_id(flavor_id: str, cfg: Config) -> BaseFlavor: log.debug("Creating and setting up flavor for id '%s'", flavor_id) flavor_id_underscore = flavor_id.replace("-", "_") diff --git a/captain/util.py b/captain/util.py index 61736fad..9e2330bf 100644 --- a/captain/util.py +++ b/captain/util.py @@ -4,6 +4,7 @@ import logging import os +import platform import subprocess import sys import tarfile @@ -53,6 +54,16 @@ def get_arch_info(arch: str) -> ArchInfo: sys.exit(1) +def detect_current_machine_arch() -> str: + machine = platform.machine().lower() + if machine in ("aarch64", "arm64"): + return "arm64" + elif machine in ("x86_64", "amd64"): + return "amd64" + else: + raise RuntimeError(f"Unsupported architecture: {machine}") + + def run( cmd: list[str], *, From 5a7ecfb84b4065fd46044fdd967a725705e00701 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 01:00:25 +0200 Subject: [PATCH 048/143] docker: build_builder(): more info Signed-off-by: Ricardo Pardini --- captain/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/captain/docker.py b/captain/docker.py index 656fd634..8f7477ce 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -50,7 +50,7 @@ def build_builder(cfg: Config) -> None: tagged_image = f"{cfg.builder_image}:{tag}" if not cfg.no_cache and _image_exists(tagged_image): - log.info("Docker image '%s' is up to date.", cfg.builder_image) + log.info("Docker image '%s' is up to date with %s.", cfg.builder_image, tagged_image) # Ensure the un-hashed tag exists so later docker-run calls that # reference cfg.builder_image (without the hash suffix) succeed. # This matters when the hashed tag was pre-loaded by CI. From 935a4f6c01fdb007d3a62d664c8feb939122d547 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 01:03:07 +0200 Subject: [PATCH 049/143] click: kill old tools cli (keep stage) click: kill old cli completely Signed-off-by: Ricardo Pardini --- build.py | 14 +- captain/cli/__init__.py | 22 - captain/cli/_commands.py | 250 -------- captain/cli/_main.py | 150 ----- captain/cli/_parser.py | 554 ------------------ captain/cli/_release.py | 241 -------- captain/{click_cli => click}/__init__.py | 2 +- captain/{click_cli => click}/_build.py | 19 +- captain/{click_cli => click}/_builder.py | 3 +- captain/{click_cli => click}/_main.py | 2 +- .../{click_cli => click}/_release_publish.py | 5 +- captain/{cli => click}/_stages.py | 0 captain/{click_cli => click}/_tools.py | 6 +- click_cli.py | 32 - pyproject.toml | 3 +- 15 files changed, 24 insertions(+), 1279 deletions(-) delete mode 100644 captain/cli/__init__.py delete mode 100644 captain/cli/_commands.py delete mode 100644 captain/cli/_main.py delete mode 100644 captain/cli/_parser.py delete mode 100644 captain/cli/_release.py rename captain/{click_cli => click}/__init__.py (92%) rename captain/{click_cli => click}/_build.py (93%) rename captain/{click_cli => click}/_builder.py (97%) rename captain/{click_cli => click}/_main.py (97%) rename captain/{click_cli => click}/_release_publish.py (98%) rename captain/{cli => click}/_stages.py (100%) rename captain/{click_cli => click}/_tools.py (94%) delete mode 100644 click_cli.py diff --git a/build.py b/build.py index df5324fc..07f38da3 100755 --- a/build.py +++ b/build.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 -"""CaptainOS build system entry point. +"""CaptainOS build system — click-based CLI entry point. -Requires: Python >= 3.13, Rich, Docker (unless all stages use native or skip). -It is recommended to use Astral's uv to run this script, which will automatically - install dependencies in an isolated environment. -This will automatically use uv to re-launch itself when stages use Docker. +Requires: Python >= 3.13 and a lot of dependencies; Use Astral's ``uv`` to run:: + + uv run click_cli.py --help + uv run click_cli.py builder + uv run click_cli.py build --arch arm64 + uv run click_cli.py release-publish --target combined """ import sys @@ -14,7 +16,7 @@ sys.exit(1) try: - from captain.cli import main + from captain.click import main except ImportError as exc: print(f"ERROR: {exc}", file=sys.stderr) uv_url = "https://docs.astral.sh/uv/getting-started/installation/" diff --git a/captain/cli/__init__.py b/captain/cli/__init__.py deleted file mode 100644 index 77481c1c..00000000 --- a/captain/cli/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -"""CLI entry point — single configargparse parser with pre-extracted subcommand. - -Every configuration parameter is both a ``--cli-flag`` and an environment -variable, following the ff priority model: - - CLI args > environment variables > defaults - -The subcommand (``build``, ``initramfs``, ``tools``, …) is extracted from -``sys.argv`` *before* parsing so that flags work in any position:: - - ./build.py --arch=arm64 initramfs # works - ./build.py initramfs --arch=arm64 # also works - ARCH=arm64 ./build.py initramfs # also works -""" - -from captain.cli._main import main -from captain.cli._parser import COMMANDS - -__all__ = [ - "COMMANDS", - "main", -] diff --git a/captain/cli/_commands.py b/captain/cli/_commands.py deleted file mode 100644 index 6d319185..00000000 --- a/captain/cli/_commands.py +++ /dev/null @@ -1,250 +0,0 @@ -"""Build and utility command handlers.""" - -from __future__ import annotations - -import logging -import shutil -import sys -from pathlib import Path - -from captain import artifacts, docker, qemu -from captain.config import Config -from captain.flavor import BaseFlavor -from captain.util import run - -from ._stages import ( - _build_iso_stage, - _build_mkosi_stage, - _build_tools_stage, -) - -log = logging.getLogger(__name__) - - -def _cmd_tools(cfg: Config, _extra_args: list[str]) -> None: - """Download tools (containerd, runc, nerdctl, CNI plugins).""" - _build_tools_stage(cfg) - log.info("Tools stage complete!") - - -def _cmd_initramfs(cfg: Config, flavor: BaseFlavor, extra_args: list[str]) -> None: - """Build only the initramfs via mkosi, then collect artifacts.""" - _build_mkosi_stage(cfg, extra_args) - artifacts.collect_initramfs(cfg) - artifacts.collect_kernel(cfg) - artifacts.collect_dtbs(cfg) - log.info("Initramfs build complete!") - - -def _cmd_iso(cfg: Config, _extra_args: list[str]) -> None: - """Build only the ISO image.""" - _build_iso_stage(cfg) - artifacts.collect_iso(cfg) - log.info("ISO build complete!") - - -def _cmd_build(cfg: Config, flavor: BaseFlavor, extra_args: list[str]) -> None: - """Full build: tools → initramfs → iso → artifacts.""" - _build_tools_stage(cfg) - _cmd_initramfs(cfg, flavor, extra_args) # delegate, so it also collects - - if flavor.has_iso(): - _build_iso_stage(cfg) - else: - log.info("Flavor '%s' does not have an ISO stage; skipping.", flavor.id) - - artifacts.collect(cfg) - log.info("Build complete!") - - -def _cmd_shell(cfg: Config, _extra_args: list[str]) -> None: - """Interactive shell inside the builder container.""" - docker.build_builder(cfg) - log.info("Entering builder shell (type 'exit' to leave)...") - docker.run_in_builder( - cfg, - *(["-it"] if sys.stdout.isatty() and sys.stdin.isatty() else []), - "--entrypoint", - "/bin/bash", - cfg.builder_image, - ) - - -def _cmd_clean(cfg: Config, _extra_args: list[str], args: object = None) -> None: - """Remove build artifacts for the selected flavor, or all.""" - clean_all = getattr(args, "clean_all", False) - - if clean_all: - _clean_all(cfg) - else: - _clean_version(cfg) - - -def _clean_version(cfg: Config) -> None: - """Remove build artifacts for a single flavor.""" - kver = cfg.flavor_id - log.info("Cleaning build artifacts for kernel %s (%s)...", kver, cfg.arch) - mkosi_output = cfg.mkosi_output - - # Version-specific directories under mkosi.output/{stage}/{version}/{arch} - version_dirs = [ - mkosi_output / "kernel" / kver / cfg.arch, - mkosi_output / "initramfs" / kver / cfg.arch, - mkosi_output / "iso" / kver / cfg.arch, - ] - - has_docker = shutil.which("docker") is not None - existing = [d for d in version_dirs if d.exists()] - if existing and has_docker: - # Use Docker to remove root-owned files from mkosi. - # Invoke rm directly (no shell) to avoid injection via path components. - container_path_args = [ - f"/work/mkosi.output/{d.relative_to(mkosi_output)}" for d in existing - ] - run( - [ - "docker", - "run", - "--rm", - "-v", - f"{cfg.project_dir}:/work", - "-w", - "/work", - "debian:trixie", - "rm", - "-rf", - "--", - *container_path_args, - ], - ) - elif existing: - for d in existing: - shutil.rmtree(d, ignore_errors=True) - - # Remove versioned artifacts from out/ - if cfg.output_dir.exists(): - for pattern in ( - f"vmlinuz-{kver}-*", - f"initramfs-{kver}-*", - f"captainos-{kver}-*", - f"sha256sums-{kver}-*", - ): - for p in cfg.output_dir.glob(pattern): - p.unlink(missing_ok=True) - - log.info("Clean complete for kernel %s.", kver) - - -def _clean_all(cfg: Config) -> None: - """Remove all build artifacts (all flavors).""" - log.info("Cleaning ALL build artifacts...") - mkosi_output = cfg.mkosi_output - mkosi_cache = cfg.project_dir / "mkosi.cache" - - has_docker = shutil.which("docker") is not None - if has_docker: - # Use Docker to remove root-owned files from mkosi - if mkosi_output.exists() or mkosi_cache.exists(): - run( - [ - "docker", - "run", - "--rm", - "-v", - f"{cfg.project_dir}:/work", - "-w", - "/work", - "debian:trixie", - "sh", - "-c", - "rm -rf /work/mkosi.output/image*" - " /work/mkosi.output/initramfs" - " /work/mkosi.output/kernel" - " /work/mkosi.output/tools" - " /work/mkosi.output/iso" - " /work/mkosi.cache", - ], - ) - else: - # No Docker available — remove directly (may need sudo for root-owned mkosi files) - for pattern in ("image*", "initramfs", "kernel", "tools", "iso"): - for p in mkosi_output.glob(pattern): - if p.is_dir(): - shutil.rmtree(p, ignore_errors=True) - else: - p.unlink(missing_ok=True) - if mkosi_cache.exists(): - shutil.rmtree(mkosi_cache, ignore_errors=True) - - if cfg.output_dir.exists(): - shutil.rmtree(cfg.output_dir) - log.info("Clean complete.") - - -def _cmd_summary(cfg: Config, _extra_args: list[str]) -> None: - """Print mkosi configuration summary.""" - tools_tree = str(cfg.tools_output) - output_dir = str(cfg.initramfs_output) - match cfg.mkosi_mode: - case "docker": - docker.build_builder(cfg) - container_tree = f"/work/mkosi.output/tools/{cfg.arch}" - container_outdir = f"/work/mkosi.output/initramfs/{cfg.flavor_id}/{cfg.arch}" - docker.run_mkosi( - cfg, - f"--extra-tree={container_tree}", - f"--output-dir={container_outdir}", - "summary", - ) - case "native": - run( - [ - "mkosi", - f"--architecture={cfg.arch_info.mkosi_arch}", - f"--extra-tree={tools_tree}", - f"--output-dir={output_dir}", - "summary", - ], - cwd=cfg.project_dir, - ) - case "skip": - log.error("Cannot show mkosi summary when MKOSI_MODE=skip.") - raise SystemExit(1) - - -def _cmd_checksums(cfg: Config, _extra_args: list[str], args: object = None) -> None: - """Compute SHA-256 checksums for the specified files.""" - files = getattr(args, "files", None) or [] - output = getattr(args, "output", None) - - if files: - # Explicit mode: user provided specific files and output. - if not output: - log.error("--output is required when specifying files explicitly.") - raise SystemExit(1) - artifacts.collect_checksums( - [Path(f) for f in files], - Path(output), - ) - else: - # Default mode: produce checksums for the selected architecture. - out = cfg.output_dir - oarch = cfg.arch_info.output_arch - kver = cfg.flavor_id - arch_files = [ - out / f"vmlinuz-{kver}-{oarch}", - out / f"initramfs-{kver}-{oarch}", - out / f"captainos-{kver}-{oarch}.iso", - ] - existing = [f for f in arch_files if f.is_file()] - if not existing: - log.error("No artifacts found for %s-%s in %s", kver, oarch, out) - raise SystemExit(1) - dest = Path(output) if output else out / f"sha256sums-{kver}-{oarch}.txt" - artifacts.collect_checksums(existing, dest) - log.info("Checksums complete!") - - -def _cmd_qemu_test(cfg: Config, _extra_args: list[str], args: object = None) -> None: - """Boot the image in QEMU for testing.""" - qemu.run_qemu(cfg, args=args) # type: ignore[arg-type] diff --git a/captain/cli/_main.py b/captain/cli/_main.py deleted file mode 100644 index a2aecfc9..00000000 --- a/captain/cli/_main.py +++ /dev/null @@ -1,150 +0,0 @@ -"""CLI entry point — single configargparse parser with pre-extracted subcommand. - -Every configuration parameter is both a ``--cli-flag`` and an environment -variable, following the ff priority model: - - CLI args > environment variables > defaults - -The subcommand (``build``, ``kernel``, ``tools``, …) is extracted from -``sys.argv`` *before* parsing so that flags work in any position:: - - ./build.py --arch=arm64 kernel # works - ./build.py kernel --arch=arm64 # also works - ARCH=arm64 ./build.py kernel # also works -""" - -from __future__ import annotations - -import logging -import sys -from pathlib import Path - -import captain.flavor -from captain import docker -from captain.config import Config -from captain.flavor import BaseFlavor -from captain.util import run - -from ._commands import ( - _cmd_build, - _cmd_checksums, - _cmd_clean, - _cmd_initramfs, - _cmd_iso, - _cmd_qemu_test, - _cmd_shell, - _cmd_summary, - _cmd_tools, -) -from ._parser import _build_parser, _extract_command -from ._release import _cmd_release - -log = logging.getLogger(__name__) - - -def main(project_dir: Path | None = None) -> None: - """Main CLI entry point.""" - - # 1. Extract the subcommand from argv before parsing so flags - # work in any position (before or after the command name). - raw_argv = sys.argv[1:] - command, flag_argv = _extract_command(raw_argv) - - # For release subcommands, defer -h/--help to _cmd_release so it - # can print subcommand-specific help instead of the generic release help. - # We defer whenever there's any positional token (not just valid ones), - # so that invalid subcommands like "push" show the proper error instead - # of the parent help. - help_deferred = False - if command == "release": - has_positional = any(not tok.startswith("-") for tok in flag_argv) - has_help = "-h" in flag_argv or "--help" in flag_argv - if has_positional and has_help: - flag_argv = [t for t in flag_argv if t not in ("-h", "--help")] - help_deferred = True - - # 2. Build the parser (TINK flags added only for qemu-test). - parser = _build_parser(command) - - # 3. Parse known args — anything unrecognised passes through to mkosi. - args, extra = parser.parse_known_args(flag_argv) - if help_deferred: - extra.append("--help") - - # 4. Separate --force (mkosi passthrough) from the rest. - mkosi_args: list[str] = [] - if getattr(args, "force", False): - mkosi_args.append("--force") - - # 5. Determine project directory. - if project_dir is None: - project_dir = Path(__file__).resolve().parent.parent.parent - - # 6. Build Config from the parsed namespace. - cfg = Config.from_args(args, project_dir) - cfg.mkosi_args = mkosi_args - - # 7. Instantiate the flavor (from cfg.flavor_id) - flavor: BaseFlavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) - - # 8. Dispatch. - dispatch: dict[str, object] = { - "build": _cmd_build, - "tools": _cmd_tools, - "initramfs": _cmd_initramfs, - "iso": _cmd_iso, - "checksums": _cmd_checksums, - "shell": _cmd_shell, - "clean": _cmd_clean, - "release": _cmd_release, - "summary": _cmd_summary, - "qemu-test": _cmd_qemu_test, - } - - log.debug("Dispatching command '%s', extra args: %s", command, extra) - handler = dispatch.get(command) - if handler is not None: - # If building, or doing initramfs (mkosi), - # generate the flavor first so the files are in place. - if command in ("build", "initramfs", "publish"): - flavor.generate() - handler(cfg, flavor, extra) # type: ignore[operator] - elif command in ("release"): - handler(cfg, flavor, extra, args=args) # type: ignore[operator] - elif command in ("qemu-test", "checksums", "clean"): - handler(cfg, extra, args=args) # type: ignore[operator] - else: - handler(cfg, extra) # type: ignore[operator] - else: - log.error("You hit ungolden path; passing stuff to mkosi directly is not reproducible.") - # Pass through to mkosi (shouldn't happen with _extract_command - # but kept as a safety net). - tools_tree = str(cfg.tools_output) - output_dir = str(cfg.initramfs_output) - match cfg.mkosi_mode: - case "docker": - docker.build_builder(cfg) - container_tree = f"/work/mkosi.output/tools/{cfg.arch}" - container_outdir = f"/work/mkosi.output/initramfs/{cfg.flavor_id}/{cfg.arch}" - docker.run_mkosi( - cfg, - f"--extra-tree={container_tree}", - f"--output-dir={container_outdir}", - command, - *extra, - ) - case "native": - run( - [ - "mkosi", - f"--architecture={cfg.arch_info.mkosi_arch}", - f"--extra-tree={tools_tree}", - f"--output-dir={output_dir}", - command, - *extra, - ], - cwd=cfg.project_dir, - ) - case "skip": - log.error("Cannot pass '%s' to mkosi when MKOSI_MODE=skip.", command) - raise SystemExit(1) diff --git a/captain/cli/_parser.py b/captain/cli/_parser.py deleted file mode 100644 index 46229c48..00000000 --- a/captain/cli/_parser.py +++ /dev/null @@ -1,554 +0,0 @@ -"""CLI parser infrastructure — formatter, constants, and flag definitions.""" - -from __future__ import annotations - -import argparse -import shutil -import sys -from collections.abc import Callable, Iterable - -import configargparse - -from captain.config import DEFAULT_FLAVOR_ID - -# --------------------------------------------------------------------------- -# Known subcommands (order matters for help text) -# --------------------------------------------------------------------------- - -COMMANDS: dict[str, str] = { - "build": "Run all build stages: kernel → tools → initramfs → iso (default)", - "kernel": "Build only the kernel + modules", - "tools": "Download tools (containerd, runc, nerdctl, CNI)", - "initramfs": "Build only the initramfs via mkosi", - "iso": "Build a UEFI-bootable ISO image", - "checksums": "Compute SHA-256 checksums for specified files", - "release": "OCI artifact operations (publish, pull, tag)", - "shell": "Interactive shell inside the builder container", - "clean": "Remove build artifacts (per kernel version or all)", - "summary": "Print mkosi configuration summary", - "qemu-test": "Boot the image in QEMU for testing", -} - -VALID_MODES = ("docker", "native", "skip") - -# Boolean (store_true) flags — these do NOT consume the next token as a value. -# Used by _extract_command to avoid treating a flag value as a subcommand. -_BOOLEAN_FLAGS = frozenset( - { - "--all", - "--force-kernel", - "--force-tools", - "--force-iso", - "--force", - "--no-cache", - "-h", - "--help", - } -) - - -class _HelpFormatter(argparse.RawDescriptionHelpFormatter): - """Clean help: word-wrapped text, raw epilog, no env-var refs, - no defaults when the value is empty / None / False, and a short - usage line.""" - - def _get_help_string(self, action: argparse.Action) -> str: - """Append ``(default: X)`` only when X is meaningful.""" - text = action.help or "" - if action.default in (None, "", False, argparse.SUPPRESS): - return text - if "%(default)" not in text: - text += " (default: %(default)s)" - return text - - def _format_usage( - self, - usage: str | None, - actions: Iterable[argparse.Action], - groups: Iterable[argparse._MutuallyExclusiveGroup], - prefix: str | None, - ) -> str: - """Show a short usage line with the command placeholder.""" - prog = self._prog - # Top-level ("build.py") and release ("build.py release") have subcommands. - if prog in ("build.py", "build.py release"): - return f"usage: {prog} [command] [flags]\n\n" - if prog == "build.py release tag": - return f"usage: {prog} [flags]\n\n" - return f"usage: {prog} [flags]\n\n" - - -def _extract_command(argv: list[str]) -> tuple[str, list[str]]: - """Remove and return the first recognised subcommand from *argv*. - - Returns ``("build", argv)`` when no subcommand is found. - - The scanner skips tokens that are likely flag *values* (the token - immediately after a ``--flag`` that is not boolean and does not use - ``=`` syntax). This prevents ``--builder-image build`` from - incorrectly extracting ``build`` as the subcommand. - """ - prev_was_value_flag = False - for i, tok in enumerate(argv): - if tok.startswith("-"): - # Boolean flags don't consume the next token. - prev_was_value_flag = False if "=" in tok else tok not in _BOOLEAN_FLAGS - continue - if prev_was_value_flag: - # This token is the value of the preceding flag — skip it. - prev_was_value_flag = False - continue - # Standalone positional token — check if it's a command. - if tok in COMMANDS: - return tok, argv[:i] + argv[i + 1 :] - # Unknown positional token — not a recognised command. - valid = ", ".join(COMMANDS) - print( - f"error: unknown command '{tok}'\nvalid commands: {valid}", - file=sys.stderr, - ) - raise SystemExit(2) - return "build", list(argv) - - -def _build_parser(command: str) -> configargparse.ArgParser: - """Construct a command-specific CLI parser. - - Only the flags relevant to *command* are added, so ``--help`` - shows a focused help message. - """ - - # -- description & epilog ------------------------------------------ - if command == "build": - desc = "Build CaptainOS images. Stages: kernel → tools → initramfs → iso." - commands_list = "\n".join(f" {name:14s} {d}" for name, d in COMMANDS.items()) - epilog = f"""\ -commands: -{commands_list} -""" - elif command == "release": - desc = "OCI release workflow: pull (or build) → publish → tag" - release_cmds = { - "publish": "Publish artifacts as a multi-arch OCI image", - "pull": "Pull and extract artifacts (amd64, arm64, or combined)", - "tag": "Tag all artifact images with a version", - } - commands_list = "\n".join(f" {name:14s} {d}" for name, d in release_cmds.items()) - epilog = f"""\ -commands: -{commands_list} -""" - else: - desc = COMMANDS.get(command, command) - epilog = None - - # Adapt to the real terminal width so argparse wraps at word - # boundaries instead of letting the terminal hard-wrap mid-word. - # max_help_position=38 accommodates the widest flag+metavar. - columns = shutil.get_terminal_size().columns - - parser = configargparse.ArgParser( - prog=f"build.py {command}" if command != "build" else "build.py", - description=desc, - epilog=epilog, - add_env_var_help=False, - formatter_class=lambda prog: _HelpFormatter( - prog, - max_help_position=38, - width=columns, - ), - ) - - # -- Add only the flag groups relevant to this command ------------- - for adder in _COMMAND_FLAGS.get(command, []): - adder(parser) - - return parser - - -# --------------------------------------------------------------------------- -# Flag-group helpers — each adds one argument group to the parser -# --------------------------------------------------------------------------- - - -def _add_common_flags(parser: configargparse.ArgParser) -> None: - """--arch, --builder-image, --no-cache""" - g = parser.add_argument_group("build configuration") - g.add_argument( - "--arch", - env_var="ARCH", - default="amd64", - choices=["amd64", "arm64"], - help="target architecture", - ) - g.add_argument( - "--builder-image", - env_var="BUILDER_IMAGE", - metavar="IMAGE", - default="captainos-builder", - help="Docker builder image name", - ) - g.add_argument( - "--no-cache", - env_var="NO_CACHE", - action="store_true", - default=False, - help="rebuild builder image without Docker cache", - ) - - -def _add_flavor_flags(parser: configargparse.ArgParser) -> None: - """--flavor-id""" - g = parser.add_argument_group("kernel") - g.add_argument( - "--flavor-id", - env_var="FLAVOR_ID", - metavar="VER", - default=DEFAULT_FLAVOR_ID, - help="kernel version to build", - ) - - -def _add_tools_flags(parser: configargparse.ArgParser) -> None: - """--tools-mode, --force-tools""" - g = parser.add_argument_group("tools") - g.add_argument( - "--tools-mode", - env_var="TOOLS_MODE", - default="docker", - choices=list(VALID_MODES), - help="tools stage execution mode", - ) - g.add_argument( - "--force-tools", - env_var="FORCE_TOOLS", - action="store_true", - default=False, - help="re-download tools even if outputs exist", - ) - - -def _add_mkosi_flags(parser: configargparse.ArgParser) -> None: - """--mkosi-mode, --force (mkosi passthrough)""" - g = parser.add_argument_group("initramfs (mkosi)") - g.add_argument( - "--mkosi-mode", - env_var="MKOSI_MODE", - default="docker", - choices=list(VALID_MODES), - help="mkosi stage execution mode", - ) - g.add_argument( - "--force", - action="store_true", - default=False, - help="passed through to mkosi as --force", - ) - - -def _add_summary_flags(parser: configargparse.ArgParser) -> None: - """--mkosi-mode only (no --force).""" - g = parser.add_argument_group("mkosi") - g.add_argument( - "--mkosi-mode", - env_var="MKOSI_MODE", - default="docker", - choices=list(VALID_MODES), - help="mkosi stage execution mode", - ) - - -def _add_iso_flags(parser: configargparse.ArgParser) -> None: - """--iso-mode, --force-iso""" - g = parser.add_argument_group("iso") - g.add_argument( - "--iso-mode", - env_var="ISO_MODE", - default="docker", - choices=list(VALID_MODES), - help="iso stage execution mode", - ) - g.add_argument( - "--force-iso", - env_var="FORCE_ISO", - action="store_true", - default=False, - help="force ISO rebuild even if outputs exist", - ) - - -def _add_mode_flags(parser: configargparse.ArgParser) -> None: - """All four --*-mode flags (used by 'shell' which checks needs_docker).""" - g = parser.add_argument_group("stage modes") - for stage in ("kernel", "tools", "mkosi", "iso"): - g.add_argument( - f"--{stage}-mode", - env_var=f"{stage.upper()}_MODE", - default="docker", - choices=list(VALID_MODES), - help=f"{stage} stage execution mode", - ) - - -def _add_clean_flags(parser: configargparse.ArgParser) -> None: - """--all flag for the clean command.""" - g = parser.add_argument_group("clean") - g.add_argument( - "--all", - env_var="CLEAN_ALL", - action="store_true", - default=False, - dest="clean_all", - help="remove ALL build artifacts instead of just the selected kernel version", - ) - - -def _add_checksums_flags(parser: configargparse.ArgParser) -> None: - """--output and positional file arguments for the checksums command.""" - g = parser.add_argument_group("checksums") - g.add_argument( - "--output", - "-o", - metavar="FILE", - default=None, - help="path to write the checksum file (default: out/sha256sums-{arch}.txt)", - ) - g.add_argument( - "files", - nargs="*", - metavar="FILE", - help="files to checksum (default: standard release artifacts in out/)", - ) - - -def _add_release_flags(parser: configargparse.ArgParser) -> None: - """--release-mode and OCI registry flags for the release command.""" - _add_release_base_flags(parser) - _add_release_target_flag(parser) - _add_release_pull_output(parser) - - -def _add_release_base_flags(parser: configargparse.ArgParser) -> None: - """Core release flags shared by all release subcommands.""" - g = parser.add_argument_group("release") - g.add_argument( - "--release-mode", - env_var="RELEASE_MODE", - default="native", - choices=list(VALID_MODES), - metavar="MODE", - help="release stage execution mode", - ) - - g = parser.add_argument_group("OCI registry") - g.add_argument( - "--registry", - env_var="REGISTRY", - metavar="HOST", - default="ghcr.io", - help="OCI registry hostname", - ) - g.add_argument( - "--repository", - env_var="GITHUB_REPOSITORY", - metavar="OWNER/NAME", - default="tinkerbell/captain", - help="repository (owner/name)", - ) - g.add_argument( - "--oci-artifact-name", - env_var="OCI_ARTIFACT_NAME", - metavar="NAME", - default="artifacts", - help="OCI artifact image name", - ) - - -def _add_release_target_flag(parser: configargparse.ArgParser) -> None: - """--target flag for publish and pull (not tag).""" - g = parser.add_argument_group("target") - g.add_argument( - "--target", - env_var="TARGET", - default=None, - choices=["amd64", "arm64", "combined"], - help="artifact target (amd64, arm64, or combined; default: --arch value)", - ) - g.add_argument( - "--git-sha", - env_var="GITHUB_SHA", - metavar="SHA", - default=None, - help="git commit SHA (default: from git rev-parse HEAD)", - ) - g.add_argument( - "--version-exclude", - env_var="VERSION_EXCLUDE", - metavar="TAG", - default=None, - help="tag to exclude from git-describe version lookup", - ) - g.add_argument( - "--force", - env_var="FORCE", - action="store_true", - default=False, - help="publish even if the image already exists in the registry", - ) - - -def _add_release_pull_output(parser: configargparse.ArgParser) -> None: - """--pull-output flag (only relevant for 'release pull').""" - g = parser.add_argument_group("pull") - g.add_argument( - "--pull-output", - metavar="DIR", - default=None, - help="output directory for pulled artifacts", - ) - - -def _add_release_tag_version(parser: configargparse.ArgParser) -> None: - """Positional argument for 'release tag'.""" - parser.add_argument( - "version", - nargs="?", - default=None, - help="version tag to apply (e.g. v1.0.0)", - ) - - -def _add_qemu_flags(parser: configargparse.ArgParser) -> None: - """--qemu-append, --qemu-mem, --qemu-smp""" - g = parser.add_argument_group("qemu") - g.add_argument( - "--qemu-append", - env_var="QEMU_APPEND", - metavar="ARGS", - default="", - help="extra kernel cmdline args for qemu-test", - ) - g.add_argument( - "--qemu-mem", - env_var="QEMU_MEM", - metavar="SIZE", - default="2G", - help="QEMU RAM size", - ) - g.add_argument( - "--qemu-smp", - env_var="QEMU_SMP", - metavar="N", - default="2", - help="QEMU CPU count", - ) - - -def _add_tink_flags(parser: configargparse.ArgParser) -> None: - """Tinkerbell kernel cmdline flags + --ipam.""" - g = parser.add_argument_group("tinkerbell") - g.add_argument( - "--tink-worker-image", - env_var="TINK_WORKER_IMAGE", - metavar="IMAGE", - default="ghcr.io/tinkerbell/tink-agent:latest", - help="tink-agent container image reference", - ) - g.add_argument( - "--tink-docker-registry", - env_var="TINK_DOCKER_REGISTRY", - metavar="HOST", - default="", - help="registry host (triggers tink-agent services)", - ) - g.add_argument( - "--tink-grpc-authority", - env_var="TINK_GRPC_AUTHORITY", - metavar="ADDR", - default="", - help="tink-server gRPC endpoint (host:port)", - ) - g.add_argument( - "--tink-worker-id", - env_var="TINK_WORKER_ID", - metavar="ID", - default="", - help="machine / worker ID", - ) - g.add_argument( - "--tink-tls", - env_var="TINK_TLS", - metavar="BOOL", - default="false", - help="enable TLS to tink-server", - ) - g.add_argument( - "--tink-insecure-tls", - env_var="TINK_INSECURE_TLS", - metavar="BOOL", - default="true", - help="allow insecure TLS", - ) - g.add_argument( - "--tink-insecure-registries", - env_var="TINK_INSECURE_REGISTRIES", - metavar="LIST", - default="", - help="comma-separated insecure registries", - ) - g.add_argument( - "--tink-registry-username", - env_var="TINK_REGISTRY_USERNAME", - metavar="USER", - default="", - help="registry auth username", - ) - g.add_argument( - "--tink-registry-password", - env_var="TINK_REGISTRY_PASSWORD", - metavar="PASS", - default="", - help="registry auth password", - ) - g.add_argument( - "--tink-syslog-host", - env_var="TINK_SYSLOG_HOST", - metavar="HOST", - default="", - help="remote syslog host", - ) - g.add_argument( - "--tink-facility", - env_var="TINK_FACILITY", - metavar="CODE", - default="", - help="facility code", - ) - g.add_argument( - "--ipam", - env_var="IPAM", - metavar="PARAM", - default="", - help="static networking IPAM parameter", - ) - - -# Map command → list of flag-group adders. -_COMMAND_FLAGS: dict[str, list[Callable[..., None]]] = { - "build": [ - _add_common_flags, - _add_flavor_flags, - _add_tools_flags, - _add_mkosi_flags, - _add_iso_flags, - ], - "tools": [_add_common_flags, _add_tools_flags], - "initramfs": [_add_common_flags, _add_flavor_flags, _add_mkosi_flags], - "iso": [_add_common_flags, _add_flavor_flags, _add_iso_flags], - "checksums": [_add_common_flags, _add_flavor_flags, _add_checksums_flags], - "release": [_add_common_flags, _add_flavor_flags, _add_release_flags], - "shell": [_add_common_flags], - "clean": [_add_common_flags, _add_flavor_flags, _add_clean_flags], - "summary": [_add_common_flags, _add_flavor_flags, _add_summary_flags], - "qemu-test": [_add_common_flags, _add_flavor_flags, _add_qemu_flags, _add_tink_flags], -} diff --git a/captain/cli/_release.py b/captain/cli/_release.py deleted file mode 100644 index 4ed0c7d6..00000000 --- a/captain/cli/_release.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Release subcommand — publish, pull, tag.""" - -from __future__ import annotations - -import logging -import shutil -import subprocess -from pathlib import Path - -import configargparse - -from captain import docker, oci -from captain.config import Config -from captain.flavor import BaseFlavor -from captain.util import check_release_dependencies - -from ._parser import ( - _add_common_flags, - _add_flavor_flags, - _add_release_base_flags, - _add_release_pull_output, - _add_release_tag_version, - _add_release_target_flag, - _HelpFormatter, -) - -log = logging.getLogger(__name__) - -_RELEASE_SUBCOMMANDS = ("publish", "pull", "tag") - -_RELEASE_SUBCMD_INFO: dict[str, tuple[str, list]] = { - "publish": ( - "Publish artifacts as a multi-arch OCI image", - [_add_common_flags, _add_flavor_flags, _add_release_base_flags, _add_release_target_flag], - ), - "pull": ( - "Pull and extract artifacts (amd64, arm64, or combined)", - [ - _add_common_flags, - _add_flavor_flags, - _add_release_base_flags, - _add_release_target_flag, - _add_release_pull_output, - ], - ), - "tag": ( - "Tag all artifact images with a version", - [_add_common_flags, _add_flavor_flags, _add_release_base_flags, _add_release_tag_version], - ), -} - - -def _print_release_subcmd_help(sub: str, *, exit_code: int = 0) -> None: - """Print help for a release subcommand and exit.""" - desc, adders = _RELEASE_SUBCMD_INFO[sub] - columns = shutil.get_terminal_size().columns - parser = configargparse.ArgParser( - prog=f"build.py release {sub}", - description=desc, - add_env_var_help=False, - formatter_class=lambda prog: _HelpFormatter( - prog, - max_help_position=38, - width=columns, - ), - ) - for adder in adders: - adder(parser) - parser.print_help() - raise SystemExit(exit_code) - - -def _resolve_git_sha(args: object, project_dir: Path) -> str: - """Return the git SHA from args or by running git rev-parse.""" - sha = getattr(args, "git_sha", None) - if sha: - return sha - - result = subprocess.run( - ["git", "rev-parse", "HEAD"], - capture_output=True, - text=True, - check=True, - cwd=project_dir, - ) - return result.stdout.strip() - - -def _cmd_release( - cfg: Config, flavor: BaseFlavor, extra_args: list[str], args: object = None -) -> None: - """OCI artifact operations: publish, pull, tag.""" - - # Peel the release subcommand from extra_args. - if not extra_args: - log.error( - "Missing release subcommand.\n usage: build.py release {%s}\n", - ",".join(_RELEASE_SUBCOMMANDS), - ) - raise SystemExit(2) - - sub = extra_args[0] - rest = extra_args[1:] - - if sub not in _RELEASE_SUBCOMMANDS: - log.error( - "Unknown release subcommand '%s'.\n valid: %s\n", - sub, - ", ".join(_RELEASE_SUBCOMMANDS), - ) - raise SystemExit(2) - - # Handle --help / -h for the subcommand. - if "-h" in rest or "--help" in rest: - _print_release_subcmd_help(sub) - - # --- validate required args early --------------------------------- - if sub == "tag" and not rest: - log.error("Missing version argument.") - _print_release_subcmd_help(sub, exit_code=2) - if sub == "pull" and not getattr(args, "pull_output", None): - log.error("--pull-output is required for 'release pull'.") - _print_release_subcmd_help(sub, exit_code=2) - - # --- skip --------------------------------------------------------- - if cfg.release_mode == "skip": - log.info("RELEASE_MODE=skip — skipping release operation") - return - - # --- docker ------------------------------------------------------- - if cfg.release_mode == "docker": - docker.build_release_image(cfg) - log.info("Running release %s (docker)...", sub) - # Forward release-specific env vars into the container. - registry = getattr(args, "registry", "ghcr.io") - repository = getattr(args, "repository", "tinkerbell/captain") - artifact_name = getattr(args, "oci_artifact_name", "artifacts") - sha = _resolve_git_sha(args, cfg.project_dir) - env_args: list[str] = [ - "-e", - f"FLAVOR_ID={cfg.flavor_id}", - "-e", - f"REGISTRY={registry}", - "-e", - f"GITHUB_REPOSITORY={repository}", - "-e", - f"OCI_ARTIFACT_NAME={artifact_name}", - "-e", - f"GITHUB_SHA={sha}", - ] - exclude = getattr(args, "version_exclude", None) - if exclude: - env_args += ["-e", f"VERSION_EXCLUDE={exclude}"] - if sub in ("publish", "pull"): - target = getattr(args, "target", None) or cfg.arch - env_args += ["-e", f"TARGET={target}"] - if getattr(args, "force", False): - env_args += ["-e", "FORCE=true"] - pull_output = getattr(args, "pull_output", None) - - # Build the inner command. - inner_cmd = ["/work/build.py", "release", sub] - if pull_output: - inner_cmd += ["--pull-output", pull_output] - inner_cmd += list(rest) - - try: - docker.run_in_release( - cfg, - *env_args, - "--entrypoint", - "/usr/bin/uv", - docker.RELEASE_IMAGE, - *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), - "run", - *inner_cmd, - ) - except subprocess.CalledProcessError as exc: - raise SystemExit(exc.returncode) from None - paths_to_fix = ["/work/out"] - if pull_output: - container_pull_output = f"/work/{pull_output.lstrip('/')}" - paths_to_fix.append(container_pull_output) - docker.fix_docker_ownership(cfg, paths_to_fix) - return - - # --- native ------------------------------------------------------- - if cfg.release_mode == "native": - missing = check_release_dependencies() - if missing: - log.error("Missing release tools: %s", ", ".join(missing)) - log.error("Install them or set --release-mode=docker.") - raise SystemExit(1) - # Common OCI parameters. - registry = getattr(args, "registry", "ghcr.io") - repository = getattr(args, "repository", "tinkerbell/captain") - artifact_name = getattr(args, "oci_artifact_name", "artifacts") - exclude = getattr(args, "version_exclude", None) - sha = _resolve_git_sha(args, cfg.project_dir) - tag = oci.compute_version_tag(cfg.project_dir, sha, exclude=exclude) - tag = f"{tag}-{cfg.flavor_id}" - - if sub == "publish": - target = getattr(args, "target", None) or cfg.arch - force = getattr(args, "force", False) - oci.publish( - cfg, - flavor, - target=target, - registry=registry, - repository=repository, - artifact_name=artifact_name, - tag=tag, - sha=sha, - force=force, - ) - - elif sub == "pull": - target = getattr(args, "target", None) or cfg.arch - pull_output = getattr(args, "pull_output", None) - if pull_output is None: - log.error("--pull-output is required for 'release pull'.") - raise SystemExit(2) - oci.pull( - registry=registry, - repository=repository, - artifact_name=artifact_name, - tag=tag, - target=target, - output_dir=Path(pull_output), - ) - - elif sub == "tag": - version = rest[0] - oci.tag_all( - registry=registry, - repository=repository, - artifact_name=artifact_name, - src_tag=tag, - new_tag=version, - ) diff --git a/captain/click_cli/__init__.py b/captain/click/__init__.py similarity index 92% rename from captain/click_cli/__init__.py rename to captain/click/__init__.py index b8b35d11..50adb243 100644 --- a/captain/click_cli/__init__.py +++ b/captain/click/__init__.py @@ -15,6 +15,6 @@ eval "$(_CAPTAIN_COMPLETE=zsh_source captain)" """ -from captain.click_cli._main import main +from captain.click._main import main __all__ = ["main"] diff --git a/captain/click_cli/_build.py b/captain/click/_build.py similarity index 93% rename from captain/click_cli/_build.py rename to captain/click/_build.py index 8d23edc4..3148ec91 100644 --- a/captain/click_cli/_build.py +++ b/captain/click/_build.py @@ -4,10 +4,15 @@ import logging -import click - import captain.flavor -from captain.click_cli._main import cli, common_options, resolve_project_dir +import click +from captain import artifacts +from captain.click._main import cli, common_options, resolve_project_dir +from captain.click._stages import ( + _build_iso_stage, + _build_mkosi_stage, + _build_tools_stage, +) from captain.config import Config log = logging.getLogger(__name__) @@ -135,14 +140,6 @@ def build_cmd( flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) flavor.generate() - # Import build commands (reuse existing stage orchestration). - from captain import artifacts - from captain.cli._stages import ( - _build_iso_stage, - _build_mkosi_stage, - _build_tools_stage, - ) - # Tools stage. _build_tools_stage(cfg) diff --git a/captain/click_cli/_builder.py b/captain/click/_builder.py similarity index 97% rename from captain/click_cli/_builder.py rename to captain/click/_builder.py index db167b6f..80e48486 100644 --- a/captain/click_cli/_builder.py +++ b/captain/click/_builder.py @@ -5,8 +5,7 @@ import logging import click - -from captain.click_cli._main import cli, common_options, resolve_project_dir +from captain.click._main import cli, common_options, resolve_project_dir from captain.config import Config from captain.docker import build_builder diff --git a/captain/click_cli/_main.py b/captain/click/_main.py similarity index 97% rename from captain/click_cli/_main.py rename to captain/click/_main.py index fe6e6bca..fd3d9f35 100644 --- a/captain/click_cli/_main.py +++ b/captain/click/_main.py @@ -117,6 +117,6 @@ def cli(ctx: click.Context) -> None: def main() -> None: """Console-script entry point.""" # Import subcommand modules to register them on the group. - from captain.click_cli import _build, _builder, _release_publish, _tools # noqa: F401 + from captain.click import _build, _builder, _release_publish, _tools # noqa: F401 cli() diff --git a/captain/click_cli/_release_publish.py b/captain/click/_release_publish.py similarity index 98% rename from captain/click_cli/_release_publish.py rename to captain/click/_release_publish.py index ec1b1006..991fa1f3 100644 --- a/captain/click_cli/_release_publish.py +++ b/captain/click/_release_publish.py @@ -6,11 +6,10 @@ import subprocess from pathlib import Path -import click - import captain.flavor +import click from captain import oci -from captain.click_cli._main import cli, common_options, resolve_project_dir +from captain.click._main import cli, common_options, resolve_project_dir from captain.config import Config from captain.util import check_release_dependencies diff --git a/captain/cli/_stages.py b/captain/click/_stages.py similarity index 100% rename from captain/cli/_stages.py rename to captain/click/_stages.py diff --git a/captain/click_cli/_tools.py b/captain/click/_tools.py similarity index 94% rename from captain/click_cli/_tools.py rename to captain/click/_tools.py index c3267d22..f857e189 100644 --- a/captain/click_cli/_tools.py +++ b/captain/click/_tools.py @@ -5,8 +5,8 @@ import logging import click - -from captain.click_cli._main import cli, common_options, resolve_project_dir +from captain.click._main import cli, common_options, resolve_project_dir +from captain.click._stages import _build_tools_stage from captain.config import Config log = logging.getLogger(__name__) @@ -87,8 +87,6 @@ def tools_cmd( force_tools=force_tools, ) - from captain.cli._stages import _build_tools_stage - _build_tools_stage(cfg) log.info("Tools stage complete!") diff --git a/click_cli.py b/click_cli.py deleted file mode 100644 index e0147bc0..00000000 --- a/click_cli.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -"""CaptainOS build system — click-based CLI entry point. - -Requires: Python >= 3.13, click, Rich, Docker (unless all stages use native/skip). -Use Astral's ``uv`` to run:: - - uv run click_cli.py --help - uv run click_cli.py builder - uv run click_cli.py build --arch arm64 - uv run click_cli.py release-publish --target combined - -Or, after ``uv pip install -e .``:: - - captain --help -""" - -import sys - -if sys.version_info < (3, 13): - print("ERROR: Python >= 3.13 is required.", file=sys.stderr) - sys.exit(1) - -try: - from captain.click_cli import main -except ImportError as exc: - print(f"ERROR: {exc}", file=sys.stderr) - uv_url = "https://docs.astral.sh/uv/getting-started/installation/" - print(f"Missing dependencies, use uv to run. See {uv_url}", file=sys.stderr) - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index e98295b9..fea7067d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,13 +8,12 @@ name = "captain" requires-python = ">=3.13" dependencies = [ "click>=8.1", - "configargparse>=1.7", "rich>=14.3.3", "jinja2>=3.1.6" ] [project.scripts] -captain = "captain.click_cli:main" +captain = "build:main" [project.optional-dependencies] dev = ["ruff>=0.15.9", "pyright>=1.1"] From b697b4edc595eec4cad3b2aa3ca6811edd6d57db Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 01:41:56 +0200 Subject: [PATCH 050/143] click: add iso command back - as it is called internally via docker Signed-off-by: Ricardo Pardini --- captain/click/_build.py | 3 +- captain/click/_builder.py | 1 + captain/click/_iso.py | 92 +++++++++++++++++++++++++++++++ captain/click/_main.py | 2 +- captain/click/_release_publish.py | 3 +- captain/click/_tools.py | 1 + 6 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 captain/click/_iso.py diff --git a/captain/click/_build.py b/captain/click/_build.py index 3148ec91..f46cf63e 100644 --- a/captain/click/_build.py +++ b/captain/click/_build.py @@ -4,8 +4,9 @@ import logging -import captain.flavor import click + +import captain.flavor from captain import artifacts from captain.click._main import cli, common_options, resolve_project_dir from captain.click._stages import ( diff --git a/captain/click/_builder.py b/captain/click/_builder.py index 80e48486..398354b6 100644 --- a/captain/click/_builder.py +++ b/captain/click/_builder.py @@ -5,6 +5,7 @@ import logging import click + from captain.click._main import cli, common_options, resolve_project_dir from captain.config import Config from captain.docker import build_builder diff --git a/captain/click/_iso.py b/captain/click/_iso.py new file mode 100644 index 00000000..32b173ad --- /dev/null +++ b/captain/click/_iso.py @@ -0,0 +1,92 @@ +"""``captain iso`` — build a bootable ISO image for the specified flavor and architecture.""" + +from __future__ import annotations + +import logging + +import click + +import captain.flavor +from captain import artifacts +from captain.click._main import cli, common_options, resolve_project_dir +from captain.click._stages import ( + _build_iso_stage, +) +from captain.config import Config + +log = logging.getLogger(__name__) + + +@cli.command( + "iso", + short_help="Build ISO image only. Part of build.", +) +@common_options +@click.option( + "--builder-image", + envvar="BUILDER_IMAGE", + default="captainos-builder", + show_default=True, + help="Docker builder image name.", +) +@click.option( + "--no-cache", + envvar="NO_CACHE", + is_flag=True, + default=False, + help="Rebuild the builder image without Docker layer cache.", +) +@click.option( + "--iso-mode", + envvar="ISO_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="ISO build stage execution mode (docker, native, skip).", +) +@click.option( + "--force-iso", + envvar="FORCE_ISO", + is_flag=True, + default=False, + help="Force ISO rebuild even if outputs already exist.", +) +def build_cmd( + *, + arch: str, + flavor_id: str, + project_dir: str | None, + verbose: bool, + builder_image: str, + no_cache: bool, + iso_mode: str, + force_iso: bool, +) -> None: + """Run the CaptainOS ISO build.""" + _configure_logging(verbose) + + proj = resolve_project_dir(project_dir) + + cfg = Config( + project_dir=proj, + output_dir=proj / "out", + arch=arch, + flavor_id=flavor_id, + builder_image=builder_image, + no_cache=no_cache, + iso_mode=iso_mode, + force_iso=force_iso, + ) + + # Instantiate the flavor + captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + + _build_iso_stage(cfg) + artifacts.collect_iso(cfg) + log.info("ISO build complete!!!") + + +def _configure_logging(verbose: bool) -> None: + level = logging.DEBUG if verbose else logging.INFO + logging.getLogger("captain").setLevel(level) diff --git a/captain/click/_main.py b/captain/click/_main.py index fd3d9f35..cd9928d1 100644 --- a/captain/click/_main.py +++ b/captain/click/_main.py @@ -117,6 +117,6 @@ def cli(ctx: click.Context) -> None: def main() -> None: """Console-script entry point.""" # Import subcommand modules to register them on the group. - from captain.click import _build, _builder, _release_publish, _tools # noqa: F401 + from captain.click import _build, _builder, _iso, _release_publish, _tools # noqa: F401 cli() diff --git a/captain/click/_release_publish.py b/captain/click/_release_publish.py index 991fa1f3..ca19dc0e 100644 --- a/captain/click/_release_publish.py +++ b/captain/click/_release_publish.py @@ -6,8 +6,9 @@ import subprocess from pathlib import Path -import captain.flavor import click + +import captain.flavor from captain import oci from captain.click._main import cli, common_options, resolve_project_dir from captain.config import Config diff --git a/captain/click/_tools.py b/captain/click/_tools.py index f857e189..ada986bf 100644 --- a/captain/click/_tools.py +++ b/captain/click/_tools.py @@ -5,6 +5,7 @@ import logging import click + from captain.click._main import cli, common_options, resolve_project_dir from captain.click._stages import _build_tools_stage from captain.config import Config From 17277831bc14ab51da08a10b37a0a3a19d6c6f4a Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 01:44:18 +0200 Subject: [PATCH 051/143] gha: switch to click cli Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 428a71e3..16b8dd55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: uses: astral-sh/setup-uv@v7 - name: Download tools - run: uv run ./build.py tools + run: uv run captain tools - name: Save tools cache if: github.ref == 'refs/heads/main' && steps.tools-cache.outputs.cache-hit != 'true' @@ -123,7 +123,7 @@ jobs: uses: astral-sh/setup-uv@v7 - name: Build initramfs - run: uv run ./build.py --arch=${{ matrix.arch }} # full build, incl initramfs and iso + run: uv run captain build --arch=${{ matrix.arch }} # full build, incl initramfs and iso when appropriate - name: Upload initramfs artifacts uses: actions/upload-artifact@v6 @@ -133,7 +133,7 @@ jobs: retention-days: 1 # ------------------------------------------------------------------- - # Build UEFI-bootable ISO (depends on initramfs) + # UEFI-bootable ISO - only for certain flavors (eg trixie-full) # ------------------------------------------------------------------- - name: Upload ISO artifact @@ -160,7 +160,7 @@ jobs: if: github.ref == 'refs/heads/main' env: TARGET: ${{ matrix.arch }} - run: uv run ./build.py release publish + run: uv run captain release publish # ------------------------------------------------------------------- # Publish combined multi-arch image (reuses per-arch registry blobs) @@ -219,4 +219,4 @@ jobs: - name: Publish combined image to GHCR env: FLAVOR_ID: "${{ env.DEFAULT_FLAVOR_ID }}" - run: uv run ./build.py release publish + run: uv run captain release publish From fcfbefe7d4684f74bd687dbf54f4f8f69b0c78a7 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 02:10:51 +0200 Subject: [PATCH 052/143] gha: release-publish Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16b8dd55..e9dc8e6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,7 +160,7 @@ jobs: if: github.ref == 'refs/heads/main' env: TARGET: ${{ matrix.arch }} - run: uv run captain release publish + run: uv run captain release-publish # ------------------------------------------------------------------- # Publish combined multi-arch image (reuses per-arch registry blobs) @@ -219,4 +219,4 @@ jobs: - name: Publish combined image to GHCR env: FLAVOR_ID: "${{ env.DEFAULT_FLAVOR_ID }}" - run: uv run captain release publish + run: uv run captain release-publish From 2b42bb8ca7d7bb288cc96f4532009820ec6692c4 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 11:34:47 +0200 Subject: [PATCH 053/143] captain: rework `obtain_builder` Signed-off-by: Ricardo Pardini --- captain/click/_build.py | 6 +- captain/click/_builder.py | 77 +++------------------ captain/click/_iso.py | 6 +- captain/click/_main.py | 23 ++++++- captain/click/_release_publish.py | 26 ++----- captain/click/_stages.py | 6 +- captain/click/_tools.py | 6 +- captain/config.py | 82 +---------------------- captain/docker.py | 108 ++++++++++++++++++------------ 9 files changed, 123 insertions(+), 217 deletions(-) diff --git a/captain/click/_build.py b/captain/click/_build.py index f46cf63e..1414b40d 100644 --- a/captain/click/_build.py +++ b/captain/click/_build.py @@ -92,8 +92,9 @@ def build_cmd( flavor_id: str, project_dir: str | None, verbose: bool, + builder_registry: str | None, + builder_repository: str | None, builder_image: str, - no_cache: bool, mkosi_mode: str, tools_mode: str, iso_mode: str, @@ -125,8 +126,9 @@ def build_cmd( output_dir=proj / "out", arch=arch, flavor_id=flavor_id, + builder_registry=builder_registry, + builder_repository=builder_repository, builder_image=builder_image, - no_cache=no_cache, tools_mode=tools_mode, mkosi_mode=mkosi_mode, iso_mode=iso_mode, diff --git a/captain/click/_builder.py b/captain/click/_builder.py index 398354b6..858bff26 100644 --- a/captain/click/_builder.py +++ b/captain/click/_builder.py @@ -8,71 +8,36 @@ from captain.click._main import cli, common_options, resolve_project_dir from captain.config import Config -from captain.docker import build_builder +from captain.docker import obtain_builder log = logging.getLogger(__name__) @cli.command( "builder", - short_help="Build the Docker builder image, optionally push it.", + short_help="Build the Docker builder image and optionally push it.", ) @common_options -@click.option( - "--builder-image", - envvar="BUILDER_IMAGE", - default="captainos-builder", - show_default=True, - help="Docker builder image name.", -) -@click.option( - "--no-cache", - envvar="NO_CACHE", - is_flag=True, - default=False, - help="Rebuild the builder image without Docker layer cache.", -) @click.option( "--push", is_flag=True, default=False, help="Push the built image to a registry after building.", ) -@click.option( - "--registry", - envvar="REGISTRY", - default=None, - help="Registry hostname (e.g. ghcr.io). Required when --push is set.", -) -@click.option( - "--registry-path", - envvar="REGISTRY_PATH", - default=None, - help="Repository path within the registry (e.g. tinkerbell/captain/builder).", -) -@click.option( - "--tag", - "push_tag", - default=None, - help="Tag to apply when pushing (default: Dockerfile content hash).", -) def builder_cmd( *, arch: str, flavor_id: str, project_dir: str | None, verbose: bool, + builder_registry: str | None, + builder_repository: str | None, builder_image: str, - no_cache: bool, push: bool, - registry: str | None, - registry_path: str | None, - push_tag: str | None, ) -> None: """Build the Docker builder image used by other build stages. - By default the image is built locally only. Pass --push together - with --registry and --registry-path to also push the image to a + By default the image is built locally only. Pass --push to also push the image to a remote container registry. \b @@ -80,8 +45,7 @@ def builder_cmd( -------- captain builder captain builder --no-cache - captain builder --push --registry ghcr.io --registry-path tinkerbell/captain/builder - captain builder --push --registry ghcr.io --registry-path tinkerbell/captain/builder --tag lt + captain builder --push """ _configure_logging(verbose) @@ -92,37 +56,16 @@ def builder_cmd( output_dir=proj / "out", arch=arch, flavor_id=flavor_id, + builder_registry=builder_registry, + builder_repository=builder_repository, builder_image=builder_image, - no_cache=no_cache, + builder_push=push, ) # 1. Build the image. - build_builder(cfg) + obtain_builder(cfg) log.info("Builder image '%s' is ready.", cfg.builder_image) - # 2. Optionally push. - if push: - if not registry or not registry_path: - raise click.UsageError( - "--registry and --registry-path are required when --push is set." - ) - import hashlib - - from captain.util import run - - if push_tag is None: - # Use the Dockerfile content hash (same logic as docker.py) - dockerfile = cfg.project_dir / "Dockerfile" - push_tag = hashlib.sha256(dockerfile.read_bytes()).hexdigest() - - full_ref = f"{registry}/{registry_path}:{push_tag}" - log.info("Pushing builder image → %s", full_ref) - - # Tag and push. - run(["docker", "tag", cfg.builder_image, full_ref]) - run(["docker", "push", full_ref]) - log.info("Push complete: %s", full_ref) - def _configure_logging(verbose: bool) -> None: """Set the captain logger level based on the --verbose flag.""" diff --git a/captain/click/_iso.py b/captain/click/_iso.py index 32b173ad..66482e24 100644 --- a/captain/click/_iso.py +++ b/captain/click/_iso.py @@ -58,8 +58,9 @@ def build_cmd( flavor_id: str, project_dir: str | None, verbose: bool, + builder_registry: str | None, + builder_repository: str | None, builder_image: str, - no_cache: bool, iso_mode: str, force_iso: bool, ) -> None: @@ -73,8 +74,9 @@ def build_cmd( output_dir=proj / "out", arch=arch, flavor_id=flavor_id, + builder_registry=builder_registry, + builder_repository=builder_repository, builder_image=builder_image, - no_cache=no_cache, iso_mode=iso_mode, force_iso=force_iso, ) diff --git a/captain/click/_main.py b/captain/click/_main.py index cd9928d1..7895a930 100644 --- a/captain/click/_main.py +++ b/captain/click/_main.py @@ -50,12 +50,33 @@ def common_options(fn: Any) -> Any: help="Project root directory (auto-detected when omitted).", ) @click.option( - "-v", "--verbose", + "-v", is_flag=True, default=False, help="Enable debug-level logging.", ) + @click.option( + "--builder-registry", + envvar="REGISTRY", + default="ghcr.io", + show_default=True, + help="OCI registry hostname for the Docker builder image", + ) + @click.option( + "--builder-repository", + envvar="GITHUB_REPOSITORY", + default="tinkerbell/captain", + show_default=True, + help="Repository path (owner/name) for the Docker builder image", + ) + @click.option( + "--builder-image", + envvar="BUILDER_IMAGE", + default="captainos-builder", + show_default=True, + help="Local name/tag of Docker builder image name", + ) @functools.wraps(fn) def wrapper(**kwargs: Any) -> Any: return fn(**kwargs) diff --git a/captain/click/_release_publish.py b/captain/click/_release_publish.py index ca19dc0e..2d2370ec 100644 --- a/captain/click/_release_publish.py +++ b/captain/click/_release_publish.py @@ -22,20 +22,6 @@ short_help="Publish build artifacts as a multi-arch OCI image.", ) @common_options -@click.option( - "--builder-image", - envvar="BUILDER_IMAGE", - default="captainos-builder", - show_default=True, - help="Docker builder image name.", -) -@click.option( - "--no-cache", - envvar="NO_CACHE", - is_flag=True, - default=False, - help="Rebuild images without Docker layer cache.", -) @click.option( "--release-mode", envvar="RELEASE_MODE", @@ -100,8 +86,9 @@ def release_publish_cmd( flavor_id: str, project_dir: str | None, verbose: bool, + builder_registry: str | None, + builder_repository: str | None, builder_image: str, - no_cache: bool, release_mode: str, registry: str, repository: str, @@ -140,8 +127,9 @@ def release_publish_cmd( output_dir=proj / "out", arch=arch, flavor_id=flavor_id, + builder_registry=builder_registry, + builder_repository=builder_repository, builder_image=builder_image, - no_cache=no_cache, release_mode=release_mode, ) @@ -154,7 +142,7 @@ def release_publish_cmd( if cfg.release_mode == "docker": from captain import docker - docker.build_release_image(cfg) + docker.obtain_builder(cfg) sha = _resolve_git_sha(git_sha, proj) env_args: list[str] = [ @@ -179,12 +167,12 @@ def release_publish_cmd( inner_cmd = ["/work/build.py", "release", "publish"] try: - docker.run_in_release( + docker.run_in_builder( cfg, *env_args, "--entrypoint", "/usr/bin/uv", - docker.RELEASE_IMAGE, + cfg.builder_image, *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), "run", *inner_cmd, diff --git a/captain/click/_stages.py b/captain/click/_stages.py index 6198128e..0caaf1a0 100644 --- a/captain/click/_stages.py +++ b/captain/click/_stages.py @@ -26,7 +26,7 @@ def _build_tools_stage(cfg: Config) -> None: return # --- docker ------------------------------------------------------- - docker.build_builder(cfg) + docker.obtain_builder(cfg) log.info("Downloading tools (nerdctl, containerd, etc.) docker...") docker.run_in_builder( cfg, @@ -82,7 +82,7 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: return # --- docker ------------------------------------------------------- - docker.build_builder(cfg) + docker.obtain_builder(cfg) log.info("Building initrd with mkosi (docker)...") tools_tree = f"/work/mkosi.output/tools/{cfg.arch}" output_dir = f"/work/mkosi.output/initramfs/{cfg.flavor_id}/{cfg.arch}" @@ -124,7 +124,7 @@ def _build_iso_stage(cfg: Config) -> None: return # --- docker ------------------------------------------------------- - docker.build_builder(cfg) + docker.obtain_builder(cfg) log.info("Building ISO (docker)...") docker.run_in_builder( cfg, diff --git a/captain/click/_tools.py b/captain/click/_tools.py index ada986bf..92d18e38 100644 --- a/captain/click/_tools.py +++ b/captain/click/_tools.py @@ -54,8 +54,9 @@ def tools_cmd( flavor_id: str, project_dir: str | None, verbose: bool, + builder_registry: str | None, + builder_repository: str | None, builder_image: str, - no_cache: bool, tools_mode: str, force_tools: bool, ) -> None: @@ -82,8 +83,9 @@ def tools_cmd( output_dir=proj / "out", arch=arch, flavor_id=flavor_id, + builder_registry=builder_registry, + builder_repository=builder_repository, builder_image=builder_image, - no_cache=no_cache, tools_mode=tools_mode, force_tools=force_tools, ) diff --git a/captain/config.py b/captain/config.py index 1baeff49..f0418d43 100644 --- a/captain/config.py +++ b/captain/config.py @@ -2,9 +2,7 @@ from __future__ import annotations -import argparse import logging -import os import sys from dataclasses import dataclass, field from pathlib import Path @@ -34,8 +32,10 @@ class Config: flavor_id: str = DEFAULT_FLAVOR_ID # Docker + builder_registry: str | None = None + builder_repository: str | None = None builder_image: str = "captainos-builder" - no_cache: bool = False + builder_push: bool = False # Per-stage mode: "docker" | "native" | "skip" tools_mode: str = "docker" @@ -71,82 +71,6 @@ def __post_init__(self) -> None: log.error("%s=%r is invalid. Valid values: %s", name, value, ", ".join(VALID_MODES)) sys.exit(1) - @property - def needs_docker(self) -> bool: - """True if any stage requires Docker.""" - return ( - self.tools_mode == "docker" - or self.mkosi_mode == "docker" - or self.iso_mode == "docker" - or self.release_mode == "docker" - ) - - @classmethod - def from_args(cls, args: argparse.Namespace, project_dir: Path | None) -> Config: - """Create a Config from a parsed :class:`argparse.Namespace`. - - The *args* namespace is produced by :mod:`configargparse` which - has already resolved the priority chain: - CLI flags > environment variables > defaults. - - ``getattr`` with fallbacks is used because per-subcommand - parsers only define the flags relevant to that subcommand. - """ - - if project_dir is None: - raise ValueError("project_dir must be provided to Config.from_args") - - log.debug( - "Creating Config from args: %s (env flavor: %s)", args, os.environ.get("FLAVOR_ID") - ) - - return cls( - project_dir=project_dir, - output_dir=project_dir / "out", - arch=getattr(args, "arch", "amd64"), - flavor_id=getattr(args, "flavor_id", DEFAULT_FLAVOR_ID), - builder_image=getattr(args, "builder_image", "captainos-builder"), - no_cache=getattr(args, "no_cache", False), - tools_mode=getattr(args, "tools_mode", "docker"), - mkosi_mode=getattr(args, "mkosi_mode", "docker"), - iso_mode=getattr(args, "iso_mode", "docker"), - release_mode=getattr(args, "release_mode", "docker"), - force_tools=getattr(args, "force_tools", False), - force_iso=getattr(args, "force_iso", False), - qemu_append=getattr(args, "qemu_append", ""), - qemu_mem=getattr(args, "qemu_mem", "2G"), - qemu_smp=getattr(args, "qemu_smp", "2"), - ) - - @classmethod - def from_env(cls, project_dir: Path) -> Config: - """Create a Config from environment variables (legacy helper). - - Prefer :meth:`from_args` in the CLI path. This method remains - for any non-CLI callers (e.g. tests, scripts) that need a - ``Config`` without going through argparse. - """ - - log.debug("Creating Config from env: %s", os.environ) - - return cls( - project_dir=project_dir, - output_dir=project_dir / "out", - arch=os.environ.get("ARCH", "amd64"), - flavor_id=os.environ.get("FLAVOR_ID", DEFAULT_FLAVOR_ID), - builder_image=os.environ.get("BUILDER_IMAGE", "captainos-builder"), - no_cache=os.environ.get("NO_CACHE") == "1", - tools_mode=os.environ.get("TOOLS_MODE", "docker"), - mkosi_mode=os.environ.get("MKOSI_MODE", "docker"), - iso_mode=os.environ.get("ISO_MODE", "docker"), - release_mode=os.environ.get("RELEASE_MODE", "docker"), - force_tools=os.environ.get("FORCE_TOOLS") == "1", - force_iso=os.environ.get("FORCE_ISO") == "1", - qemu_append=os.environ.get("QEMU_APPEND", ""), - qemu_mem=os.environ.get("QEMU_MEM", "2G"), - qemu_smp=os.environ.get("QEMU_SMP", "2"), - ) - @property def tools_output(self) -> Path: """Per-arch staging directory for downloaded tools. diff --git a/captain/docker.py b/captain/docker.py index 8f7477ce..83a4a924 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -9,7 +9,7 @@ from pathlib import Path from captain.config import Config -from captain.util import run +from captain.util import detect_current_machine_arch, run log = logging.getLogger(__name__) @@ -28,46 +28,89 @@ def _dockerfile_hash(cfg: Config) -> str: """Return the SHA-256 hex digest of the Dockerfile content. This is used as an image tag so that Dockerfile changes are detected - automatically. The value intentionally matches what GitHub Actions - ``hashFiles('Dockerfile')`` produces, allowing the CI - ``docker/build-push-action`` step to pre-load an image with the same - tag that ``build_builder`` will look for. + automatically. """ dockerfile = cfg.project_dir / "Dockerfile" - return hashlib.sha256(dockerfile.read_bytes()).hexdigest() + local_arch = detect_current_machine_arch() + hex_digest = hashlib.sha256(dockerfile.read_bytes()).hexdigest() + return f"{local_arch}-{hex_digest}" -def build_builder(cfg: Config) -> None: +def obtain_builder(cfg: Config) -> None: """Build the Docker builder image when the Dockerfile has changed. The image is tagged with a content hash of the Dockerfile so that changes are detected even when the base image name stays the same. - When the matching tag already exists locally (e.g. pre-loaded by a CI - ``docker/build-push-action`` step with ``load: true``), we skip the - build entirely. Use ``NO_CACHE=1`` to force a full rebuild. """ tag = _dockerfile_hash(cfg) - tagged_image = f"{cfg.builder_image}:{tag}" + remote_tagged_image = f"{cfg.builder_registry}/{cfg.builder_repository}/builder:{tag}" + local_tagged_image = f"{cfg.builder_image}:{tag}" + + log.debug( + "Checking for existing builder image with tag '%s' or remote image '%s'", + local_tagged_image, + remote_tagged_image, + ) - if not cfg.no_cache and _image_exists(tagged_image): - log.info("Docker image '%s' is up to date with %s.", cfg.builder_image, tagged_image) + if _image_exists(local_tagged_image): + log.info("Docker image '%s' is up to date with %s.", cfg.builder_image, local_tagged_image) # Ensure the un-hashed tag exists so later docker-run calls that # reference cfg.builder_image (without the hash suffix) succeed. - # This matters when the hashed tag was pre-loaded by CI. - run(["docker", "tag", tagged_image, cfg.builder_image], check=False) + run(["docker", "tag", local_tagged_image, cfg.builder_image], check=False) + return + + # Check if the remote name exists locally... (was pre-pulled somehow) + if _image_exists(remote_tagged_image): + log.info( + "Docker image '%s' already exists locally (pre-pulled). Tagging as '%s'.", + remote_tagged_image, + cfg.builder_image, + ) + run(["docker", "tag", remote_tagged_image, cfg.builder_image], check=False) + return + + # Check if we can pull the remote image (exists in registry and matches our Dockerfile hash) + if ( + run( + ["docker", "pull", remote_tagged_image], + check=False, + capture=False, + ).returncode + == 0 + ): + log.info( + "Pulled Docker image '%s' from registry. Tagging as '%s'.", + remote_tagged_image, + cfg.builder_image, + ) + run(["docker", "tag", remote_tagged_image, cfg.builder_image], check=False) return + # build locally if no existing image was found. log.info("Building Docker image '%s'...", cfg.builder_image) - cmd = ["docker", "buildx", "build", "--load"] - if cfg.no_cache: - cmd.append("--no-cache") - cmd.extend( - ["--progress=plain", "-t", tagged_image, "-t", cfg.builder_image, str(cfg.project_dir)] + run( + [ + "docker", + "buildx", + "build", + "--progress=plain", + "-t", + local_tagged_image, + "-t", + cfg.builder_image, + str(cfg.project_dir), + ] ) - run(cmd) - -RELEASE_IMAGE = "captainos-release" + # Optionally push the image after building it + if cfg.builder_push: + log.info( + "Pushing Docker image '%s' to registry as '%s'...", + cfg.builder_image, + remote_tagged_image, + ) + run(["docker", "tag", local_tagged_image, remote_tagged_image], check=False) + run(["docker", "push", remote_tagged_image]) def _release_dockerfile_hash(cfg: Config) -> str: @@ -76,25 +119,6 @@ def _release_dockerfile_hash(cfg: Config) -> str: return hashlib.sha256(dockerfile.read_bytes()).hexdigest() -def build_release_image(cfg: Config) -> None: - """Build the release Docker image from ``Dockerfile.release``.""" - tag = _release_dockerfile_hash(cfg) - tagged_image = f"{RELEASE_IMAGE}:{tag}" - - if not cfg.no_cache and _image_exists(tagged_image): - log.info("Docker image '%s' is up to date.", RELEASE_IMAGE) - run(["docker", "tag", tagged_image, RELEASE_IMAGE]) - return - - log.info("Building Docker image '%s'...", RELEASE_IMAGE) - cmd = ["docker", "buildx", "build", "--load", "-f", str(cfg.project_dir / "Dockerfile.release")] - if cfg.no_cache: - cmd.append("--no-cache") - cmd.extend(["--progress=plain"]) - cmd.extend(["-t", tagged_image, "-t", RELEASE_IMAGE, str(cfg.project_dir)]) - run(cmd) - - def run_in_release(cfg: Config, *extra_args: str) -> None: """Run a command inside the release container. From 57a2a3ba7da65f35af044598495e1c2ddd3328cd Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 11:35:16 +0200 Subject: [PATCH 054/143] gha: separate build-dockerfile arch-based matrix job Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9dc8e6e..7093d79f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,12 +83,40 @@ jobs: mkosi.output/tools/${{ matrix.arch }}/opt/cni retention-days: 1 + # ------------------------------------------------------------------- + # Build Docker builder image, per-arch; pushes to ghcr.io. + # ------------------------------------------------------------------- + build-dockerfile: + needs: [ lint ] + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: { arch: [ amd64, arm64 ] } + env: + ARCH: ${{ matrix.arch }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Dockerfile and push + run: uv run captain builder --push + # ------------------------------------------------------------------- # Build initramfs via mkosi (depends on tools) # ------------------------------------------------------------------- build-all: runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [ download-tools ] + needs: [ download-tools, build-dockerfile ] strategy: fail-fast: false matrix: From 7e847b380f6587ab101ba0aa516bf6fa7f7337a4 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 11:51:11 +0200 Subject: [PATCH 055/143] captain: adapt to options moved to common Signed-off-by: Ricardo Pardini --- captain/click/_build.py | 21 --------------------- captain/click/_builder.py | 9 --------- captain/click/_iso.py | 21 --------------------- captain/click/_main.py | 7 ------- captain/click/_release_publish.py | 7 ------- captain/click/_tools.py | 21 --------------------- 6 files changed, 86 deletions(-) diff --git a/captain/click/_build.py b/captain/click/_build.py index 1414b40d..ffdfcaf3 100644 --- a/captain/click/_build.py +++ b/captain/click/_build.py @@ -24,20 +24,6 @@ short_help="Run the full build pipeline via mkosi.", ) @common_options -@click.option( - "--builder-image", - envvar="BUILDER_IMAGE", - default="captainos-builder", - show_default=True, - help="Docker builder image name.", -) -@click.option( - "--no-cache", - envvar="NO_CACHE", - is_flag=True, - default=False, - help="Rebuild the builder image without Docker layer cache.", -) @click.option( "--mkosi-mode", envvar="MKOSI_MODE", @@ -91,7 +77,6 @@ def build_cmd( arch: str, flavor_id: str, project_dir: str | None, - verbose: bool, builder_registry: str | None, builder_repository: str | None, builder_image: str, @@ -117,7 +102,6 @@ def build_cmd( captain build --mkosi-mode native --tools-mode native captain build --force --force-tools """ - _configure_logging(verbose) proj = resolve_project_dir(project_dir) @@ -162,8 +146,3 @@ def build_cmd( # Final artifact collection. artifacts.collect(cfg) log.info("Build complete!") - - -def _configure_logging(verbose: bool) -> None: - level = logging.DEBUG if verbose else logging.INFO - logging.getLogger("captain").setLevel(level) diff --git a/captain/click/_builder.py b/captain/click/_builder.py index 858bff26..ecd6ef5b 100644 --- a/captain/click/_builder.py +++ b/captain/click/_builder.py @@ -29,7 +29,6 @@ def builder_cmd( arch: str, flavor_id: str, project_dir: str | None, - verbose: bool, builder_registry: str | None, builder_repository: str | None, builder_image: str, @@ -47,8 +46,6 @@ def builder_cmd( captain builder --no-cache captain builder --push """ - _configure_logging(verbose) - proj = resolve_project_dir(project_dir) cfg = Config( @@ -65,9 +62,3 @@ def builder_cmd( # 1. Build the image. obtain_builder(cfg) log.info("Builder image '%s' is ready.", cfg.builder_image) - - -def _configure_logging(verbose: bool) -> None: - """Set the captain logger level based on the --verbose flag.""" - level = logging.DEBUG if verbose else logging.INFO - logging.getLogger("captain").setLevel(level) diff --git a/captain/click/_iso.py b/captain/click/_iso.py index 66482e24..46a353ff 100644 --- a/captain/click/_iso.py +++ b/captain/click/_iso.py @@ -22,20 +22,6 @@ short_help="Build ISO image only. Part of build.", ) @common_options -@click.option( - "--builder-image", - envvar="BUILDER_IMAGE", - default="captainos-builder", - show_default=True, - help="Docker builder image name.", -) -@click.option( - "--no-cache", - envvar="NO_CACHE", - is_flag=True, - default=False, - help="Rebuild the builder image without Docker layer cache.", -) @click.option( "--iso-mode", envvar="ISO_MODE", @@ -57,7 +43,6 @@ def build_cmd( arch: str, flavor_id: str, project_dir: str | None, - verbose: bool, builder_registry: str | None, builder_repository: str | None, builder_image: str, @@ -65,7 +50,6 @@ def build_cmd( force_iso: bool, ) -> None: """Run the CaptainOS ISO build.""" - _configure_logging(verbose) proj = resolve_project_dir(project_dir) @@ -87,8 +71,3 @@ def build_cmd( _build_iso_stage(cfg) artifacts.collect_iso(cfg) log.info("ISO build complete!!!") - - -def _configure_logging(verbose: bool) -> None: - level = logging.DEBUG if verbose else logging.INFO - logging.getLogger("captain").setLevel(level) diff --git a/captain/click/_main.py b/captain/click/_main.py index 7895a930..a07f47fe 100644 --- a/captain/click/_main.py +++ b/captain/click/_main.py @@ -49,13 +49,6 @@ def common_options(fn: Any) -> Any: type=click.Path(exists=True, file_okay=False, resolve_path=True), help="Project root directory (auto-detected when omitted).", ) - @click.option( - "--verbose", - "-v", - is_flag=True, - default=False, - help="Enable debug-level logging.", - ) @click.option( "--builder-registry", envvar="REGISTRY", diff --git a/captain/click/_release_publish.py b/captain/click/_release_publish.py index 2d2370ec..884590b5 100644 --- a/captain/click/_release_publish.py +++ b/captain/click/_release_publish.py @@ -85,7 +85,6 @@ def release_publish_cmd( arch: str, flavor_id: str, project_dir: str | None, - verbose: bool, builder_registry: str | None, builder_repository: str | None, builder_image: str, @@ -115,7 +114,6 @@ def release_publish_cmd( captain release-publish --target combined --force captain release-publish --registry ghcr.io --repository tinkerbell/captain """ - _configure_logging(verbose) proj = resolve_project_dir(project_dir) @@ -225,8 +223,3 @@ def _resolve_git_sha(sha: str | None, project_dir: Path) -> str: cwd=project_dir, ) return result.stdout.strip() - - -def _configure_logging(verbose: bool) -> None: - level = logging.DEBUG if verbose else logging.INFO - logging.getLogger("captain").setLevel(level) diff --git a/captain/click/_tools.py b/captain/click/_tools.py index 92d18e38..3aec4815 100644 --- a/captain/click/_tools.py +++ b/captain/click/_tools.py @@ -18,20 +18,6 @@ short_help="Download tools (containerd, runc, nerdctl, CNI).", ) @common_options -@click.option( - "--builder-image", - envvar="BUILDER_IMAGE", - default="captainos-builder", - show_default=True, - help="Docker builder image name.", -) -@click.option( - "--no-cache", - envvar="NO_CACHE", - is_flag=True, - default=False, - help="Rebuild the builder image without Docker layer cache.", -) @click.option( "--tools-mode", envvar="TOOLS_MODE", @@ -53,7 +39,6 @@ def tools_cmd( arch: str, flavor_id: str, project_dir: str | None, - verbose: bool, builder_registry: str | None, builder_repository: str | None, builder_image: str, @@ -74,7 +59,6 @@ def tools_cmd( captain tools --tools-mode native captain tools --force-tools """ - _configure_logging(verbose) proj = resolve_project_dir(project_dir) @@ -92,8 +76,3 @@ def tools_cmd( _build_tools_stage(cfg) log.info("Tools stage complete!") - - -def _configure_logging(verbose: bool) -> None: - level = logging.DEBUG if verbose else logging.INFO - logging.getLogger("captain").setLevel(level) From 799f6751185d46f6e4dd96438b85d16900d00265 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 12:04:41 +0200 Subject: [PATCH 056/143] captain: simplify Rich logging and trace handler captain: simplify Rich logging and trace handler; let's not have two `click` packages Signed-off-by: Ricardo Pardini --- build.py | 10 +++--- captain/__init__.py | 38 ++++++++++------------ captain/{click => cli}/__init__.py | 2 +- captain/{click => cli}/_build.py | 4 +-- captain/{click => cli}/_builder.py | 2 +- captain/{click => cli}/_iso.py | 4 +-- captain/{click => cli}/_main.py | 2 +- captain/{click => cli}/_release_publish.py | 2 +- captain/{click => cli}/_stages.py | 0 captain/{click => cli}/_tools.py | 4 +-- 10 files changed, 33 insertions(+), 35 deletions(-) rename captain/{click => cli}/__init__.py (93%) rename captain/{click => cli}/_build.py (97%) rename captain/{click => cli}/_builder.py (95%) rename captain/{click => cli}/_iso.py (93%) rename captain/{click => cli}/_main.py (97%) rename captain/{click => cli}/_release_publish.py (98%) rename captain/{click => cli}/_stages.py (100%) rename captain/{click => cli}/_tools.py (93%) diff --git a/build.py b/build.py index 07f38da3..2375ec03 100755 --- a/build.py +++ b/build.py @@ -3,10 +3,10 @@ Requires: Python >= 3.13 and a lot of dependencies; Use Astral's ``uv`` to run:: - uv run click_cli.py --help - uv run click_cli.py builder - uv run click_cli.py build --arch arm64 - uv run click_cli.py release-publish --target combined + uv run build.py --help + uv run build.py builder + uv run build.py build --arch arm64 + uv run build.py release-publish --target combined """ import sys @@ -16,7 +16,7 @@ sys.exit(1) try: - from captain.click import main + from captain.cli import main except ImportError as exc: print(f"ERROR: {exc}", file=sys.stderr) uv_url = "https://docs.astral.sh/uv/getting-started/installation/" diff --git a/captain/__init__.py b/captain/__init__.py index 4cdd54ea..efb57c62 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -9,6 +9,7 @@ import logging import os +import click from rich.console import Console from rich.logging import RichHandler from rich.traceback import install as _install_rich_traceback @@ -21,12 +22,12 @@ console: Console = Console(stderr=True, color_system="standard", width=160, highlight=False) # Install Rich traceback handler globally (once, at import time). -_install_rich_traceback(console=console, show_locals=True, width=None) +_install_rich_traceback( + console=console, show_locals=False, width=None, suppress=[click], max_frames=2 +) class _StageFormatter(logging.Formatter): - """Show the module path relative to the ``captain`` package as a prefix.""" - def format(self, record: logging.LogRecord) -> str: name = record.name record.__dict__["stage"] = name @@ -36,20 +37,17 @@ def format(self, record: logging.LogRecord) -> str: return super().format(record) -# Configure the ``captain`` logger hierarchy once. -_root = logging.getLogger("captain") - -if not _root.handlers: - _handler = RichHandler( - console=console, - show_time=False, - show_level=True, - show_path=True, - markup=True, # interprets [braket]stuff[/bracket] in log messages, beware - rich_tracebacks=True, - tracebacks_show_locals=True, - ) - _handler.setFormatter(_StageFormatter("%(stage)s: %(message)s")) - _root.addHandler(_handler) - _root.setLevel(logging.DEBUG) - _root.propagate = False +_handler = RichHandler( + console=console, + show_time=False, + show_level=True, + show_path=True, + markup=True, # interprets [braket]stuff[/bracket] in log messages, beware + rich_tracebacks=True, + tracebacks_show_locals=False, + tracebacks_max_frames=2, # too many frames and we get confused + tracebacks_suppress=[click], # don't wanna see click infra code in traces +) +_handler.setFormatter(_StageFormatter("%(stage)s: %(message)s")) + +logging.basicConfig(level="DEBUG", datefmt="[%X]", handlers=[_handler]) diff --git a/captain/click/__init__.py b/captain/cli/__init__.py similarity index 93% rename from captain/click/__init__.py rename to captain/cli/__init__.py index 50adb243..442221bd 100644 --- a/captain/click/__init__.py +++ b/captain/cli/__init__.py @@ -15,6 +15,6 @@ eval "$(_CAPTAIN_COMPLETE=zsh_source captain)" """ -from captain.click._main import main +from captain.cli._main import main __all__ = ["main"] diff --git a/captain/click/_build.py b/captain/cli/_build.py similarity index 97% rename from captain/click/_build.py rename to captain/cli/_build.py index ffdfcaf3..ee356a70 100644 --- a/captain/click/_build.py +++ b/captain/cli/_build.py @@ -8,8 +8,8 @@ import captain.flavor from captain import artifacts -from captain.click._main import cli, common_options, resolve_project_dir -from captain.click._stages import ( +from captain.cli._main import cli, common_options, resolve_project_dir +from captain.cli._stages import ( _build_iso_stage, _build_mkosi_stage, _build_tools_stage, diff --git a/captain/click/_builder.py b/captain/cli/_builder.py similarity index 95% rename from captain/click/_builder.py rename to captain/cli/_builder.py index ecd6ef5b..1caad5ee 100644 --- a/captain/click/_builder.py +++ b/captain/cli/_builder.py @@ -6,7 +6,7 @@ import click -from captain.click._main import cli, common_options, resolve_project_dir +from captain.cli._main import cli, common_options, resolve_project_dir from captain.config import Config from captain.docker import obtain_builder diff --git a/captain/click/_iso.py b/captain/cli/_iso.py similarity index 93% rename from captain/click/_iso.py rename to captain/cli/_iso.py index 46a353ff..1081acda 100644 --- a/captain/click/_iso.py +++ b/captain/cli/_iso.py @@ -8,8 +8,8 @@ import captain.flavor from captain import artifacts -from captain.click._main import cli, common_options, resolve_project_dir -from captain.click._stages import ( +from captain.cli._main import cli, common_options, resolve_project_dir +from captain.cli._stages import ( _build_iso_stage, ) from captain.config import Config diff --git a/captain/click/_main.py b/captain/cli/_main.py similarity index 97% rename from captain/click/_main.py rename to captain/cli/_main.py index a07f47fe..0fe79ca8 100644 --- a/captain/click/_main.py +++ b/captain/cli/_main.py @@ -131,6 +131,6 @@ def cli(ctx: click.Context) -> None: def main() -> None: """Console-script entry point.""" # Import subcommand modules to register them on the group. - from captain.click import _build, _builder, _iso, _release_publish, _tools # noqa: F401 + from captain.cli import _build, _builder, _iso, _release_publish, _tools # noqa: F401 cli() diff --git a/captain/click/_release_publish.py b/captain/cli/_release_publish.py similarity index 98% rename from captain/click/_release_publish.py rename to captain/cli/_release_publish.py index 884590b5..f8da0b7c 100644 --- a/captain/click/_release_publish.py +++ b/captain/cli/_release_publish.py @@ -10,7 +10,7 @@ import captain.flavor from captain import oci -from captain.click._main import cli, common_options, resolve_project_dir +from captain.cli._main import cli, common_options, resolve_project_dir from captain.config import Config from captain.util import check_release_dependencies diff --git a/captain/click/_stages.py b/captain/cli/_stages.py similarity index 100% rename from captain/click/_stages.py rename to captain/cli/_stages.py diff --git a/captain/click/_tools.py b/captain/cli/_tools.py similarity index 93% rename from captain/click/_tools.py rename to captain/cli/_tools.py index 3aec4815..187bd310 100644 --- a/captain/click/_tools.py +++ b/captain/cli/_tools.py @@ -6,8 +6,8 @@ import click -from captain.click._main import cli, common_options, resolve_project_dir -from captain.click._stages import _build_tools_stage +from captain.cli._main import cli, common_options, resolve_project_dir +from captain.cli._stages import _build_tools_stage from captain.config import Config log = logging.getLogger(__name__) From 79b45693216d9a6087e2648ea3e2a5d91602d7f5 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 12:12:47 +0200 Subject: [PATCH 057/143] gha: force tools to run native Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7093d79f..6a5647c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: uses: astral-sh/setup-uv@v7 - name: Download tools - run: uv run captain tools + run: uv run captain tools --tools-mode=native - name: Save tools cache if: github.ref == 'refs/heads/main' && steps.tools-cache.outputs.cache-hit != 'true' From 556e8e8a1fad2eeee93ffbfac0eaf0c9a02c75e5 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 14:33:23 +0200 Subject: [PATCH 058/143] docker/Dockerfile: consolidate single Dockerfile; split and balance layers to optimize for parallel pulls Signed-off-by: Ricardo Pardini --- Dockerfile | 148 +++++++++++++++++++++----------- Dockerfile.release | 46 ---------- captain/cli/_release_publish.py | 4 +- captain/docker.py | 99 ++++++++++----------- 4 files changed, 142 insertions(+), 155 deletions(-) delete mode 100644 Dockerfile.release diff --git a/Dockerfile b/Dockerfile index 07061d59..41996818 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,23 @@ # Builder container for CaptainOS using mkosi # Encapsulates all mkosi dependencies for reproducible builds. -# Usage: docker build -t captainos-builder . && docker run --rm --privileged -v $(pwd):/work captainos-builder build +# Includes skopeo and buildah for OCI image manipulation, and uv for Python tool management. FROM debian:trixie -# Pinned post-v26 to pick up systemd/mkosi@1f811f05 ("tools: move grub-pc-bin -# to arch-specific drop-in"), which fixes arm64 builds failing on the default -# tools-tree pulling in grub-pc-bin (BIOS GRUB, x86-only). Bump to a release -# tag once v27 lands. -ARG MKOSI_VERSION=1f811f0524be3096872e79161c8e6ab3e7c2bb1f - # Avoid interactive prompts -ENV DEBIAN_FRONTEND=noninteractive +ENV DEBIAN_FRONTEND=noninteractive BUILDAH_ISOLATION=chroot FORCE_COLOR=1 -# Install mkosi runtime dependencies and kernel build dependencies in one layer -RUN apt-get -o "Dpkg::Use-Pty=0" update && apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ +# Add foreign architecture for cross-compilation (arm64 on amd64 and vice versa) and apt-update +# Immediately install the cross-arch grub dependencies +RUN <<-FRAGMENT_WITH_VARIABLES +# Determine arch/cross-arch and install grub and other basic packages (around 200mb layer) +NATIVE_ARCH="$(dpkg --print-architecture)" +FOREIGN_ARCH=$([ "$NATIVE_ARCH" = "amd64" ] && echo "arm64" || echo "amd64") +dpkg --add-architecture "$FOREIGN_ARCH" +apt-get -o "Dpkg::Use-Pty=0" update +apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ + "grub-efi-${NATIVE_ARCH}-bin" \ + "grub-efi-${FOREIGN_ARCH}-bin:${FOREIGN_ARCH}" \ + grub-common \ apt \ dpkg \ debian-archive-keyring \ @@ -25,71 +29,111 @@ RUN apt-get -o "Dpkg::Use-Pty=0" update && apt-get -o "Dpkg::Use-Pty=0" install systemd-container \ systemd \ udev \ - bubblewrap \ squashfs-tools \ mtools \ erofs-utils \ dosfstools \ e2fsprogs \ btrfs-progs \ - # Kernel build deps - build-essential \ - gcc \ - gcc-aarch64-linux-gnu \ + tree +FRAGMENT_WITH_VARIABLES + +# Cross-architecture support (arm64 on x86_64 and vice versa) - huge single package +RUN <<-QEMU_USER_FRAGMENT +# Install qemu-user but then delete all un-needed qemu binaries to save space (we only need aarch64 and x86_64) +apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends qemu-user +echo 'All qemus: ' +ls -lah /usr/bin/qemu-* +# keep only qemu binary for the arches we're interested in: aarch64 and x86_64 +echo 'To be deleted: ' +find /usr/bin -name 'qemu-*' -not -name 'qemu-aarch64' -not -name 'qemu-x86_64' -not -name 'qemu-arm*' -not -name 'qemu-amd*' -print0 | xargs -0 ls -lah +echo 'Deleting: ' +find /usr/bin -name 'qemu-*' -not -name 'qemu-aarch64' -not -name 'qemu-x86_64' -not -name 'qemu-arm*' -not -name 'qemu-amd*' -print0 | xargs -0 rm -fv +echo 'Remaining: ' +ls -lah /usr/bin/qemu-* + +QEMU_USER_FRAGMENT + +# Extra kernel build tools +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ make \ flex \ bison \ bc \ libelf-dev \ libssl-dev \ + dpkg-dev \ dwarves \ - pahole \ + pahole + +# Those are pulled by build-essential (cross...), but are quite big; pull them ealier to balance layer size +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ + binutils-common \ + libasan8 \ + liblsan0 \ + libubsan1 \ + libhwasan0 \ + binutils-x86-64-linux-gnu \ + libasan8-amd64-cross \ + liblsan0-amd64-cross \ + libtsan2-amd64-cross \ + libc6-amd64-cross \ + linux-libc-dev-amd64-cross \ + libc6-dev-amd64-cross + +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ rsync \ coreutils \ - # Cross-architecture support (arm64 on x86_64 and vice versa) - qemu-user-static \ - # Network tools (for fetching kernel source etc.) git \ curl \ ca-certificates \ - # Binary compression - upx-ucl \ - # ISO image creation - xorriso \ - grub-common \ - && NATIVE_ARCH="$(dpkg --print-architecture)" \ - && FOREIGN_ARCH=$([ "$NATIVE_ARCH" = "amd64" ] && echo "arm64" || echo "amd64") \ - && apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends "grub-efi-${NATIVE_ARCH}-bin" \ - && dpkg --add-architecture "$FOREIGN_ARCH" \ - && apt-get -o "Dpkg::Use-Pty=0" update \ - && apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends "grub-efi-${FOREIGN_ARCH}-bin:${FOREIGN_ARCH}" \ - && rm -rf /var/lib/apt/lists/* + qemu-user-static -# Install astral-sh's uv with a script - install to /usr for global access -RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/bin" sh +# Then both, of which one will already be fulfilled +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends crossbuild-essential-arm64 +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends crossbuild-essential-amd64 -# Verify uv is functional -RUN uv --version +# Buildah and Skopeo +# Binary compression +# ISO image creation +# Kernel build deps: build-essential +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ + build-essential \ + containernetworking-plugins \ + bubblewrap \ + skopeo \ + upx-ucl \ + xorriso -# Install mkosi from GitHub (not on PyPI) via uv; symlink to /usr/bin for global access -RUN uv tool install "git+https://github.com/systemd/mkosi.git@${MKOSI_VERSION}" -RUN ln -sf ~/.local/bin/mkosi /usr/bin/mkosi +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ + buildah + +RUN <<-CONFIG_FRAG +## A few small config fragments to make life easier +# git: Ignore owner mismatches in /work, which will be bind-mounted from the host +git config --global --add safe.directory /work \ +# buildah: Configure rootless storage driver and chroot isolation (no user-namespace required — we only assemble scratch images, never RUN anything inside them). +printf '[storage]\ndriver = "vfs"\nrunroot = "/var/tmp/buildah-runroot"\ngraphroot = "/var/tmp/buildah-storage"\n' > /etc/containers/storage.conf +# Buildah 1.39+ on Debian requires netavark but we never need networking +# (all images are FROM scratch with no RUN steps). A no-op stub satisfies +# the startup check. +mkdir -p /usr/libexec/podman +printf '#!/bin/sh\nexit 0\n' > /usr/libexec/podman/netavark +chmod +x /usr/libexec/podman/netavark +CONFIG_FRAG -# Verify mkosi is functional -RUN mkosi --version +# Install astral-sh's uv with a script - install to /usr for global access +RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/bin" sh && uv --version -# Install project dependencies into a persistent venv so that -# `uv run` inside the container reuses it instead of recreating one. -COPY pyproject.toml /opt/captain/pyproject.toml -COPY captain /opt/captain/captain -COPY build.py /opt/captain/build.py -RUN uv venv /opt/captain-venv && \ - VIRTUAL_ENV=/opt/captain-venv uv pip install --project /opt/captain /opt/captain +# Install mkosi from GitHub (not on PyPI) via uv; symlink to /usr/bin for global access +ARG MKOSI_VERSION=v26 +RUN uv tool install "git+https://github.com/systemd/mkosi.git@${MKOSI_VERSION}" && ln -sf ~/.local/bin/mkosi /usr/bin/mkosi && mkosi --version -# Point uv at the pre-built venv for all future runs. -ENV VIRTUAL_ENV=/opt/captain-venv -ENV UV_PROJECT_ENVIRONMENT=/opt/captain-venv +# Prime uv's cache with our pyproject.toml to speed up runtime +COPY pyproject.toml /tmp/pyproject.toml +COPY captain /tmp/captain +COPY build.py /tmp/build.py +WORKDIR /tmp +RUN uv --verbose run captain --version WORKDIR /work -ENTRYPOINT ["mkosi"] -CMD ["build"] diff --git a/Dockerfile.release b/Dockerfile.release deleted file mode 100644 index b63204cd..00000000 --- a/Dockerfile.release +++ /dev/null @@ -1,46 +0,0 @@ -# Lightweight container for OCI release operations (publish, index, pull, tag). -# -# Usage: -# docker build -f Dockerfile.release -t captainos-release . -# docker run --rm -v $(pwd):/work captainos-release release publish -FROM debian:trixie - -# Install buildah, skopeo, curl and git -RUN apt-get -o "Dpkg::Use-Pty=0" update && apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ - buildah \ - skopeo \ - curl \ - git \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* \ - && git config --global --add safe.directory /work - -# Configure rootless storage driver and chroot isolation (no user-namespace -# required — we only assemble scratch images, never RUN anything inside them). -RUN printf '[storage]\ndriver = "vfs"\nrunroot = "/var/tmp/buildah-runroot"\ngraphroot = "/var/tmp/buildah-storage"\n' \ - > /etc/containers/storage.conf -ENV BUILDAH_ISOLATION=chroot - -# Buildah 1.39+ on Debian requires netavark but we never need networking -# (all images are FROM scratch with no RUN steps). A no-op stub satisfies -# the startup check. -RUN mkdir -p /usr/libexec/podman \ - && printf '#!/bin/sh\nexit 0\n' > /usr/libexec/podman/netavark \ - && chmod +x /usr/libexec/podman/netavark - -# Install astral-sh's uv with a script - install to /usr for global access -RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/bin" sh - -# Verify uv is functional -RUN uv --version - -# Prime uv's cache with our pyproject.toml to speed up runtime -COPY pyproject.toml /tmp/pyproject.toml -COPY captain /tmp/captain -COPY build.py /tmp/build.py -WORKDIR /tmp -RUN uv --verbose run build.py --help - -WORKDIR /work -ENTRYPOINT ["/usr/bin/uv", "run", "/work/build.py"] -CMD ["release", "--help"] diff --git a/captain/cli/_release_publish.py b/captain/cli/_release_publish.py index f8da0b7c..51045530 100644 --- a/captain/cli/_release_publish.py +++ b/captain/cli/_release_publish.py @@ -59,7 +59,7 @@ type=click.Choice(["amd64", "arm64", "combined"], case_sensitive=False), metavar="TARGET", help="Artifact target: amd64, arm64, or combined (default: value of --arch); " - "combined requires trixie-full or equivalent flavor with both architectures' outputs present.", + "combined requires trixie-full or equivalent flavor with both arch's outputs present.", ) @click.option( "--git-sha", @@ -162,7 +162,7 @@ def release_publish_cmd( if force: env_args += ["-e", "FORCE=true"] - inner_cmd = ["/work/build.py", "release", "publish"] + inner_cmd = ["captain", "release-publish"] try: docker.run_in_builder( diff --git a/captain/docker.py b/captain/docker.py index 83a4a924..65845373 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -102,6 +102,32 @@ def obtain_builder(cfg: Config) -> None: ] ) + # Show the layer size distribution for the built image to help with debugging and optimization. + log.info("Docker image '%s' built successfully. Layer size distribution:", local_tagged_image) + layer_sizes_lines = run( + [ + "docker", + "history", + "--no-trunc", + "--format", + "-> {{.Size}} :: '{{.CreatedBy}}'", + local_tagged_image, + ], + capture=True, + check=True, + ) + layers = [] + for line in layer_sizes_lines.stdout.strip().splitlines(): + line = line.strip() + if line.startswith("-> "): + # remove double whitespace chars to make it easier to read + line = " ".join(line.split()) + layers.append(line) + # reverse the array to match the order + layers.reverse() + for layer in layers: + log.info("Layer info: %s", layer) + # Optionally push the image after building it if cfg.builder_push: log.info( @@ -113,60 +139,6 @@ def obtain_builder(cfg: Config) -> None: run(["docker", "push", remote_tagged_image]) -def _release_dockerfile_hash(cfg: Config) -> str: - """Return the SHA-256 hex digest of the Dockerfile.release content.""" - dockerfile = cfg.project_dir / "Dockerfile.release" - return hashlib.sha256(dockerfile.read_bytes()).hexdigest() - - -def run_in_release(cfg: Config, *extra_args: str) -> None: - """Run a command inside the release container. - - Similar to :func:`run_in_builder` but uses the lightweight release - image which has buildah, skopeo, Python, and git. - """ - docker_args: list[str] = [ - "docker", - "run", - "--rm", - # Buildah needs mount/remount capabilities for layer operations. - "--privileged", - "-v", - f"{cfg.project_dir}:/work", - "-w", - "/work", - "-e", - "CAPTAIN_IN_DOCKER=docker", - "-e", - f"ARCH={cfg.arch}", - "-e", - "RELEASE_MODE=native", - # Chroot isolation lets buildah work inside an unprivileged container - # (no user namespaces needed — we only assemble scratch images). - "-e", - "BUILDAH_ISOLATION=chroot", - "-e", - f"BUILDAH_INSECURE={os.environ.get('BUILDAH_INSECURE', '')}", - "-e", - f"TERM={os.environ.get('TERM', 'xterm-256color')}", - "-e", - "FORCE_COLOR=1", - "-e", - f"COLUMNS={os.environ.get('COLUMNS', '200')}", - "-e", - f"GITHUB_ACTIONS={os.environ.get('GITHUB_ACTIONS', '')}", - ] - # Forward host registry credentials so buildah/skopeo can authenticate. - # The caller sets these env vars on the host (e.g. via docker login or - # CI secrets); they are passed through to the container as-is. - for var in ("REGISTRY_AUTH_FILE", "REGISTRY_USERNAME", "REGISTRY_PASSWORD"): - val = os.environ.get(var) - if val: - docker_args += ["-e", f"{var}={val}"] - docker_args.extend(extra_args) - run(docker_args) - - def run_in_builder(cfg: Config, *extra_args: str) -> None: """Run a command inside the Docker builder container. @@ -176,7 +148,7 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "docker", "run", "--rm", - "--privileged", + "--privileged", # yes, this is required for both buildah and mkosi in the container "-w", "/work", "-e", @@ -189,6 +161,14 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: f"FORCE_TOOLS={int(cfg.force_tools)}", "-e", f"FORCE_ISO={int(cfg.force_iso)}", + # Chroot isolation lets buildah work inside an unprivileged container + # (no user namespaces needed — we only assemble scratch images). + "-e", + "BUILDAH_ISOLATION=chroot", + "-e", + f"BUILDAH_INSECURE={os.environ.get('BUILDAH_INSECURE', '')}", + "-e", + "RELEASE_MODE=native", "-e", "TOOLS_MODE=native", "-e", @@ -207,6 +187,15 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: f"GITHUB_ACTIONS={os.environ.get('GITHUB_ACTIONS', '')}", ] + # Forward host registry credentials so buildah/skopeo can authenticate. + # The caller sets these env vars on the host (e.g. via docker login or + # CI secrets); they are passed through to the container as-is. + for var in ("REGISTRY_AUTH_FILE", "REGISTRY_USERNAME", "REGISTRY_PASSWORD"): + val = os.environ.get(var) + if val: + docker_args += ["-e", f"{var}={val}"] + docker_args.extend(extra_args) + docker_args += ["--mount", "type=volume,source=captain-workdir,target=/work"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.output:/work/mkosi.output"] From 59b01b4c36d94abdf0bafb9af189388dd9381a48 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 16:30:24 +0200 Subject: [PATCH 059/143] docker: rework re-launching inside docker & docker envs Signed-off-by: Ricardo Pardini --- captain/cli/_stages.py | 24 ++---------- captain/docker.py | 85 +++++++++++++++++++++--------------------- 2 files changed, 45 insertions(+), 64 deletions(-) diff --git a/captain/cli/_stages.py b/captain/cli/_stages.py index 0caaf1a0..ae94ade2 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/_stages.py @@ -28,16 +28,7 @@ def _build_tools_stage(cfg: Config) -> None: # --- docker ------------------------------------------------------- docker.obtain_builder(cfg) log.info("Downloading tools (nerdctl, containerd, etc.) docker...") - docker.run_in_builder( - cfg, - "--entrypoint", - "/usr/bin/uv", - cfg.builder_image, - *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), - "run", - "/work/build.py", - "tools", - ) + docker.run_captain_in_builder(cfg, "tools") docker.fix_docker_ownership(cfg, ["/work/mkosi.output"]) @@ -86,7 +77,7 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: log.info("Building initrd with mkosi (docker)...") tools_tree = f"/work/mkosi.output/tools/{cfg.arch}" output_dir = f"/work/mkosi.output/initramfs/{cfg.flavor_id}/{cfg.arch}" - docker.run_mkosi( + docker.run_mkosi_in_builder( cfg, f"--extra-tree={tools_tree}", f"--output-dir={output_dir}", @@ -126,16 +117,7 @@ def _build_iso_stage(cfg: Config) -> None: # --- docker ------------------------------------------------------- docker.obtain_builder(cfg) log.info("Building ISO (docker)...") - docker.run_in_builder( - cfg, - "--entrypoint", - "/usr/bin/uv", - cfg.builder_image, - *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), - "run", - "/work/build.py", - "iso", - ) + docker.run_captain_in_builder(cfg, "iso") docker.fix_docker_ownership( cfg, [ diff --git a/captain/docker.py b/captain/docker.py index 65845373..53204961 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -144,6 +144,31 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: *extra_args* are appended after the docker run flags and image name. """ + + docker_envs: dict[str, str] = { + "CAPTAIN_IN_DOCKER": "docker", + "ARCH": cfg.arch, + "FLAVOR_ID": cfg.flavor_id, + "FORCE_TOOLS": str(int(cfg.force_tools)), + "FORCE_ISO": str(int(cfg.force_iso)), + "BUILDAH_ISOLATION": "chroot", + "BUILDAH_INSECURE": os.environ.get("BUILDAH_INSECURE", ""), + "RELEASE_MODE": "native", + "TOOLS_MODE": "native", + "MKOSI_MODE": "native", + "ISO_MODE": "native", + "TERM": os.environ.get("TERM", "xterm-256color"), + "FORCE_COLOR": "1", + "COLUMNS": os.environ.get("COLUMNS", "200"), + "GITHUB_ACTIONS": os.environ.get("GITHUB_ACTIONS", ""), + # Forward host registry credentials so buildah/skopeo can authenticate. + # The caller sets these env vars on the host (e.g. via docker login or + # CI secrets); they are passed through to the container as-is. + "REGISTRY_AUTH_FILE": os.environ.get("REGISTRY_AUTH_FILE", ""), + "REGISTRY_USERNAME": os.environ.get("REGISTRY_USERNAME", ""), + "REGISTRY_PASSWORD": os.environ.get("REGISTRY_PASSWORD", ""), + } + docker_args: list[str] = [ "docker", "run", @@ -151,50 +176,10 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "--privileged", # yes, this is required for both buildah and mkosi in the container "-w", "/work", - "-e", - "CAPTAIN_IN_DOCKER=docker", - "-e", - f"ARCH={cfg.arch}", - "-e", - f"FLAVOR_ID={cfg.flavor_id}", - "-e", - f"FORCE_TOOLS={int(cfg.force_tools)}", - "-e", - f"FORCE_ISO={int(cfg.force_iso)}", - # Chroot isolation lets buildah work inside an unprivileged container - # (no user namespaces needed — we only assemble scratch images). - "-e", - "BUILDAH_ISOLATION=chroot", - "-e", - f"BUILDAH_INSECURE={os.environ.get('BUILDAH_INSECURE', '')}", - "-e", - "RELEASE_MODE=native", - "-e", - "TOOLS_MODE=native", - "-e", - "MKOSI_MODE=native", - "-e", - "ISO_MODE=native", - "-e", - "RELEASE_MODE=native", - "-e", - f"TERM={os.environ.get('TERM', 'xterm-256color')}", - "-e", - "FORCE_COLOR=1", - "-e", - f"COLUMNS={os.environ.get('COLUMNS', '200')}", - "-e", - f"GITHUB_ACTIONS={os.environ.get('GITHUB_ACTIONS', '')}", ] - # Forward host registry credentials so buildah/skopeo can authenticate. - # The caller sets these env vars on the host (e.g. via docker login or - # CI secrets); they are passed through to the container as-is. - for var in ("REGISTRY_AUTH_FILE", "REGISTRY_USERNAME", "REGISTRY_PASSWORD"): - val = os.environ.get(var) - if val: - docker_args += ["-e", f"{var}={val}"] - docker_args.extend(extra_args) + for k, v in docker_envs.items(): + docker_args += ["-e", f"{k}={v}"] docker_args += ["--mount", "type=volume,source=captain-workdir,target=/work"] @@ -218,12 +203,26 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: run(docker_args) -def run_mkosi(cfg: Config, *mkosi_args: str) -> None: +def run_captain_in_builder(cfg: Config, *extra_args: str): + log.debug("Running 'captain %s' in builder container...", extra_args) + run_in_builder( + cfg, + cfg.builder_image, + "/usr/bin/uv", + *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), + "run", + "captain", + *extra_args, + ) + + +def run_mkosi_in_builder(cfg: Config, *mkosi_args: str) -> None: """Run mkosi inside the builder container.""" ensure_binfmt(cfg) run_in_builder( cfg, cfg.builder_image, + "/usr/bin/mkosi", f"--architecture={cfg.arch_info.mkosi_arch}", *mkosi_args, ) From 38b26aa7861d993138c15cb19787808207a030b8 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 17:05:59 +0200 Subject: [PATCH 060/143] docker/mkosi: get rid of tools tree; back to Debian's trixie system Python + pip for mkosi - mkosi re-launches itself and ignores uv's env without a tools tree - uv instead does use the system Python if it matches constraints - build in /work in Dockerfile, allows to reuse .venv - don't mount a volume in /work, instead use the one provided by the Dockerfile Signed-off-by: Ricardo Pardini --- Dockerfile | 23 +++++++++++---------- captain/docker.py | 7 +++---- captain/flavors/common_debian/mkosi.conf.j2 | 7 +------ 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 41996818..b02bcdb5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -105,8 +105,11 @@ RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ upx-ucl \ xorriso -RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ - buildah +# Buildah is pretty huge, gets its own layer. +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends buildah + +# This is just to appease mkosi's later stages. +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends python3 python3-pip python3-pefile RUN <<-CONFIG_FRAG ## A few small config fragments to make life easier @@ -123,17 +126,15 @@ chmod +x /usr/libexec/podman/netavark CONFIG_FRAG # Install astral-sh's uv with a script - install to /usr for global access -RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/bin" sh && uv --version +RUN echo -n 'System Python: ' && python3 --version && curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/bin" sh && uv --version -# Install mkosi from GitHub (not on PyPI) via uv; symlink to /usr/bin for global access +# Install mkosi from GitHub (not on PyPI) via the system pip3 ARG MKOSI_VERSION=v26 -RUN uv tool install "git+https://github.com/systemd/mkosi.git@${MKOSI_VERSION}" && ln -sf ~/.local/bin/mkosi /usr/bin/mkosi && mkosi --version +RUN pip3 install --break-system-packages --root-user-action=ignore "git+https://github.com/systemd/mkosi.git@${MKOSI_VERSION}" && mkosi --version && command -v mkosi # Prime uv's cache with our pyproject.toml to speed up runtime -COPY pyproject.toml /tmp/pyproject.toml -COPY captain /tmp/captain -COPY build.py /tmp/build.py -WORKDIR /tmp -RUN uv --verbose run captain --version - +COPY pyproject.toml /work/pyproject.toml +COPY captain /work/captain +COPY build.py /work/build.py WORKDIR /work +RUN uv --verbose run captain --version diff --git a/captain/docker.py b/captain/docker.py index 53204961..d7ec38cf 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -181,13 +181,12 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: for k, v in docker_envs.items(): docker_args += ["-e", f"{k}={v}"] - docker_args += ["--mount", "type=volume,source=captain-workdir,target=/work"] - docker_args += ["-v", f"{cfg.project_dir}/mkosi.output:/work/mkosi.output"] + docker_args += ["-v", f"{cfg.project_dir}/out:/work/out"] + docker_args += ["-v", f"{cfg.project_dir}/mkosi.extra:/work/mkosi.extra"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.sandbox:/work/mkosi.sandbox"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.skeleton:/work/mkosi.skeleton"] - docker_args += ["-v", f"{cfg.project_dir}/out:/work/out"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.conf:/work/mkosi.conf"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.finalize:/work/mkosi.finalize"] @@ -222,7 +221,7 @@ def run_mkosi_in_builder(cfg: Config, *mkosi_args: str) -> None: run_in_builder( cfg, cfg.builder_image, - "/usr/bin/mkosi", + "/usr/local/bin/mkosi", f"--architecture={cfg.arch_info.mkosi_arch}", *mkosi_args, ) diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index 5913c5ae..ce128aa5 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -10,13 +10,8 @@ CompressLevel=19 OutputDirectory=mkosi.output [Build] -Environment=TERM=dumb -ToolsTree=yes -ToolsTreeDistribution=debian -ToolsTreeRelease=trixie -ToolsTreeSandboxTrees=mkosi.sandbox Incremental=yes -CacheDirectory=mkosi.cache +CacheDirectory=mkosi.cache.for.incremental [Content] Bootable=no From 9e0ba97d262cc92d32a6ee7d6e35694a84c7c04a Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 7 Apr 2026 17:48:13 +0200 Subject: [PATCH 061/143] captain: honor FORCE_COLOR=1 for internal logging Signed-off-by: Ricardo Pardini --- captain/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/captain/__init__.py b/captain/__init__.py index efb57c62..2d69888b 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -15,16 +15,17 @@ from rich.traceback import install as _install_rich_traceback # Rich console — writes to stderr so log output never pollutes piped stdout. -# If running under GHA, force colors. -if os.environ.get("GITHUB_ACTIONS", "") == "": - console: Console = Console(stderr=True) -else: - console: Console = Console(stderr=True, color_system="standard", width=160, highlight=False) - # Install Rich traceback handler globally (once, at import time). -_install_rich_traceback( - console=console, show_locals=False, width=None, suppress=[click], max_frames=2 -) +if os.environ.get("FORCE_COLOR", "0") == "1": + console: Console = Console(stderr=True, color_system="standard", width=160, highlight=False) + _install_rich_traceback( + console=console, show_locals=False, width=160, suppress=[click], max_frames=2 + ) +else: + console: Console = Console(stderr=True) + _install_rich_traceback( + console=console, show_locals=False, width=None, suppress=[click], max_frames=2 + ) class _StageFormatter(logging.Formatter): From e35205099e6247d025fc78594c1b8d82923744f3 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 8 Apr 2026 11:03:33 +0200 Subject: [PATCH 062/143] flavors: tighten semantics, refactor - refactor add_static_dir() into BaseFlavor - make has_iso() abstractmethod at BaseFlavor level - make kernel_packages() abstractmethod at common-debian level Signed-off-by: Ricardo Pardini --- captain/flavor.py | 10 ++++++ captain/flavors/common_armbian/__init__.py | 12 +++---- captain/flavors/common_debian/__init__.py | 39 ++++++++-------------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/captain/flavor.py b/captain/flavor.py index 18098da7..ea248df2 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -4,6 +4,7 @@ import logging import shutil +from abc import abstractmethod from pathlib import Path from typing import Protocol, runtime_checkable @@ -69,6 +70,14 @@ def specific_flavor_dir(self, flavor_id: str) -> Path: raise SystemExit(1) return flavor_dir + def add_static_dir(self, dir_to_include: str, flavor_dir: Path): + extra_dir = flavor_dir / dir_to_include + if extra_dir.exists() and extra_dir.is_dir(): + for extra_file in extra_dir.rglob("*"): + if extra_file.is_file(): + relative_path = extra_file.relative_to(flavor_dir) + self.static_map[str(relative_path)] = extra_file + def render_templates(self, output_dir: Path): log.debug("Called BaseFlavor.render_templates() with output_dir: %s", output_dir) # Use jinja2 to render all templates in self.template_map, writing output to output_dir @@ -111,6 +120,7 @@ def copy_static_files(self, project_dir): destination_path.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source_path, destination_path) + @abstractmethod def has_iso(self) -> bool: return False diff --git a/captain/flavors/common_armbian/__init__.py b/captain/flavors/common_armbian/__init__.py index e79bdfd8..388cf31b 100644 --- a/captain/flavors/common_armbian/__init__.py +++ b/captain/flavors/common_armbian/__init__.py @@ -16,13 +16,9 @@ class ArmbianCommonFlavor(DebianCommonFlavor): def setup(self, cfg: Config, flavor_dir: Path) -> None: super().setup(cfg, flavor_dir) + this_flavor_dir = self.specific_flavor_dir("common-armbian") + self.add_static_dir("mkosi.sandbox", this_flavor_dir) - # Now, lets enumerate and add all the static files this flavor's mkosi.sandbox directory - # and add them to self.static_map with the key being the relative path from the flavor dir - extra_dir = this_flavor_dir / "mkosi.sandbox" - if extra_dir.exists() and extra_dir.is_dir(): - for extra_file in extra_dir.rglob("*"): - if extra_file.is_file(): - relative_path = extra_file.relative_to(this_flavor_dir) - self.static_map[str(relative_path)] = extra_file + def has_iso(self) -> bool: + return False diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py index 60c84dca..051db43c 100644 --- a/captain/flavors/common_debian/__init__.py +++ b/captain/flavors/common_debian/__init__.py @@ -1,4 +1,5 @@ import logging +from abc import abstractmethod from dataclasses import dataclass from pathlib import Path @@ -18,39 +19,25 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: super().setup(cfg, flavor_dir) this_flavor_dir = self.specific_flavor_dir("common-debian") + + # Templates self.template_map["mkosi.conf"] = [this_flavor_dir / "mkosi.conf.j2"] + self.template_map["mkosi.postinst"] = [ this_flavor_dir / "bash.header.sh", this_flavor_dir / "mkosi.postinst.sh.j2", ] + self.template_map["mkosi.finalize"] = [ this_flavor_dir / "bash.header.sh", this_flavor_dir / "mkosi.finalize.sh.j2", ] - # Now, lets enumerate and add all the static files this flavor's mkosi.extra directory - # and add them to self.static_map with the key being the relative path from the flavor dir - extra_dir = this_flavor_dir / "mkosi.extra" - if extra_dir.exists() and extra_dir.is_dir(): - for extra_file in extra_dir.rglob("*"): - if extra_file.is_file(): - relative_path = extra_file.relative_to(this_flavor_dir) - self.static_map[str(relative_path)] = extra_file - - # Now, lets enumerate and add all the static files this flavor's mkosi.sandbox directory - # and add them to self.static_map with the key being the relative path from the flavor dir - extra_dir = this_flavor_dir / "mkosi.sandbox" - if extra_dir.exists() and extra_dir.is_dir(): - for extra_file in extra_dir.rglob("*"): - if extra_file.is_file(): - relative_path = extra_file.relative_to(this_flavor_dir) - self.static_map[str(relative_path)] = extra_file - - # Now, lets enumerate and add all the static files this flavor's mkosi.skeleton directory - # and add them to self.static_map with the key being the relative path from the flavor dir - extra_dir = this_flavor_dir / "mkosi.skeleton" - if extra_dir.exists() and extra_dir.is_dir(): - for extra_file in extra_dir.rglob("*"): - if extra_file.is_file(): - relative_path = extra_file.relative_to(this_flavor_dir) - self.static_map[str(relative_path)] = extra_file + # Static files + self.add_static_dir("mkosi.extra", this_flavor_dir) + self.add_static_dir("mkosi.sandbox", this_flavor_dir) + self.add_static_dir("mkosi.skeleton", this_flavor_dir) + + @abstractmethod + def kernel_packages(self) -> set[str]: + pass From 26fdd1bdc759cd66f79fe4d5245147d5facd441f Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 8 Apr 2026 11:23:49 +0200 Subject: [PATCH 063/143] flavors: introduce common-acpi, move acpi/impi stuff there - so it's `common-debian > common-acpi > trixie-full` - in the future: `common-debian > common-acpi > trixie-mainline` - common-acpi implements `has_iso()` as True Signed-off-by: Ricardo Pardini --- captain/flavors/common_acpi/__init__.py | 28 +++++++++++++++++++ .../mkosi.extra/etc/acpi/events/powerbtn | 0 .../mkosi.extra/etc/acpi/powerbtn.sh | 0 .../mkosi.extra/etc/modules-load.d/ipmi.conf | 0 captain/flavors/trixie_full/__init__.py | 8 ++---- 5 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 captain/flavors/common_acpi/__init__.py rename captain/flavors/{common_debian => common_acpi}/mkosi.extra/etc/acpi/events/powerbtn (100%) rename captain/flavors/{common_debian => common_acpi}/mkosi.extra/etc/acpi/powerbtn.sh (100%) rename captain/flavors/{common_debian => common_acpi}/mkosi.extra/etc/modules-load.d/ipmi.conf (100%) diff --git a/captain/flavors/common_acpi/__init__.py b/captain/flavors/common_acpi/__init__.py new file mode 100644 index 00000000..b9e35eb6 --- /dev/null +++ b/captain/flavors/common_acpi/__init__.py @@ -0,0 +1,28 @@ +import logging +from dataclasses import dataclass +from pathlib import Path + +from captain.config import Config +from captain.flavors.common_debian import DebianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +@dataclass +class TrixieACPIFlavor(DebianCommonFlavor): + id = "trixie-acpi" + name = "Trixie ACPI Common" + description = "Debian Trixie based on UEFI+ACPI machines" + supported_architectures = frozenset(["amd64", "arm64"]) + + def setup(self, cfg: Config, flavor_dir: Path) -> None: + super().setup(cfg, flavor_dir) + + this_flavor_dir = self.specific_flavor_dir("common-acpi") + + # Static files + self.add_static_dir("mkosi.extra", this_flavor_dir) + + # This flavor can produce working ISO images (generic UEFI/ACPI) + def has_iso(self) -> bool: + return True diff --git a/captain/flavors/common_debian/mkosi.extra/etc/acpi/events/powerbtn b/captain/flavors/common_acpi/mkosi.extra/etc/acpi/events/powerbtn similarity index 100% rename from captain/flavors/common_debian/mkosi.extra/etc/acpi/events/powerbtn rename to captain/flavors/common_acpi/mkosi.extra/etc/acpi/events/powerbtn diff --git a/captain/flavors/common_debian/mkosi.extra/etc/acpi/powerbtn.sh b/captain/flavors/common_acpi/mkosi.extra/etc/acpi/powerbtn.sh similarity index 100% rename from captain/flavors/common_debian/mkosi.extra/etc/acpi/powerbtn.sh rename to captain/flavors/common_acpi/mkosi.extra/etc/acpi/powerbtn.sh diff --git a/captain/flavors/common_debian/mkosi.extra/etc/modules-load.d/ipmi.conf b/captain/flavors/common_acpi/mkosi.extra/etc/modules-load.d/ipmi.conf similarity index 100% rename from captain/flavors/common_debian/mkosi.extra/etc/modules-load.d/ipmi.conf rename to captain/flavors/common_acpi/mkosi.extra/etc/modules-load.d/ipmi.conf diff --git a/captain/flavors/trixie_full/__init__.py b/captain/flavors/trixie_full/__init__.py index dd28880b..9b81c31e 100644 --- a/captain/flavors/trixie_full/__init__.py +++ b/captain/flavors/trixie_full/__init__.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from captain.flavor import BaseFlavor -from captain.flavors.common_debian import DebianCommonFlavor +from captain.flavors.common_acpi import TrixieACPIFlavor log: logging.Logger = logging.getLogger(__name__) @@ -12,7 +12,7 @@ def create_flavor() -> BaseFlavor: @dataclass -class TrixieFullFlavor(DebianCommonFlavor): +class TrixieFullFlavor(TrixieACPIFlavor): id = "trixie-full" name = "Trixie Full" description = "Debian Trixie based with linux-image-generic standard Debian kernel" @@ -20,7 +20,3 @@ class TrixieFullFlavor(DebianCommonFlavor): def kernel_packages(self) -> set[str]: return {"linux-image-generic"} - - # This flavor can produce working ISO images (generic UEFI/ACPI) - def has_iso(self) -> bool: - return True From bb203e9356b2745ead6638918656dfaea41c958c Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 8 Apr 2026 13:03:52 +0200 Subject: [PATCH 064/143] click: introduce CliContext and reuse common options via @click.Group - CliContext is a Config factory, taking both common and specific options - actually implement --verbose, thus new default logging level is INFO Signed-off-by: Ricardo Pardini --- captain/__init__.py | 2 +- captain/cli/_build.py | 23 +---- captain/cli/_builder.py | 25 +---- captain/cli/_iso.py | 23 +---- captain/cli/_main.py | 157 +++++++++++++++++++++----------- captain/cli/_release_publish.py | 36 ++------ captain/cli/_tools.py | 23 +---- 7 files changed, 129 insertions(+), 160 deletions(-) diff --git a/captain/__init__.py b/captain/__init__.py index 2d69888b..3b7ff4f2 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -51,4 +51,4 @@ def format(self, record: logging.LogRecord) -> str: ) _handler.setFormatter(_StageFormatter("%(stage)s: %(message)s")) -logging.basicConfig(level="DEBUG", datefmt="[%X]", handlers=[_handler]) +logging.basicConfig(level="INFO", datefmt="[%X]", handlers=[_handler]) diff --git a/captain/cli/_build.py b/captain/cli/_build.py index ee356a70..b18dfe6d 100644 --- a/captain/cli/_build.py +++ b/captain/cli/_build.py @@ -8,13 +8,12 @@ import captain.flavor from captain import artifacts -from captain.cli._main import cli, common_options, resolve_project_dir +from captain.cli._main import CliContext, cli from captain.cli._stages import ( _build_iso_stage, _build_mkosi_stage, _build_tools_stage, ) -from captain.config import Config log = logging.getLogger(__name__) @@ -23,7 +22,6 @@ "build", short_help="Run the full build pipeline via mkosi.", ) -@common_options @click.option( "--mkosi-mode", envvar="MKOSI_MODE", @@ -72,14 +70,10 @@ default=False, help="Force ISO rebuild even if outputs already exist.", ) +@click.pass_obj def build_cmd( + cli_ctx: CliContext, *, - arch: str, - flavor_id: str, - project_dir: str | None, - builder_registry: str | None, - builder_repository: str | None, - builder_image: str, mkosi_mode: str, tools_mode: str, iso_mode: str, @@ -103,16 +97,7 @@ def build_cmd( captain build --force --force-tools """ - proj = resolve_project_dir(project_dir) - - cfg = Config( - project_dir=proj, - output_dir=proj / "out", - arch=arch, - flavor_id=flavor_id, - builder_registry=builder_registry, - builder_repository=builder_repository, - builder_image=builder_image, + cfg = cli_ctx.make_config( tools_mode=tools_mode, mkosi_mode=mkosi_mode, iso_mode=iso_mode, diff --git a/captain/cli/_builder.py b/captain/cli/_builder.py index 1caad5ee..8e8d3ea2 100644 --- a/captain/cli/_builder.py +++ b/captain/cli/_builder.py @@ -6,8 +6,7 @@ import click -from captain.cli._main import cli, common_options, resolve_project_dir -from captain.config import Config +from captain.cli._main import CliContext, cli from captain.docker import obtain_builder log = logging.getLogger(__name__) @@ -17,21 +16,16 @@ "builder", short_help="Build the Docker builder image and optionally push it.", ) -@common_options @click.option( "--push", is_flag=True, default=False, help="Push the built image to a registry after building.", ) +@click.pass_obj def builder_cmd( + cli_ctx: CliContext, *, - arch: str, - flavor_id: str, - project_dir: str | None, - builder_registry: str | None, - builder_repository: str | None, - builder_image: str, push: bool, ) -> None: """Build the Docker builder image used by other build stages. @@ -46,18 +40,7 @@ def builder_cmd( captain builder --no-cache captain builder --push """ - proj = resolve_project_dir(project_dir) - - cfg = Config( - project_dir=proj, - output_dir=proj / "out", - arch=arch, - flavor_id=flavor_id, - builder_registry=builder_registry, - builder_repository=builder_repository, - builder_image=builder_image, - builder_push=push, - ) + cfg = cli_ctx.make_config(builder_push=push) # 1. Build the image. obtain_builder(cfg) diff --git a/captain/cli/_iso.py b/captain/cli/_iso.py index 1081acda..2e72b44f 100644 --- a/captain/cli/_iso.py +++ b/captain/cli/_iso.py @@ -8,11 +8,10 @@ import captain.flavor from captain import artifacts -from captain.cli._main import cli, common_options, resolve_project_dir +from captain.cli._main import CliContext, cli from captain.cli._stages import ( _build_iso_stage, ) -from captain.config import Config log = logging.getLogger(__name__) @@ -21,7 +20,6 @@ "iso", short_help="Build ISO image only. Part of build.", ) -@common_options @click.option( "--iso-mode", envvar="ISO_MODE", @@ -38,29 +36,16 @@ default=False, help="Force ISO rebuild even if outputs already exist.", ) +@click.pass_obj def build_cmd( + cli_ctx: CliContext, *, - arch: str, - flavor_id: str, - project_dir: str | None, - builder_registry: str | None, - builder_repository: str | None, - builder_image: str, iso_mode: str, force_iso: bool, ) -> None: """Run the CaptainOS ISO build.""" - proj = resolve_project_dir(project_dir) - - cfg = Config( - project_dir=proj, - output_dir=proj / "out", - arch=arch, - flavor_id=flavor_id, - builder_registry=builder_registry, - builder_repository=builder_repository, - builder_image=builder_image, + cfg = cli_ctx.make_config( iso_mode=iso_mode, force_iso=force_iso, ) diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 0fe79ca8..352c7305 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -2,15 +2,15 @@ from __future__ import annotations -import functools import logging import sys +from dataclasses import dataclass from pathlib import Path from typing import Any import click -from captain.config import DEFAULT_FLAVOR_ID +from captain.config import DEFAULT_FLAVOR_ID, Config from captain.flavor import list_available_flavors from captain.util import detect_current_machine_arch @@ -18,63 +18,33 @@ # --------------------------------------------------------------------------- -# Shared option decorators +# CLI context object — shared state for all subcommands # --------------------------------------------------------------------------- -def common_options(fn: Any) -> Any: - """Decorate a click command with options shared by every subcommand.""" +@dataclass(slots=True) +class CliContext: + """Resolved common CLI options, passed to subcommands via ``@click.pass_obj``.""" - @click.option( - "--arch", - envvar="ARCH", - default=(detect_current_machine_arch()), - show_default=True, - type=click.Choice(["amd64", "arm64"], case_sensitive=False), - metavar="ARCH", - help="Target architecture (amd64, arm64).", - ) - @click.option( - "--flavor-id", - envvar="FLAVOR_ID", - default=DEFAULT_FLAVOR_ID, - show_default=True, - type=click.Choice(list_available_flavors(), case_sensitive=False), - help="Flavor (kernel/board config) to build.", - ) - @click.option( - "--project-dir", - envvar="CAPTAIN_PROJECT_DIR", - default=None, - type=click.Path(exists=True, file_okay=False, resolve_path=True), - help="Project root directory (auto-detected when omitted).", - ) - @click.option( - "--builder-registry", - envvar="REGISTRY", - default="ghcr.io", - show_default=True, - help="OCI registry hostname for the Docker builder image", - ) - @click.option( - "--builder-repository", - envvar="GITHUB_REPOSITORY", - default="tinkerbell/captain", - show_default=True, - help="Repository path (owner/name) for the Docker builder image", - ) - @click.option( - "--builder-image", - envvar="BUILDER_IMAGE", - default="captainos-builder", - show_default=True, - help="Local name/tag of Docker builder image name", - ) - @functools.wraps(fn) - def wrapper(**kwargs: Any) -> Any: - return fn(**kwargs) + project_dir: Path + arch: str + flavor_id: str + builder_registry: str | None + builder_repository: str | None + builder_image: str - return wrapper + def make_config(self, **overrides: Any) -> Config: + """Build a :class:`Config` from the common options plus per-command *overrides*.""" + return Config( + project_dir=self.project_dir, + output_dir=self.project_dir / "out", + arch=self.arch, + flavor_id=self.flavor_id, + builder_registry=self.builder_registry, + builder_repository=self.builder_repository, + builder_image=self.builder_image, + **overrides, + ) # --------------------------------------------------------------------------- @@ -116,11 +86,88 @@ def resolve_project_dir(project_dir: str | None) -> Path: ), ) @click.version_option(package_name="captain") +@click.option( + "-v", + "--verbose", + is_flag=True, + default=False, + envvar="CAPTAIN_VERBOSE", + help="Enable verbose (DEBUG-level) logging.", +) +@click.option( + "--arch", + envvar="ARCH", + default=(detect_current_machine_arch()), + show_default=True, + type=click.Choice(["amd64", "arm64"], case_sensitive=False), + metavar="ARCH", + help="Target architecture (amd64, arm64).", +) +@click.option( + "--flavor-id", + envvar="FLAVOR_ID", + default=DEFAULT_FLAVOR_ID, + show_default=True, + type=click.Choice(list_available_flavors(), case_sensitive=False), + help="Flavor (kernel/board config) to build.", +) +@click.option( + "--project-dir", + envvar="CAPTAIN_PROJECT_DIR", + default=None, + type=click.Path(exists=True, file_okay=False, resolve_path=True), + help="Project root directory (auto-detected when omitted).", +) +@click.option( + "--builder-registry", + envvar="REGISTRY", + default="ghcr.io", + show_default=True, + help="OCI registry hostname for the Docker builder image", +) +@click.option( + "--builder-repository", + envvar="GITHUB_REPOSITORY", + default="tinkerbell/captain", + show_default=True, + help="Repository path (owner/name) for the Docker builder image", +) +@click.option( + "--builder-image", + envvar="BUILDER_IMAGE", + default="captainos-builder", + show_default=True, + help="Local name/tag of Docker builder image name", +) @click.pass_context -def cli(ctx: click.Context) -> None: +def cli( + ctx: click.Context, + *, + verbose: bool, + arch: str, + flavor_id: str, + project_dir: str | None, + builder_registry: str | None, + builder_repository: str | None, + builder_image: str, +) -> None: """CaptainOS build system — click CLI.""" + # Configure log level based on --verbose. + logging.getLogger().setLevel(logging.DEBUG if verbose else logging.INFO) + if ctx.invoked_subcommand is None: click.echo(ctx.get_help()) + return + + # Build the shared context object for subcommands. + ctx.obj = CliContext( + project_dir=resolve_project_dir(project_dir), + arch=arch, + flavor_id=flavor_id, + builder_registry=builder_registry, + builder_repository=builder_repository, + builder_image=builder_image, + ) # --------------------------------------------------------------------------- diff --git a/captain/cli/_release_publish.py b/captain/cli/_release_publish.py index 51045530..1ac129ca 100644 --- a/captain/cli/_release_publish.py +++ b/captain/cli/_release_publish.py @@ -10,8 +10,7 @@ import captain.flavor from captain import oci -from captain.cli._main import cli, common_options, resolve_project_dir -from captain.config import Config +from captain.cli._main import CliContext, cli from captain.util import check_release_dependencies log = logging.getLogger(__name__) @@ -21,7 +20,6 @@ "release-publish", short_help="Publish build artifacts as a multi-arch OCI image.", ) -@common_options @click.option( "--release-mode", envvar="RELEASE_MODE", @@ -80,14 +78,10 @@ default=False, help="Publish even if the image already exists in the registry.", ) +@click.pass_obj def release_publish_cmd( + cli_ctx: CliContext, *, - arch: str, - flavor_id: str, - project_dir: str | None, - builder_registry: str | None, - builder_repository: str | None, - builder_image: str, release_mode: str, registry: str, repository: str, @@ -115,21 +109,11 @@ def release_publish_cmd( captain release-publish --registry ghcr.io --repository tinkerbell/captain """ - proj = resolve_project_dir(project_dir) - if target is None: - target = arch - - cfg = Config( - project_dir=proj, - output_dir=proj / "out", - arch=arch, - flavor_id=flavor_id, - builder_registry=builder_registry, - builder_repository=builder_repository, - builder_image=builder_image, - release_mode=release_mode, - ) + target = cli_ctx.arch + assert isinstance(target, str) + + cfg = cli_ctx.make_config(release_mode=release_mode) # --- skip mode -------------------------------------------------------- if cfg.release_mode == "skip": @@ -141,7 +125,7 @@ def release_publish_cmd( from captain import docker docker.obtain_builder(cfg) - sha = _resolve_git_sha(git_sha, proj) + sha = _resolve_git_sha(git_sha, cfg.project_dir) env_args: list[str] = [ "-e", @@ -187,8 +171,8 @@ def release_publish_cmd( log.error("Install them or set --release-mode=docker.") raise SystemExit(1) - sha = _resolve_git_sha(git_sha, proj) - tag = oci.compute_version_tag(proj, sha, exclude=version_exclude) + sha = _resolve_git_sha(git_sha, cfg.project_dir) + tag = oci.compute_version_tag(cfg.project_dir, sha, exclude=version_exclude) tag = f"{tag}-{cfg.flavor_id}" flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) diff --git a/captain/cli/_tools.py b/captain/cli/_tools.py index 187bd310..16e0fd35 100644 --- a/captain/cli/_tools.py +++ b/captain/cli/_tools.py @@ -6,9 +6,8 @@ import click -from captain.cli._main import cli, common_options, resolve_project_dir +from captain.cli._main import CliContext, cli from captain.cli._stages import _build_tools_stage -from captain.config import Config log = logging.getLogger(__name__) @@ -17,7 +16,6 @@ "tools", short_help="Download tools (containerd, runc, nerdctl, CNI).", ) -@common_options @click.option( "--tools-mode", envvar="TOOLS_MODE", @@ -34,14 +32,10 @@ default=False, help="Re-download tools even if outputs already exist.", ) +@click.pass_obj def tools_cmd( + cli_ctx: CliContext, *, - arch: str, - flavor_id: str, - project_dir: str | None, - builder_registry: str | None, - builder_repository: str | None, - builder_image: str, tools_mode: str, force_tools: bool, ) -> None: @@ -60,16 +54,7 @@ def tools_cmd( captain tools --force-tools """ - proj = resolve_project_dir(project_dir) - - cfg = Config( - project_dir=proj, - output_dir=proj / "out", - arch=arch, - flavor_id=flavor_id, - builder_registry=builder_registry, - builder_repository=builder_repository, - builder_image=builder_image, + cfg = cli_ctx.make_config( tools_mode=tools_mode, force_tools=force_tools, ) From e568d90f1f9b8bece7d53a9781beabd30f1ede3f Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 8 Apr 2026 13:34:11 +0200 Subject: [PATCH 065/143] captain: make Rich richer - show Locals - show all frames in traces - use Highlights for FORCE_COLOR=1 Signed-off-by: Ricardo Pardini --- captain/__init__.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/captain/__init__.py b/captain/__init__.py index 3b7ff4f2..9b50f462 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -17,15 +17,11 @@ # Rich console — writes to stderr so log output never pollutes piped stdout. # Install Rich traceback handler globally (once, at import time). if os.environ.get("FORCE_COLOR", "0") == "1": - console: Console = Console(stderr=True, color_system="standard", width=160, highlight=False) - _install_rich_traceback( - console=console, show_locals=False, width=160, suppress=[click], max_frames=2 - ) + console: Console = Console(stderr=True, color_system="standard", width=160) + _install_rich_traceback(console=console, show_locals=True, width=160, suppress=[click]) else: console: Console = Console(stderr=True) - _install_rich_traceback( - console=console, show_locals=False, width=None, suppress=[click], max_frames=2 - ) + _install_rich_traceback(console=console, show_locals=True, width=None, suppress=[click]) class _StageFormatter(logging.Formatter): @@ -45,8 +41,7 @@ def format(self, record: logging.LogRecord) -> str: show_path=True, markup=True, # interprets [braket]stuff[/bracket] in log messages, beware rich_tracebacks=True, - tracebacks_show_locals=False, - tracebacks_max_frames=2, # too many frames and we get confused + tracebacks_show_locals=True, tracebacks_suppress=[click], # don't wanna see click infra code in traces ) _handler.setFormatter(_StageFormatter("%(stage)s: %(message)s")) From 515bb24519478e29f79cfc20875dfcfe661f82e4 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 8 Apr 2026 13:37:03 +0200 Subject: [PATCH 066/143] captain: introduce Trogon(/Textual) to auto-create a TUI from Click - See https://github.com/Textualize/trogon and https://github.com/Textualize/textual - this cashes out on the Click / Rich investment Signed-off-by: Ricardo Pardini --- captain/cli/_main.py | 4 ++++ pyproject.toml | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 352c7305..664007ea 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -9,6 +9,7 @@ from typing import Any import click +from trogon import tui from captain.config import DEFAULT_FLAVOR_ID, Config from captain.flavor import list_available_flavors @@ -67,6 +68,8 @@ def resolve_project_dir(project_dir: str | None) -> Path: # --------------------------------------------------------------------------- # Top-level Click group # --------------------------------------------------------------------------- +# Important: decorator order matters here. The @tui() decorator must be outermost +# to properly wrap the entire CLI, including subcommands. CONTEXT_SETTINGS = dict( help_option_names=["-h", "--help"], @@ -74,6 +77,7 @@ def resolve_project_dir(project_dir: str | None) -> Path: ) +@tui() @click.group( context_settings=CONTEXT_SETTINGS, invoke_without_command=True, diff --git a/pyproject.toml b/pyproject.toml index fea7067d..3ecbea2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,8 @@ requires-python = ">=3.13" dependencies = [ "click>=8.1", "rich>=14.3.3", - "jinja2>=3.1.6" + "jinja2>=3.1.6", + "trogon>=0.6.0", ] [project.scripts] From 9ef0bc1e9fdee46601c56ea068e775aa0cb1f98e Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 8 Apr 2026 15:13:18 +0200 Subject: [PATCH 067/143] gha: use envvars, not --options - reduce suffering while I juggle Click Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a5647c1..ed55f81a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,9 @@ jobs: uses: astral-sh/setup-uv@v7 - name: Download tools - run: uv run captain tools --tools-mode=native + env: + TOOLS_MODE: native + run: uv run captain tools - name: Save tools cache if: github.ref == 'refs/heads/main' && steps.tools-cache.outputs.cache-hit != 'true' @@ -151,7 +153,7 @@ jobs: uses: astral-sh/setup-uv@v7 - name: Build initramfs - run: uv run captain build --arch=${{ matrix.arch }} # full build, incl initramfs and iso when appropriate + run: uv run captain build # full build, incl initramfs and iso when appropriate - name: Upload initramfs artifacts uses: actions/upload-artifact@v6 From c4fe100abce0529c1202ac0a7ab17dc96e0200e2 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 8 Apr 2026 15:16:04 +0200 Subject: [PATCH 068/143] captain: even Richer Rich - uv: split `Config::verbose_uv` from --verbose/Logging.DEBUG; use --quiet otherwise - use `shutil.get_terminal_size` to obtain and pass-down-Docker COLUMNS - nicer logging format, use a whale emoji for in-Docker logs - show Rich Table with Docker environment vars if --verbose - show Rich Panel and Rich Syntax for `util.run()` if --verbose Signed-off-by: Ricardo Pardini --- captain/__init__.py | 15 +++++++++------ captain/cli/_release_publish.py | 2 +- captain/config.py | 3 +++ captain/docker.py | 17 +++++++++++++++-- captain/util.py | 11 ++++++++++- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/captain/__init__.py b/captain/__init__.py index 9b50f462..c9620023 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -8,17 +8,21 @@ import logging import os +import shutil import click from rich.console import Console from rich.logging import RichHandler from rich.traceback import install as _install_rich_traceback +# Obtain terminal width +env_columns = shutil.get_terminal_size(fallback=(161, 24)).columns + # Rich console — writes to stderr so log output never pollutes piped stdout. # Install Rich traceback handler globally (once, at import time). if os.environ.get("FORCE_COLOR", "0") == "1": - console: Console = Console(stderr=True, color_system="standard", width=160) - _install_rich_traceback(console=console, show_locals=True, width=160, suppress=[click]) + console: Console = Console(stderr=True, color_system="standard", width=env_columns) + _install_rich_traceback(console=console, show_locals=True, width=env_columns, suppress=[click]) else: console: Console = Console(stderr=True) _install_rich_traceback(console=console, show_locals=True, width=None, suppress=[click]) @@ -26,11 +30,10 @@ class _StageFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: - name = record.name + name = record.name.replace("captain.", "") record.__dict__["stage"] = name if os.environ.get("CAPTAIN_IN_DOCKER", "") == "docker": - # Running on host: show stage names in blue for visual clarity. - record.__dict__["stage"] = f"[bold][blue]in-docker[/bold]: [/blue]{name}" + record.__dict__["stage"] = f"🐳 {name}" return super().format(record) @@ -44,6 +47,6 @@ def format(self, record: logging.LogRecord) -> str: tracebacks_show_locals=True, tracebacks_suppress=[click], # don't wanna see click infra code in traces ) -_handler.setFormatter(_StageFormatter("%(stage)s: %(message)s")) +_handler.setFormatter(_StageFormatter("[bold][cyan]%(stage)s[/cyan][/bold]: %(message)s")) logging.basicConfig(level="INFO", datefmt="[%X]", handlers=[_handler]) diff --git a/captain/cli/_release_publish.py b/captain/cli/_release_publish.py index 1ac129ca..daa4b662 100644 --- a/captain/cli/_release_publish.py +++ b/captain/cli/_release_publish.py @@ -155,7 +155,7 @@ def release_publish_cmd( "--entrypoint", "/usr/bin/uv", cfg.builder_image, - *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), + *(["--verbose"] if cfg.verbose_uv else ["--quiet"]), "run", *inner_cmd, ) diff --git a/captain/config.py b/captain/config.py index f0418d43..fee1ad3a 100644 --- a/captain/config.py +++ b/captain/config.py @@ -58,6 +58,9 @@ class Config: # Derived (set in __post_init__) arch_info: ArchInfo = field(init=False) + # Call uv (eg in Docker) using its own --verbose flag + verbose_uv: bool = False + def __post_init__(self) -> None: self.arch_info = get_arch_info(self.arch) self.arch = self.arch_info.arch # normalise aliases (x86_64 → amd64, etc.) diff --git a/captain/docker.py b/captain/docker.py index d7ec38cf..5ce7fd83 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -8,6 +8,9 @@ import platform from pathlib import Path +from rich.table import Table + +import captain from captain.config import Config from captain.util import detect_current_machine_arch, run @@ -159,7 +162,7 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "ISO_MODE": "native", "TERM": os.environ.get("TERM", "xterm-256color"), "FORCE_COLOR": "1", - "COLUMNS": os.environ.get("COLUMNS", "200"), + "COLUMNS": str(captain.env_columns), "GITHUB_ACTIONS": os.environ.get("GITHUB_ACTIONS", ""), # Forward host registry credentials so buildah/skopeo can authenticate. # The caller sets these env vars on the host (e.g. via docker login or @@ -178,6 +181,16 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "/work", ] + if log.isEnabledFor(logging.DEBUG): + table = Table( + title="Docker Environment Variables", show_header=True, header_style="bold cyan" + ) + table.add_column("Environment Variable", style="green") + table.add_column("Value", style="yellow") + for key, value in sorted(docker_envs.items()): + table.add_row(key, value) + captain.console.print(table) + for k, v in docker_envs.items(): docker_args += ["-e", f"{k}={v}"] @@ -208,7 +221,7 @@ def run_captain_in_builder(cfg: Config, *extra_args: str): cfg, cfg.builder_image, "/usr/bin/uv", - *(["--verbose"] if log.isEnabledFor(logging.DEBUG) else []), + *(["--verbose"] if cfg.verbose_uv else ["--quiet"]), "run", "captain", *extra_args, diff --git a/captain/util.py b/captain/util.py index 9e2330bf..5ed8aea2 100644 --- a/captain/util.py +++ b/captain/util.py @@ -13,6 +13,7 @@ from rich.panel import Panel from rich.rule import Rule +from rich.syntax import Syntax import captain @@ -79,7 +80,15 @@ def run( # If not capturing, and debugging, emit a Rich separator line, for visual clarity. if not capture and log.isEnabledFor(logging.DEBUG): - captain.console.print(Panel(f"Running command: {' '.join(cmd)}", style="green")) + syntax = Syntax( + " ".join(cmd), "bash", theme="monokai", word_wrap=True, background_color="default" + ) + panel = Panel( + syntax, + title="Executing shell command", + width=captain.console.width, + ) + captain.console.print(panel) captain.console.print(Rule(f"⮕ Starting subprocess: {cmd} ⮕", style="green")) proc = subprocess.run( From f7589f620e51577d843e2df848896eb28c5f9260 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 8 Apr 2026 18:31:55 +0200 Subject: [PATCH 069/143] gha: don't upload .iso as part of initramfs artifact - we've a separate artifact for .iso Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed55f81a..da145ccc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,8 +159,12 @@ jobs: uses: actions/upload-artifact@v6 with: name: initramfs-${{ matrix.FLAVOR_ID }}-${{ matrix.arch }} - path: out/ + # The full 'out/' directory contents, but not any .iso files (if any) since those are uploaded later + path: | + out/ + !out/**/*.iso retention-days: 1 + # do not upload any .iso files (exclude) # ------------------------------------------------------------------- # UEFI-bootable ISO - only for certain flavors (eg trixie-full) From d91f15d2852e299929c72660406c3c5117a8b6cc Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 9 Apr 2026 16:37:56 +0200 Subject: [PATCH 070/143] flavor/gha: add `trixie-rockchip64-vendor` and `trixie-armbian-rpi` flavors - also build them - `trixie-armbian-rpi` strays from standard naming as I think we might need to build our own kernel for RPi to achieve small-enough-for-eeprom-netboot eventually Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 2 ++ .../flavors/trixie_armbian_rpi/__init__.py | 22 +++++++++++++++++++ .../trixie_rockchip64_vendor/__init__.py | 22 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 captain/flavors/trixie_armbian_rpi/__init__.py create mode 100644 captain/flavors/trixie_rockchip64_vendor/__init__.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da145ccc..1ea951b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,9 @@ jobs: - { arch: amd64, output_arch: x86_64, iso: true, FLAVOR_ID: "trixie-full" } - { arch: arm64, output_arch: aarch64, iso: true, FLAVOR_ID: "trixie-full" } - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-rockchip64" } + - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-rockchip64-vendor" } - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-meson64" } + - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-armbian-rpi" } env: ARCH: ${{ matrix.arch }} MKOSI_MODE: docker diff --git a/captain/flavors/trixie_armbian_rpi/__init__.py b/captain/flavors/trixie_armbian_rpi/__init__.py new file mode 100644 index 00000000..a5c42572 --- /dev/null +++ b/captain/flavors/trixie_armbian_rpi/__init__.py @@ -0,0 +1,22 @@ +import logging +from dataclasses import dataclass + +from captain.flavor import BaseFlavor +from captain.flavors.common_armbian import ArmbianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieArmbianRPiFlavor() + + +@dataclass +class TrixieArmbianRPiFlavor(ArmbianCommonFlavor): + id = "trixie-armbian-rpi" + name = "Trixie for Raspberry Pi - Armbian bcm2711-current Kernel" + description = "Debian Trixie based on Armbian's rockchip64-edge kernel" + supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + + def kernel_packages(self) -> set[str]: + return {"linux-image-current-bcm2711"} diff --git a/captain/flavors/trixie_rockchip64_vendor/__init__.py b/captain/flavors/trixie_rockchip64_vendor/__init__.py new file mode 100644 index 00000000..0b9213f6 --- /dev/null +++ b/captain/flavors/trixie_rockchip64_vendor/__init__.py @@ -0,0 +1,22 @@ +import logging +from dataclasses import dataclass + +from captain.flavor import BaseFlavor +from captain.flavors.common_armbian import ArmbianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieRockchip64VendorFlavor() + + +@dataclass +class TrixieRockchip64VendorFlavor(ArmbianCommonFlavor): + id = "trixie-rockchip64-vendor" + name = "Trixie for Rockchip 64-bit ARM machines - Rockchip Vendor Kernel" + description = "Debian Trixie based with Armbian's rk35xx-vendor kernel" + supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + + def kernel_packages(self) -> set[str]: + return {"linux-image-vendor-rk35xx"} From 48afa8bdac7e049c9729b6bf91f6e0f5fb200779 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 12 Apr 2026 08:50:43 +0200 Subject: [PATCH 071/143] captain: tink-agent-setup Wants/After time-set.target - so time is set to something more reasonable, otherwise pulling fails with date-related TLS issues Signed-off-by: Ricardo Pardini --- .../mkosi.extra/etc/systemd/system/tink-agent-setup.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service index 69540149..d2193707 100644 --- a/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service @@ -1,7 +1,7 @@ [Unit] Description=Tink Agent Setup (pull image and extract binary) -After=containerd.service network-online.target systemd-resolved.service -Wants=network-online.target +After=containerd.service network-online.target systemd-resolved.service time-set.target +Wants=network-online.target time-set.target Requires=containerd.service # Only start when Tinkerbell parameters are on the kernel command line ConditionKernelCommandLine=|tink_worker_image From 6136807968d4a6f586912461640c49e6453d3b95 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 12 Apr 2026 09:34:03 +0200 Subject: [PATCH 072/143] captain: move systemd enablement/disablement from postinst to systemd preset files - doing this in postinst has no effect, as mkosi applies presets _after_ postinst - it _could_ be done in finalize, but that would un-do the presets applied by mkosi - so instead do it all in two preset files: - one for generic OS-related enablement and disablement (20-captainos-base) - one for tink-agent related stuff (10-captainos-tink) Signed-off-by: Ricardo Pardini --- .../system-preset/10-captainos-tink.preset | 6 ++ .../system-preset/20-captainos-base.preset | 19 +++++++ .../common_debian/mkosi.postinst.sh.j2 | 55 ++----------------- 3 files changed, 30 insertions(+), 50 deletions(-) create mode 100644 captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/10-captainos-tink.preset create mode 100644 captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/20-captainos-base.preset diff --git a/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/10-captainos-tink.preset b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/10-captainos-tink.preset new file mode 100644 index 00000000..3970f43b --- /dev/null +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/10-captainos-tink.preset @@ -0,0 +1,6 @@ +# CaptainOS preset: Tink-agent and related CaptainOS services +enable tink-agent.service +enable tink-agent-setup.service +enable captainos-static-network.service +enable captainos-banner.service + diff --git a/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/20-captainos-base.preset b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/20-captainos-base.preset new file mode 100644 index 00000000..80632451 --- /dev/null +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/20-captainos-base.preset @@ -0,0 +1,19 @@ +# CaptainOS preset: Base system services and cleanup +enable systemd-networkd.service +enable systemd-resolved.service +enable systemd-timesyncd.service +enable systemd-time-wait-sync.service +enable containerd.service +enable rsyslog.service +enable rsyslog-hostname-reload.path + +disable apt-daily.timer +disable apt-daily-upgrade.timer +disable e2scrub_all.timer +disable e2scrub_reap.service +disable fstrim.timer +disable logrotate.timer +disable man-db.timer +disable remote-fs.target +disable systemd-firstboot.service + diff --git a/captain/flavors/common_debian/mkosi.postinst.sh.j2 b/captain/flavors/common_debian/mkosi.postinst.sh.j2 index e090e19c..0dd8da04 100755 --- a/captain/flavors/common_debian/mkosi.postinst.sh.j2 +++ b/captain/flavors/common_debian/mkosi.postinst.sh.j2 @@ -16,59 +16,14 @@ mount || true log info "Generating fresh CA certificate bundle..." update-ca-certificates --fresh 2>/dev/null || true -# @TODO: this is the wrong place to do any systemd enablement/disablement/masking: mkosi will -# after this still apply systemd "presets" which will change the game completely. -# This is better done in a finalize.chroot script which would run after that. -# alternatively: adopt/introduce systemd presets, but that's a can of worms. - -log info "Listing all systemd units..." -systemctl list-unit-files --no-pager || true - -log info "CaptainOS post-install: configuring services..." -# Enable core services -declare -a units_to_enable=( - systemd-networkd systemd-resolved systemd-timesyncd containerd captainos-banner - systemd-time-wait-sync # wait for time sync - captainos-static-network tink-agent-setup tink-agent rsyslog rsyslog-hostname-reload.path -) -declare unit -for unit in "${units_to_enable[@]}"; do - log info "Enabling systemd unit ${unit}..." - if systemctl enable "${unit}"; then - log info " ${unit} enabled successfully." - else - log warning " Failed to enable ${unit}, but continuing anyway..." - fi -done +# Service enablement/disablement is handled via systemd preset files: +# - mkosi.extra/etc/systemd/system-preset/10-captainos-tink.preset (tink-agent and CaptainOS services) +# - mkosi.extra/etc/systemd/system-preset/20-captainos-base.preset (base system services and cleanup) +# mkosi applies presets automatically after post-install, so no manual +# systemctl enable/disable calls are needed here. # Root password is set via mkosi.conf RootPassword= setting -# Disable unnecessary systemd units to speed up boot -declare -a units_to_disable=( - apt-daily.timer - apt-daily-upgrade.timer - e2scrub_all.timer - e2scrub_reap.service - fstrim.timer - logrotate.timer - man-db.timer - remote-fs.target - systemd-firstboot.service -) -for unit in "${units_to_disable[@]}"; do - log info "Disabling and masking systemd unit ${unit}..." - if systemctl disable "${unit}"; then - log info " ${unit} disabled successfully." - else - log warning " Failed to disable ${unit}, but continuing anyway..." - fi - - if systemctl mask "${unit}"; then - log info " ${unit} masked successfully." - else - log warning " Failed to mask ${unit}, but continuing anyway..." - fi -done # Set default target to multi-user (no graphical) log info "Setting default systemd target to multi-user.target (no graphical)..." From cc52acd8584f0da2a19d8d76a38c265616241b89 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 12 Apr 2026 09:59:09 +0200 Subject: [PATCH 073/143] captain: loosen systemd-networkd-wait-online to accept any interface and 15s timeout - otherwise hangs and delays on machines with multiple interfaces Signed-off-by: Ricardo Pardini --- .../systemd-networkd-wait-online.service.d/override.conf | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf diff --git a/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf new file mode 100644 index 00000000..8437ae74 --- /dev/null +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf @@ -0,0 +1,5 @@ +[Service] +ExecStart= +ExecStart=/usr/lib/systemd/systemd-networkd-wait-online --any --timeout=15 +TimeoutStartSec=15 + From e98d3646420ed391dc6725ed2e98e7e51a0bfd7a Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 12 Apr 2026 11:32:34 +0200 Subject: [PATCH 074/143] captain: tink-agent-setup Wants/After time-sync.target - time-set.target is _not_ enough for machines without an RTC and working battery - time-sync.target requires timesyncd to be fully sync'ed - so time is set to something more reasonable, otherwise pulling fails with date-related TLS issues Signed-off-by: Ricardo Pardini --- .../mkosi.extra/etc/systemd/system/tink-agent-setup.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service index d2193707..d7242a36 100644 --- a/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service @@ -1,7 +1,7 @@ [Unit] Description=Tink Agent Setup (pull image and extract binary) -After=containerd.service network-online.target systemd-resolved.service time-set.target -Wants=network-online.target time-set.target +After=containerd.service network-online.target systemd-resolved.service time-sync.target +Wants=network-online.target time-sync.target Requires=containerd.service # Only start when Tinkerbell parameters are on the kernel command line ConditionKernelCommandLine=|tink_worker_image From 73ed248a4813f27f4b988db60176424deabef0ee Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 13 Apr 2026 19:12:37 +0200 Subject: [PATCH 075/143] kernel: bring back pre-Click kernel impl Signed-off-by: Ricardo Pardini --- captain/cli/_kernel.py | 62 +++ captain/cli/_main.py | 2 +- captain/cli/_stages.py | 58 +- captain/config.py | 38 +- captain/docker.py | 9 + captain/kernel.py | 289 ++++++++++ captain/util.py | 28 +- kernel.configs/6.18.y.amd64 | 1032 +++++++++++++++++++++++++++++++++++ kernel.configs/6.18.y.arm64 | 515 +++++++++++++++++ 9 files changed, 2028 insertions(+), 5 deletions(-) create mode 100644 captain/cli/_kernel.py create mode 100644 captain/kernel.py create mode 100644 kernel.configs/6.18.y.amd64 create mode 100644 kernel.configs/6.18.y.arm64 diff --git a/captain/cli/_kernel.py b/captain/cli/_kernel.py new file mode 100644 index 00000000..e1dc7951 --- /dev/null +++ b/captain/cli/_kernel.py @@ -0,0 +1,62 @@ +"""``captain tools`` — download tools (containerd, runc, nerdctl, CNI plugins).""" + +from __future__ import annotations + +import logging + +import click + +from captain import artifacts, config +from captain.cli._main import CliContext, cli +from captain.cli._stages import _build_kernel_stage + +log = logging.getLogger(__name__) + + +@cli.command( + "kernel", + short_help="Build Linux Kernel for CaptainOS trixie-slim flavor.", +) +@click.option( + "--kernel-mode", + envvar="KERNEL_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Kernel build stage execution mode (docker, native, skip).", +) +@click.option( + "--force-kernel", + envvar="FORCE_KERNEL", + is_flag=True, + default=False, + help="Build kernel even if outputs already exist.", +) +@click.option( + "--kernel-version", + envvar="KERNEL_VERSION", + default=config.DEFAULT_KERNEL_VERSION, + show_default=True, + help="Kernel version to build. Must match an official tarball.", +) +@click.pass_obj +def tools_cmd( + cli_ctx: CliContext, + *, + kernel_mode: str, + force_kernel: bool, + kernel_version: str, +) -> None: + log.warning("CLI kernel mode: %s", kernel_mode) + + cfg = cli_ctx.make_config( + force_kernel=force_kernel, + kernel_version=kernel_version, + kernel_mode=kernel_mode, + build_kernel=True, + ) + + _build_kernel_stage(cfg) + artifacts.collect_kernel(cfg) + log.info("Kernel build stage complete!") diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 664007ea..14b6168d 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -182,6 +182,6 @@ def cli( def main() -> None: """Console-script entry point.""" # Import subcommand modules to register them on the group. - from captain.cli import _build, _builder, _iso, _release_publish, _tools # noqa: F401 + from captain.cli import _build, _builder, _iso, _kernel, _release_publish, _tools # noqa: F401 cli() diff --git a/captain/cli/_stages.py b/captain/cli/_stages.py index ae94ade2..074295b9 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/_stages.py @@ -4,13 +4,67 @@ import logging -from captain import docker, iso, tools +from captain import docker, iso, kernel, tools from captain.config import Config -from captain.util import check_mkosi_dependencies, run +from captain.util import check_kernel_dependencies, check_mkosi_dependencies, run log = logging.getLogger(__name__) +def _build_kernel_stage(cfg: Config) -> None: + """Run the kernel build stage according to *cfg.kernel_mode*.""" + log.warning("Building kernel stage in mode %s", cfg.kernel_mode) + + # --- skip --------------------------------------------------------- + if cfg.kernel_mode == "skip": + log.info("KERNEL_MODE=skip — skipping kernel build") + return + + # --- idempotency -------------------------------------------------- + modules_dir = cfg.modules_output / "usr" / "lib" / "modules" + vmlinuz_dir = cfg.kernel_output + has_vmlinuz = vmlinuz_dir.is_dir() and any(vmlinuz_dir.glob("vmlinuz-*")) + log.debug("Kernel build idempotency check: modules_dir=%s exists=%s", modules_dir, + modules_dir.is_dir()) + log.debug("Kernel build idempotency check: vmlinuz_dir=%s exists=%s", vmlinuz_dir, + vmlinuz_dir.is_dir()) + log.debug( + "Checking kernel build idempotency: modules_dir=%s, has_vmlinuz=%s", + modules_dir, + has_vmlinuz, + ) + + if modules_dir.is_dir() and has_vmlinuz and not cfg.force_kernel: + log.info("Kernel already built (use --force-kernel to rebuild)") + return + + if modules_dir.is_dir() and not has_vmlinuz: + log.warning("Modules exist but vmlinuz is missing — rebuilding kernel") + + # --- native ------------------------------------------------------- + if cfg.kernel_mode == "native": + missing = check_kernel_dependencies(cfg.arch) + if missing: + log.error("Missing kernel build tools: %s", ", ".join(missing)) + log.error("Install them or set --kernel-mode=docker.") + raise SystemExit(1) + log.info("Building kernel (native)...") + kernel.build(cfg) + return + + # --- docker ------------------------------------------------------- + docker.obtain_builder(cfg) + log.info("Building kernel (docker relaunch)...") + docker.run_captain_in_builder(cfg, "kernel") + docker.fix_docker_ownership( + cfg, + [ + f"/work/mkosi.output/kernel/{cfg.kernel_version}/{cfg.arch}", + "/work/out", + ], + ) + + def _build_tools_stage(cfg: Config) -> None: """Run the tools download stage according to *cfg.tools_mode*.""" diff --git a/captain/config.py b/captain/config.py index fee1ad3a..ddf39eca 100644 --- a/captain/config.py +++ b/captain/config.py @@ -11,13 +11,19 @@ log = logging.getLogger(__name__) -# Valid values for ISO_MODE and MKOSI_MODE. +# Valid values for KERNEL_MODE and MKOSI_MODE. VALID_MODES = ("docker", "native", "skip") # The single source of truth for the default flavor. # Override at runtime via --flavor-id or FLAVOR_ID env var. DEFAULT_FLAVOR_ID = "trixie-full" +# Fallback for the default kernel version. +# The latest version can be obtained and passed as --kernel-version/KERNEL_VERSION +# via <@TODO cmd for listing from cached releases.json +# Override at runtime via --kernel-version or KERNEL_VERSION env var. +DEFAULT_KERNEL_VERSION = "6.18.22" + @dataclass(slots=True) class Config: @@ -31,6 +37,13 @@ class Config: arch: str = "amd64" flavor_id: str = DEFAULT_FLAVOR_ID + # For kernel build + build_kernel: bool = False + kernel_version: str = DEFAULT_KERNEL_VERSION + kernel_config: str | None = None + kernel_src: str | None = None + force_kernel: bool = False + # Docker builder_registry: str | None = None builder_repository: str | None = None @@ -42,6 +55,7 @@ class Config: mkosi_mode: str = "docker" iso_mode: str = "docker" release_mode: str = "docker" + kernel_mode: str = "docker" # Force flags force_tools: bool = False @@ -69,6 +83,7 @@ def __post_init__(self) -> None: ("MKOSI_MODE", self.mkosi_mode), ("ISO_MODE", self.iso_mode), ("RELEASE_MODE", self.release_mode), + ("KERNEL_MODE", self.kernel_mode), ): if value not in VALID_MODES: log.error("%s=%r is invalid. Valid values: %s", name, value, ", ".join(VALID_MODES)) @@ -84,6 +99,27 @@ def tools_output(self) -> Path: """ return self.project_dir / "mkosi.output" / "tools" / self.arch + @property + def kernel_output(self) -> Path: + """Per-version, per-arch directory for all kernel build artifacts. + + Contains the vmlinuz image (loaded separately by iPXE) and + a ``modules/`` subtree that mirrors a root filesystem layout + (``usr/lib/modules/{kver}/``) so it can be passed directly + as an ``--extra-tree=`` to mkosi. + """ + return self.project_dir / "mkosi.output" / "kernel" / self.kernel_version / self.arch + + @property + def modules_output(self) -> Path: + """Per-version, per-arch root for kernel modules. + + Returns ``kernel/{version}/{arch}/modules`` which contains a + merged-usr tree (``usr/lib/modules/{kver}/``) suitable for + passing as ``--extra-tree=`` to mkosi. + """ + return self.kernel_output / "modules" + @property def mkosi_output(self) -> Path: return self.project_dir / "mkosi.output" diff --git a/captain/docker.py b/captain/docker.py index 5ce7fd83..254ac57b 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -12,6 +12,7 @@ import captain from captain.config import Config +from captain.kernel import KERNEL_BUILD_BASE_PATH from captain.util import detect_current_machine_arch, run log = logging.getLogger(__name__) @@ -152,10 +153,13 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "CAPTAIN_IN_DOCKER": "docker", "ARCH": cfg.arch, "FLAVOR_ID": cfg.flavor_id, + "KERNEL_VERSION": cfg.kernel_version, "FORCE_TOOLS": str(int(cfg.force_tools)), "FORCE_ISO": str(int(cfg.force_iso)), + "FORCE_KERNEL": f"{int(cfg.force_kernel)!s}", "BUILDAH_ISOLATION": "chroot", "BUILDAH_INSECURE": os.environ.get("BUILDAH_INSECURE", ""), + "KERNEL_MODE": "native", "RELEASE_MODE": "native", "TOOLS_MODE": "native", "MKOSI_MODE": "native", @@ -194,6 +198,7 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: for k, v in docker_envs.items(): docker_args += ["-e", f"{k}={v}"] + docker_args += ["-v", f"{cfg.project_dir}/kernel.configs:/work/kernel.configs"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.output:/work/mkosi.output"] docker_args += ["-v", f"{cfg.project_dir}/out:/work/out"] @@ -210,6 +215,10 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: docker_args += ["-v", f"{cfg.project_dir}/build.py:/work/build.py"] docker_args += ["--mount", "type=volume,source=captain-cache-packages,target=/cache/packages"] + docker_args += [ + "--mount", + f"type=volume,source=captain-kernel-build,target={KERNEL_BUILD_BASE_PATH}", + ] docker_args.extend(extra_args) run(docker_args) diff --git a/captain/kernel.py b/captain/kernel.py new file mode 100644 index 00000000..0b307aa8 --- /dev/null +++ b/captain/kernel.py @@ -0,0 +1,289 @@ +"""Kernel download, configuration, compilation, and installation. + +Heavy lifting (make, strip) is still done via subprocess — only the +orchestration is in Python. Called directly by ``cli._build_kernel_stage`` +in both native and Docker modes (inside the container ``build.py kernel`` +re-enters via the CLI with all modes forced to native). +""" + +from __future__ import annotations + +import logging +import os +import re +import shutil +import tarfile +import urllib.error +import urllib.request +from pathlib import Path + +from rich.progress import ( + BarColumn, + DownloadColumn, + Progress, + TextColumn, + TimeRemainingColumn, + TransferSpeedColumn, +) + +from captain import console +from captain.config import Config +from captain.util import ensure_dir, run, safe_extractall + +KERNEL_BUILD_BASE_PATH = "/var/tmp/kernel-build" + +log = logging.getLogger(__name__) + +_DOWNLOAD_TIMEOUT = 60 # seconds + + +def _download_with_progress(url: str, filename: Path) -> None: + """Download *url* to *filename* with a Rich progress bar.""" + req = urllib.request.Request(url) + with urllib.request.urlopen(req, timeout=_DOWNLOAD_TIMEOUT) as resp: + total = int(resp.headers.get("Content-Length", 0)) or None + with Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(), + DownloadColumn(), + TransferSpeedColumn(), + TimeRemainingColumn(), + console=console, + ) as progress: + task = progress.add_task(" Downloading", total=total) + with open(filename, "wb") as out: + while True: + buf = resp.read(8192) + if not buf: + break + out.write(buf) + progress.update(task, advance=len(buf)) + + +def download_kernel(version: str, dest_dir: Path) -> Path: + """Download and extract a kernel tarball. Returns the source directory.""" + src_dir = dest_dir / f"linux-{version}" + if src_dir.is_dir(): + log.info("Using cached kernel source at %s", src_dir) + return src_dir + + major = version.split(".")[0] + url = f"https://cdn.kernel.org/pub/linux/kernel/v{major}.x/linux-{version}.tar.xz" + tarball = dest_dir / f"linux-{version}.tar.xz" + + log.info("Downloading kernel %s...", version) + log.info(" URL: %s", url) + ensure_dir(dest_dir) + try: + _download_with_progress(url, tarball) + except urllib.error.HTTPError as exc: + log.error("Download failed: %s — %s", exc, url) + raise SystemExit(1) from None + except urllib.error.URLError as exc: + log.error("Download failed: %s — %s", exc.reason, url) + raise SystemExit(1) from None + + log.info("Extracting kernel source...") + with tarfile.open(tarball, "r:xz") as tf: + safe_extractall(tf, path=dest_dir) + tarball.unlink() + + return src_dir + + +def _kernel_branch(version: str) -> str: + """Derive the stable branch prefix from a full kernel version.""" + parts = version.split(".") + if len(parts) < 2: + log.error("Invalid kernel version format: %s", version) + raise SystemExit(1) + return f"{parts[0]}.{parts[1]}.y" + + +def _find_defconfig(cfg: Config) -> Path: + """Locate the defconfig for the current kernel version and architecture.""" + if cfg.kernel_config: + explicit = Path(cfg.kernel_config) + if not explicit.is_absolute(): + explicit = cfg.project_dir / explicit + if explicit.is_file(): + return explicit + log.error("Kernel config not found: %s", explicit) + raise SystemExit(1) + + ai = cfg.arch_info + branch = _kernel_branch(cfg.kernel_version) + defconfig = cfg.project_dir / "kernel.configs" / f"{branch}.{ai.arch}" + if defconfig.is_file(): + return defconfig + + configs_dir = cfg.project_dir / "kernel.configs" + available = sorted( + { + p.name.rsplit(".", 1)[0] + for p in configs_dir.glob(f"*.{ai.arch}") + if not p.name.startswith(".") + } + ) + avail_str = ", ".join(available) if available else "(none)" + log.error( + "No kernel config found for %s on %s\n Expected: %s\n Available branches for %s: %s", + branch, + ai.arch, + defconfig, + ai.arch, + avail_str, + ) + raise SystemExit(1) + + +def configure_kernel(cfg: Config, src_dir: Path) -> None: + """Apply defconfig and run olddefconfig.""" + ai = cfg.arch_info + defconfig = _find_defconfig(cfg) + + make_env = {"ARCH": ai.kernel_arch} + if ai.cross_compile: + make_env["CROSS_COMPILE"] = ai.cross_compile + + log.info("Using defconfig: %s", defconfig) + shutil.copy2(defconfig, src_dir / ".config") + run(["make", "olddefconfig"], env=make_env, cwd=src_dir) + branch = _kernel_branch(cfg.kernel_version) + resolved = cfg.project_dir / "kernel.configs" / f".config.resolved.{branch}.{ai.arch}" + shutil.copy2(src_dir / ".config", resolved) + log.info("Resolved config saved to kernel.configs/.config.resolved.%s.%s", branch, ai.arch) + + if ai.kernel_arch == "x86_64": + log.info("Increasing COMMAND_LINE_SIZE to 4096 (x86_64)...") + setup_h = src_dir / "arch" / "x86" / "include" / "asm" / "setup.h" + text = setup_h.read_text() + new_text = re.sub( + r"#define COMMAND_LINE_SIZE\s+2048", + "#define COMMAND_LINE_SIZE 4096", + text, + ) + if new_text == text: + log.warning( + "COMMAND_LINE_SIZE patch did not match — the kernel default may have changed" + ) + setup_h.write_text(new_text) + + +def build_kernel(cfg: Config, src_dir: Path) -> str: + """Compile the kernel image and modules. Returns the built kernel version string.""" + ai = cfg.arch_info + nproc = os.cpu_count() or 1 + + make_env = {"ARCH": ai.kernel_arch} + if ai.cross_compile: + make_env["CROSS_COMPILE"] = ai.cross_compile + + log.info("Building kernel with %d jobs...", nproc) + run( + ["make", f"-j{nproc}", ai.image_target, "modules"], + env=make_env, + cwd=src_dir, + ) + + result = run( + ["make", "-s", "kernelrelease"], + env={"ARCH": ai.kernel_arch}, + capture=True, + cwd=src_dir, + ) + built_kver = result.stdout.strip() + log.info("Built kernel version: %s", built_kver) + return built_kver + + +def install_kernel(cfg: Config, src_dir: Path, built_kver: str) -> None: + """Install modules and vmlinuz into mkosi.output/kernel/{version}/{arch}/.""" + ai = cfg.arch_info + modules_root = cfg.modules_output + + make_env = {"ARCH": ai.kernel_arch} + if ai.cross_compile: + make_env["CROSS_COMPILE"] = ai.cross_compile + + log.info("Installing modules...") + run( + ["make", f"INSTALL_MOD_PATH={modules_root}", "modules_install"], + env=make_env, + cwd=src_dir, + ) + + log.info("Stripping debug symbols from modules...") + strip_cmd = f"{ai.strip_prefix}strip" + for ko in modules_root.rglob("*.ko"): + run([strip_cmd, "--strip-unneeded", str(ko)], check=False) + + log.info("Compressing kernel modules with zstd...") + for ko in modules_root.rglob("*.ko"): + run(["zstd", "--rm", "-q", "-19", str(ko)], check=True) + + mod_base = modules_root / "lib" / "modules" / built_kver + (mod_base / "build").unlink(missing_ok=True) + (mod_base / "source").unlink(missing_ok=True) + + usr_moddir = ensure_dir(modules_root / "usr" / "lib" / "modules" / built_kver) + if mod_base.is_dir(): + for item in mod_base.iterdir(): + dest = usr_moddir / item.name + if dest.exists(): + if dest.is_dir(): + shutil.rmtree(dest) + else: + dest.unlink() + shutil.move(str(item), str(dest)) + shutil.rmtree(modules_root / "lib", ignore_errors=True) + + log.info("Running depmod for compressed modules...") + run( + ["depmod", "-a", "-b", str(modules_root / "usr"), built_kver], + check=True, + ) + + kernel_image = src_dir / ai.kernel_image_path + vmlinuz_dir = ensure_dir(cfg.kernel_output) + + for old in vmlinuz_dir.glob("vmlinuz-*"): + old.unlink(missing_ok=True) + + shutil.copy2(kernel_image, vmlinuz_dir / f"vmlinuz-{built_kver}") + + log.info("Kernel build complete:") + vmlinuz = vmlinuz_dir / f"vmlinuz-{built_kver}" + vmlinuz_size = vmlinuz.stat().st_size / (1024 * 1024) + log.info(" Image: %s (%.1fM)", vmlinuz, vmlinuz_size) + log.info(" Modules: %s/", usr_moddir) + log.info(" Version: %s", built_kver) + log.info(" Output: %s", cfg.kernel_output) + + +def build(cfg: Config) -> None: + """Full kernel build pipeline — download, configure, build, install.""" + if cfg.kernel_output.exists(): + shutil.rmtree(cfg.kernel_output) + ensure_dir(cfg.kernel_output) + + log.info("Preparing kernel source...") + build_dir = Path(KERNEL_BUILD_BASE_PATH) + + if cfg.kernel_src and Path(cfg.kernel_src).is_dir(): + log.info("Using provided kernel source at %s", cfg.kernel_src) + src_dir = Path(cfg.kernel_src) + else: + src_dir = download_kernel(cfg.kernel_version, build_dir) + + log.debug("Kernel source directory: %s", src_dir) + + log.info("Configuring kernel...") + configure_kernel(cfg, src_dir) + + log.info("Building kernel...") + built_kver = build_kernel(cfg, src_dir) + log.debug("Built kernel version: %s", built_kver) + + log.info("Installing kernel...") + install_kernel(cfg, src_dir, built_kver) diff --git a/captain/util.py b/captain/util.py index 5ed8aea2..5edf5966 100644 --- a/captain/util.py +++ b/captain/util.py @@ -26,9 +26,14 @@ class ArchInfo: arch: str # canonical name: amd64 | arm64 output_arch: str # user-facing name in artifact filenames: x86_64 | aarch64 + kernel_arch: str # kernel ARCH value + cross_compile: str # CROSS_COMPILE prefix (empty for native) + image_target: str # kernel image make target + kernel_image_path: str # relative path to built kernel image dl_arch: str # architecture name in download URLs mkosi_arch: str # mkosi --architecture value qemu_binary: str # QEMU system emulator binary + strip_prefix: str # prefix for strip command def get_arch_info(arch: str) -> ArchInfo: @@ -38,17 +43,27 @@ def get_arch_info(arch: str) -> ArchInfo: return ArchInfo( arch="amd64", output_arch="x86_64", + kernel_arch="x86_64", + cross_compile="", + image_target="bzImage", + kernel_image_path="arch/x86/boot/bzImage", dl_arch="amd64", mkosi_arch="x86-64", qemu_binary="qemu-system-x86_64", + strip_prefix="", ) case "arm64" | "aarch64": return ArchInfo( arch="arm64", output_arch="aarch64", + kernel_arch="arm64", + cross_compile="aarch64-linux-gnu-", + image_target="Image", + kernel_image_path="arch/arm64/boot/Image", dl_arch="arm64", mkosi_arch="arm64", qemu_binary="qemu-system-aarch64", + strip_prefix="aarch64-linux-gnu-", ) case _: log.error("Unsupported architecture: %s", arch) @@ -162,6 +177,17 @@ def _missing(cmds: list[str]) -> list[str]: return [cmd for cmd in cmds if _shutil.which(cmd) is None] +def check_kernel_dependencies(arch: str) -> list[str]: + """Check host tools required for a native kernel build. + + Returns a list of missing command names (empty if all found). + """ + required = ["make", "gcc", "flex", "bison", "bc", "rsync", "strip", "zstd", "depmod"] + if arch in ("arm64", "aarch64"): + required += ["aarch64-linux-gnu-gcc", "aarch64-linux-gnu-strip"] + return _missing(required) + + def check_mkosi_dependencies() -> list[str]: """Check host tools required for a native mkosi image build. @@ -192,4 +218,4 @@ def check_dependencies(arch: str) -> list[str]: Returns a list of missing command names (empty if all found). """ - return check_mkosi_dependencies() + return check_kernel_dependencies(arch) + check_mkosi_dependencies() diff --git a/kernel.configs/6.18.y.amd64 b/kernel.configs/6.18.y.amd64 new file mode 100644 index 00000000..c6d243cc --- /dev/null +++ b/kernel.configs/6.18.y.amd64 @@ -0,0 +1,1032 @@ +# CaptainOS kernel defconfig for x86_64 +# Comprehensive config for bare-metal provisioning with container/network support. +# Derived from HookOS generic-6.6.y-x86_64 proven config, adapted for kernel 6.18. +# +# Generate a full .config from this: +# cp 6.18.y.amd64 .config && make olddefconfig + +# --- Architecture --- +CONFIG_64BIT=y +CONFIG_X86_64=y +CONFIG_SMP=y +CONFIG_NR_CPUS=128 +# CONFIG_X86_EXTENDED_PLATFORM is not set +CONFIG_X86_INTEL_LPSS=y +CONFIG_HYPERVISOR_GUEST=y +CONFIG_PARAVIRT_SPINLOCKS=y +CONFIG_XEN=y +CONFIG_XEN_PVH=y +CONFIG_X86_REROUTE_FOR_BROKEN_BOOT_IRQS=y +# CONFIG_X86_MCE is not set +CONFIG_X86_MSR=y +CONFIG_X86_CPUID=y +# CONFIG_X86_5LEVEL is not set +CONFIG_HZ_1000=y +CONFIG_PHYSICAL_ALIGN=0x1000000 +CONFIG_LEGACY_VSYSCALL_NONE=y +# CONFIG_MODIFY_LDT_SYSCALL is not set +CONFIG_IA32_EMULATION=y + +# --- General --- +CONFIG_LOCALVERSION="-captainos" +# CONFIG_LOCALVERSION_AUTO is not set +CONFIG_DEFAULT_HOSTNAME="captainos" +CONFIG_SYSVIPC=y +CONFIG_POSIX_MQUEUE=y +CONFIG_AUDIT=y +CONFIG_NO_HZ_IDLE=y +CONFIG_HIGH_RES_TIMERS=y +CONFIG_PREEMPT_VOLUNTARY=y +CONFIG_BSD_PROCESS_ACCT=y +CONFIG_BSD_PROCESS_ACCT_V3=y +CONFIG_IKCONFIG=y +CONFIG_IKCONFIG_PROC=y +CONFIG_CC_OPTIMIZE_FOR_SIZE=y +CONFIG_EXPERT=y +CONFIG_KEXEC=y +CONFIG_KEXEC_FILE=y +CONFIG_SCHED_AUTOGROUP=y +CONFIG_CHECKPOINT_RESTORE=y +CONFIG_KPROBES=y +CONFIG_JUMP_LABEL=y +CONFIG_BINFMT_MISC=y + +# --- Cgroups --- +CONFIG_CGROUPS=y +CONFIG_CGROUP_FREEZER=y +CONFIG_CGROUP_PIDS=y +CONFIG_CGROUP_RDMA=y +CONFIG_CGROUP_DEVICE=y +CONFIG_CGROUP_CPUACCT=y +CONFIG_CGROUP_PERF=y +CONFIG_CGROUP_BPF=y +CONFIG_CGROUP_MISC=y +CONFIG_CGROUP_HUGETLB=y +CONFIG_CGROUP_NET_PRIO=y +CONFIG_MEMCG=y +CONFIG_BLK_CGROUP=y +CONFIG_CFS_BANDWIDTH=y +CONFIG_RT_GROUP_SCHED=y +CONFIG_CGROUP_SCHED=y +CONFIG_CPUSETS=y + +# --- Namespaces --- +CONFIG_NAMESPACES=y +CONFIG_USER_NS=y +CONFIG_PID_NS=y +CONFIG_NET_NS=y +CONFIG_UTS_NS=y +CONFIG_IPC_NS=y + +# --- Init / Initramfs --- +CONFIG_BLK_DEV_INITRD=y +CONFIG_INITRAMFS_SOURCE="" +# CONFIG_RD_BZIP2 is not set +# CONFIG_RD_LZMA is not set +CONFIG_RD_GZIP=y +CONFIG_RD_XZ=y +# CONFIG_RD_LZO is not set +# CONFIG_RD_LZ4 is not set +CONFIG_RD_ZSTD=y + +# --- Memory management --- +CONFIG_SLAB_FREELIST_RANDOM=y +# CONFIG_COMPAT_BRK is not set +CONFIG_MEMORY_HOTPLUG=y +CONFIG_MEMORY_HOTREMOVE=y +CONFIG_KSM=y +CONFIG_DEFAULT_MMAP_MIN_ADDR=65536 +CONFIG_TRANSPARENT_HUGEPAGE=y +CONFIG_ZONE_DEVICE=y +CONFIG_HUGETLBFS=y + +# --- Power / Suspend --- +# CONFIG_SUSPEND is not set + +# --- ACPI --- +CONFIG_ACPI=y +# CONFIG_ACPI_REV_OVERRIDE_POSSIBLE is not set +CONFIG_ACPI_DOCK=y +CONFIG_ACPI_IPMI=y +CONFIG_ACPI_PROCESSOR_AGGREGATOR=y +CONFIG_ACPI_SBS=y +CONFIG_ACPI_NFIT=y +CONFIG_ACPI_APEI=y +CONFIG_ACPI_APEI_GHES=y + +# --- CPU frequency --- +CONFIG_CPU_FREQ_STAT=y +CONFIG_CPU_FREQ_GOV_POWERSAVE=y +CONFIG_CPU_FREQ_GOV_USERSPACE=y +CONFIG_CPU_FREQ_GOV_ONDEMAND=y +CONFIG_CPU_FREQ_GOV_CONSERVATIVE=y +CONFIG_X86_PCC_CPUFREQ=y +CONFIG_X86_ACPI_CPUFREQ=y +CONFIG_X86_POWERNOW_K8=y +CONFIG_X86_P4_CLOCKMOD=y +CONFIG_CPU_IDLE_GOV_LADDER=y +CONFIG_INTEL_IDLE=y + +# --- KVM --- +CONFIG_KVM=m +CONFIG_KVM_INTEL=m +CONFIG_KVM_AMD=m + +# --- Networking --- +CONFIG_NET=y +CONFIG_PACKET=y +CONFIG_PACKET_DIAG=y +CONFIG_UNIX=y +CONFIG_UNIX_DIAG=y +CONFIG_XFRM_USER=m +CONFIG_XFRM_SUB_POLICY=y +CONFIG_XFRM_STATISTICS=y +CONFIG_NET_KEY=m +CONFIG_NET_KEY_MIGRATE=y +CONFIG_XDP_SOCKETS=y +CONFIG_INET=y +CONFIG_IP_MULTICAST=y +CONFIG_IP_ADVANCED_ROUTER=y +CONFIG_IP_FIB_TRIE_STATS=y +CONFIG_IP_MULTIPLE_TABLES=y +CONFIG_IP_ROUTE_MULTIPATH=y +CONFIG_IP_ROUTE_VERBOSE=y +CONFIG_IP_PNP=y +CONFIG_IP_PNP_DHCP=y +CONFIG_IP_PNP_BOOTP=y +CONFIG_NET_IPIP=y +CONFIG_NET_IPGRE_DEMUX=y +CONFIG_NET_IPGRE=m +CONFIG_NET_IPGRE_BROADCAST=y +CONFIG_IP_MROUTE=y +CONFIG_IP_MROUTE_MULTIPLE_TABLES=y +CONFIG_IP_PIMSM_V1=y +CONFIG_IP_PIMSM_V2=y +CONFIG_NET_IPVTI=m +CONFIG_NET_FOU_IP_TUNNELS=y +CONFIG_INET_AH=m +CONFIG_INET_ESP=m +CONFIG_INET_IPCOMP=m +CONFIG_INET_UDP_DIAG=y +CONFIG_TCP_MD5SIG=y +CONFIG_SYN_COOKIES=y +CONFIG_IPV6=y +CONFIG_IPV6_ROUTER_PREF=y +CONFIG_INET6_AH=m +CONFIG_INET6_ESP=m +CONFIG_INET6_IPCOMP=m +CONFIG_IPV6_MIP6=m +CONFIG_IPV6_ILA=m +CONFIG_IPV6_VTI=m +CONFIG_IPV6_SIT=m +CONFIG_IPV6_SIT_6RD=y +CONFIG_IPV6_GRE=m +CONFIG_IPV6_MULTIPLE_TABLES=y +CONFIG_IPV6_SUBTREES=y +CONFIG_NETLABEL=y +CONFIG_NETWORK_SECMARK=y +CONFIG_BRIDGE=y +CONFIG_BRIDGE_VLAN_FILTERING=y +CONFIG_VLAN_8021Q=y +CONFIG_BONDING=m +CONFIG_TUN=y +CONFIG_VETH=y +CONFIG_MACVLAN=y +CONFIG_MACVTAP=y +CONFIG_IPVLAN=y +CONFIG_VXLAN=y +CONFIG_GENEVE=m +CONFIG_NETCONSOLE=y +CONFIG_TAP=y +CONFIG_DUMMY=m +CONFIG_NLMON=y +CONFIG_IP_SCTP=m +CONFIG_L2TP=m +CONFIG_NET_TEAM=m +CONFIG_NET_TEAM_MODE_BROADCAST=m +CONFIG_NET_TEAM_MODE_ROUNDROBIN=m +CONFIG_NET_TEAM_MODE_RANDOM=m +CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=m +CONFIG_NET_TEAM_MODE_LOADBALANCE=m +CONFIG_OPENVSWITCH=m +CONFIG_VSOCKETS=m +CONFIG_VIRTIO_VSOCKETS=m +CONFIG_HYPERV_VSOCKETS=m +CONFIG_NETLINK_DIAG=y +CONFIG_MPLS_ROUTING=m +CONFIG_MPLS_IPTUNNEL=m +CONFIG_NET_SWITCHDEV=y +CONFIG_NET_9P=y +CONFIG_NET_9P_VIRTIO=y + +# --- Traffic control --- +CONFIG_NET_SCHED=y +CONFIG_NET_SCH_HTB=m +CONFIG_NET_SCH_HFSC=m +CONFIG_NET_SCH_PRIO=m +CONFIG_NET_SCH_MULTIQ=m +CONFIG_NET_SCH_RED=m +CONFIG_NET_SCH_SFB=m +CONFIG_NET_SCH_SFQ=m +CONFIG_NET_SCH_TEQL=m +CONFIG_NET_SCH_TBF=m +CONFIG_NET_SCH_GRED=m +CONFIG_NET_SCH_NETEM=m +CONFIG_NET_SCH_DRR=m +CONFIG_NET_SCH_MQPRIO=m +CONFIG_NET_SCH_CHOKE=m +CONFIG_NET_SCH_QFQ=m +CONFIG_NET_SCH_INGRESS=m +CONFIG_NET_CLS_BASIC=y +CONFIG_NET_CLS_ROUTE4=y +CONFIG_NET_CLS_FW=y +CONFIG_NET_CLS_U32=y +CONFIG_CLS_U32_PERF=y +CONFIG_CLS_U32_MARK=y +CONFIG_NET_CLS_FLOW=y +CONFIG_NET_CLS_CGROUP=y +CONFIG_NET_CLS_BPF=y +CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_CMP=y +CONFIG_NET_EMATCH_NBYTE=y +CONFIG_NET_EMATCH_U32=y +CONFIG_NET_EMATCH_META=y +CONFIG_NET_EMATCH_TEXT=y +CONFIG_NET_EMATCH_IPSET=y +CONFIG_NET_CLS_ACT=y +CONFIG_NET_ACT_POLICE=y +CONFIG_NET_ACT_GACT=y +CONFIG_GACT_PROB=y +CONFIG_NET_ACT_MIRRED=y +CONFIG_NET_ACT_IPT=y +CONFIG_NET_ACT_NAT=y +CONFIG_NET_ACT_PEDIT=y +CONFIG_NET_ACT_SIMP=y +CONFIG_NET_ACT_SKBEDIT=y +CONFIG_NET_ACT_CSUM=y +CONFIG_NET_ACT_BPF=y + +# --- Netfilter --- +CONFIG_NETFILTER=y +CONFIG_NETFILTER_ADVANCED=y +CONFIG_BRIDGE_NETFILTER=y +CONFIG_NF_CONNTRACK=y +CONFIG_NF_CONNTRACK_ZONES=y +CONFIG_NF_CONNTRACK_PROCFS=y +CONFIG_NF_CONNTRACK_EVENTS=y +CONFIG_NF_CONNTRACK_TIMEOUT=y +CONFIG_NF_CONNTRACK_TIMESTAMP=y +CONFIG_NF_CONNTRACK_AMANDA=y +CONFIG_NF_CONNTRACK_FTP=y +CONFIG_NF_CONNTRACK_H323=y +CONFIG_NF_CONNTRACK_IRC=y +CONFIG_NF_CONNTRACK_NETBIOS_NS=y +CONFIG_NF_CONNTRACK_SNMP=y +CONFIG_NF_CONNTRACK_PPTP=y +CONFIG_NF_CONNTRACK_SANE=y +CONFIG_NF_CONNTRACK_SIP=y +CONFIG_NF_CONNTRACK_TFTP=y +CONFIG_NF_CT_NETLINK=y +CONFIG_NF_CT_NETLINK_TIMEOUT=y +CONFIG_NF_CT_NETLINK_HELPER=y +CONFIG_NETFILTER_NETLINK_GLUE_CT=y +CONFIG_NF_NAT=y +CONFIG_NF_TABLES=y +CONFIG_NF_TABLES_INET=y +CONFIG_NF_TABLES_NETDEV=y +CONFIG_NFT_CT=y +CONFIG_NFT_CONNLIMIT=y +CONFIG_NFT_LOG=y +CONFIG_NFT_LIMIT=y +CONFIG_NFT_MASQ=y +CONFIG_NFT_REDIR=y +CONFIG_NFT_NAT=y +CONFIG_NFT_TUNNEL=y +CONFIG_NFT_QUEUE=y +CONFIG_NFT_REJECT=y +CONFIG_NFT_COMPAT=y +CONFIG_NFT_HASH=y +CONFIG_NFT_OSF=y +CONFIG_NFT_TPROXY=y +CONFIG_NFT_DUP_NETDEV=y +CONFIG_NFT_FWD_NETDEV=y +CONFIG_NETFILTER_XT_SET=y +CONFIG_NETFILTER_XT_TARGET_CHECKSUM=y +CONFIG_NETFILTER_XT_TARGET_CLASSIFY=y +CONFIG_NETFILTER_XT_TARGET_CONNMARK=y +CONFIG_NETFILTER_XT_TARGET_DSCP=y +CONFIG_NETFILTER_XT_TARGET_HMARK=y +CONFIG_NETFILTER_XT_TARGET_IDLETIMER=y +CONFIG_NETFILTER_XT_TARGET_LOG=y +CONFIG_NETFILTER_XT_TARGET_MARK=y +CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y +CONFIG_NETFILTER_XT_TARGET_NFLOG=y +CONFIG_NETFILTER_XT_TARGET_NFQUEUE=y +CONFIG_NETFILTER_XT_TARGET_NOTRACK=y +CONFIG_NETFILTER_XT_TARGET_TEE=y +CONFIG_NETFILTER_XT_TARGET_TPROXY=y +CONFIG_NETFILTER_XT_TARGET_TRACE=y +CONFIG_NETFILTER_XT_TARGET_TCPMSS=y +CONFIG_NETFILTER_XT_TARGET_TCPOPTSTRIP=y +CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y +CONFIG_NETFILTER_XT_MATCH_BPF=y +CONFIG_NETFILTER_XT_MATCH_CGROUP=y +CONFIG_NETFILTER_XT_MATCH_CLUSTER=y +CONFIG_NETFILTER_XT_MATCH_COMMENT=y +CONFIG_NETFILTER_XT_MATCH_CONNBYTES=y +CONFIG_NETFILTER_XT_MATCH_CONNLABEL=y +CONFIG_NETFILTER_XT_MATCH_CONNLIMIT=y +CONFIG_NETFILTER_XT_MATCH_CONNMARK=y +CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y +CONFIG_NETFILTER_XT_MATCH_CPU=y +CONFIG_NETFILTER_XT_MATCH_DCCP=y +CONFIG_NETFILTER_XT_MATCH_DEVGROUP=y +CONFIG_NETFILTER_XT_MATCH_DSCP=y +CONFIG_NETFILTER_XT_MATCH_ESP=y +CONFIG_NETFILTER_XT_MATCH_HASHLIMIT=y +CONFIG_NETFILTER_XT_MATCH_HELPER=y +CONFIG_NETFILTER_XT_MATCH_IPCOMP=y +CONFIG_NETFILTER_XT_MATCH_IPRANGE=y +CONFIG_NETFILTER_XT_MATCH_IPVS=y +CONFIG_NETFILTER_XT_MATCH_L2TP=y +CONFIG_NETFILTER_XT_MATCH_LENGTH=y +CONFIG_NETFILTER_XT_MATCH_LIMIT=y +CONFIG_NETFILTER_XT_MATCH_MAC=y +CONFIG_NETFILTER_XT_MATCH_MARK=y +CONFIG_NETFILTER_XT_MATCH_MULTIPORT=y +CONFIG_NETFILTER_XT_MATCH_NFACCT=y +CONFIG_NETFILTER_XT_MATCH_OSF=y +CONFIG_NETFILTER_XT_MATCH_OWNER=y +CONFIG_NETFILTER_XT_MATCH_POLICY=y +CONFIG_NETFILTER_XT_MATCH_PHYSDEV=y +CONFIG_NETFILTER_XT_MATCH_PKTTYPE=y +CONFIG_NETFILTER_XT_MATCH_QUOTA=y +CONFIG_NETFILTER_XT_MATCH_RATEEST=y +CONFIG_NETFILTER_XT_MATCH_REALM=y +CONFIG_NETFILTER_XT_MATCH_RECENT=y +CONFIG_NETFILTER_XT_MATCH_SCTP=y +CONFIG_NETFILTER_XT_MATCH_SOCKET=y +CONFIG_NETFILTER_XT_MATCH_STATE=y +CONFIG_NETFILTER_XT_MATCH_STATISTIC=y +CONFIG_NETFILTER_XT_MATCH_STRING=y +CONFIG_NETFILTER_XT_MATCH_TCPMSS=y +CONFIG_NETFILTER_XT_MATCH_TIME=y +CONFIG_NETFILTER_XT_MATCH_U32=y +CONFIG_IP_SET=y +CONFIG_IP_SET_BITMAP_IP=y +CONFIG_IP_SET_BITMAP_IPMAC=y +CONFIG_IP_SET_BITMAP_PORT=y +CONFIG_IP_SET_HASH_IP=y +CONFIG_IP_SET_HASH_IPPORT=y +CONFIG_IP_SET_HASH_IPPORTIP=y +CONFIG_IP_SET_HASH_IPPORTNET=y +CONFIG_IP_SET_HASH_NET=y +CONFIG_IP_SET_HASH_NETPORT=y +CONFIG_IP_SET_HASH_NETIFACE=y +CONFIG_IP_SET_LIST_SET=y +CONFIG_IP_VS=y +CONFIG_IP_VS_IPV6=y +CONFIG_IP_VS_PROTO_TCP=y +CONFIG_IP_VS_PROTO_UDP=y +CONFIG_IP_VS_PROTO_ESP=y +CONFIG_IP_VS_PROTO_AH=y +CONFIG_IP_VS_PROTO_SCTP=y +CONFIG_IP_VS_RR=y +CONFIG_IP_VS_WRR=y +CONFIG_IP_VS_LC=y +CONFIG_IP_VS_WLC=y +CONFIG_IP_VS_FO=y +CONFIG_IP_VS_OVF=y +CONFIG_IP_VS_LBLC=y +CONFIG_IP_VS_LBLCR=y +CONFIG_IP_VS_DH=y +CONFIG_IP_VS_SH=y +CONFIG_IP_VS_MH=y +CONFIG_IP_VS_SED=y +CONFIG_IP_VS_NQ=y +CONFIG_IP_VS_FTP=y +CONFIG_NFT_DUP_IPV4=y +CONFIG_NF_TABLES_ARP=y +CONFIG_NF_LOG_ARP=y +CONFIG_IP_NF_IPTABLES=y +CONFIG_IP_NF_MATCH_AH=y +CONFIG_IP_NF_MATCH_ECN=y +CONFIG_IP_NF_MATCH_RPFILTER=y +CONFIG_IP_NF_MATCH_TTL=y +CONFIG_IP_NF_FILTER=y +CONFIG_IP_NF_TARGET_REJECT=y +CONFIG_IP_NF_TARGET_SYNPROXY=y +CONFIG_IP_NF_NAT=y +CONFIG_IP_NF_TARGET_MASQUERADE=y +CONFIG_IP_NF_TARGET_NETMAP=y +CONFIG_IP_NF_TARGET_REDIRECT=y +CONFIG_IP_NF_MANGLE=y +CONFIG_IP_NF_TARGET_ECN=y +CONFIG_IP_NF_TARGET_TTL=y +CONFIG_IP_NF_RAW=y +CONFIG_IP_NF_SECURITY=y +CONFIG_IP_NF_ARPTABLES=y +CONFIG_IP_NF_ARPFILTER=y +CONFIG_IP_NF_ARP_MANGLE=y +CONFIG_NFT_DUP_IPV6=y +CONFIG_IP6_NF_IPTABLES=y +CONFIG_IP6_NF_MATCH_AH=y +CONFIG_IP6_NF_MATCH_EUI64=y +CONFIG_IP6_NF_MATCH_FRAG=y +CONFIG_IP6_NF_MATCH_OPTS=y +CONFIG_IP6_NF_MATCH_HL=y +CONFIG_IP6_NF_MATCH_IPV6HEADER=y +CONFIG_IP6_NF_MATCH_MH=y +CONFIG_IP6_NF_MATCH_RPFILTER=y +CONFIG_IP6_NF_MATCH_RT=y +CONFIG_IP6_NF_TARGET_HL=y +CONFIG_IP6_NF_FILTER=y +CONFIG_IP6_NF_TARGET_REJECT=y +CONFIG_IP6_NF_TARGET_SYNPROXY=y +CONFIG_IP6_NF_MANGLE=y +CONFIG_IP6_NF_RAW=y +CONFIG_IP6_NF_SECURITY=y +CONFIG_IP6_NF_NAT=y +CONFIG_IP6_NF_TARGET_MASQUERADE=y +CONFIG_IP6_NF_TARGET_NPT=y +CONFIG_NF_TABLES_BRIDGE=y +CONFIG_NFT_BRIDGE_REJECT=y +CONFIG_BRIDGE_NF_EBTABLES=y +CONFIG_BRIDGE_EBT_BROUTE=y +CONFIG_BRIDGE_EBT_T_FILTER=y +CONFIG_BRIDGE_EBT_T_NAT=y +CONFIG_BRIDGE_EBT_802_3=y +CONFIG_BRIDGE_EBT_AMONG=y +CONFIG_BRIDGE_EBT_ARP=y +CONFIG_BRIDGE_EBT_IP=y +CONFIG_BRIDGE_EBT_IP6=y +CONFIG_BRIDGE_EBT_LIMIT=y +CONFIG_BRIDGE_EBT_MARK=y +CONFIG_BRIDGE_EBT_PKTTYPE=y +CONFIG_BRIDGE_EBT_STP=y +CONFIG_BRIDGE_EBT_VLAN=y +CONFIG_BRIDGE_EBT_ARPREPLY=y +CONFIG_BRIDGE_EBT_DNAT=y +CONFIG_BRIDGE_EBT_MARK_T=y +CONFIG_BRIDGE_EBT_REDIRECT=y +CONFIG_BRIDGE_EBT_SNAT=y +CONFIG_BRIDGE_EBT_LOG=y +CONFIG_BRIDGE_EBT_NFLOG=y + +# --- Block devices --- +CONFIG_BLK_DEV=y +CONFIG_BLK_DEV_INTEGRITY=y +CONFIG_BLK_DEV_THROTTLING=y +CONFIG_BLK_CGROUP_IOLATENCY=y +CONFIG_PARTITION_ADVANCED=y +CONFIG_LDM_PARTITION=y +CONFIG_CMDLINE_PARTITION=y +CONFIG_BLK_DEV_LOOP=y +CONFIG_BLK_DEV_DRBD=m +CONFIG_BLK_DEV_NBD=y +CONFIG_BLK_DEV_RAM=y +CONFIG_BLK_DEV_RAM_SIZE=65536 +CONFIG_ATA_OVER_ETH=m +CONFIG_BLK_DEV_NVME=y + +# --- SCSI --- +CONFIG_SCSI=y +# CONFIG_SCSI_PROC_FS is not set +CONFIG_BLK_DEV_SD=y +CONFIG_BLK_DEV_SR=y +CONFIG_CHR_DEV_SG=y +CONFIG_ISCSI_TCP=m +CONFIG_SCSI_HPSA=y +CONFIG_MEGARAID_SAS=y +CONFIG_SCSI_MPT3SAS=y +CONFIG_SCSI_MPI3MR=m +CONFIG_SCSI_SMARTPQI=m +CONFIG_VMWARE_PVSCSI=y +CONFIG_XEN_SCSI_FRONTEND=y +CONFIG_SCSI_VIRTIO=y + +# --- ATA / SATA --- +CONFIG_ATA=y +# CONFIG_ATA_VERBOSE_ERROR is not set +# CONFIG_SATA_PMP is not set +CONFIG_SATA_AHCI=y +CONFIG_ATA_PIIX=y +CONFIG_SATA_MV=y +CONFIG_SATA_NV=y +CONFIG_SATA_PROMISE=y +CONFIG_SATA_SIL=y +CONFIG_SATA_SIS=y +CONFIG_SATA_SVW=y +CONFIG_SATA_ULI=y +CONFIG_SATA_VIA=y +CONFIG_SATA_VITESSE=y +CONFIG_ATA_GENERIC=y + +# --- Device mapper / MD --- +CONFIG_MD=y +CONFIG_BLK_DEV_MD=y +CONFIG_MD_LINEAR=y +CONFIG_MD_RAID0=y +CONFIG_MD_RAID1=y +CONFIG_MD_RAID10=y +CONFIG_MD_RAID456=y +CONFIG_MD_MULTIPATH=y +CONFIG_BLK_DEV_DM=y +CONFIG_DM_CRYPT=y +CONFIG_DM_SNAPSHOT=y +CONFIG_DM_THIN_PROVISIONING=y +CONFIG_DM_MULTIPATH=m +CONFIG_DM_MULTIPATH_QL=m +CONFIG_DM_MULTIPATH_ST=m + +# --- Fusion --- +CONFIG_FUSION=y +CONFIG_FUSION_SPI=y + +# --- Network drivers (built-in for reliable boot) --- +CONFIG_NETDEVICES=y +# CONFIG_NET_VENDOR_3COM is not set +# CONFIG_NET_VENDOR_ADAPTEC is not set +# CONFIG_NET_VENDOR_AGERE is not set +# CONFIG_NET_VENDOR_ALACRITECH is not set +# CONFIG_NET_VENDOR_ALTEON is not set +# CONFIG_NET_VENDOR_AMD is not set +# CONFIG_NET_VENDOR_AQUANTIA is not set +# CONFIG_NET_VENDOR_ARC is not set +# CONFIG_NET_VENDOR_ATHEROS is not set +# CONFIG_NET_VENDOR_CADENCE is not set +# CONFIG_NET_VENDOR_CAVIUM is not set +# CONFIG_NET_VENDOR_CHELSIO is not set +# CONFIG_NET_VENDOR_CORTINA is not set +# CONFIG_NET_VENDOR_DEC is not set +# CONFIG_NET_VENDOR_DLINK is not set +# CONFIG_NET_VENDOR_EMULEX is not set +# CONFIG_NET_VENDOR_EZCHIP is not set +# CONFIG_NET_VENDOR_HUAWEI is not set +# CONFIG_NET_VENDOR_I825XX is not set +# CONFIG_NET_VENDOR_MARVELL is not set +# CONFIG_NET_VENDOR_MICREL is not set +# CONFIG_NET_VENDOR_MICROCHIP is not set +# CONFIG_NET_VENDOR_MYRI is not set +# CONFIG_NET_VENDOR_NI is not set +# CONFIG_NET_VENDOR_NATSEMI is not set +# CONFIG_NET_VENDOR_NETERION is not set +# CONFIG_NET_VENDOR_NVIDIA is not set +# CONFIG_NET_VENDOR_OKI is not set +# CONFIG_NET_VENDOR_PACKET_ENGINES is not set +# CONFIG_NET_VENDOR_QLOGIC is not set +# CONFIG_NET_VENDOR_BROCADE is not set +# CONFIG_NET_VENDOR_QUALCOMM is not set +# CONFIG_NET_VENDOR_RDC is not set +# CONFIG_NET_VENDOR_RENESAS is not set +# CONFIG_NET_VENDOR_ROCKER is not set +# CONFIG_NET_VENDOR_SAMSUNG is not set +# CONFIG_NET_VENDOR_SEEQ is not set +# CONFIG_NET_VENDOR_SILAN is not set +# CONFIG_NET_VENDOR_SIS is not set +# CONFIG_NET_VENDOR_SMSC is not set +# CONFIG_NET_VENDOR_SOCIONEXT is not set +# CONFIG_NET_VENDOR_STMICRO is not set +# CONFIG_NET_VENDOR_SUN is not set +# CONFIG_NET_VENDOR_SYNOPSYS is not set +# CONFIG_NET_VENDOR_TEHUTI is not set +# CONFIG_NET_VENDOR_TI is not set +# CONFIG_NET_VENDOR_VIA is not set +# CONFIG_NET_VENDOR_WIZNET is not set +CONFIG_E1000=y +CONFIG_E1000E=y +CONFIG_IGB=y +CONFIG_IGBVF=y +CONFIG_IXGBE=y +CONFIG_IXGBEVF=y +CONFIG_I40E=y +CONFIG_I40EVF=y +CONFIG_ICE=y +CONFIG_IGC=y +CONFIG_MLX4_EN=y +CONFIG_MLX5_CORE=y +CONFIG_MLX5_CORE_EN=y +CONFIG_BNXT=y +CONFIG_CNIC=m +CONFIG_TIGON3=y +CONFIG_BNX2X=y +CONFIG_R8169=y +CONFIG_8139CP=y +CONFIG_8139TOO=y +CONFIG_VMXNET3=y +CONFIG_HYPERV_NET=y +CONFIG_ENA_ETHERNET=m +CONFIG_GVE=m +CONFIG_NFP=m +CONFIG_IONIC=y +CONFIG_VIRTIO_NET=y + +# --- USB network --- +CONFIG_USB_CATC=m +CONFIG_USB_KAWETH=m +CONFIG_USB_PEGASUS=m +CONFIG_USB_RTL8150=y +CONFIG_USB_RTL8152=y +CONFIG_USB_LAN78XX=m +CONFIG_USB_USBNET=y +CONFIG_USB_NET_AX8817X=m +CONFIG_USB_NET_AX88179_178A=m +CONFIG_USB_NET_CDC_EEM=m +CONFIG_USB_NET_CDC_NCM=m +CONFIG_USB_NET_NET1080=m +CONFIG_USB_NET_CDC_SUBSET=m +CONFIG_USB_NET_ZAURUS=m + +# --- WAN --- +CONFIG_WAN=y +CONFIG_HDLC=m +CONFIG_HDLC_CISCO=m + +# --- PPP --- +CONFIG_PPP=m +CONFIG_PPP_BSDCOMP=m +CONFIG_PPP_DEFLATE=m +CONFIG_PPP_FILTER=y +CONFIG_PPP_MPPE=m +CONFIG_PPP_MULTILINK=y +CONFIG_PPPOE=m +CONFIG_PPTP=m +CONFIG_PPPOL2TP=m +CONFIG_PPP_ASYNC=m +CONFIG_PPP_SYNC_TTY=m + +# --- USB --- +CONFIG_USB_SUPPORT=y +CONFIG_USB=y +CONFIG_USB_XHCI_HCD=y +CONFIG_USB_EHCI_HCD=y +CONFIG_USB_OHCI_HCD=y +CONFIG_USB_UHCI_HCD=y +CONFIG_USB_XHCI_PCI=y +CONFIG_USB_UAS=y +CONFIG_USB_STORAGE=y + +# --- USB serial (BMC/console access) --- +CONFIG_USB_SERIAL=y +CONFIG_USB_SERIAL_CONSOLE=y +CONFIG_USB_SERIAL_GENERIC=y +CONFIG_USB_SERIAL_CH341=y +CONFIG_USB_SERIAL_CP210X=y +CONFIG_USB_SERIAL_FTDI_SIO=y +CONFIG_USB_SERIAL_PL2303=y +CONFIG_USB_SERIAL_TI=y +CONFIG_USB_SERIAL_OPTION=y + +# --- Input --- +CONFIG_INPUT_FF_MEMLESS=y +CONFIG_INPUT_SPARSEKMAP=y +CONFIG_INPUT_MOUSEDEV=y +CONFIG_INPUT_MOUSEDEV_PSAUX=y +CONFIG_INPUT_EVDEV=y +# CONFIG_INPUT_MOUSE is not set +CONFIG_INPUT_MISC=y +CONFIG_INPUT_PCSPKR=y +CONFIG_INPUT_ATLAS_BTNS=y +CONFIG_INPUT_UINPUT=y +CONFIG_SERIO_PCIPS2=y +CONFIG_SERIO_RAW=y +# CONFIG_LEGACY_PTYS is not set +# CONFIG_INPUT_JOYSTICK is not set +# CONFIG_INPUT_TOUCHSCREEN is not set + +# --- MMC --- +CONFIG_MMC=y +CONFIG_MMC_SDHCI=y +CONFIG_MMC_SDHCI_PCI=y +# CONFIG_MMC_RICOH_MMC is not set +CONFIG_MMC_SDHCI_ACPI=y +CONFIG_MMC_SDHCI_PLTFM=y + +# --- Filesystems --- +CONFIG_EXT4_FS=y +CONFIG_EXT4_FS_POSIX_ACL=y +CONFIG_EXT4_FS_SECURITY=y +CONFIG_XFS_FS=y +CONFIG_XFS_QUOTA=y +CONFIG_XFS_POSIX_ACL=y +CONFIG_BTRFS_FS=m +CONFIG_BTRFS_FS_POSIX_ACL=y +CONFIG_FS_DAX=y +CONFIG_FS_ENCRYPTION=y +CONFIG_FANOTIFY=y +CONFIG_QUOTA=y +CONFIG_QUOTA_NETLINK_INTERFACE=y +CONFIG_VFAT_FS=y +CONFIG_MSDOS_FS=y +CONFIG_FAT_DEFAULT_IOCHARSET="utf8" +CONFIG_NTFS_FS=m +CONFIG_TMPFS=y +CONFIG_TMPFS_POSIX_ACL=y +CONFIG_TMPFS_XATTR=y +CONFIG_PROC_FS=y +CONFIG_PROC_KCORE=y +CONFIG_SYSFS=y +CONFIG_EFIVAR_FS=y +CONFIG_OVERLAY_FS=y +CONFIG_SQUASHFS=y +CONFIG_SQUASHFS_XATTR=y +CONFIG_SQUASHFS_LZ4=y +CONFIG_SQUASHFS_LZO=y +CONFIG_SQUASHFS_XZ=y +CONFIG_FUSE_FS=y +CONFIG_CUSE=y +CONFIG_ISO9660_FS=y +CONFIG_JOLIET=y +CONFIG_ZISOFS=y +CONFIG_UDF_FS=y +CONFIG_FSCACHE=y +CONFIG_FSCACHE_STATS=y +CONFIG_CACHEFILES=y +CONFIG_NFS_FS=m +# CONFIG_NFS_V2 is not set +CONFIG_NFS_V4=m +CONFIG_NFS_V4_1=y +CONFIG_NFS_V4_2=y +CONFIG_NFS_FSCACHE=y +CONFIG_NFSD=m +CONFIG_NFSD_V4=y +CONFIG_CEPH_FS=m +CONFIG_CEPH_FSCACHE=y +CONFIG_CEPH_FS_POSIX_ACL=y +CONFIG_CIFS=y +# CONFIG_CIFS_ALLOW_INSECURE_LEGACY is not set +CONFIG_CIFS_XATTR=y +CONFIG_CIFS_DFS_UPCALL=y +CONFIG_CIFS_FSCACHE=y +CONFIG_9P_FS=y +CONFIG_9P_FSCACHE=y +CONFIG_9P_FS_POSIX_ACL=y +CONFIG_9P_FS_SECURITY=y + +# --- NLS --- +CONFIG_NLS_CODEPAGE_437=y +CONFIG_NLS_ASCII=y +CONFIG_NLS_ISO8859_1=y +CONFIG_NLS_UTF8=y + +# --- Virtio (built-in for reliable boot) --- +CONFIG_VIRTIO=y +CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_BLK=y +CONFIG_VIRTIO_CONSOLE=y +CONFIG_VIRTIO_BALLOON=y +CONFIG_VIRTIO_INPUT=y +CONFIG_VIRTIO_MMIO=y +CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y +CONFIG_HW_RANDOM_VIRTIO=y +CONFIG_VHOST_NET=m +CONFIG_VHOST_VSOCK=m + +# --- Console / Serial --- +CONFIG_VT=y +CONFIG_VT_CONSOLE=y +CONFIG_SERIAL_8250=y +CONFIG_SERIAL_8250_CONSOLE=y +CONFIG_SERIAL_8250_NR_UARTS=32 +# CONFIG_SERIAL_8250_MID is not set +CONFIG_SERIAL_DEV_BUS=y +CONFIG_TTY_PRINTK=y +CONFIG_HW_CONSOLE=y +CONFIG_VGA_CONSOLE=y +CONFIG_DUMMY_CONSOLE=y + +# --- IPMI --- +CONFIG_IPMI_HANDLER=y +CONFIG_IPMI_DMI_DECODE=y +CONFIG_IPMI_DEVICE_INTERFACE=y +CONFIG_IPMI_SI=y +CONFIG_IPMI_POWEROFF=y + +# --- Hardware RNG --- +CONFIG_HW_RANDOM=y +CONFIG_HW_RANDOM_TIMERIOMEM=y + +# --- I2C --- +CONFIG_I2C_CHARDEV=y +CONFIG_I2C_MUX=y + +# --- TPM --- +CONFIG_TCG_TIS_I2C_ATMEL=m +CONFIG_TCG_TIS_I2C_INFINEON=m +CONFIG_TCG_TIS_I2C_NUVOTON=m +CONFIG_TCG_NSC=m +CONFIG_TCG_ATMEL=m +CONFIG_TCG_INFINEON=m +CONFIG_TCG_XEN=m +CONFIG_TCG_VTPM_PROXY=m +CONFIG_TCG_TIS_ST33ZP24_I2C=m + +# --- Platform / MFD --- +CONFIG_NVRAM=y +CONFIG_HPET=y +CONFIG_HANGCHECK_TIMER=y +CONFIG_LPC_ICH=y +CONFIG_LPC_SCH=y +CONFIG_MFD_INTEL_LPSS_ACPI=y +CONFIG_MFD_INTEL_LPSS_PCI=y +CONFIG_MFD_SM501=y +CONFIG_MFD_VX855=y +# CONFIG_THERMAL_HWMON is not set + +# --- RTC --- +CONFIG_RTC_CLASS=y + +# --- DMA --- +CONFIG_DMADEVICES=y + +# --- Security --- +CONFIG_SECCOMP=y +CONFIG_SECCOMP_FILTER=y +CONFIG_SECURITY=y +CONFIG_SECURITY_NETWORK=y +CONFIG_SECURITY_NETWORK_XFRM=y +CONFIG_SECURITY_PATH=y +CONFIG_SECURITY_DMESG_RESTRICT=y +CONFIG_KEYS=y +CONFIG_PERSISTENT_KEYRINGS=y +CONFIG_TRUSTED_KEYS=y +CONFIG_KEY_DH_OPERATIONS=y +CONFIG_HARDENED_USERCOPY=y +CONFIG_FORTIFY_SOURCE=y +CONFIG_STATIC_USERMODEHELPER=y +CONFIG_SECURITY_YAMA=y +CONFIG_INTEGRITY_SIGNATURE=y +CONFIG_INTEGRITY_ASYMMETRIC_KEYS=y +CONFIG_IMA=y +CONFIG_IMA_DEFAULT_HASH_SHA256=y +CONFIG_IMA_READ_POLICY=y +CONFIG_IMA_APPRAISE=y +CONFIG_EVM=y +CONFIG_LSM="yama,loadpin,safesetid,integrity" + +# --- Crypto --- +CONFIG_CRYPTO=y +CONFIG_CRYPTO_USER=y +CONFIG_CRYPTO_AES=y +CONFIG_CRYPTO_SHA256=y +CONFIG_CRYPTO_SHA512=y +CONFIG_CRYPTO_XTS=y +CONFIG_CRYPTO_ANUBIS=y +CONFIG_CRYPTO_BLOWFISH=y +CONFIG_CRYPTO_CAMELLIA=y +CONFIG_CRYPTO_DES=y +CONFIG_CRYPTO_FCRYPT=y +CONFIG_CRYPTO_KHAZAD=y +CONFIG_CRYPTO_SEED=y +CONFIG_CRYPTO_TEA=y +CONFIG_CRYPTO_TWOFISH=y +CONFIG_CRYPTO_ARC4=y +CONFIG_CRYPTO_KEYWRAP=y +CONFIG_CRYPTO_LRW=y +CONFIG_CRYPTO_PCBC=y +CONFIG_CRYPTO_CHACHA20POLY1305=y +CONFIG_CRYPTO_SEQIV=y +CONFIG_CRYPTO_ECHAINIV=y +CONFIG_CRYPTO_MICHAEL_MIC=y +CONFIG_CRYPTO_RMD160=y +CONFIG_CRYPTO_VMAC=y +CONFIG_CRYPTO_WP512=y +CONFIG_CRYPTO_XCBC=y +CONFIG_CRYPTO_CRC32=y +CONFIG_CRYPTO_LZO=y +CONFIG_CRYPTO_842=y +CONFIG_CRYPTO_LZ4=y +CONFIG_CRYPTO_LZ4HC=y +CONFIG_CRYPTO_ANSI_CPRNG=y +CONFIG_CRYPTO_USER_API_HASH=y +CONFIG_CRYPTO_USER_API_SKCIPHER=y +CONFIG_CRYPTO_USER_API_RNG=y +CONFIG_CRYPTO_USER_API_AEAD=y + +# --- x86_64 hardware crypto acceleration --- +CONFIG_CRYPTO_AES_NI_INTEL=y +CONFIG_CRYPTO_BLOWFISH_X86_64=y +CONFIG_CRYPTO_CAMELLIA_AESNI_AVX2_X86_64=y +CONFIG_CRYPTO_CAST5_AVX_X86_64=y +CONFIG_CRYPTO_CAST6_AVX_X86_64=y +CONFIG_CRYPTO_DES3_EDE_X86_64=y +CONFIG_CRYPTO_SERPENT_SSE2_X86_64=y +CONFIG_CRYPTO_SERPENT_AVX2_X86_64=y +CONFIG_CRYPTO_TWOFISH_AVX_X86_64=y +CONFIG_CRYPTO_CHACHA20_X86_64=y +CONFIG_CRYPTO_POLY1305_X86_64=y +CONFIG_CRYPTO_SHA1_SSSE3=y +CONFIG_CRYPTO_SHA256_SSSE3=y +CONFIG_CRYPTO_SHA512_SSSE3=y +CONFIG_CRYPTO_GHASH_CLMUL_NI_INTEL=y +CONFIG_CRYPTO_CRC32C_INTEL=y +CONFIG_CRYPTO_CRC32_PCLMUL=y +CONFIG_CRYPTO_DEV_PADLOCK=y +CONFIG_CRYPTO_DEV_PADLOCK_AES=y +CONFIG_CRYPTO_DEV_PADLOCK_SHA=y +CONFIG_CRYPTO_DEV_VIRTIO=m +CONFIG_PKCS7_MESSAGE_PARSER=y + +# --- BPF --- +CONFIG_BPF=y +CONFIG_BPF_SYSCALL=y +CONFIG_BPF_JIT=y +CONFIG_BPF_JIT_ALWAYS_ON=y + +# --- PCI --- +CONFIG_PCI=y +CONFIG_PCIEPORTBUS=y +CONFIG_HOTPLUG_PCI_PCIE=y +CONFIG_PCI_STUB=y +CONFIG_PCI_IOV=y +# CONFIG_VGA_ARB is not set +CONFIG_HOTPLUG_PCI=y +CONFIG_HOTPLUG_PCI_ACPI=y +CONFIG_HOTPLUG_PCI_SHPC=y +CONFIG_PCI_HYPERV_INTERFACE=m + +# --- IOMMU --- +CONFIG_AMD_IOMMU=y +CONFIG_INTEL_IOMMU=y +CONFIG_IRQ_REMAP=y + +# --- Xen --- +# CONFIG_XEN_BACKEND is not set +CONFIG_XEN_GNTDEV=y +CONFIG_XEN_GRANT_DEV_ALLOC=y +CONFIG_XEN_PVCALLS_FRONTEND=y +CONFIG_XEN_ACPI_PROCESSOR=y +# CONFIG_XEN_SYMS is not set + +# --- Intel platform --- +CONFIG_INTEL_IPS=y + +# --- DEVFREQ --- +CONFIG_PM_DEVFREQ=y +CONFIG_DEVFREQ_GOV_SIMPLE_ONDEMAND=y + +# --- Reset --- +CONFIG_RESET_CONTROLLER=y + +# --- DAX --- +CONFIG_DEV_DAX=y +CONFIG_DEV_DAX_PMEM=m + +# --- Misc --- +CONFIG_FW_LOADER=y +CONFIG_PRINTK=y +CONFIG_PRINTK_TIME=y +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_EARLY_PRINTK=y +CONFIG_PANIC_TIMEOUT=10 +CONFIG_PANIC_ON_OOPS=y +CONFIG_MAGIC_SYSRQ=y +CONFIG_UEVENT_HELPER=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_CONNECTOR=y +CONFIG_DMI_SYSFS=y +CONFIG_SYSFB_SIMPLEFB=y +CONFIG_RESET_ATTACK_MITIGATION=y +# CONFIG_PNP_DEBUG_MESSAGES is not set +CONFIG_HYPERV=y +CONFIG_HYPERV_UTILS=y +CONFIG_HYPERV_BALLOON=y + +# --- Module compression --- +CONFIG_MODULE_COMPRESS_ZSTD=y +CONFIG_MODULE_DECOMPRESS=y + +# --- EFI boot support --- +CONFIG_EFI=y +CONFIG_EFI_STUB=y +CONFIG_EFI_MIXED=y + +# --- Framebuffer/Console (required for EFI boot video output) --- +CONFIG_DRM=y +CONFIG_DRM_I915=y +CONFIG_DRM_SIMPLEDRM=y +CONFIG_FB=y +CONFIG_FB_EFI=y +CONFIG_FRAMEBUFFER_CONSOLE=y + +# --- Debugging / Diagnostics --- +CONFIG_FRAME_WARN=1024 +CONFIG_BUG_ON_DATA_CORRUPTION=y +CONFIG_HARDLOCKUP_DETECTOR=y +CONFIG_WQ_WATCHDOG=y +CONFIG_DEBUG_NOTIFIERS=y +CONFIG_RCU_CPU_STALL_TIMEOUT=60 +# CONFIG_RCU_TRACE is not set +CONFIG_IO_STRICT_DEVMEM=y + +# --- Disable unnecessary subsystems --- +# CONFIG_SOUND is not set +# CONFIG_WIRELESS is not set +# CONFIG_WLAN is not set +# CONFIG_BLUETOOTH is not set +# CONFIG_NFC is not set +# CONFIG_INFINIBAND is not set +# CONFIG_MEDIA_SUPPORT is not set diff --git a/kernel.configs/6.18.y.arm64 b/kernel.configs/6.18.y.arm64 new file mode 100644 index 00000000..795c91e6 --- /dev/null +++ b/kernel.configs/6.18.y.arm64 @@ -0,0 +1,515 @@ +# CaptainOS kernel defconfig for arm64 (aarch64) +# Minimal config for bare-metal provisioning with container/network support. +# Based on 6.12.y config with targeted additions for CaptainOS use case, adapted for kernel 6.18. +# +# Generate a full .config from this: +# cp 6.18.y.arm64 .config && make ARCH=arm64 olddefconfig + +# --- Architecture --- +CONFIG_ARM64=y +CONFIG_SMP=y +CONFIG_NR_CPUS=128 +CONFIG_NUMA=y +CONFIG_ARM64_VA_BITS_48=y +CONFIG_SCHED_MC=y +CONFIG_SCHED_SMT=y +CONFIG_HZ_1000=y +CONFIG_XEN=y +CONFIG_COMPAT=y +CONFIG_RANDOMIZE_BASE=y +CONFIG_ARM64_ACPI_PARKING_PROTOCOL=y + +# --- General --- +CONFIG_LOCALVERSION="-captainos" +CONFIG_DEFAULT_HOSTNAME="captainos" +CONFIG_SYSVIPC=y +CONFIG_POSIX_MQUEUE=y +CONFIG_AUDIT=y +CONFIG_NO_HZ_IDLE=y +CONFIG_HIGH_RES_TIMERS=y +CONFIG_PREEMPT_VOLUNTARY=y +CONFIG_BSD_PROCESS_ACCT=y +CONFIG_KEXEC=y +CONFIG_KEXEC_FILE=y +CONFIG_CC_OPTIMIZE_FOR_SIZE=y +CONFIG_CGROUPS=y +CONFIG_CGROUP_FREEZER=y +CONFIG_CGROUP_PIDS=y +CONFIG_CGROUP_DEVICE=y +CONFIG_CGROUP_CPUACCT=y +CONFIG_CGROUP_PERF=y +CONFIG_CGROUP_BPF=y +CONFIG_CGROUP_MISC=y +CONFIG_CGROUP_HUGETLB=y +CONFIG_CGROUP_RDMA=y +CONFIG_CPUSETS=y +CONFIG_MEMCG=y +CONFIG_BLK_CGROUP=y +CONFIG_BLK_CGROUP_IOLATENCY=y +CONFIG_BLK_DEV_THROTTLING=y +CONFIG_CFS_BANDWIDTH=y +CONFIG_RT_GROUP_SCHED=y +CONFIG_CGROUP_SCHED=y +CONFIG_CGROUP_NET_PRIO=y +CONFIG_NAMESPACES=y +CONFIG_USER_NS=y +CONFIG_PID_NS=y +CONFIG_NET_NS=y +CONFIG_UTS_NS=y +CONFIG_IPC_NS=y +CONFIG_CHECKPOINT_RESTORE=y +# Memory management +CONFIG_TRANSPARENT_HUGEPAGE=y +CONFIG_KSM=y +CONFIG_MEMORY_HOTPLUG=y +CONFIG_MEMORY_HOTREMOVE=y +CONFIG_HUGETLBFS=y + +# --- Init / Initramfs --- +CONFIG_BLK_DEV_INITRD=y +CONFIG_INITRAMFS_SOURCE="" + +# --- Networking --- +CONFIG_NET=y +CONFIG_PACKET=y +CONFIG_UNIX=y +CONFIG_INET=y +CONFIG_IP_MULTICAST=y +CONFIG_IP_ADVANCED_ROUTER=y +CONFIG_IP_MULTIPLE_TABLES=y +CONFIG_IP_ROUTE_MULTIPATH=y +CONFIG_IP_PNP=y +CONFIG_IP_PNP_DHCP=y +CONFIG_IP_PNP_BOOTP=y +CONFIG_NET_IPIP=m +CONFIG_NET_IPGRE_DEMUX=m +CONFIG_NET_IPGRE=m +CONFIG_SYN_COOKIES=y +CONFIG_IPV6=y +CONFIG_BRIDGE=y +CONFIG_VLAN_8021Q=m +CONFIG_BONDING=m +CONFIG_TUN=y +CONFIG_VETH=y +CONFIG_MACVLAN=y +CONFIG_MACVTAP=y +CONFIG_IPVLAN=y +CONFIG_TAP=y +CONFIG_DUMMY=m +CONFIG_VXLAN=y +CONFIG_GENEVE=m +CONFIG_NLMON=y +# Traffic control / QoS (needed by CNI plugins) +CONFIG_NET_SCHED=y +CONFIG_NET_SCH_HTB=m +CONFIG_NET_SCH_PRIO=m +CONFIG_NET_SCH_SFQ=m +CONFIG_NET_SCH_TBF=m +CONFIG_NET_SCH_NETEM=m +CONFIG_NET_SCH_INGRESS=m +CONFIG_NET_CLS_BASIC=y +CONFIG_NET_CLS_FW=y +CONFIG_NET_CLS_U32=y +CONFIG_NET_CLS_CGROUP=y +CONFIG_NET_CLS_BPF=y +CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_CLS_ACT=y +CONFIG_NET_ACT_POLICE=y +CONFIG_NET_ACT_GACT=y +CONFIG_NET_ACT_MIRRED=y +CONFIG_NET_ACT_BPF=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_U32=y + +# --- Netfilter --- +CONFIG_NETFILTER=y +CONFIG_NETFILTER_ADVANCED=y +CONFIG_NF_CONNTRACK=y +CONFIG_NF_NAT=y +CONFIG_NF_TABLES=y +CONFIG_NF_TABLES_INET=y +CONFIG_NF_TABLES_IPV4=y +CONFIG_NF_TABLES_IPV6=y +CONFIG_NFT_NAT=y +CONFIG_NFT_MASQ=y +CONFIG_NFT_CT=y +CONFIG_NFT_COMPAT=y +CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y +CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y +CONFIG_NETFILTER_XT_MATCH_COMMENT=y +CONFIG_NETFILTER_XT_MATCH_MARK=y +CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y +CONFIG_IP_NF_IPTABLES=y +CONFIG_IP_NF_FILTER=y +CONFIG_IP_NF_NAT=y +CONFIG_IP6_NF_IPTABLES=y +CONFIG_IP6_NF_FILTER=y +CONFIG_IP6_NF_NAT=y +CONFIG_BRIDGE_NETFILTER=y +CONFIG_BRIDGE_NF_EBTABLES=y +CONFIG_BRIDGE_EBT_BROUTE=y +CONFIG_BRIDGE_EBT_T_FILTER=y +CONFIG_BRIDGE_EBT_T_NAT=y +CONFIG_BRIDGE_EBT_ARP=y +CONFIG_BRIDGE_EBT_IP=y +CONFIG_BRIDGE_EBT_IP6=y +CONFIG_BRIDGE_EBT_MARK=y +CONFIG_BRIDGE_EBT_MARK_T=y +CONFIG_BRIDGE_EBT_VLAN=y +# IPVS (container load balancing / kube-proxy) +CONFIG_IP_VS=y +CONFIG_IP_VS_IPV6=y +CONFIG_IP_VS_PROTO_TCP=y +CONFIG_IP_VS_PROTO_UDP=y +CONFIG_IP_VS_RR=y +CONFIG_IP_VS_WRR=y +CONFIG_IP_VS_SH=y +CONFIG_IP_VS_NFCT=y +# IP sets (used by iptables/nftables for efficient matching) +CONFIG_IP_SET=y +CONFIG_IP_SET_HASH_IP=y +CONFIG_IP_SET_HASH_IPPORT=y +CONFIG_IP_SET_HASH_IPPORTNET=y +CONFIG_IP_SET_HASH_NET=y +CONFIG_IP_SET_HASH_NETPORT=y +CONFIG_IP_SET_BITMAP_IP=y +CONFIG_IP_SET_BITMAP_PORT=y +CONFIG_IP_SET_LIST_SET=y +CONFIG_NETFILTER_XT_SET=y + +# --- Block devices --- +CONFIG_BLK_DEV=y +CONFIG_BLK_DEV_LOOP=y +CONFIG_BLK_DEV_NBD=y +CONFIG_BLK_DEV_RAM=y +CONFIG_BLK_DEV_RAM_SIZE=65536 +CONFIG_BLK_DEV_NVME=y +CONFIG_ATA=y +CONFIG_SATA_AHCI=y +CONFIG_SATA_AHCI_PLATFORM=y + +# --- SCSI --- +CONFIG_SCSI=y +CONFIG_BLK_DEV_SD=y +CONFIG_CHR_DEV_SG=y +CONFIG_MEGARAID_SAS=y +CONFIG_SCSI_MPT3SAS=y +CONFIG_SCSI_MPI3MR=m +CONFIG_SCSI_HPSA=y +CONFIG_SCSI_HISI_SAS=y +CONFIG_SCSI_HISI_SAS_PCI=y +CONFIG_XEN_SCSI_FRONTEND=y +CONFIG_SCSI_VIRTIO=y + +# --- Device mapper --- +CONFIG_MD=y +CONFIG_BLK_DEV_DM=y +CONFIG_DM_CRYPT=y +CONFIG_DM_THIN_PROVISIONING=y +CONFIG_DM_SNAPSHOT=y +CONFIG_DM_MULTIPATH=m +CONFIG_MD_RAID0=m +CONFIG_MD_RAID1=m + +# --- Network drivers (built-in for reliable boot) --- +CONFIG_NETDEVICES=y +CONFIG_VIRTIO_NET=y +CONFIG_E1000E=y +CONFIG_IGB=y +CONFIG_IXGBE=y +CONFIG_I40E=y +CONFIG_ICE=y +CONFIG_IGC=y +CONFIG_MLX4_EN=y +CONFIG_MLX5_CORE=y +CONFIG_MLX5_CORE_EN=y +CONFIG_BNXT=y +CONFIG_TIGON3=y +CONFIG_R8169=y +CONFIG_THUNDER_NIC_BGX=m +CONFIG_THUNDER_NIC_PF=m +CONFIG_THUNDER_NIC_VF=m +CONFIG_MACB=m +CONFIG_HNS3=m +CONFIG_NET_XGENE=y +CONFIG_MVNETA=y +CONFIG_MVPP2=y +CONFIG_STMMAC_ETH=m +CONFIG_ENA_ETHERNET=m +CONFIG_GVE=m +CONFIG_VMXNET3=y +CONFIG_AMD_XGBE=y +# Broadcom GENET (Raspberry Pi 4B built-in Ethernet) +CONFIG_BCMGENET=y +# PHY subsystem (required by GENET and other MAC drivers) +CONFIG_PHYLIB=y +CONFIG_MDIO_BUS=y +CONFIG_BROADCOM_PHY=y +CONFIG_BCM7XXX_PHY=y +CONFIG_MDIO_BCM_UNIMAC=y +CONFIG_MARVELL_PHY=y +CONFIG_MARVELL_10G_PHY=y +CONFIG_MICREL_PHY=y +CONFIG_ROCKCHIP_PHY=y +CONFIG_REALTEK_PHY=y +CONFIG_MESON_GXL_PHY=y +CONFIG_AMD_PHY=y + +# --- USB --- +CONFIG_USB_SUPPORT=y +CONFIG_USB=y +CONFIG_USB_XHCI_HCD=y +CONFIG_USB_EHCI_HCD=y +CONFIG_USB_OHCI_HCD=y +CONFIG_USB_XHCI_PCI=y +CONFIG_USB_UAS=y +CONFIG_USB_STORAGE=y +CONFIG_USB_DWC3=y +CONFIG_USB_DWC3_HOST=y +CONFIG_USB_DWC2=y +CONFIG_USB_ISP1760=y + +# --- MMC/SD (Raspberry Pi, Rockchip, and other SBCs) --- +CONFIG_MMC=y +CONFIG_MMC_BLOCK_MINORS=32 +CONFIG_MMC_SDHCI=y +CONFIG_MMC_SDHCI_PCI=y +CONFIG_MMC_SDHCI_ACPI=y +CONFIG_MMC_SDHCI_PLTFM=y +CONFIG_MMC_SDHCI_OF_ARASAN=y +CONFIG_MMC_SDHCI_OF_DWCMSHC=y +CONFIG_MMC_SDHCI_CADENCE=y +CONFIG_MMC_SDHCI_TEGRA=y +CONFIG_MMC_SDHCI_MSM=y +CONFIG_MMC_SDHCI_XENON=y +CONFIG_MMC_SDHCI_F_SDH30=y +CONFIG_MMC_ARMMMCI=y +CONFIG_MMC_DW=y +CONFIG_MMC_DW_ROCKCHIP=y +CONFIG_MMC_DW_K3=y +CONFIG_MMC_MESON_GX=y +CONFIG_MMC_SUNXI=m +CONFIG_MMC_SPI=y +CONFIG_MMC_HSQ=y + +# --- Filesystems --- +CONFIG_EXT4_FS=y +CONFIG_XFS_FS=m +CONFIG_VFAT_FS=y +CONFIG_TMPFS=y +CONFIG_TMPFS_POSIX_ACL=y +CONFIG_PROC_FS=y +CONFIG_SYSFS=y +CONFIG_OVERLAY_FS=y +CONFIG_SQUASHFS=y +CONFIG_SQUASHFS_XATTR=y +CONFIG_SQUASHFS_LZ4=y +CONFIG_SQUASHFS_LZO=y +CONFIG_SQUASHFS_XZ=y +CONFIG_NFS_FS=m +CONFIG_NFS_V4=m +CONFIG_NFS_V4_1=y +CONFIG_NFS_V4_2=y +CONFIG_FUSE_FS=y +CONFIG_CUSE=y +CONFIG_EFIVAR_FS=y +CONFIG_ISO9660_FS=y +CONFIG_JOLIET=y +CONFIG_UDF_FS=y +CONFIG_MSDOS_FS=y +CONFIG_FAT_DEFAULT_IOCHARSET="utf8" +CONFIG_NTFS_FS=m +CONFIG_TMPFS_XATTR=y +CONFIG_NLS_CODEPAGE_437=y +CONFIG_NLS_ASCII=y +CONFIG_NLS_ISO8859_1=y +CONFIG_NLS_UTF8=y + +# --- Virtio (built-in for reliable boot) --- +CONFIG_VIRTIO=y +CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_BLK=y +CONFIG_VIRTIO_CONSOLE=y +CONFIG_VIRTIO_BALLOON=y +CONFIG_VIRTIO_INPUT=y +CONFIG_VIRTIO_MMIO=y +CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y +CONFIG_HW_RANDOM_VIRTIO=y + +# --- Console / Serial --- +CONFIG_SERIAL_AMBA_PL011=y +CONFIG_SERIAL_AMBA_PL011_CONSOLE=y +CONFIG_SERIAL_8250=y +CONFIG_SERIAL_8250_CONSOLE=y +CONFIG_VT=y +CONFIG_VT_CONSOLE=y +CONFIG_HW_CONSOLE=y +CONFIG_DUMMY_CONSOLE=y + +# --- Security --- +CONFIG_SECCOMP=y +CONFIG_SECCOMP_FILTER=y +CONFIG_SECURITY=y +CONFIG_KEYS=y + +# --- Crypto --- +CONFIG_CRYPTO=y +CONFIG_CRYPTO_AES=y +CONFIG_CRYPTO_SHA256=y +CONFIG_CRYPTO_SHA512=y +CONFIG_CRYPTO_XTS=y +CONFIG_ARM64_CRYPTO=y +CONFIG_CRYPTO_AES_ARM64_CE=y +CONFIG_CRYPTO_SHA2_ARM64_CE=y + +# --- BPF --- +CONFIG_BPF=y +CONFIG_BPF_SYSCALL=y +CONFIG_BPF_JIT=y +CONFIG_BPF_JIT_ALWAYS_ON=y +CONFIG_XDP_SOCKETS=y + +# --- ARM64 SoC/platform support --- +CONFIG_ARCH_BCM2835=y +CONFIG_ARCH_ROCKCHIP=y +CONFIG_ARCH_MESON=y +CONFIG_ARCH_SUNXI=y +CONFIG_ARCH_TEGRA=y +CONFIG_ARCH_MVEBU=y +CONFIG_ARCH_HISI=y +CONFIG_ARCH_QCOM=y +CONFIG_ARCH_EXYNOS=y +CONFIG_ARCH_VEXPRESS=y +CONFIG_ARCH_XGENE=y +CONFIG_ARCH_THUNDER=y +CONFIG_ARCH_THUNDER2=y +CONFIG_ARCH_SEATTLE=y +CONFIG_ARCH_SYNQUACER=y +CONFIG_ARCH_UNIPHIER=y +# ACPI / UEFI +CONFIG_ACPI=y +CONFIG_ACPI_DOCK=y +CONFIG_ACPI_IPMI=y +CONFIG_ACPI_APEI=y +CONFIG_ACPI_APEI_GHES=y +CONFIG_IPMI_HANDLER=m +CONFIG_IPMI_DMI_DECODE=y +CONFIG_IPMI_DEVICE_INTERFACE=m +CONFIG_IPMI_SI=m +CONFIG_PCI=y +CONFIG_PCIEPORTBUS=y +CONFIG_HOTPLUG_PCI=y +CONFIG_PCI_HOST_GENERIC=y +CONFIG_PCIE_ROCKCHIP_HOST=y +CONFIG_PCIE_ARMADA_8K=y +CONFIG_PCIE_TEGRA194_HOST=y +# Broadcom STB PCIe controller (Raspberry Pi 4B — needed for VL805 USB 3.0) +CONFIG_PCIE_BRCMSTB=y +CONFIG_PCIE_ALTERA=y +CONFIG_PCI_AARDVARK=y +CONFIG_PCI_TEGRA=y +CONFIG_PCI_XGENE=y +CONFIG_PCI_HOST_THUNDER_PEM=y +CONFIG_PCI_HOST_THUNDER_ECAM=y +CONFIG_PCIE_QCOM=y +CONFIG_PCIE_AL=y +CONFIG_PCI_MESON=y +CONFIG_PCI_HISI=y +CONFIG_PCIE_KIRIN=y +CONFIG_PCIE_HISI_STB=y +CONFIG_PCI_IOV=y +CONFIG_PCI_PASID=y +CONFIG_HOTPLUG_PCI_ACPI=y +# IOMMU +CONFIG_ARM_SMMU=y +CONFIG_ARM_SMMU_V3=y +CONFIG_FW_LOADER=y +# Raspberry Pi firmware mailbox (required for GENET MAC address, clocks, etc.) +CONFIG_RASPBERRYPI_FIRMWARE=y +CONFIG_RASPBERRYPI_POWER=y +CONFIG_ARM_SCMI_PROTOCOL=y +CONFIG_ARM_SCPI_PROTOCOL=y +CONFIG_OF=y +CONFIG_USE_OF=y +# RTC +CONFIG_RTC_CLASS=y +CONFIG_RTC_DRV_EFI=y +# DMA +CONFIG_DMADEVICES=y +CONFIG_PL330_DMA=y +CONFIG_PRINTK=y +CONFIG_PRINTK_TIME=y +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_PANIC_TIMEOUT=10 +CONFIG_PANIC_ON_OOPS=y +CONFIG_MAGIC_SYSRQ=y +CONFIG_RD_GZIP=y +CONFIG_RD_XZ=y +CONFIG_RD_ZSTD=y +CONFIG_UEVENT_HELPER=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_CONNECTOR=y +CONFIG_DMI_SYSFS=y +CONFIG_EFI=y +CONFIG_EFI_STUB=y + +# --- Framebuffer/Console (required for EFI boot video output) --- +CONFIG_DRM=y +CONFIG_DRM_SIMPLEDRM=y +CONFIG_DRM_VIRTIO_GPU=y +CONFIG_FB=y +CONFIG_FB_EFI=y +CONFIG_FRAMEBUFFER_CONSOLE=y +CONFIG_SYSFB_SIMPLEFB=y +# Virtualization +CONFIG_VIRTUALIZATION=y +CONFIG_KVM=y +CONFIG_VFIO=y +CONFIG_VFIO_PCI=y +CONFIG_VHOST_NET=m +CONFIG_VHOST_VSOCK=m + +# --- Module compression --- +CONFIG_MODULE_COMPRESS_ZSTD=y +CONFIG_MODULE_DECOMPRESS=y + +# --- I2C (SBC board management, EEPROM, sensors) --- +CONFIG_I2C=y +CONFIG_I2C_CHARDEV=y +CONFIG_I2C_MV64XXX=y +CONFIG_I2C_RK3X=y +CONFIG_I2C_TEGRA=y +CONFIG_I2C_DESIGNWARE_PLATFORM=y +# SPI +CONFIG_SPI=y +CONFIG_SPI_ROCKCHIP=y +CONFIG_SPI_PL022=y +CONFIG_SPI_ORION=y +# GPIO +CONFIG_GPIOLIB=y +# Hardware watchdog +CONFIG_WATCHDOG=y +CONFIG_SOFTLOCKUP_DETECTOR=y +CONFIG_WQ_WATCHDOG=y +# Hardware RNG +CONFIG_HW_RANDOM=y +CONFIG_HW_RANDOM_TIMERIOMEM=y +# TPM +CONFIG_TCG_TIS=y +CONFIG_TCG_FTPM_TEE=y +CONFIG_TEE=y +CONFIG_OPTEE=y + +# --- Disable unnecessary subsystems --- +# CONFIG_SOUND is not set +# CONFIG_WIRELESS is not set +# CONFIG_WLAN is not set +# CONFIG_BLUETOOTH is not set +# CONFIG_NFC is not set +# CONFIG_INFINIBAND is not set +# CONFIG_MEDIA_SUPPORT is not set +CONFIG_INPUT_EVDEV=y +# CONFIG_INPUT_JOYSTICK is not set +# CONFIG_INPUT_TOUCHSCREEN is not set From 67c135331dc622eab7061d84daf674a57fe0269a Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 13 Apr 2026 22:30:32 +0200 Subject: [PATCH 076/143] util: show `run()`'s env vars in a Rich Table Signed-off-by: Ricardo Pardini --- captain/util.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/captain/util.py b/captain/util.py index 5edf5966..040af171 100644 --- a/captain/util.py +++ b/captain/util.py @@ -14,6 +14,7 @@ from rich.panel import Panel from rich.rule import Rule from rich.syntax import Syntax +from rich.table import Table import captain @@ -104,6 +105,15 @@ def run( width=captain.console.width, ) captain.console.print(panel) + if env: + # add a Rich Table with the env vars, two columns + table = Table(show_header=True, header_style="bold magenta") + table.add_column("Env Var", style="cyan", no_wrap=True) + table.add_column("Value", style="white") + for k, v in env.items(): + table.add_row(k, v) + captain.console.print(table) + captain.console.print(Rule(f"⮕ Starting subprocess: {cmd} ⮕", style="green")) proc = subprocess.run( From c5e2b78629006dc3a12bfe5536ddbf99fc16b59e Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 13 Apr 2026 22:32:48 +0200 Subject: [PATCH 077/143] docker: rework `run_in_builder()` with both `command_and_args` and `extra_docker_args` - also introduce `cfg.verbose_docker` for setting env `CAPTAIN_VERBOSE=1` when relaunching Signed-off-by: Ricardo Pardini --- captain/cli/_main.py | 3 +++ captain/cli/_release_publish.py | 16 ++++++++------- captain/docker.py | 35 ++++++++++++++++++++++----------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 14b6168d..2fef687d 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -33,6 +33,7 @@ class CliContext: builder_registry: str | None builder_repository: str | None builder_image: str + verbose_docker: bool def make_config(self, **overrides: Any) -> Config: """Build a :class:`Config` from the common options plus per-command *overrides*.""" @@ -44,6 +45,7 @@ def make_config(self, **overrides: Any) -> Config: builder_registry=self.builder_registry, builder_repository=self.builder_repository, builder_image=self.builder_image, + verbose_docker=self.verbose_docker, **overrides, ) @@ -171,6 +173,7 @@ def cli( builder_registry=builder_registry, builder_repository=builder_repository, builder_image=builder_image, + verbose_docker=verbose, ) diff --git a/captain/cli/_release_publish.py b/captain/cli/_release_publish.py index daa4b662..6561cd00 100644 --- a/captain/cli/_release_publish.py +++ b/captain/cli/_release_publish.py @@ -151,13 +151,15 @@ def release_publish_cmd( try: docker.run_in_builder( cfg, - *env_args, - "--entrypoint", - "/usr/bin/uv", - cfg.builder_image, - *(["--verbose"] if cfg.verbose_uv else ["--quiet"]), - "run", - *inner_cmd, + command_and_args=[ + "--entrypoint", + "/usr/bin/uv", + cfg.builder_image, + *(["--verbose"] if cfg.verbose_uv else ["--quiet"]), + "run", + *inner_cmd, + ], + extra_docker_args=env_args, ) except subprocess.CalledProcessError as exc: raise SystemExit(exc.returncode) from None diff --git a/captain/docker.py b/captain/docker.py index 254ac57b..a54b9b72 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -143,7 +143,9 @@ def obtain_builder(cfg: Config) -> None: run(["docker", "push", remote_tagged_image]) -def run_in_builder(cfg: Config, *extra_args: str) -> None: +def run_in_builder( + cfg: Config, command_and_args: list[str], extra_docker_args: list[str] | None = None +) -> None: """Run a command inside the Docker builder container. *extra_args* are appended after the docker run flags and image name. @@ -174,6 +176,7 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "REGISTRY_AUTH_FILE": os.environ.get("REGISTRY_AUTH_FILE", ""), "REGISTRY_USERNAME": os.environ.get("REGISTRY_USERNAME", ""), "REGISTRY_PASSWORD": os.environ.get("REGISTRY_PASSWORD", ""), + "CAPTAIN_VERBOSE": "1" if cfg.verbose_docker else "0", } docker_args: list[str] = [ @@ -185,6 +188,10 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "/work", ] + if extra_docker_args is not None: + log.debug("Adding extra Docker args: %s", extra_docker_args) + docker_args.extend(extra_docker_args) + if log.isEnabledFor(logging.DEBUG): table = Table( title="Docker Environment Variables", show_header=True, header_style="bold cyan" @@ -219,8 +226,9 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: "--mount", f"type=volume,source=captain-kernel-build,target={KERNEL_BUILD_BASE_PATH}", ] + docker_args += [cfg.builder_image] - docker_args.extend(extra_args) + docker_args.extend(command_and_args) run(docker_args) @@ -228,12 +236,14 @@ def run_captain_in_builder(cfg: Config, *extra_args: str): log.debug("Running 'captain %s' in builder container...", extra_args) run_in_builder( cfg, - cfg.builder_image, - "/usr/bin/uv", - *(["--verbose"] if cfg.verbose_uv else ["--quiet"]), - "run", - "captain", - *extra_args, + [ + "/usr/bin/uv", + *(["--verbose"] if cfg.verbose_uv else ["--quiet"]), + "run", + "captain", + *extra_args, + ], + extra_docker_args=[], ) @@ -242,10 +252,11 @@ def run_mkosi_in_builder(cfg: Config, *mkosi_args: str) -> None: ensure_binfmt(cfg) run_in_builder( cfg, - cfg.builder_image, - "/usr/local/bin/mkosi", - f"--architecture={cfg.arch_info.mkosi_arch}", - *mkosi_args, + command_and_args=[ + "/usr/local/bin/mkosi", + f"--architecture={cfg.arch_info.mkosi_arch}", + *mkosi_args, + ], ) From c2f68c51d4031f0f8f97d17476e7cf541b696263 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 13 Apr 2026 22:33:39 +0200 Subject: [PATCH 078/143] captain/cli: `--verbose` sets root level as well as current logger to DEBUG Signed-off-by: Ricardo Pardini --- captain/cli/_main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 2fef687d..303d1152 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -159,7 +159,11 @@ def cli( ) -> None: """CaptainOS build system — click CLI.""" # Configure log level based on --verbose. - logging.getLogger().setLevel(logging.DEBUG if verbose else logging.INFO) + # Configure the root logger (configged via basicLogging() in captain __init__) + # since subcommands and imported modules will use it. + log_level = logging.DEBUG if verbose else logging.INFO + logging.getLogger().setLevel(log_level) + logging.getLogger("captain").setLevel(log_level) if ctx.invoked_subcommand is None: click.echo(ctx.get_help()) From eab0b22921bc980ec3ca5be39afff00b7907262b Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 13 Apr 2026 22:34:49 +0200 Subject: [PATCH 079/143] cli: re-introduce `shell` subcommand - which runs bash interactively under Docker - using same envs / volumes / mounts as regular run_in_builder() Signed-off-by: Ricardo Pardini --- captain/cli/_main.py | 10 +++++++++- captain/cli/_shell.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 captain/cli/_shell.py diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 303d1152..3a433577 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -189,6 +189,14 @@ def cli( def main() -> None: """Console-script entry point.""" # Import subcommand modules to register them on the group. - from captain.cli import _build, _builder, _iso, _kernel, _release_publish, _tools # noqa: F401 + from captain.cli import ( # noqa: F401 + _build, + _builder, + _iso, + _kernel, + _release_publish, + _shell, + _tools, + ) cli() diff --git a/captain/cli/_shell.py b/captain/cli/_shell.py new file mode 100644 index 00000000..29c543cd --- /dev/null +++ b/captain/cli/_shell.py @@ -0,0 +1,28 @@ +"""``captain tools`` — download tools (containerd, runc, nerdctl, CNI plugins).""" + +from __future__ import annotations + +import logging + +import click + +import captain.docker as docker +from captain.cli._main import CliContext, cli + +log = logging.getLogger(__name__) + + +@cli.command( + "shell", + short_help="Get a shell in the builder environment.", +) +@click.pass_obj +def shell_cmd(cli_ctx: CliContext) -> None: + log.debug("Starting shell in docker...") + + cfg = cli_ctx.make_config() + + docker.obtain_builder(cfg) + docker.run_in_builder(cfg, command_and_args=["bash"], extra_docker_args=["-i", "-t"]) + + log.info("Shell stage complete!") From e45c5b46fa51cfd037cfdb1697999515b7424da6 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 13 Apr 2026 22:38:43 +0200 Subject: [PATCH 080/143] kernel: rework with kernel's own `make bindeb-pkg` into a .deb output - kernel packaging code already knows how to: - strip modules - zstd modules - handle Image name differences - handle depmod - produce a .deb package - but we gotta go 2-deep as .deb's are output in parent directory by kbuild - Dockerfile: add layers with deps: - rust compiler layer (inevitable after 7.0.y) - debhelper + etc needed for kernel deb packaging Signed-off-by: Ricardo Pardini --- Dockerfile | 6 ++ captain/cli/_kernel.py | 7 +- captain/cli/_stages.py | 20 +----- captain/config.py | 16 +---- captain/kernel.py | 156 +++++++++++++++++------------------------ 5 files changed, 77 insertions(+), 128 deletions(-) diff --git a/Dockerfile b/Dockerfile index b02bcdb5..bd561c64 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,6 +111,12 @@ RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends buildah # This is just to appease mkosi's later stages. RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends python3 python3-pip python3-pefile +# Rust stuff for kernel build. +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends rustc rust-src bindgen rustfmt rust-clippy + +# For kernel's bindeb-pkg and menuconfig +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends debhelper libdw-dev lsb-release libncurses-dev + RUN <<-CONFIG_FRAG ## A few small config fragments to make life easier # git: Ignore owner mismatches in /work, which will be bind-mounted from the host diff --git a/captain/cli/_kernel.py b/captain/cli/_kernel.py index e1dc7951..0d386c09 100644 --- a/captain/cli/_kernel.py +++ b/captain/cli/_kernel.py @@ -1,12 +1,10 @@ -"""``captain tools`` — download tools (containerd, runc, nerdctl, CNI plugins).""" - from __future__ import annotations import logging import click -from captain import artifacts, config +from captain import config from captain.cli._main import CliContext, cli from captain.cli._stages import _build_kernel_stage @@ -41,7 +39,7 @@ help="Kernel version to build. Must match an official tarball.", ) @click.pass_obj -def tools_cmd( +def kernel_cmd( cli_ctx: CliContext, *, kernel_mode: str, @@ -58,5 +56,4 @@ def tools_cmd( ) _build_kernel_stage(cfg) - artifacts.collect_kernel(cfg) log.info("Kernel build stage complete!") diff --git a/captain/cli/_stages.py b/captain/cli/_stages.py index 074295b9..96f76ae0 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/_stages.py @@ -21,26 +21,12 @@ def _build_kernel_stage(cfg: Config) -> None: return # --- idempotency -------------------------------------------------- - modules_dir = cfg.modules_output / "usr" / "lib" / "modules" - vmlinuz_dir = cfg.kernel_output - has_vmlinuz = vmlinuz_dir.is_dir() and any(vmlinuz_dir.glob("vmlinuz-*")) - log.debug("Kernel build idempotency check: modules_dir=%s exists=%s", modules_dir, - modules_dir.is_dir()) - log.debug("Kernel build idempotency check: vmlinuz_dir=%s exists=%s", vmlinuz_dir, - vmlinuz_dir.is_dir()) - log.debug( - "Checking kernel build idempotency: modules_dir=%s, has_vmlinuz=%s", - modules_dir, - has_vmlinuz, - ) - - if modules_dir.is_dir() and has_vmlinuz and not cfg.force_kernel: + image_deb_package = kernel.obtain_target_artifact_path(cfg) + log.debug("Checking for existing kernel artifact at %s", image_deb_package) + if image_deb_package.is_file() and not cfg.force_kernel: log.info("Kernel already built (use --force-kernel to rebuild)") return - if modules_dir.is_dir() and not has_vmlinuz: - log.warning("Modules exist but vmlinuz is missing — rebuilding kernel") - # --- native ------------------------------------------------------- if cfg.kernel_mode == "native": missing = check_kernel_dependencies(cfg.arch) diff --git a/captain/config.py b/captain/config.py index ddf39eca..35a573c3 100644 --- a/captain/config.py +++ b/captain/config.py @@ -40,15 +40,15 @@ class Config: # For kernel build build_kernel: bool = False kernel_version: str = DEFAULT_KERNEL_VERSION - kernel_config: str | None = None - kernel_src: str | None = None force_kernel: bool = False + kernel_clean: bool = False # Docker builder_registry: str | None = None builder_repository: str | None = None builder_image: str = "captainos-builder" builder_push: bool = False + verbose_docker: bool = False # Pass --verbose to captain when re-launched in Docker # Per-stage mode: "docker" | "native" | "skip" tools_mode: str = "docker" @@ -108,17 +108,7 @@ def kernel_output(self) -> Path: (``usr/lib/modules/{kver}/``) so it can be passed directly as an ``--extra-tree=`` to mkosi. """ - return self.project_dir / "mkosi.output" / "kernel" / self.kernel_version / self.arch - - @property - def modules_output(self) -> Path: - """Per-version, per-arch root for kernel modules. - - Returns ``kernel/{version}/{arch}/modules`` which contains a - merged-usr tree (``usr/lib/modules/{kver}/``) suitable for - passing as ``--extra-tree=`` to mkosi. - """ - return self.kernel_output / "modules" + return self.project_dir / "mkosi.output" / "kernel" / self.arch @property def mkosi_output(self) -> Path: diff --git a/captain/kernel.py b/captain/kernel.py index 0b307aa8..16c8b6c0 100644 --- a/captain/kernel.py +++ b/captain/kernel.py @@ -1,9 +1,9 @@ -"""Kernel download, configuration, compilation, and installation. +"""Kernel download, configuration, compilation, and packaging. -Heavy lifting (make, strip) is still done via subprocess — only the -orchestration is in Python. Called directly by ``cli._build_kernel_stage`` -in both native and Docker modes (inside the container ``build.py kernel`` -re-enters via the CLI with all modes forced to native). +Heavy lifting (make) is still done via subprocess — only the +orchestration is in Python. Called directly by ``cli.build_kernel_stage`` +in both native and Docker modes (inside the container we re-enter +the CLI with all modes forced to native). """ from __future__ import annotations @@ -30,7 +30,7 @@ from captain.config import Config from captain.util import ensure_dir, run, safe_extractall -KERNEL_BUILD_BASE_PATH = "/var/tmp/kernel-build" +KERNEL_BUILD_BASE_PATH = "/work/kernel-build" log = logging.getLogger(__name__) @@ -60,18 +60,18 @@ def _download_with_progress(url: str, filename: Path) -> None: progress.update(task, advance=len(buf)) -def download_kernel(version: str, dest_dir: Path) -> Path: +def download_kernel(cfg: Config, dest_dir: Path) -> Path: """Download and extract a kernel tarball. Returns the source directory.""" - src_dir = dest_dir / f"linux-{version}" + src_dir = dest_dir / f"linux-{cfg.kernel_version}" # predict kernel tarball 1st-level dir name if src_dir.is_dir(): log.info("Using cached kernel source at %s", src_dir) return src_dir - major = version.split(".")[0] - url = f"https://cdn.kernel.org/pub/linux/kernel/v{major}.x/linux-{version}.tar.xz" - tarball = dest_dir / f"linux-{version}.tar.xz" + major = cfg.kernel_version.split(".")[0] + url = f"https://cdn.kernel.org/pub/linux/kernel/v{major}.x/linux-{cfg.kernel_version}.tar.xz" + tarball = dest_dir / f"linux-{cfg.kernel_version}.tar.xz" - log.info("Downloading kernel %s...", version) + log.info("Downloading kernel %s...", cfg.kernel_version) log.info(" URL: %s", url) ensure_dir(dest_dir) try: @@ -102,15 +102,6 @@ def _kernel_branch(version: str) -> str: def _find_defconfig(cfg: Config) -> Path: """Locate the defconfig for the current kernel version and architecture.""" - if cfg.kernel_config: - explicit = Path(cfg.kernel_config) - if not explicit.is_absolute(): - explicit = cfg.project_dir / explicit - if explicit.is_file(): - return explicit - log.error("Kernel config not found: %s", explicit) - raise SystemExit(1) - ai = cfg.arch_info branch = _kernel_branch(cfg.kernel_version) defconfig = cfg.project_dir / "kernel.configs" / f"{branch}.{ai.arch}" @@ -178,87 +169,69 @@ def build_kernel(cfg: Config, src_dir: Path) -> str: make_env = {"ARCH": ai.kernel_arch} if ai.cross_compile: make_env["CROSS_COMPILE"] = ai.cross_compile + make_env["DPKG_DEB_COMPRESSOR_TYPE"] = "none" # don't waste time compressing .deb + + if cfg.kernel_clean: + log.info("Cleaning kernel...") + run( + ["make", f"-j{nproc}", "clean"], + env=make_env, + cwd=src_dir, + ) - log.info("Building kernel with %d jobs...", nproc) - run( - ["make", f"-j{nproc}", ai.image_target, "modules"], - env=make_env, - cwd=src_dir, - ) - - result = run( + built_kver = run( ["make", "-s", "kernelrelease"], env={"ARCH": ai.kernel_arch}, capture=True, cwd=src_dir, - ) - built_kver = result.stdout.strip() - log.info("Built kernel version: %s", built_kver) - return built_kver - - -def install_kernel(cfg: Config, src_dir: Path, built_kver: str) -> None: - """Install modules and vmlinuz into mkosi.output/kernel/{version}/{arch}/.""" - ai = cfg.arch_info - modules_root = cfg.modules_output + ).stdout.strip() - make_env = {"ARCH": ai.kernel_arch} - if ai.cross_compile: - make_env["CROSS_COMPILE"] = ai.cross_compile - - log.info("Installing modules...") + log.info("Building kernel '%s' with %d jobs...", built_kver, nproc) run( - ["make", f"INSTALL_MOD_PATH={modules_root}", "modules_install"], + ["make", f"-j{nproc}", "bindeb-pkg"], env=make_env, cwd=src_dir, ) - log.info("Stripping debug symbols from modules...") - strip_cmd = f"{ai.strip_prefix}strip" - for ko in modules_root.rglob("*.ko"): - run([strip_cmd, "--strip-unneeded", str(ko)], check=False) - - log.info("Compressing kernel modules with zstd...") - for ko in modules_root.rglob("*.ko"): - run(["zstd", "--rm", "-q", "-19", str(ko)], check=True) - - mod_base = modules_root / "lib" / "modules" / built_kver - (mod_base / "build").unlink(missing_ok=True) - (mod_base / "source").unlink(missing_ok=True) - - usr_moddir = ensure_dir(modules_root / "usr" / "lib" / "modules" / built_kver) - if mod_base.is_dir(): - for item in mod_base.iterdir(): - dest = usr_moddir / item.name - if dest.exists(): - if dest.is_dir(): - shutil.rmtree(dest) - else: - dest.unlink() - shutil.move(str(item), str(dest)) - shutil.rmtree(modules_root / "lib", ignore_errors=True) - - log.info("Running depmod for compressed modules...") - run( - ["depmod", "-a", "-b", str(modules_root / "usr"), built_kver], - check=True, + log.info("Built kernel version: %s", built_kver) + return built_kver + + +def obtain_target_artifact_path(cfg, ensure_parent: bool = False) -> Path: + if ensure_parent: + ensure_dir(cfg.kernel_output) + return ( + cfg.kernel_output / f"linux-image-{cfg.kernel_version}-captainos" + f"_{cfg.kernel_version}-1_" + f"{cfg.arch_info.kernel_arch}.deb" ) - kernel_image = src_dir / ai.kernel_image_path - vmlinuz_dir = ensure_dir(cfg.kernel_output) - for old in vmlinuz_dir.glob("vmlinuz-*"): - old.unlink(missing_ok=True) +def deploy_deb_to_output(cfg: Config, build_dir: Path, built_kver: str) -> None: + # Find the newest file in build_dir matching linux-image-{built_kver}*.deb + image_deb_package = build_dir.glob(f"linux-image-{built_kver}*.deb") + image_deb_package = sorted(image_deb_package, key=lambda p: p.stat().st_mtime, reverse=True) + if not image_deb_package: + log.error("No linux-image-*.deb package found in %s", build_dir) + raise SystemExit(1) + image_deb_package = image_deb_package[0] + log.info("Found built kernel package: %s", image_deb_package) + + # copy the deb package to the output directory + target_deb_output = obtain_target_artifact_path(cfg, ensure_parent=True) + log.debug("Copying built kernel package from %s to %s", image_deb_package, target_deb_output) + shutil.copy2(image_deb_package, target_deb_output) - shutil.copy2(kernel_image, vmlinuz_dir / f"vmlinuz-{built_kver}") + # Show dpkg info for the copied package + log.info("Kernel package info:") + run(["dpkg", "-I", str(target_deb_output)]) log.info("Kernel build complete:") - vmlinuz = vmlinuz_dir / f"vmlinuz-{built_kver}" - vmlinuz_size = vmlinuz.stat().st_size / (1024 * 1024) - log.info(" Image: %s (%.1fM)", vmlinuz, vmlinuz_size) - log.info(" Modules: %s/", usr_moddir) - log.info(" Version: %s", built_kver) - log.info(" Output: %s", cfg.kernel_output) + log.info( + " linux-image: %s (%.1fM)", + target_deb_output, + (target_deb_output.stat().st_size / (1024 * 1024)), + ) def build(cfg: Config) -> None: @@ -268,13 +241,10 @@ def build(cfg: Config) -> None: ensure_dir(cfg.kernel_output) log.info("Preparing kernel source...") - build_dir = Path(KERNEL_BUILD_BASE_PATH) - - if cfg.kernel_src and Path(cfg.kernel_src).is_dir(): - log.info("Using provided kernel source at %s", cfg.kernel_src) - src_dir = Path(cfg.kernel_src) - else: - src_dir = download_kernel(cfg.kernel_version, build_dir) + # We've a 2-deep because make bindeb-pkg outputs to parent directory of source proper + # Also because one can't share cache between architectures -- full rebuild everytime otherwise + build_dir = Path(KERNEL_BUILD_BASE_PATH) / f"build-{cfg.arch_info.kernel_arch}" + src_dir = download_kernel(cfg, build_dir) # this is one down from build_dir log.debug("Kernel source directory: %s", src_dir) @@ -286,4 +256,4 @@ def build(cfg: Config) -> None: log.debug("Built kernel version: %s", built_kver) log.info("Installing kernel...") - install_kernel(cfg, src_dir, built_kver) + deploy_deb_to_output(cfg, build_dir, built_kver) From 6ca4eed575e6e774ba39c7af3bdae3c6eeab90aa Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 14 Apr 2026 00:42:15 +0200 Subject: [PATCH 081/143] flavor: introduce BaseFlavor::pre_mkosi_stage() - so flavors can assert their requirements are built/met Signed-off-by: Ricardo Pardini --- captain/cli/_build.py | 3 +++ captain/flavor.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/captain/cli/_build.py b/captain/cli/_build.py index b18dfe6d..85a2d599 100644 --- a/captain/cli/_build.py +++ b/captain/cli/_build.py @@ -115,6 +115,9 @@ def build_cmd( # Tools stage. _build_tools_stage(cfg) + # Delegate to flavor, for eg obtaining it's own dependencies. + flavor.pre_mkosi_stage() + # Initramfs (mkosi) stage + artifact collection. _build_mkosi_stage(cfg, list(cfg.mkosi_args)) artifacts.collect_initramfs(cfg) diff --git a/captain/flavor.py b/captain/flavor.py index ea248df2..b02c08aa 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -124,6 +124,9 @@ def copy_static_files(self, project_dir): def has_iso(self) -> bool: return False + def pre_mkosi_stage(self): + pass + def list_available_flavors() -> list[str]: import importlib From c17be81b84431771c7d746ee77bf9ef58c464fe6 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 14 Apr 2026 00:43:17 +0200 Subject: [PATCH 082/143] mkosi: show Rich.Syntax'ed mkosi.conf before running mkosi - ... since we already paid the price of Pygments... Signed-off-by: Ricardo Pardini --- captain/cli/_stages.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/captain/cli/_stages.py b/captain/cli/_stages.py index 96f76ae0..7e155fd2 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/_stages.py @@ -89,6 +89,22 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: mkosi_args = list(cfg.mkosi_args) + list(extra_args) + # Use a Rich Syntax and Rich Panel to display mkosi.conf before running it if debugging: + if log.isEnabledFor(logging.DEBUG): + from rich import print + from rich.panel import Panel + from rich.syntax import Syntax + + mkosi_conf_path = cfg.project_dir / "mkosi.conf" + if mkosi_conf_path.is_file(): + mkosi_conf_content = mkosi_conf_path.read_text() + syntax = Syntax(mkosi_conf_content, "ini", theme="monokai", line_numbers=True, + background_color="default") + panel = Panel(syntax, title="mkosi.conf", border_style="blue") + print(panel) + else: + log.debug("No mkosi.conf found at %s", mkosi_conf_path) + # --- native ------------------------------------------------------- if cfg.mkosi_mode == "native": missing = check_mkosi_dependencies() From fc145ddb876a58958a2bd26c02bb7f6fff52d6bd Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 14 Apr 2026 01:00:51 +0200 Subject: [PATCH 083/143] captain: allow building amd64 kernel on arm64 - specify a CROSS_COMPILE for x86_64 Signed-off-by: Ricardo Pardini --- captain/util.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/captain/util.py b/captain/util.py index 040af171..97d81a17 100644 --- a/captain/util.py +++ b/captain/util.py @@ -34,7 +34,6 @@ class ArchInfo: dl_arch: str # architecture name in download URLs mkosi_arch: str # mkosi --architecture value qemu_binary: str # QEMU system emulator binary - strip_prefix: str # prefix for strip command def get_arch_info(arch: str) -> ArchInfo: @@ -45,13 +44,12 @@ def get_arch_info(arch: str) -> ArchInfo: arch="amd64", output_arch="x86_64", kernel_arch="x86_64", - cross_compile="", + cross_compile="x86_64-linux-gnu-", image_target="bzImage", kernel_image_path="arch/x86/boot/bzImage", dl_arch="amd64", mkosi_arch="x86-64", qemu_binary="qemu-system-x86_64", - strip_prefix="", ) case "arm64" | "aarch64": return ArchInfo( @@ -64,7 +62,6 @@ def get_arch_info(arch: str) -> ArchInfo: dl_arch="arm64", mkosi_arch="arm64", qemu_binary="qemu-system-aarch64", - strip_prefix="aarch64-linux-gnu-", ) case _: log.error("Unsupported architecture: %s", arch) From 899de31c8c73a3e225e6449281ddfae1f88a04bd Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 14 Apr 2026 01:02:48 +0200 Subject: [PATCH 084/143] common_debian: introduce `package_directories()` for mkosi's `PackageDirectories=` - this allows flavors to add .deb's in directories as a local apt repo - .deb's can then be installed by name - mkosi handles the repo creation and usage directly (via apt-utils) Signed-off-by: Ricardo Pardini --- captain/flavors/common_debian/__init__.py | 3 +++ captain/flavors/common_debian/mkosi.conf.j2 | 3 +++ 2 files changed, 6 insertions(+) diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py index 051db43c..e81da689 100644 --- a/captain/flavors/common_debian/__init__.py +++ b/captain/flavors/common_debian/__init__.py @@ -41,3 +41,6 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: @abstractmethod def kernel_packages(self) -> set[str]: pass + + def package_directories(self) -> set[str]: + return set({}) diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index ce128aa5..a957732a 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -19,6 +19,9 @@ CleanPackageMetadata=yes Autologin=yes # Skips installing documentation/man pages WithDocs=no +# Adds .deb's found in those directories to a local apt repo that can be installed from. +PackageDirectories= + {{ ' '.join(flavor.package_directories()) }} # Do NOT set MakeInitrd=yes — that adds /etc/initrd-release which causes # systemd to enter initrd mode and try switch-root to a real rootfs. From 980770998af2a02f90f3223c8fbd1eaed541f085 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 14 Apr 2026 01:04:26 +0200 Subject: [PATCH 085/143] builder: Dockerfile: add cross-arch libssl-dev and native apt-utils - cross libssl-dev needed for `make bindeb-pkg` to work cross-arch - apt-utils needed for mkosi's PackageDirectories= Signed-off-by: Ricardo Pardini --- Dockerfile | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index bd561c64..ea532c9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,7 @@ apt-get -o "Dpkg::Use-Pty=0" update apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ "grub-efi-${NATIVE_ARCH}-bin" \ "grub-efi-${FOREIGN_ARCH}-bin:${FOREIGN_ARCH}" \ + "libssl-dev:${FOREIGN_ARCH}" \ grub-common \ apt \ dpkg \ @@ -108,14 +109,21 @@ RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ # Buildah is pretty huge, gets its own layer. RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends buildah -# This is just to appease mkosi's later stages. -RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends python3 python3-pip python3-pefile +# FYI: Rust stuff for kernel build is about 1.3gb +# RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends rustc rust-src bindgen rustfmt rust-clippy -# Rust stuff for kernel build. -RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends rustc rust-src bindgen rustfmt rust-clippy +RUN <<-EXTRA_FRAG +# Extra packages for mkosi and kernel builds + +# This is just to appease mkosi's later stages, as it re-launches itself using the system Python +apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends python3 python3-pip python3-pefile # For kernel's bindeb-pkg and menuconfig -RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends debhelper libdw-dev lsb-release libncurses-dev +apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends debhelper libdw-dev lsb-release libncurses-dev + +# For mkosi using Content.PackageDirectories (creating local repo) +apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends apt-utils +EXTRA_FRAG RUN <<-CONFIG_FRAG ## A few small config fragments to make life easier From 9f779bb0f91ceaf2495644142c7959aafe1535ec Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 14 Apr 2026 01:06:19 +0200 Subject: [PATCH 086/143] cli: expose `stages` (nee `_stages`) module - this skews package protection to allow flavors to reuse code - also expose function `build_kernel_stage()` Signed-off-by: Ricardo Pardini --- captain/cli/_build.py | 2 +- captain/cli/_iso.py | 2 +- captain/cli/_kernel.py | 4 ++-- captain/cli/_tools.py | 2 +- captain/cli/{_stages.py => stages.py} | 11 ++++++++--- 5 files changed, 13 insertions(+), 8 deletions(-) rename captain/cli/{_stages.py => stages.py} (96%) diff --git a/captain/cli/_build.py b/captain/cli/_build.py index 85a2d599..2a251948 100644 --- a/captain/cli/_build.py +++ b/captain/cli/_build.py @@ -9,7 +9,7 @@ import captain.flavor from captain import artifacts from captain.cli._main import CliContext, cli -from captain.cli._stages import ( +from captain.cli.stages import ( _build_iso_stage, _build_mkosi_stage, _build_tools_stage, diff --git a/captain/cli/_iso.py b/captain/cli/_iso.py index 2e72b44f..631d2409 100644 --- a/captain/cli/_iso.py +++ b/captain/cli/_iso.py @@ -9,7 +9,7 @@ import captain.flavor from captain import artifacts from captain.cli._main import CliContext, cli -from captain.cli._stages import ( +from captain.cli.stages import ( _build_iso_stage, ) diff --git a/captain/cli/_kernel.py b/captain/cli/_kernel.py index 0d386c09..ab83285c 100644 --- a/captain/cli/_kernel.py +++ b/captain/cli/_kernel.py @@ -6,7 +6,7 @@ from captain import config from captain.cli._main import CliContext, cli -from captain.cli._stages import _build_kernel_stage +from captain.cli.stages import build_kernel_stage log = logging.getLogger(__name__) @@ -55,5 +55,5 @@ def kernel_cmd( build_kernel=True, ) - _build_kernel_stage(cfg) + build_kernel_stage(cfg) log.info("Kernel build stage complete!") diff --git a/captain/cli/_tools.py b/captain/cli/_tools.py index 16e0fd35..d5211f3e 100644 --- a/captain/cli/_tools.py +++ b/captain/cli/_tools.py @@ -7,7 +7,7 @@ import click from captain.cli._main import CliContext, cli -from captain.cli._stages import _build_tools_stage +from captain.cli.stages import _build_tools_stage log = logging.getLogger(__name__) diff --git a/captain/cli/_stages.py b/captain/cli/stages.py similarity index 96% rename from captain/cli/_stages.py rename to captain/cli/stages.py index 7e155fd2..bba89188 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/stages.py @@ -11,7 +11,7 @@ log = logging.getLogger(__name__) -def _build_kernel_stage(cfg: Config) -> None: +def build_kernel_stage(cfg: Config) -> None: """Run the kernel build stage according to *cfg.kernel_mode*.""" log.warning("Building kernel stage in mode %s", cfg.kernel_mode) @@ -98,8 +98,13 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: mkosi_conf_path = cfg.project_dir / "mkosi.conf" if mkosi_conf_path.is_file(): mkosi_conf_content = mkosi_conf_path.read_text() - syntax = Syntax(mkosi_conf_content, "ini", theme="monokai", line_numbers=True, - background_color="default") + syntax = Syntax( + mkosi_conf_content, + "ini", + theme="monokai", + line_numbers=True, + background_color="default", + ) panel = Panel(syntax, title="mkosi.conf", border_style="blue") print(panel) else: From 34c02044f407869076dccf5e3391080771c6844e Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 14 Apr 2026 01:12:15 +0200 Subject: [PATCH 087/143] flavors: introduce `trixie-slim` flavor, using captainos kernel - this one uses the captain-build-from-source kernel .deb - let's violate some package boundaries: - cli acrobatics: delegates to kernel build stage before initramfs build - impls package_directories() by calling kernel Signed-off-by: Ricardo Pardini --- captain/flavors/trixie_slim/__init__.py | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 captain/flavors/trixie_slim/__init__.py diff --git a/captain/flavors/trixie_slim/__init__.py b/captain/flavors/trixie_slim/__init__.py new file mode 100644 index 00000000..9af4a51f --- /dev/null +++ b/captain/flavors/trixie_slim/__init__.py @@ -0,0 +1,32 @@ +import logging +from dataclasses import dataclass + +from captain.cli import stages +from captain.flavor import BaseFlavor +from captain.flavors.common_acpi import TrixieACPIFlavor +from captain.kernel import obtain_target_artifact_path + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieSlimFlavor() + + +@dataclass +class TrixieSlimFlavor(TrixieACPIFlavor): + id = "trixie-slim" + name = "Trixie Slim" + description = "Debian Trixie based with captainos slim kernel" + supported_architectures = frozenset(["amd64", "arm64"]) + + def pre_mkosi_stage(self): + log.debug("Flavor delegating to build_kernel_stage to ensure kernel .deb is present") + # call the kernel build stage, to ensure kernel .deb is in mkosi.output/kernel/ + stages.build_kernel_stage(self.cfg) + + def kernel_packages(self) -> set[str]: + return {f"linux-image-{self.cfg.kernel_version}-captainos"} + + def package_directories(self) -> set[str]: + return {str(obtain_target_artifact_path(self.cfg).parent.relative_to(self.cfg.project_dir))} From c9a1ae9ec16cddd560c4304cfb8974213e7aa1ef Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 15 Apr 2026 22:26:44 +0200 Subject: [PATCH 088/143] kernel: introduce `--config` for round-trip menuconfig / savedefconfig - interactive, handle when re-launching in Docker - just "Exit" to update a defconfig with no changes - eg `uv run captain --verbose --arch amd64 kernel --config` Signed-off-by: Ricardo Pardini --- captain/cli/_kernel.py | 18 +++++++++++------- captain/cli/stages.py | 10 +++++++--- captain/config.py | 1 + captain/docker.py | 11 +++++++---- captain/kernel.py | 20 ++++++++++++++++++++ 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/captain/cli/_kernel.py b/captain/cli/_kernel.py index ab83285c..9f8a519b 100644 --- a/captain/cli/_kernel.py +++ b/captain/cli/_kernel.py @@ -38,21 +38,25 @@ show_default=True, help="Kernel version to build. Must match an official tarball.", ) +@click.option( + "--config", + envvar="CONFIG_KERNEL", + is_flag=True, + default=False, + help="Skip building and instead do interactive kernel configuration (menuconfig).", +) @click.pass_obj def kernel_cmd( - cli_ctx: CliContext, - *, - kernel_mode: str, - force_kernel: bool, - kernel_version: str, + cli_ctx: CliContext, *, kernel_mode: str, force_kernel: bool, kernel_version: str, config: bool ) -> None: - log.warning("CLI kernel mode: %s", kernel_mode) + log.debug("CLI kernel mode: %s", kernel_mode) cfg = cli_ctx.make_config( - force_kernel=force_kernel, + force_kernel=force_kernel or config, kernel_version=kernel_version, kernel_mode=kernel_mode, build_kernel=True, + kernel_menuconfig=config, ) build_kernel_stage(cfg) diff --git a/captain/cli/stages.py b/captain/cli/stages.py index bba89188..a44863ec 100644 --- a/captain/cli/stages.py +++ b/captain/cli/stages.py @@ -41,7 +41,11 @@ def build_kernel_stage(cfg: Config) -> None: # --- docker ------------------------------------------------------- docker.obtain_builder(cfg) log.info("Building kernel (docker relaunch)...") - docker.run_captain_in_builder(cfg, "kernel") + if cfg.kernel_menuconfig: + log.info("Launching interactive kernel configuration (menuconfig)...") + docker.run_captain_in_builder(cfg, ["kernel"], extra_docker_args=["-i", "-t"]) + else: + docker.run_captain_in_builder(cfg, ["kernel"]) docker.fix_docker_ownership( cfg, [ @@ -68,7 +72,7 @@ def _build_tools_stage(cfg: Config) -> None: # --- docker ------------------------------------------------------- docker.obtain_builder(cfg) log.info("Downloading tools (nerdctl, containerd, etc.) docker...") - docker.run_captain_in_builder(cfg, "tools") + docker.run_captain_in_builder(cfg, ["tools"]) docker.fix_docker_ownership(cfg, ["/work/mkosi.output"]) @@ -178,7 +182,7 @@ def _build_iso_stage(cfg: Config) -> None: # --- docker ------------------------------------------------------- docker.obtain_builder(cfg) log.info("Building ISO (docker)...") - docker.run_captain_in_builder(cfg, "iso") + docker.run_captain_in_builder(cfg, ["iso"]) docker.fix_docker_ownership( cfg, [ diff --git a/captain/config.py b/captain/config.py index 35a573c3..87897649 100644 --- a/captain/config.py +++ b/captain/config.py @@ -42,6 +42,7 @@ class Config: kernel_version: str = DEFAULT_KERNEL_VERSION force_kernel: bool = False kernel_clean: bool = False + kernel_menuconfig: bool = False # Docker builder_registry: str | None = None diff --git a/captain/docker.py b/captain/docker.py index a54b9b72..20584955 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -177,6 +177,7 @@ def run_in_builder( "REGISTRY_USERNAME": os.environ.get("REGISTRY_USERNAME", ""), "REGISTRY_PASSWORD": os.environ.get("REGISTRY_PASSWORD", ""), "CAPTAIN_VERBOSE": "1" if cfg.verbose_docker else "0", + "CONFIG_KERNEL": "1" if cfg.kernel_menuconfig else "0", } docker_args: list[str] = [ @@ -232,8 +233,10 @@ def run_in_builder( run(docker_args) -def run_captain_in_builder(cfg: Config, *extra_args: str): - log.debug("Running 'captain %s' in builder container...", extra_args) +def run_captain_in_builder( + cfg: Config, command_and_args: list[str], extra_docker_args: list[str] | None = None +): + log.debug("Running 'captain %s' in builder container...", command_and_args) run_in_builder( cfg, [ @@ -241,9 +244,9 @@ def run_captain_in_builder(cfg: Config, *extra_args: str): *(["--verbose"] if cfg.verbose_uv else ["--quiet"]), "run", "captain", - *extra_args, + *command_and_args, ], - extra_docker_args=[], + extra_docker_args=extra_docker_args, ) diff --git a/captain/kernel.py b/captain/kernel.py index 16c8b6c0..41046063 100644 --- a/captain/kernel.py +++ b/captain/kernel.py @@ -140,6 +140,26 @@ def configure_kernel(cfg: Config, src_dir: Path) -> None: log.info("Using defconfig: %s", defconfig) shutil.copy2(defconfig, src_dir / ".config") run(["make", "olddefconfig"], env=make_env, cwd=src_dir) + + log.warning( + "Considering whether to launch menuconfig for manual config tweaks... %s", + cfg.kernel_menuconfig, + ) + + if cfg.kernel_menuconfig: + log.info("Launching menuconfig...") + run(["make", "menuconfig"], env=make_env, cwd=src_dir) + + # run `make savedefconfig` + log.info("Saving modified config...") + run(["make", "savedefconfig"], env=make_env, cwd=src_dir) + + # copy the 'defconfig' back to the source - ready to commit + shutil.copy(src_dir / "defconfig", defconfig) + log.info("Updated defconfig saved to %s", defconfig) + + raise SystemExit(0) # exit after menuconfig - no build + branch = _kernel_branch(cfg.kernel_version) resolved = cfg.project_dir / "kernel.configs" / f".config.resolved.{branch}.{ai.arch}" shutil.copy2(src_dir / ".config", resolved) From 64704de41eb4394e108e747e2903c4af126ca642 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 15 Apr 2026 22:28:58 +0200 Subject: [PATCH 089/143] kernel: 6.18.y.amd64: defconfig standard Signed-off-by: Ricardo Pardini --- kernel.configs/6.18.y.amd64 | 950 ++++++++++++++---------------------- 1 file changed, 370 insertions(+), 580 deletions(-) diff --git a/kernel.configs/6.18.y.amd64 b/kernel.configs/6.18.y.amd64 index c6d243cc..9acb6e7c 100644 --- a/kernel.configs/6.18.y.amd64 +++ b/kernel.configs/6.18.y.amd64 @@ -1,33 +1,3 @@ -# CaptainOS kernel defconfig for x86_64 -# Comprehensive config for bare-metal provisioning with container/network support. -# Derived from HookOS generic-6.6.y-x86_64 proven config, adapted for kernel 6.18. -# -# Generate a full .config from this: -# cp 6.18.y.amd64 .config && make olddefconfig - -# --- Architecture --- -CONFIG_64BIT=y -CONFIG_X86_64=y -CONFIG_SMP=y -CONFIG_NR_CPUS=128 -# CONFIG_X86_EXTENDED_PLATFORM is not set -CONFIG_X86_INTEL_LPSS=y -CONFIG_HYPERVISOR_GUEST=y -CONFIG_PARAVIRT_SPINLOCKS=y -CONFIG_XEN=y -CONFIG_XEN_PVH=y -CONFIG_X86_REROUTE_FOR_BROKEN_BOOT_IRQS=y -# CONFIG_X86_MCE is not set -CONFIG_X86_MSR=y -CONFIG_X86_CPUID=y -# CONFIG_X86_5LEVEL is not set -CONFIG_HZ_1000=y -CONFIG_PHYSICAL_ALIGN=0x1000000 -CONFIG_LEGACY_VSYSCALL_NONE=y -# CONFIG_MODIFY_LDT_SYSCALL is not set -CONFIG_IA32_EMULATION=y - -# --- General --- CONFIG_LOCALVERSION="-captainos" # CONFIG_LOCALVERSION_AUTO is not set CONFIG_DEFAULT_HOSTNAME="captainos" @@ -36,75 +6,61 @@ CONFIG_POSIX_MQUEUE=y CONFIG_AUDIT=y CONFIG_NO_HZ_IDLE=y CONFIG_HIGH_RES_TIMERS=y +CONFIG_BPF_SYSCALL=y +CONFIG_BPF_JIT=y +CONFIG_BPF_JIT_ALWAYS_ON=y CONFIG_PREEMPT_VOLUNTARY=y CONFIG_BSD_PROCESS_ACCT=y CONFIG_BSD_PROCESS_ACCT_V3=y CONFIG_IKCONFIG=y CONFIG_IKCONFIG_PROC=y -CONFIG_CC_OPTIMIZE_FOR_SIZE=y -CONFIG_EXPERT=y -CONFIG_KEXEC=y -CONFIG_KEXEC_FILE=y -CONFIG_SCHED_AUTOGROUP=y -CONFIG_CHECKPOINT_RESTORE=y -CONFIG_KPROBES=y -CONFIG_JUMP_LABEL=y -CONFIG_BINFMT_MISC=y - -# --- Cgroups --- -CONFIG_CGROUPS=y -CONFIG_CGROUP_FREEZER=y +CONFIG_MEMCG=y +CONFIG_BLK_CGROUP=y +CONFIG_CFS_BANDWIDTH=y +CONFIG_RT_GROUP_SCHED=y CONFIG_CGROUP_PIDS=y CONFIG_CGROUP_RDMA=y +CONFIG_CGROUP_FREEZER=y +CONFIG_CGROUP_HUGETLB=y +CONFIG_CPUSETS=y CONFIG_CGROUP_DEVICE=y CONFIG_CGROUP_CPUACCT=y CONFIG_CGROUP_PERF=y CONFIG_CGROUP_BPF=y CONFIG_CGROUP_MISC=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CGROUP_NET_PRIO=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_CFS_BANDWIDTH=y -CONFIG_RT_GROUP_SCHED=y -CONFIG_CGROUP_SCHED=y -CONFIG_CPUSETS=y - -# --- Namespaces --- CONFIG_NAMESPACES=y CONFIG_USER_NS=y -CONFIG_PID_NS=y -CONFIG_NET_NS=y -CONFIG_UTS_NS=y -CONFIG_IPC_NS=y - -# --- Init / Initramfs --- +CONFIG_CHECKPOINT_RESTORE=y +CONFIG_SCHED_AUTOGROUP=y CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="" # CONFIG_RD_BZIP2 is not set # CONFIG_RD_LZMA is not set -CONFIG_RD_GZIP=y -CONFIG_RD_XZ=y # CONFIG_RD_LZO is not set # CONFIG_RD_LZ4 is not set -CONFIG_RD_ZSTD=y - -# --- Memory management --- -CONFIG_SLAB_FREELIST_RANDOM=y -# CONFIG_COMPAT_BRK is not set -CONFIG_MEMORY_HOTPLUG=y -CONFIG_MEMORY_HOTREMOVE=y -CONFIG_KSM=y -CONFIG_DEFAULT_MMAP_MIN_ADDR=65536 -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_ZONE_DEVICE=y -CONFIG_HUGETLBFS=y - -# --- Power / Suspend --- +CONFIG_CC_OPTIMIZE_FOR_SIZE=y +CONFIG_EXPERT=y +CONFIG_KEXEC=y +CONFIG_KEXEC_FILE=y +CONFIG_SMP=y +# CONFIG_X86_EXTENDED_PLATFORM is not set +CONFIG_X86_INTEL_LPSS=y +CONFIG_HYPERVISOR_GUEST=y +CONFIG_PARAVIRT_SPINLOCKS=y +CONFIG_XEN=y +CONFIG_XEN_PVH=y +CONFIG_NR_CPUS=128 +CONFIG_X86_REROUTE_FOR_BROKEN_BOOT_IRQS=y +# CONFIG_X86_MCE is not set +CONFIG_X86_MSR=y +CONFIG_X86_CPUID=y +CONFIG_EFI=y +CONFIG_EFI_STUB=y +CONFIG_EFI_MIXED=y +CONFIG_HZ_1000=y +CONFIG_PHYSICAL_ALIGN=0x1000000 +CONFIG_LEGACY_VSYSCALL_NONE=y +# CONFIG_MODIFY_LDT_SYSCALL is not set # CONFIG_SUSPEND is not set - -# --- ACPI --- -CONFIG_ACPI=y # CONFIG_ACPI_REV_OVERRIDE_POSSIBLE is not set CONFIG_ACPI_DOCK=y CONFIG_ACPI_IPMI=y @@ -113,8 +69,6 @@ CONFIG_ACPI_SBS=y CONFIG_ACPI_NFIT=y CONFIG_ACPI_APEI=y CONFIG_ACPI_APEI_GHES=y - -# --- CPU frequency --- CONFIG_CPU_FREQ_STAT=y CONFIG_CPU_FREQ_GOV_POWERSAVE=y CONFIG_CPU_FREQ_GOV_USERSPACE=y @@ -126,17 +80,31 @@ CONFIG_X86_POWERNOW_K8=y CONFIG_X86_P4_CLOCKMOD=y CONFIG_CPU_IDLE_GOV_LADDER=y CONFIG_INTEL_IDLE=y - -# --- KVM --- +CONFIG_IA32_EMULATION=y CONFIG_KVM=m CONFIG_KVM_INTEL=m CONFIG_KVM_AMD=m - -# --- Networking --- +CONFIG_KPROBES=y +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_BLK_DEV_INTEGRITY=y +CONFIG_BLK_DEV_THROTTLING=y +CONFIG_BLK_CGROUP_IOLATENCY=y +CONFIG_PARTITION_ADVANCED=y +CONFIG_LDM_PARTITION=y +CONFIG_CMDLINE_PARTITION=y +CONFIG_BINFMT_MISC=y +CONFIG_SLAB_FREELIST_RANDOM=y +# CONFIG_COMPAT_BRK is not set +CONFIG_MEMORY_HOTPLUG=y +CONFIG_MEMORY_HOTREMOVE=y +CONFIG_KSM=y +CONFIG_DEFAULT_MMAP_MIN_ADDR=65536 +CONFIG_TRANSPARENT_HUGEPAGE=y +CONFIG_ZONE_DEVICE=y CONFIG_NET=y CONFIG_PACKET=y CONFIG_PACKET_DIAG=y -CONFIG_UNIX=y CONFIG_UNIX_DIAG=y CONFIG_XFRM_USER=m CONFIG_XFRM_SUB_POLICY=y @@ -144,7 +112,6 @@ CONFIG_XFRM_STATISTICS=y CONFIG_NET_KEY=m CONFIG_NET_KEY_MIGRATE=y CONFIG_XDP_SOCKETS=y -CONFIG_INET=y CONFIG_IP_MULTICAST=y CONFIG_IP_ADVANCED_ROUTER=y CONFIG_IP_FIB_TRIE_STATS=y @@ -169,8 +136,6 @@ CONFIG_INET_ESP=m CONFIG_INET_IPCOMP=m CONFIG_INET_UDP_DIAG=y CONFIG_TCP_MD5SIG=y -CONFIG_SYN_COOKIES=y -CONFIG_IPV6=y CONFIG_IPV6_ROUTER_PREF=y CONFIG_INET6_AH=m CONFIG_INET6_ESP=m @@ -185,91 +150,7 @@ CONFIG_IPV6_MULTIPLE_TABLES=y CONFIG_IPV6_SUBTREES=y CONFIG_NETLABEL=y CONFIG_NETWORK_SECMARK=y -CONFIG_BRIDGE=y -CONFIG_BRIDGE_VLAN_FILTERING=y -CONFIG_VLAN_8021Q=y -CONFIG_BONDING=m -CONFIG_TUN=y -CONFIG_VETH=y -CONFIG_MACVLAN=y -CONFIG_MACVTAP=y -CONFIG_IPVLAN=y -CONFIG_VXLAN=y -CONFIG_GENEVE=m -CONFIG_NETCONSOLE=y -CONFIG_TAP=y -CONFIG_DUMMY=m -CONFIG_NLMON=y -CONFIG_IP_SCTP=m -CONFIG_L2TP=m -CONFIG_NET_TEAM=m -CONFIG_NET_TEAM_MODE_BROADCAST=m -CONFIG_NET_TEAM_MODE_ROUNDROBIN=m -CONFIG_NET_TEAM_MODE_RANDOM=m -CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=m -CONFIG_NET_TEAM_MODE_LOADBALANCE=m -CONFIG_OPENVSWITCH=m -CONFIG_VSOCKETS=m -CONFIG_VIRTIO_VSOCKETS=m -CONFIG_HYPERV_VSOCKETS=m -CONFIG_NETLINK_DIAG=y -CONFIG_MPLS_ROUTING=m -CONFIG_MPLS_IPTUNNEL=m -CONFIG_NET_SWITCHDEV=y -CONFIG_NET_9P=y -CONFIG_NET_9P_VIRTIO=y - -# --- Traffic control --- -CONFIG_NET_SCHED=y -CONFIG_NET_SCH_HTB=m -CONFIG_NET_SCH_HFSC=m -CONFIG_NET_SCH_PRIO=m -CONFIG_NET_SCH_MULTIQ=m -CONFIG_NET_SCH_RED=m -CONFIG_NET_SCH_SFB=m -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_SCH_TEQL=m -CONFIG_NET_SCH_TBF=m -CONFIG_NET_SCH_GRED=m -CONFIG_NET_SCH_NETEM=m -CONFIG_NET_SCH_DRR=m -CONFIG_NET_SCH_MQPRIO=m -CONFIG_NET_SCH_CHOKE=m -CONFIG_NET_SCH_QFQ=m -CONFIG_NET_SCH_INGRESS=m -CONFIG_NET_CLS_BASIC=y -CONFIG_NET_CLS_ROUTE4=y -CONFIG_NET_CLS_FW=y -CONFIG_NET_CLS_U32=y -CONFIG_CLS_U32_PERF=y -CONFIG_CLS_U32_MARK=y -CONFIG_NET_CLS_FLOW=y -CONFIG_NET_CLS_CGROUP=y -CONFIG_NET_CLS_BPF=y -CONFIG_NET_CLS_MATCHALL=y -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_CMP=y -CONFIG_NET_EMATCH_NBYTE=y -CONFIG_NET_EMATCH_U32=y -CONFIG_NET_EMATCH_META=y -CONFIG_NET_EMATCH_TEXT=y -CONFIG_NET_EMATCH_IPSET=y -CONFIG_NET_CLS_ACT=y -CONFIG_NET_ACT_POLICE=y -CONFIG_NET_ACT_GACT=y -CONFIG_GACT_PROB=y -CONFIG_NET_ACT_MIRRED=y -CONFIG_NET_ACT_IPT=y -CONFIG_NET_ACT_NAT=y -CONFIG_NET_ACT_PEDIT=y -CONFIG_NET_ACT_SIMP=y -CONFIG_NET_ACT_SKBEDIT=y -CONFIG_NET_ACT_CSUM=y -CONFIG_NET_ACT_BPF=y - -# --- Netfilter --- CONFIG_NETFILTER=y -CONFIG_NETFILTER_ADVANCED=y CONFIG_BRIDGE_NETFILTER=y CONFIG_NF_CONNTRACK=y CONFIG_NF_CONNTRACK_ZONES=y @@ -291,7 +172,6 @@ CONFIG_NF_CT_NETLINK=y CONFIG_NF_CT_NETLINK_TIMEOUT=y CONFIG_NF_CT_NETLINK_HELPER=y CONFIG_NETFILTER_NETLINK_GLUE_CT=y -CONFIG_NF_NAT=y CONFIG_NF_TABLES=y CONFIG_NF_TABLES_INET=y CONFIG_NF_TABLES_NETDEV=y @@ -320,13 +200,11 @@ CONFIG_NETFILTER_XT_TARGET_HMARK=y CONFIG_NETFILTER_XT_TARGET_IDLETIMER=y CONFIG_NETFILTER_XT_TARGET_LOG=y CONFIG_NETFILTER_XT_TARGET_MARK=y -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y CONFIG_NETFILTER_XT_TARGET_NFLOG=y CONFIG_NETFILTER_XT_TARGET_NFQUEUE=y -CONFIG_NETFILTER_XT_TARGET_NOTRACK=y +CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y CONFIG_NETFILTER_XT_TARGET_TEE=y CONFIG_NETFILTER_XT_TARGET_TPROXY=y -CONFIG_NETFILTER_XT_TARGET_TRACE=y CONFIG_NETFILTER_XT_TARGET_TCPMSS=y CONFIG_NETFILTER_XT_TARGET_TCPOPTSTRIP=y CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y @@ -414,20 +292,9 @@ CONFIG_IP_NF_MATCH_AH=y CONFIG_IP_NF_MATCH_ECN=y CONFIG_IP_NF_MATCH_RPFILTER=y CONFIG_IP_NF_MATCH_TTL=y -CONFIG_IP_NF_FILTER=y CONFIG_IP_NF_TARGET_REJECT=y CONFIG_IP_NF_TARGET_SYNPROXY=y -CONFIG_IP_NF_NAT=y -CONFIG_IP_NF_TARGET_MASQUERADE=y -CONFIG_IP_NF_TARGET_NETMAP=y -CONFIG_IP_NF_TARGET_REDIRECT=y -CONFIG_IP_NF_MANGLE=y CONFIG_IP_NF_TARGET_ECN=y -CONFIG_IP_NF_TARGET_TTL=y -CONFIG_IP_NF_RAW=y -CONFIG_IP_NF_SECURITY=y -CONFIG_IP_NF_ARPTABLES=y -CONFIG_IP_NF_ARPFILTER=y CONFIG_IP_NF_ARP_MANGLE=y CONFIG_NFT_DUP_IPV6=y CONFIG_IP6_NF_IPTABLES=y @@ -440,22 +307,12 @@ CONFIG_IP6_NF_MATCH_IPV6HEADER=y CONFIG_IP6_NF_MATCH_MH=y CONFIG_IP6_NF_MATCH_RPFILTER=y CONFIG_IP6_NF_MATCH_RT=y -CONFIG_IP6_NF_TARGET_HL=y -CONFIG_IP6_NF_FILTER=y CONFIG_IP6_NF_TARGET_REJECT=y CONFIG_IP6_NF_TARGET_SYNPROXY=y -CONFIG_IP6_NF_MANGLE=y -CONFIG_IP6_NF_RAW=y -CONFIG_IP6_NF_SECURITY=y -CONFIG_IP6_NF_NAT=y -CONFIG_IP6_NF_TARGET_MASQUERADE=y CONFIG_IP6_NF_TARGET_NPT=y CONFIG_NF_TABLES_BRIDGE=y CONFIG_NFT_BRIDGE_REJECT=y CONFIG_BRIDGE_NF_EBTABLES=y -CONFIG_BRIDGE_EBT_BROUTE=y -CONFIG_BRIDGE_EBT_T_FILTER=y -CONFIG_BRIDGE_EBT_T_NAT=y CONFIG_BRIDGE_EBT_802_3=y CONFIG_BRIDGE_EBT_AMONG=y CONFIG_BRIDGE_EBT_ARP=y @@ -473,112 +330,214 @@ CONFIG_BRIDGE_EBT_REDIRECT=y CONFIG_BRIDGE_EBT_SNAT=y CONFIG_BRIDGE_EBT_LOG=y CONFIG_BRIDGE_EBT_NFLOG=y - -# --- Block devices --- -CONFIG_BLK_DEV=y -CONFIG_BLK_DEV_INTEGRITY=y -CONFIG_BLK_DEV_THROTTLING=y -CONFIG_BLK_CGROUP_IOLATENCY=y -CONFIG_PARTITION_ADVANCED=y -CONFIG_LDM_PARTITION=y -CONFIG_CMDLINE_PARTITION=y -CONFIG_BLK_DEV_LOOP=y -CONFIG_BLK_DEV_DRBD=m -CONFIG_BLK_DEV_NBD=y -CONFIG_BLK_DEV_RAM=y -CONFIG_BLK_DEV_RAM_SIZE=65536 -CONFIG_ATA_OVER_ETH=m -CONFIG_BLK_DEV_NVME=y - -# --- SCSI --- -CONFIG_SCSI=y -# CONFIG_SCSI_PROC_FS is not set -CONFIG_BLK_DEV_SD=y -CONFIG_BLK_DEV_SR=y -CONFIG_CHR_DEV_SG=y -CONFIG_ISCSI_TCP=m -CONFIG_SCSI_HPSA=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=y -CONFIG_SCSI_MPI3MR=m -CONFIG_SCSI_SMARTPQI=m -CONFIG_VMWARE_PVSCSI=y -CONFIG_XEN_SCSI_FRONTEND=y -CONFIG_SCSI_VIRTIO=y - -# --- ATA / SATA --- -CONFIG_ATA=y -# CONFIG_ATA_VERBOSE_ERROR is not set -# CONFIG_SATA_PMP is not set -CONFIG_SATA_AHCI=y -CONFIG_ATA_PIIX=y -CONFIG_SATA_MV=y -CONFIG_SATA_NV=y -CONFIG_SATA_PROMISE=y -CONFIG_SATA_SIL=y -CONFIG_SATA_SIS=y -CONFIG_SATA_SVW=y -CONFIG_SATA_ULI=y -CONFIG_SATA_VIA=y -CONFIG_SATA_VITESSE=y -CONFIG_ATA_GENERIC=y - -# --- Device mapper / MD --- -CONFIG_MD=y -CONFIG_BLK_DEV_MD=y -CONFIG_MD_LINEAR=y -CONFIG_MD_RAID0=y -CONFIG_MD_RAID1=y -CONFIG_MD_RAID10=y -CONFIG_MD_RAID456=y -CONFIG_MD_MULTIPATH=y -CONFIG_BLK_DEV_DM=y -CONFIG_DM_CRYPT=y -CONFIG_DM_SNAPSHOT=y -CONFIG_DM_THIN_PROVISIONING=y -CONFIG_DM_MULTIPATH=m -CONFIG_DM_MULTIPATH_QL=m -CONFIG_DM_MULTIPATH_ST=m - -# --- Fusion --- -CONFIG_FUSION=y -CONFIG_FUSION_SPI=y - -# --- Network drivers (built-in for reliable boot) --- -CONFIG_NETDEVICES=y -# CONFIG_NET_VENDOR_3COM is not set -# CONFIG_NET_VENDOR_ADAPTEC is not set -# CONFIG_NET_VENDOR_AGERE is not set -# CONFIG_NET_VENDOR_ALACRITECH is not set -# CONFIG_NET_VENDOR_ALTEON is not set -# CONFIG_NET_VENDOR_AMD is not set -# CONFIG_NET_VENDOR_AQUANTIA is not set -# CONFIG_NET_VENDOR_ARC is not set -# CONFIG_NET_VENDOR_ATHEROS is not set -# CONFIG_NET_VENDOR_CADENCE is not set -# CONFIG_NET_VENDOR_CAVIUM is not set -# CONFIG_NET_VENDOR_CHELSIO is not set -# CONFIG_NET_VENDOR_CORTINA is not set -# CONFIG_NET_VENDOR_DEC is not set -# CONFIG_NET_VENDOR_DLINK is not set -# CONFIG_NET_VENDOR_EMULEX is not set -# CONFIG_NET_VENDOR_EZCHIP is not set -# CONFIG_NET_VENDOR_HUAWEI is not set -# CONFIG_NET_VENDOR_I825XX is not set -# CONFIG_NET_VENDOR_MARVELL is not set -# CONFIG_NET_VENDOR_MICREL is not set -# CONFIG_NET_VENDOR_MICROCHIP is not set -# CONFIG_NET_VENDOR_MYRI is not set +CONFIG_IP_SCTP=m +CONFIG_L2TP=m +CONFIG_BRIDGE=y +CONFIG_BRIDGE_VLAN_FILTERING=y +CONFIG_VLAN_8021Q=y +CONFIG_NET_SCHED=y +CONFIG_NET_SCH_HTB=m +CONFIG_NET_SCH_HFSC=m +CONFIG_NET_SCH_PRIO=m +CONFIG_NET_SCH_MULTIQ=m +CONFIG_NET_SCH_RED=m +CONFIG_NET_SCH_SFB=m +CONFIG_NET_SCH_SFQ=m +CONFIG_NET_SCH_TEQL=m +CONFIG_NET_SCH_TBF=m +CONFIG_NET_SCH_GRED=m +CONFIG_NET_SCH_NETEM=m +CONFIG_NET_SCH_DRR=m +CONFIG_NET_SCH_MQPRIO=m +CONFIG_NET_SCH_CHOKE=m +CONFIG_NET_SCH_QFQ=m +CONFIG_NET_SCH_INGRESS=m +CONFIG_NET_CLS_BASIC=y +CONFIG_NET_CLS_ROUTE4=y +CONFIG_NET_CLS_FW=y +CONFIG_NET_CLS_U32=y +CONFIG_CLS_U32_PERF=y +CONFIG_CLS_U32_MARK=y +CONFIG_NET_CLS_FLOW=y +CONFIG_NET_CLS_CGROUP=y +CONFIG_NET_CLS_BPF=y +CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_CMP=y +CONFIG_NET_EMATCH_NBYTE=y +CONFIG_NET_EMATCH_U32=y +CONFIG_NET_EMATCH_META=y +CONFIG_NET_EMATCH_TEXT=y +CONFIG_NET_EMATCH_IPSET=y +CONFIG_NET_CLS_ACT=y +CONFIG_NET_ACT_POLICE=y +CONFIG_NET_ACT_GACT=y +CONFIG_GACT_PROB=y +CONFIG_NET_ACT_MIRRED=y +CONFIG_NET_ACT_NAT=y +CONFIG_NET_ACT_PEDIT=y +CONFIG_NET_ACT_SIMP=y +CONFIG_NET_ACT_SKBEDIT=y +CONFIG_NET_ACT_CSUM=y +CONFIG_NET_ACT_BPF=y +CONFIG_OPENVSWITCH=m +CONFIG_VSOCKETS=m +CONFIG_VIRTIO_VSOCKETS=m +CONFIG_HYPERV_VSOCKETS=m +CONFIG_NETLINK_DIAG=y +CONFIG_MPLS_ROUTING=m +CONFIG_MPLS_IPTUNNEL=m +CONFIG_NET_SWITCHDEV=y +CONFIG_CGROUP_NET_PRIO=y +# CONFIG_WIRELESS is not set +CONFIG_NET_9P=y +CONFIG_NET_9P_VIRTIO=y +CONFIG_PCI=y +CONFIG_PCIEPORTBUS=y +CONFIG_HOTPLUG_PCI_PCIE=y +CONFIG_PCI_STUB=y +CONFIG_PCI_IOV=y +# CONFIG_VGA_ARB is not set +CONFIG_HOTPLUG_PCI=y +CONFIG_HOTPLUG_PCI_ACPI=y +CONFIG_HOTPLUG_PCI_SHPC=y +CONFIG_PCI_HYPERV_INTERFACE=m +CONFIG_UEVENT_HELPER=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_CONNECTOR=y +CONFIG_DMI_SYSFS=y +CONFIG_SYSFB_SIMPLEFB=y +CONFIG_RESET_ATTACK_MITIGATION=y +# CONFIG_PNP_DEBUG_MESSAGES is not set +CONFIG_BLK_DEV_LOOP=y +CONFIG_BLK_DEV_DRBD=m +CONFIG_BLK_DEV_NBD=y +CONFIG_BLK_DEV_RAM=y +CONFIG_BLK_DEV_RAM_SIZE=65536 +CONFIG_ATA_OVER_ETH=m +CONFIG_VIRTIO_BLK=y +CONFIG_BLK_DEV_NVME=y +# CONFIG_SCSI_PROC_FS is not set +CONFIG_BLK_DEV_SD=y +CONFIG_BLK_DEV_SR=y +CONFIG_CHR_DEV_SG=y +CONFIG_ISCSI_TCP=m +CONFIG_SCSI_HPSA=y +CONFIG_MEGARAID_SAS=y +CONFIG_SCSI_MPT3SAS=y +CONFIG_SCSI_MPI3MR=m +CONFIG_SCSI_SMARTPQI=m +CONFIG_VMWARE_PVSCSI=y +CONFIG_XEN_SCSI_FRONTEND=y +CONFIG_SCSI_VIRTIO=y +CONFIG_ATA=y +# CONFIG_ATA_VERBOSE_ERROR is not set +# CONFIG_SATA_PMP is not set +CONFIG_SATA_AHCI=y +CONFIG_ATA_PIIX=y +CONFIG_SATA_MV=y +CONFIG_SATA_NV=y +CONFIG_SATA_PROMISE=y +CONFIG_SATA_SIL=y +CONFIG_SATA_SIS=y +CONFIG_SATA_SVW=y +CONFIG_SATA_ULI=y +CONFIG_SATA_VIA=y +CONFIG_SATA_VITESSE=y +CONFIG_ATA_GENERIC=y +CONFIG_MD=y +CONFIG_BLK_DEV_MD=y +CONFIG_MD_LINEAR=y +CONFIG_MD_RAID0=y +CONFIG_MD_RAID1=y +CONFIG_MD_RAID10=y +CONFIG_MD_RAID456=y +CONFIG_BLK_DEV_DM=y +CONFIG_DM_CRYPT=y +CONFIG_DM_SNAPSHOT=y +CONFIG_DM_THIN_PROVISIONING=y +CONFIG_DM_MULTIPATH=m +CONFIG_DM_MULTIPATH_QL=m +CONFIG_DM_MULTIPATH_ST=m +CONFIG_FUSION=y +CONFIG_FUSION_SPI=y +CONFIG_NETDEVICES=y +CONFIG_BONDING=m +CONFIG_DUMMY=m +CONFIG_NET_TEAM=m +CONFIG_NET_TEAM_MODE_BROADCAST=m +CONFIG_NET_TEAM_MODE_ROUNDROBIN=m +CONFIG_NET_TEAM_MODE_RANDOM=m +CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=m +CONFIG_NET_TEAM_MODE_LOADBALANCE=m +CONFIG_MACVLAN=y +CONFIG_MACVTAP=y +CONFIG_IPVLAN=y +CONFIG_VXLAN=y +CONFIG_GENEVE=m +CONFIG_NETCONSOLE=y +CONFIG_TUN=y +CONFIG_VETH=y +CONFIG_VIRTIO_NET=y +CONFIG_NLMON=y +# CONFIG_NET_VENDOR_3COM is not set +# CONFIG_NET_VENDOR_ADAPTEC is not set +# CONFIG_NET_VENDOR_AGERE is not set +# CONFIG_NET_VENDOR_ALACRITECH is not set +# CONFIG_NET_VENDOR_ALTEON is not set +CONFIG_ENA_ETHERNET=m +# CONFIG_NET_VENDOR_AMD is not set +# CONFIG_NET_VENDOR_AQUANTIA is not set +# CONFIG_NET_VENDOR_ARC is not set +# CONFIG_NET_VENDOR_ATHEROS is not set +CONFIG_CNIC=m +CONFIG_TIGON3=y +CONFIG_BNX2X=y +CONFIG_BNXT=y +# CONFIG_NET_VENDOR_CADENCE is not set +# CONFIG_NET_VENDOR_CAVIUM is not set +# CONFIG_NET_VENDOR_CHELSIO is not set +# CONFIG_NET_VENDOR_CORTINA is not set +# CONFIG_NET_VENDOR_DEC is not set +# CONFIG_NET_VENDOR_DLINK is not set +# CONFIG_NET_VENDOR_EMULEX is not set +# CONFIG_NET_VENDOR_EZCHIP is not set +CONFIG_GVE=m +# CONFIG_NET_VENDOR_HUAWEI is not set +# CONFIG_NET_VENDOR_I825XX is not set +CONFIG_E1000=y +CONFIG_E1000E=y +CONFIG_IGB=y +CONFIG_IGBVF=y +CONFIG_IXGBE=y +CONFIG_IXGBEVF=y +CONFIG_I40E=y +CONFIG_I40EVF=y +CONFIG_ICE=y +CONFIG_IGC=y +# CONFIG_NET_VENDOR_MARVELL is not set +CONFIG_MLX4_EN=y +CONFIG_MLX5_CORE=m +CONFIG_MLX5_CORE_EN=y +# CONFIG_NET_VENDOR_MICREL is not set +# CONFIG_NET_VENDOR_MICROCHIP is not set +# CONFIG_NET_VENDOR_MYRI is not set # CONFIG_NET_VENDOR_NI is not set # CONFIG_NET_VENDOR_NATSEMI is not set # CONFIG_NET_VENDOR_NETERION is not set +CONFIG_NFP=m # CONFIG_NET_VENDOR_NVIDIA is not set # CONFIG_NET_VENDOR_OKI is not set # CONFIG_NET_VENDOR_PACKET_ENGINES is not set +CONFIG_IONIC=y # CONFIG_NET_VENDOR_QLOGIC is not set # CONFIG_NET_VENDOR_BROCADE is not set # CONFIG_NET_VENDOR_QUALCOMM is not set # CONFIG_NET_VENDOR_RDC is not set +CONFIG_8139CP=y +CONFIG_8139TOO=y +CONFIG_R8169=y # CONFIG_NET_VENDOR_RENESAS is not set # CONFIG_NET_VENDOR_ROCKER is not set # CONFIG_NET_VENDOR_SAMSUNG is not set @@ -594,35 +553,17 @@ CONFIG_NETDEVICES=y # CONFIG_NET_VENDOR_TI is not set # CONFIG_NET_VENDOR_VIA is not set # CONFIG_NET_VENDOR_WIZNET is not set -CONFIG_E1000=y -CONFIG_E1000E=y -CONFIG_IGB=y -CONFIG_IGBVF=y -CONFIG_IXGBE=y -CONFIG_IXGBEVF=y -CONFIG_I40E=y -CONFIG_I40EVF=y -CONFIG_ICE=y -CONFIG_IGC=y -CONFIG_MLX4_EN=y -CONFIG_MLX5_CORE=y -CONFIG_MLX5_CORE_EN=y -CONFIG_BNXT=y -CONFIG_CNIC=m -CONFIG_TIGON3=y -CONFIG_BNX2X=y -CONFIG_R8169=y -CONFIG_8139CP=y -CONFIG_8139TOO=y -CONFIG_VMXNET3=y -CONFIG_HYPERV_NET=y -CONFIG_ENA_ETHERNET=m -CONFIG_GVE=m -CONFIG_NFP=m -CONFIG_IONIC=y -CONFIG_VIRTIO_NET=y - -# --- USB network --- +CONFIG_PPP=m +CONFIG_PPP_BSDCOMP=m +CONFIG_PPP_DEFLATE=m +CONFIG_PPP_FILTER=y +CONFIG_PPP_MPPE=m +CONFIG_PPP_MULTILINK=y +CONFIG_PPPOE=m +CONFIG_PPTP=m +CONFIG_PPPOL2TP=m +CONFIG_PPP_ASYNC=m +CONFIG_PPP_SYNC_TTY=m CONFIG_USB_CATC=m CONFIG_USB_KAWETH=m CONFIG_USB_PEGASUS=m @@ -637,37 +578,72 @@ CONFIG_USB_NET_CDC_NCM=m CONFIG_USB_NET_NET1080=m CONFIG_USB_NET_CDC_SUBSET=m CONFIG_USB_NET_ZAURUS=m - -# --- WAN --- +# CONFIG_WLAN is not set CONFIG_WAN=y CONFIG_HDLC=m CONFIG_HDLC_CISCO=m - -# --- PPP --- -CONFIG_PPP=m -CONFIG_PPP_BSDCOMP=m -CONFIG_PPP_DEFLATE=m -CONFIG_PPP_FILTER=y -CONFIG_PPP_MPPE=m -CONFIG_PPP_MULTILINK=y -CONFIG_PPPOE=m -CONFIG_PPTP=m -CONFIG_PPPOL2TP=m -CONFIG_PPP_ASYNC=m -CONFIG_PPP_SYNC_TTY=m - -# --- USB --- -CONFIG_USB_SUPPORT=y +CONFIG_VMXNET3=y +CONFIG_HYPERV_NET=y +CONFIG_INPUT_FF_MEMLESS=y +CONFIG_INPUT_SPARSEKMAP=y +CONFIG_INPUT_MOUSEDEV=y +CONFIG_INPUT_MOUSEDEV_PSAUX=y +CONFIG_INPUT_EVDEV=y +# CONFIG_INPUT_MOUSE is not set +CONFIG_INPUT_MISC=y +CONFIG_INPUT_PCSPKR=y +CONFIG_INPUT_ATLAS_BTNS=y +CONFIG_INPUT_UINPUT=y +CONFIG_SERIO_PCIPS2=y +CONFIG_SERIO_RAW=y +# CONFIG_LEGACY_PTYS is not set +CONFIG_SERIAL_8250=y +CONFIG_SERIAL_8250_CONSOLE=y +CONFIG_SERIAL_8250_NR_UARTS=32 +# CONFIG_SERIAL_8250_MID is not set +CONFIG_SERIAL_DEV_BUS=y +CONFIG_TTY_PRINTK=y +CONFIG_VIRTIO_CONSOLE=y +CONFIG_IPMI_HANDLER=y +CONFIG_IPMI_DEVICE_INTERFACE=y +CONFIG_IPMI_SI=y +CONFIG_IPMI_POWEROFF=y +CONFIG_HW_RANDOM=y +CONFIG_HW_RANDOM_TIMERIOMEM=y +CONFIG_HW_RANDOM_VIRTIO=y +CONFIG_NVRAM=y +CONFIG_HPET=y +CONFIG_HANGCHECK_TIMER=y +CONFIG_TCG_TIS_I2C_ATMEL=m +CONFIG_TCG_TIS_I2C_INFINEON=m +CONFIG_TCG_TIS_I2C_NUVOTON=m +CONFIG_TCG_NSC=m +CONFIG_TCG_ATMEL=m +CONFIG_TCG_INFINEON=m +CONFIG_TCG_XEN=m +CONFIG_TCG_VTPM_PROXY=m +CONFIG_TCG_TIS_ST33ZP24_I2C=m +CONFIG_I2C_CHARDEV=y +CONFIG_I2C_MUX=y +# CONFIG_THERMAL_HWMON is not set +CONFIG_LPC_ICH=y +CONFIG_LPC_SCH=y +CONFIG_MFD_INTEL_LPSS_ACPI=y +CONFIG_MFD_INTEL_LPSS_PCI=y +CONFIG_MFD_SM501=y +CONFIG_MFD_VX855=y +CONFIG_DRM=y +CONFIG_DRM_SIMPLEDRM=y +CONFIG_DRM_I915=y +CONFIG_FB=y +CONFIG_FB_EFI=y CONFIG_USB=y CONFIG_USB_XHCI_HCD=y CONFIG_USB_EHCI_HCD=y CONFIG_USB_OHCI_HCD=y CONFIG_USB_UHCI_HCD=y -CONFIG_USB_XHCI_PCI=y -CONFIG_USB_UAS=y CONFIG_USB_STORAGE=y - -# --- USB serial (BMC/console access) --- +CONFIG_USB_UAS=y CONFIG_USB_SERIAL=y CONFIG_USB_SERIAL_CONSOLE=y CONFIG_USB_SERIAL_GENERIC=y @@ -677,33 +653,39 @@ CONFIG_USB_SERIAL_FTDI_SIO=y CONFIG_USB_SERIAL_PL2303=y CONFIG_USB_SERIAL_TI=y CONFIG_USB_SERIAL_OPTION=y - -# --- Input --- -CONFIG_INPUT_FF_MEMLESS=y -CONFIG_INPUT_SPARSEKMAP=y -CONFIG_INPUT_MOUSEDEV=y -CONFIG_INPUT_MOUSEDEV_PSAUX=y -CONFIG_INPUT_EVDEV=y -# CONFIG_INPUT_MOUSE is not set -CONFIG_INPUT_MISC=y -CONFIG_INPUT_PCSPKR=y -CONFIG_INPUT_ATLAS_BTNS=y -CONFIG_INPUT_UINPUT=y -CONFIG_SERIO_PCIPS2=y -CONFIG_SERIO_RAW=y -# CONFIG_LEGACY_PTYS is not set -# CONFIG_INPUT_JOYSTICK is not set -# CONFIG_INPUT_TOUCHSCREEN is not set - -# --- MMC --- CONFIG_MMC=y CONFIG_MMC_SDHCI=y CONFIG_MMC_SDHCI_PCI=y # CONFIG_MMC_RICOH_MMC is not set CONFIG_MMC_SDHCI_ACPI=y CONFIG_MMC_SDHCI_PLTFM=y - -# --- Filesystems --- +CONFIG_RTC_CLASS=y +CONFIG_DMADEVICES=y +CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_BALLOON=y +CONFIG_VIRTIO_INPUT=y +CONFIG_VIRTIO_MMIO=y +CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y +CONFIG_VHOST_NET=m +CONFIG_VHOST_VSOCK=m +CONFIG_HYPERV=y +CONFIG_HYPERV_UTILS=y +CONFIG_HYPERV_BALLOON=y +# CONFIG_XEN_BACKEND is not set +CONFIG_XEN_GNTDEV=y +CONFIG_XEN_GRANT_DEV_ALLOC=y +CONFIG_XEN_PVCALLS_FRONTEND=y +CONFIG_XEN_ACPI_PROCESSOR=y +# CONFIG_XEN_SYMS is not set +CONFIG_INTEL_IPS=y +CONFIG_AMD_IOMMU=y +CONFIG_INTEL_IOMMU=y +CONFIG_IRQ_REMAP=y +CONFIG_PM_DEVFREQ=y +CONFIG_DEVFREQ_GOV_SIMPLE_ONDEMAND=y +CONFIG_RESET_CONTROLLER=y +CONFIG_DEV_DAX=y +CONFIG_DEV_DAX_PMEM=m CONFIG_EXT4_FS=y CONFIG_EXT4_FS_POSIX_ACL=y CONFIG_EXT4_FS_SECURITY=y @@ -717,34 +699,29 @@ CONFIG_FS_ENCRYPTION=y CONFIG_FANOTIFY=y CONFIG_QUOTA=y CONFIG_QUOTA_NETLINK_INTERFACE=y -CONFIG_VFAT_FS=y +CONFIG_FUSE_FS=y +CONFIG_CUSE=y +CONFIG_OVERLAY_FS=y +CONFIG_FSCACHE_STATS=y +CONFIG_CACHEFILES=y +CONFIG_ISO9660_FS=y +CONFIG_JOLIET=y +CONFIG_ZISOFS=y +CONFIG_UDF_FS=y CONFIG_MSDOS_FS=y +CONFIG_VFAT_FS=y CONFIG_FAT_DEFAULT_IOCHARSET="utf8" CONFIG_NTFS_FS=m -CONFIG_TMPFS=y -CONFIG_TMPFS_POSIX_ACL=y -CONFIG_TMPFS_XATTR=y -CONFIG_PROC_FS=y CONFIG_PROC_KCORE=y -CONFIG_SYSFS=y +CONFIG_TMPFS_POSIX_ACL=y +CONFIG_HUGETLBFS=y CONFIG_EFIVAR_FS=y -CONFIG_OVERLAY_FS=y CONFIG_SQUASHFS=y CONFIG_SQUASHFS_XATTR=y CONFIG_SQUASHFS_LZ4=y CONFIG_SQUASHFS_LZO=y CONFIG_SQUASHFS_XZ=y -CONFIG_FUSE_FS=y -CONFIG_CUSE=y -CONFIG_ISO9660_FS=y -CONFIG_JOLIET=y -CONFIG_ZISOFS=y -CONFIG_UDF_FS=y -CONFIG_FSCACHE=y -CONFIG_FSCACHE_STATS=y -CONFIG_CACHEFILES=y CONFIG_NFS_FS=m -# CONFIG_NFS_V2 is not set CONFIG_NFS_V4=m CONFIG_NFS_V4_1=y CONFIG_NFS_V4_2=y @@ -763,97 +740,17 @@ CONFIG_9P_FS=y CONFIG_9P_FSCACHE=y CONFIG_9P_FS_POSIX_ACL=y CONFIG_9P_FS_SECURITY=y - -# --- NLS --- CONFIG_NLS_CODEPAGE_437=y CONFIG_NLS_ASCII=y CONFIG_NLS_ISO8859_1=y CONFIG_NLS_UTF8=y - -# --- Virtio (built-in for reliable boot) --- -CONFIG_VIRTIO=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BLK=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_INPUT=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y -CONFIG_HW_RANDOM_VIRTIO=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m - -# --- Console / Serial --- -CONFIG_VT=y -CONFIG_VT_CONSOLE=y -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_SERIAL_8250_NR_UARTS=32 -# CONFIG_SERIAL_8250_MID is not set -CONFIG_SERIAL_DEV_BUS=y -CONFIG_TTY_PRINTK=y -CONFIG_HW_CONSOLE=y -CONFIG_VGA_CONSOLE=y -CONFIG_DUMMY_CONSOLE=y - -# --- IPMI --- -CONFIG_IPMI_HANDLER=y -CONFIG_IPMI_DMI_DECODE=y -CONFIG_IPMI_DEVICE_INTERFACE=y -CONFIG_IPMI_SI=y -CONFIG_IPMI_POWEROFF=y - -# --- Hardware RNG --- -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_TIMERIOMEM=y - -# --- I2C --- -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MUX=y - -# --- TPM --- -CONFIG_TCG_TIS_I2C_ATMEL=m -CONFIG_TCG_TIS_I2C_INFINEON=m -CONFIG_TCG_TIS_I2C_NUVOTON=m -CONFIG_TCG_NSC=m -CONFIG_TCG_ATMEL=m -CONFIG_TCG_INFINEON=m -CONFIG_TCG_XEN=m -CONFIG_TCG_VTPM_PROXY=m -CONFIG_TCG_TIS_ST33ZP24_I2C=m - -# --- Platform / MFD --- -CONFIG_NVRAM=y -CONFIG_HPET=y -CONFIG_HANGCHECK_TIMER=y -CONFIG_LPC_ICH=y -CONFIG_LPC_SCH=y -CONFIG_MFD_INTEL_LPSS_ACPI=y -CONFIG_MFD_INTEL_LPSS_PCI=y -CONFIG_MFD_SM501=y -CONFIG_MFD_VX855=y -# CONFIG_THERMAL_HWMON is not set - -# --- RTC --- -CONFIG_RTC_CLASS=y - -# --- DMA --- -CONFIG_DMADEVICES=y - -# --- Security --- -CONFIG_SECCOMP=y -CONFIG_SECCOMP_FILTER=y -CONFIG_SECURITY=y -CONFIG_SECURITY_NETWORK=y -CONFIG_SECURITY_NETWORK_XFRM=y -CONFIG_SECURITY_PATH=y -CONFIG_SECURITY_DMESG_RESTRICT=y -CONFIG_KEYS=y CONFIG_PERSISTENT_KEYRINGS=y CONFIG_TRUSTED_KEYS=y CONFIG_KEY_DH_OPERATIONS=y -CONFIG_HARDENED_USERCOPY=y -CONFIG_FORTIFY_SOURCE=y +CONFIG_SECURITY_DMESG_RESTRICT=y +CONFIG_SECURITY=y +CONFIG_SECURITY_NETWORK=y +CONFIG_SECURITY_NETWORK_XFRM=y CONFIG_STATIC_USERMODEHELPER=y CONFIG_SECURITY_YAMA=y CONFIG_INTEGRITY_SIGNATURE=y @@ -864,14 +761,10 @@ CONFIG_IMA_READ_POLICY=y CONFIG_IMA_APPRAISE=y CONFIG_EVM=y CONFIG_LSM="yama,loadpin,safesetid,integrity" - -# --- Crypto --- -CONFIG_CRYPTO=y +CONFIG_FORTIFY_SOURCE=y +CONFIG_HARDENED_USERCOPY=y +CONFIG_BUG_ON_DATA_CORRUPTION=y CONFIG_CRYPTO_USER=y -CONFIG_CRYPTO_AES=y -CONFIG_CRYPTO_SHA256=y -CONFIG_CRYPTO_SHA512=y -CONFIG_CRYPTO_XTS=y CONFIG_CRYPTO_ANUBIS=y CONFIG_CRYPTO_BLOWFISH=y CONFIG_CRYPTO_CAMELLIA=y @@ -882,7 +775,6 @@ CONFIG_CRYPTO_SEED=y CONFIG_CRYPTO_TEA=y CONFIG_CRYPTO_TWOFISH=y CONFIG_CRYPTO_ARC4=y -CONFIG_CRYPTO_KEYWRAP=y CONFIG_CRYPTO_LRW=y CONFIG_CRYPTO_PCBC=y CONFIG_CRYPTO_CHACHA20POLY1305=y @@ -890,7 +782,6 @@ CONFIG_CRYPTO_SEQIV=y CONFIG_CRYPTO_ECHAINIV=y CONFIG_CRYPTO_MICHAEL_MIC=y CONFIG_CRYPTO_RMD160=y -CONFIG_CRYPTO_VMAC=y CONFIG_CRYPTO_WP512=y CONFIG_CRYPTO_XCBC=y CONFIG_CRYPTO_CRC32=y @@ -903,8 +794,6 @@ CONFIG_CRYPTO_USER_API_HASH=y CONFIG_CRYPTO_USER_API_SKCIPHER=y CONFIG_CRYPTO_USER_API_RNG=y CONFIG_CRYPTO_USER_API_AEAD=y - -# --- x86_64 hardware crypto acceleration --- CONFIG_CRYPTO_AES_NI_INTEL=y CONFIG_CRYPTO_BLOWFISH_X86_64=y CONFIG_CRYPTO_CAMELLIA_AESNI_AVX2_X86_64=y @@ -914,119 +803,20 @@ CONFIG_CRYPTO_DES3_EDE_X86_64=y CONFIG_CRYPTO_SERPENT_SSE2_X86_64=y CONFIG_CRYPTO_SERPENT_AVX2_X86_64=y CONFIG_CRYPTO_TWOFISH_AVX_X86_64=y -CONFIG_CRYPTO_CHACHA20_X86_64=y -CONFIG_CRYPTO_POLY1305_X86_64=y -CONFIG_CRYPTO_SHA1_SSSE3=y -CONFIG_CRYPTO_SHA256_SSSE3=y -CONFIG_CRYPTO_SHA512_SSSE3=y CONFIG_CRYPTO_GHASH_CLMUL_NI_INTEL=y -CONFIG_CRYPTO_CRC32C_INTEL=y -CONFIG_CRYPTO_CRC32_PCLMUL=y CONFIG_CRYPTO_DEV_PADLOCK=y CONFIG_CRYPTO_DEV_PADLOCK_AES=y CONFIG_CRYPTO_DEV_PADLOCK_SHA=y CONFIG_CRYPTO_DEV_VIRTIO=m CONFIG_PKCS7_MESSAGE_PARSER=y - -# --- BPF --- -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y - -# --- PCI --- -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_HOTPLUG_PCI_PCIE=y -CONFIG_PCI_STUB=y -CONFIG_PCI_IOV=y -# CONFIG_VGA_ARB is not set -CONFIG_HOTPLUG_PCI=y -CONFIG_HOTPLUG_PCI_ACPI=y -CONFIG_HOTPLUG_PCI_SHPC=y -CONFIG_PCI_HYPERV_INTERFACE=m - -# --- IOMMU --- -CONFIG_AMD_IOMMU=y -CONFIG_INTEL_IOMMU=y -CONFIG_IRQ_REMAP=y - -# --- Xen --- -# CONFIG_XEN_BACKEND is not set -CONFIG_XEN_GNTDEV=y -CONFIG_XEN_GRANT_DEV_ALLOC=y -CONFIG_XEN_PVCALLS_FRONTEND=y -CONFIG_XEN_ACPI_PROCESSOR=y -# CONFIG_XEN_SYMS is not set - -# --- Intel platform --- -CONFIG_INTEL_IPS=y - -# --- DEVFREQ --- -CONFIG_PM_DEVFREQ=y -CONFIG_DEVFREQ_GOV_SIMPLE_ONDEMAND=y - -# --- Reset --- -CONFIG_RESET_CONTROLLER=y - -# --- DAX --- -CONFIG_DEV_DAX=y -CONFIG_DEV_DAX_PMEM=m - -# --- Misc --- -CONFIG_FW_LOADER=y -CONFIG_PRINTK=y CONFIG_PRINTK_TIME=y -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_EARLY_PRINTK=y -CONFIG_PANIC_TIMEOUT=10 -CONFIG_PANIC_ON_OOPS=y -CONFIG_MAGIC_SYSRQ=y -CONFIG_UEVENT_HELPER=y -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_CONNECTOR=y -CONFIG_DMI_SYSFS=y -CONFIG_SYSFB_SIMPLEFB=y -CONFIG_RESET_ATTACK_MITIGATION=y -# CONFIG_PNP_DEBUG_MESSAGES is not set -CONFIG_HYPERV=y -CONFIG_HYPERV_UTILS=y -CONFIG_HYPERV_BALLOON=y - -# --- Module compression --- -CONFIG_MODULE_COMPRESS_ZSTD=y -CONFIG_MODULE_DECOMPRESS=y - -# --- EFI boot support --- -CONFIG_EFI=y -CONFIG_EFI_STUB=y -CONFIG_EFI_MIXED=y - -# --- Framebuffer/Console (required for EFI boot video output) --- -CONFIG_DRM=y -CONFIG_DRM_I915=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_FRAMEBUFFER_CONSOLE=y - -# --- Debugging / Diagnostics --- CONFIG_FRAME_WARN=1024 -CONFIG_BUG_ON_DATA_CORRUPTION=y +CONFIG_MAGIC_SYSRQ=y +CONFIG_PANIC_ON_OOPS=y +CONFIG_PANIC_TIMEOUT=10 CONFIG_HARDLOCKUP_DETECTOR=y CONFIG_WQ_WATCHDOG=y CONFIG_DEBUG_NOTIFIERS=y CONFIG_RCU_CPU_STALL_TIMEOUT=60 # CONFIG_RCU_TRACE is not set CONFIG_IO_STRICT_DEVMEM=y - -# --- Disable unnecessary subsystems --- -# CONFIG_SOUND is not set -# CONFIG_WIRELESS is not set -# CONFIG_WLAN is not set -# CONFIG_BLUETOOTH is not set -# CONFIG_NFC is not set -# CONFIG_INFINIBAND is not set -# CONFIG_MEDIA_SUPPORT is not set From 99f870eb819a8f6ba595f2c9ac672e944f30da78 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 15 Apr 2026 22:29:41 +0200 Subject: [PATCH 090/143] kernel: 6.18.y.amd64: `CONFIG_MODULE_COMPRESS_ZSTD=y` Signed-off-by: Ricardo Pardini --- kernel.configs/6.18.y.amd64 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kernel.configs/6.18.y.amd64 b/kernel.configs/6.18.y.amd64 index 9acb6e7c..36f25819 100644 --- a/kernel.configs/6.18.y.amd64 +++ b/kernel.configs/6.18.y.amd64 @@ -87,6 +87,8 @@ CONFIG_KVM_AMD=m CONFIG_KPROBES=y CONFIG_MODULES=y CONFIG_MODULE_UNLOAD=y +CONFIG_MODULE_COMPRESS=y +CONFIG_MODULE_COMPRESS_ZSTD=y CONFIG_BLK_DEV_INTEGRITY=y CONFIG_BLK_DEV_THROTTLING=y CONFIG_BLK_CGROUP_IOLATENCY=y From b3e193eb68f1a6cebca2fb7e7a6adfe04a8328ad Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 13 Apr 2026 20:57:22 +0200 Subject: [PATCH 091/143] kernel: 6.18.y.arm64: defconfig standard Signed-off-by: Ricardo Pardini --- kernel.configs/6.18.y.arm64 | 602 ++++++++++++++---------------------- 1 file changed, 224 insertions(+), 378 deletions(-) diff --git a/kernel.configs/6.18.y.arm64 b/kernel.configs/6.18.y.arm64 index 795c91e6..5f7cff8f 100644 --- a/kernel.configs/6.18.y.arm64 +++ b/kernel.configs/6.18.y.arm64 @@ -1,25 +1,3 @@ -# CaptainOS kernel defconfig for arm64 (aarch64) -# Minimal config for bare-metal provisioning with container/network support. -# Based on 6.12.y config with targeted additions for CaptainOS use case, adapted for kernel 6.18. -# -# Generate a full .config from this: -# cp 6.18.y.arm64 .config && make ARCH=arm64 olddefconfig - -# --- Architecture --- -CONFIG_ARM64=y -CONFIG_SMP=y -CONFIG_NR_CPUS=128 -CONFIG_NUMA=y -CONFIG_ARM64_VA_BITS_48=y -CONFIG_SCHED_MC=y -CONFIG_SCHED_SMT=y -CONFIG_HZ_1000=y -CONFIG_XEN=y -CONFIG_COMPAT=y -CONFIG_RANDOMIZE_BASE=y -CONFIG_ARM64_ACPI_PARKING_PROTOCOL=y - -# --- General --- CONFIG_LOCALVERSION="-captainos" CONFIG_DEFAULT_HOSTNAME="captainos" CONFIG_SYSVIPC=y @@ -27,52 +5,74 @@ CONFIG_POSIX_MQUEUE=y CONFIG_AUDIT=y CONFIG_NO_HZ_IDLE=y CONFIG_HIGH_RES_TIMERS=y +CONFIG_BPF_SYSCALL=y +CONFIG_BPF_JIT=y +CONFIG_BPF_JIT_ALWAYS_ON=y CONFIG_PREEMPT_VOLUNTARY=y CONFIG_BSD_PROCESS_ACCT=y -CONFIG_KEXEC=y -CONFIG_KEXEC_FILE=y -CONFIG_CC_OPTIMIZE_FOR_SIZE=y CONFIG_CGROUPS=y -CONFIG_CGROUP_FREEZER=y +CONFIG_MEMCG=y +CONFIG_BLK_CGROUP=y +CONFIG_CGROUP_SCHED=y +CONFIG_CFS_BANDWIDTH=y +CONFIG_RT_GROUP_SCHED=y CONFIG_CGROUP_PIDS=y +CONFIG_CGROUP_RDMA=y +CONFIG_CGROUP_FREEZER=y +CONFIG_CGROUP_HUGETLB=y +CONFIG_CPUSETS=y CONFIG_CGROUP_DEVICE=y CONFIG_CGROUP_CPUACCT=y -CONFIG_CGROUP_PERF=y CONFIG_CGROUP_BPF=y CONFIG_CGROUP_MISC=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CGROUP_RDMA=y -CONFIG_CPUSETS=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_BLK_CGROUP_IOLATENCY=y -CONFIG_BLK_DEV_THROTTLING=y -CONFIG_CFS_BANDWIDTH=y -CONFIG_RT_GROUP_SCHED=y -CONFIG_CGROUP_SCHED=y -CONFIG_CGROUP_NET_PRIO=y -CONFIG_NAMESPACES=y CONFIG_USER_NS=y -CONFIG_PID_NS=y -CONFIG_NET_NS=y -CONFIG_UTS_NS=y -CONFIG_IPC_NS=y CONFIG_CHECKPOINT_RESTORE=y -# Memory management -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_KSM=y +CONFIG_BLK_DEV_INITRD=y +CONFIG_CC_OPTIMIZE_FOR_SIZE=y +CONFIG_KEXEC=y +CONFIG_KEXEC_FILE=y +CONFIG_ARCH_SUNXI=y +CONFIG_ARCH_EXYNOS=y +CONFIG_ARCH_HISI=y +CONFIG_ARCH_MESON=y +CONFIG_ARCH_MVEBU=y +CONFIG_ARCH_QCOM=y +CONFIG_ARCH_ROCKCHIP=y +CONFIG_ARCH_SEATTLE=y +CONFIG_ARCH_SYNQUACER=y +CONFIG_ARCH_TEGRA=y +CONFIG_ARCH_THUNDER=y +CONFIG_ARCH_THUNDER2=y +CONFIG_ARCH_UNIPHIER=y +CONFIG_ARCH_VEXPRESS=y +CONFIG_ARCH_XGENE=y +CONFIG_ARM64_VA_BITS_48=y +CONFIG_NR_CPUS=128 +CONFIG_NUMA=y +CONFIG_HZ_1000=y +CONFIG_XEN=y +CONFIG_COMPAT=y +CONFIG_RANDOMIZE_BASE=y +CONFIG_ARM64_ACPI_PARKING_PROTOCOL=y +CONFIG_ACPI=y +CONFIG_ACPI_DOCK=y +CONFIG_ACPI_IPMI=m +CONFIG_ACPI_APEI=y +CONFIG_ACPI_APEI_GHES=y +CONFIG_VIRTUALIZATION=y +CONFIG_KVM=y +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_BLK_DEV_THROTTLING=y +CONFIG_BLK_CGROUP_IOLATENCY=y CONFIG_MEMORY_HOTPLUG=y CONFIG_MEMORY_HOTREMOVE=y -CONFIG_HUGETLBFS=y - -# --- Init / Initramfs --- -CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="" - -# --- Networking --- +CONFIG_KSM=y +CONFIG_TRANSPARENT_HUGEPAGE=y CONFIG_NET=y CONFIG_PACKET=y CONFIG_UNIX=y +CONFIG_XDP_SOCKETS=y CONFIG_INET=y CONFIG_IP_MULTICAST=y CONFIG_IP_ADVANCED_ROUTER=y @@ -85,21 +85,49 @@ CONFIG_NET_IPIP=m CONFIG_NET_IPGRE_DEMUX=m CONFIG_NET_IPGRE=m CONFIG_SYN_COOKIES=y -CONFIG_IPV6=y +CONFIG_NETFILTER=y +CONFIG_BRIDGE_NETFILTER=y +CONFIG_NF_CONNTRACK=y +CONFIG_NF_TABLES=y +CONFIG_NF_TABLES_INET=y +CONFIG_NFT_CT=y +CONFIG_NFT_MASQ=y +CONFIG_NFT_NAT=y +CONFIG_NFT_COMPAT=y +CONFIG_NETFILTER_XT_SET=y +CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y +CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y +CONFIG_NETFILTER_XT_MATCH_COMMENT=y +CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y +CONFIG_NETFILTER_XT_MATCH_MARK=y +CONFIG_IP_SET=y +CONFIG_IP_SET_BITMAP_IP=y +CONFIG_IP_SET_BITMAP_PORT=y +CONFIG_IP_SET_HASH_IP=y +CONFIG_IP_SET_HASH_IPPORT=y +CONFIG_IP_SET_HASH_IPPORTNET=y +CONFIG_IP_SET_HASH_NET=y +CONFIG_IP_SET_HASH_NETPORT=y +CONFIG_IP_SET_LIST_SET=y +CONFIG_IP_VS=y +CONFIG_IP_VS_IPV6=y +CONFIG_IP_VS_PROTO_TCP=y +CONFIG_IP_VS_PROTO_UDP=y +CONFIG_IP_VS_RR=y +CONFIG_IP_VS_WRR=y +CONFIG_IP_VS_SH=y +CONFIG_IP_VS_NFCT=y +CONFIG_IP_NF_IPTABLES=y +CONFIG_IP6_NF_IPTABLES=y +CONFIG_BRIDGE_NF_EBTABLES=y +CONFIG_BRIDGE_EBT_ARP=y +CONFIG_BRIDGE_EBT_IP=y +CONFIG_BRIDGE_EBT_IP6=y +CONFIG_BRIDGE_EBT_MARK=y +CONFIG_BRIDGE_EBT_VLAN=y +CONFIG_BRIDGE_EBT_MARK_T=y CONFIG_BRIDGE=y CONFIG_VLAN_8021Q=m -CONFIG_BONDING=m -CONFIG_TUN=y -CONFIG_VETH=y -CONFIG_MACVLAN=y -CONFIG_MACVTAP=y -CONFIG_IPVLAN=y -CONFIG_TAP=y -CONFIG_DUMMY=m -CONFIG_VXLAN=y -CONFIG_GENEVE=m -CONFIG_NLMON=y -# Traffic control / QoS (needed by CNI plugins) CONFIG_NET_SCHED=y CONFIG_NET_SCH_HTB=m CONFIG_NET_SCH_PRIO=m @@ -113,165 +141,151 @@ CONFIG_NET_CLS_U32=y CONFIG_NET_CLS_CGROUP=y CONFIG_NET_CLS_BPF=y CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_U32=y CONFIG_NET_CLS_ACT=y CONFIG_NET_ACT_POLICE=y CONFIG_NET_ACT_GACT=y CONFIG_NET_ACT_MIRRED=y CONFIG_NET_ACT_BPF=y -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_U32=y - -# --- Netfilter --- -CONFIG_NETFILTER=y -CONFIG_NETFILTER_ADVANCED=y -CONFIG_NF_CONNTRACK=y -CONFIG_NF_NAT=y -CONFIG_NF_TABLES=y -CONFIG_NF_TABLES_INET=y -CONFIG_NF_TABLES_IPV4=y -CONFIG_NF_TABLES_IPV6=y -CONFIG_NFT_NAT=y -CONFIG_NFT_MASQ=y -CONFIG_NFT_CT=y -CONFIG_NFT_COMPAT=y -CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y -CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y -CONFIG_NETFILTER_XT_MATCH_COMMENT=y -CONFIG_NETFILTER_XT_MATCH_MARK=y -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y -CONFIG_IP_NF_IPTABLES=y -CONFIG_IP_NF_FILTER=y -CONFIG_IP_NF_NAT=y -CONFIG_IP6_NF_IPTABLES=y -CONFIG_IP6_NF_FILTER=y -CONFIG_IP6_NF_NAT=y -CONFIG_BRIDGE_NETFILTER=y -CONFIG_BRIDGE_NF_EBTABLES=y -CONFIG_BRIDGE_EBT_BROUTE=y -CONFIG_BRIDGE_EBT_T_FILTER=y -CONFIG_BRIDGE_EBT_T_NAT=y -CONFIG_BRIDGE_EBT_ARP=y -CONFIG_BRIDGE_EBT_IP=y -CONFIG_BRIDGE_EBT_IP6=y -CONFIG_BRIDGE_EBT_MARK=y -CONFIG_BRIDGE_EBT_MARK_T=y -CONFIG_BRIDGE_EBT_VLAN=y -# IPVS (container load balancing / kube-proxy) -CONFIG_IP_VS=y -CONFIG_IP_VS_IPV6=y -CONFIG_IP_VS_PROTO_TCP=y -CONFIG_IP_VS_PROTO_UDP=y -CONFIG_IP_VS_RR=y -CONFIG_IP_VS_WRR=y -CONFIG_IP_VS_SH=y -CONFIG_IP_VS_NFCT=y -# IP sets (used by iptables/nftables for efficient matching) -CONFIG_IP_SET=y -CONFIG_IP_SET_HASH_IP=y -CONFIG_IP_SET_HASH_IPPORT=y -CONFIG_IP_SET_HASH_IPPORTNET=y -CONFIG_IP_SET_HASH_NET=y -CONFIG_IP_SET_HASH_NETPORT=y -CONFIG_IP_SET_BITMAP_IP=y -CONFIG_IP_SET_BITMAP_PORT=y -CONFIG_IP_SET_LIST_SET=y -CONFIG_NETFILTER_XT_SET=y - -# --- Block devices --- -CONFIG_BLK_DEV=y +CONFIG_CGROUP_NET_PRIO=y +# CONFIG_WIRELESS is not set +CONFIG_PCI=y +CONFIG_PCIEPORTBUS=y +CONFIG_PCI_IOV=y +CONFIG_PCI_PASID=y +CONFIG_HOTPLUG_PCI=y +CONFIG_HOTPLUG_PCI_ACPI=y +CONFIG_PCI_AARDVARK=y +CONFIG_PCIE_ALTERA=y +CONFIG_PCI_HOST_THUNDER_PEM=y +CONFIG_PCI_HOST_THUNDER_ECAM=y +CONFIG_PCI_HOST_GENERIC=y +CONFIG_PCI_TEGRA=y +CONFIG_PCIE_ROCKCHIP_HOST=y +CONFIG_PCI_XGENE=y +CONFIG_PCIE_AL=y +CONFIG_PCI_MESON=y +CONFIG_PCI_HISI=y +CONFIG_PCIE_KIRIN=y +CONFIG_PCIE_HISI_STB=y +CONFIG_PCIE_ARMADA_8K=y +CONFIG_PCIE_QCOM=y +CONFIG_UEVENT_HELPER=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_CONNECTOR=y +CONFIG_ARM_SCMI_PROTOCOL=y +CONFIG_DMI_SYSFS=y +CONFIG_SYSFB_SIMPLEFB=y CONFIG_BLK_DEV_LOOP=y CONFIG_BLK_DEV_NBD=y CONFIG_BLK_DEV_RAM=y CONFIG_BLK_DEV_RAM_SIZE=65536 +CONFIG_VIRTIO_BLK=y CONFIG_BLK_DEV_NVME=y -CONFIG_ATA=y -CONFIG_SATA_AHCI=y -CONFIG_SATA_AHCI_PLATFORM=y - -# --- SCSI --- -CONFIG_SCSI=y CONFIG_BLK_DEV_SD=y CONFIG_CHR_DEV_SG=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=y -CONFIG_SCSI_MPI3MR=m CONFIG_SCSI_HPSA=y CONFIG_SCSI_HISI_SAS=y CONFIG_SCSI_HISI_SAS_PCI=y +CONFIG_MEGARAID_SAS=y +CONFIG_SCSI_MPT3SAS=y +CONFIG_SCSI_MPI3MR=m CONFIG_XEN_SCSI_FRONTEND=y CONFIG_SCSI_VIRTIO=y - -# --- Device mapper --- +CONFIG_ATA=y +CONFIG_SATA_AHCI=y +CONFIG_SATA_AHCI_PLATFORM=y CONFIG_MD=y CONFIG_BLK_DEV_DM=y CONFIG_DM_CRYPT=y -CONFIG_DM_THIN_PROVISIONING=y CONFIG_DM_SNAPSHOT=y +CONFIG_DM_THIN_PROVISIONING=y CONFIG_DM_MULTIPATH=m -CONFIG_MD_RAID0=m -CONFIG_MD_RAID1=m - -# --- Network drivers (built-in for reliable boot) --- CONFIG_NETDEVICES=y +CONFIG_BONDING=m +CONFIG_DUMMY=m +CONFIG_MACVLAN=y +CONFIG_MACVTAP=y +CONFIG_IPVLAN=y +CONFIG_VXLAN=y +CONFIG_GENEVE=m +CONFIG_TUN=y +CONFIG_VETH=y CONFIG_VIRTIO_NET=y +CONFIG_NLMON=y +CONFIG_ENA_ETHERNET=m +CONFIG_AMD_XGBE=y +CONFIG_NET_XGENE=y +CONFIG_BCMGENET=y +CONFIG_TIGON3=y +CONFIG_BNXT=y +CONFIG_MACB=m +CONFIG_THUNDER_NIC_PF=m +CONFIG_THUNDER_NIC_VF=m +CONFIG_GVE=m +CONFIG_HNS3=m CONFIG_E1000E=y CONFIG_IGB=y CONFIG_IXGBE=y CONFIG_I40E=y CONFIG_ICE=y CONFIG_IGC=y +CONFIG_MVNETA=y +CONFIG_MVPP2=y CONFIG_MLX4_EN=y CONFIG_MLX5_CORE=y CONFIG_MLX5_CORE_EN=y -CONFIG_BNXT=y -CONFIG_TIGON3=y CONFIG_R8169=y -CONFIG_THUNDER_NIC_BGX=m -CONFIG_THUNDER_NIC_PF=m -CONFIG_THUNDER_NIC_VF=m -CONFIG_MACB=m -CONFIG_HNS3=m -CONFIG_NET_XGENE=y -CONFIG_MVNETA=y -CONFIG_MVPP2=y CONFIG_STMMAC_ETH=m -CONFIG_ENA_ETHERNET=m -CONFIG_GVE=m -CONFIG_VMXNET3=y -CONFIG_AMD_XGBE=y -# Broadcom GENET (Raspberry Pi 4B built-in Ethernet) -CONFIG_BCMGENET=y -# PHY subsystem (required by GENET and other MAC drivers) -CONFIG_PHYLIB=y -CONFIG_MDIO_BUS=y +CONFIG_AMD_PHY=y +CONFIG_MESON_GXL_PHY=y CONFIG_BROADCOM_PHY=y -CONFIG_BCM7XXX_PHY=y -CONFIG_MDIO_BCM_UNIMAC=y CONFIG_MARVELL_PHY=y CONFIG_MARVELL_10G_PHY=y CONFIG_MICREL_PHY=y CONFIG_ROCKCHIP_PHY=y -CONFIG_REALTEK_PHY=y -CONFIG_MESON_GXL_PHY=y -CONFIG_AMD_PHY=y - -# --- USB --- -CONFIG_USB_SUPPORT=y +# CONFIG_WLAN is not set +CONFIG_VMXNET3=y +CONFIG_INPUT_EVDEV=y +CONFIG_SERIAL_8250=y +CONFIG_SERIAL_8250_CONSOLE=y +CONFIG_SERIAL_AMBA_PL011=y +CONFIG_SERIAL_AMBA_PL011_CONSOLE=y +CONFIG_VIRTIO_CONSOLE=y +CONFIG_IPMI_HANDLER=m +CONFIG_IPMI_DEVICE_INTERFACE=m +CONFIG_IPMI_SI=m +CONFIG_HW_RANDOM=y +CONFIG_HW_RANDOM_TIMERIOMEM=y +CONFIG_HW_RANDOM_VIRTIO=y +CONFIG_I2C_CHARDEV=y +CONFIG_I2C_MV64XXX=y +CONFIG_I2C_RK3X=y +CONFIG_I2C_TEGRA=y +CONFIG_SPI=y +CONFIG_SPI_ORION=y +CONFIG_SPI_PL022=y +CONFIG_SPI_ROCKCHIP=y +CONFIG_WATCHDOG=y +CONFIG_DRM=y +CONFIG_DRM_SIMPLEDRM=y +CONFIG_DRM_VIRTIO_GPU=y +CONFIG_FB=y +CONFIG_FB_EFI=y CONFIG_USB=y CONFIG_USB_XHCI_HCD=y CONFIG_USB_EHCI_HCD=y CONFIG_USB_OHCI_HCD=y -CONFIG_USB_XHCI_PCI=y -CONFIG_USB_UAS=y CONFIG_USB_STORAGE=y +CONFIG_USB_UAS=y CONFIG_USB_DWC3=y -CONFIG_USB_DWC3_HOST=y CONFIG_USB_DWC2=y CONFIG_USB_ISP1760=y - -# --- MMC/SD (Raspberry Pi, Rockchip, and other SBCs) --- CONFIG_MMC=y CONFIG_MMC_BLOCK_MINORS=32 +CONFIG_MMC_ARMMMCI=y CONFIG_MMC_SDHCI=y CONFIG_MMC_SDHCI_PCI=y CONFIG_MMC_SDHCI_ACPI=y @@ -280,27 +294,48 @@ CONFIG_MMC_SDHCI_OF_ARASAN=y CONFIG_MMC_SDHCI_OF_DWCMSHC=y CONFIG_MMC_SDHCI_CADENCE=y CONFIG_MMC_SDHCI_TEGRA=y -CONFIG_MMC_SDHCI_MSM=y -CONFIG_MMC_SDHCI_XENON=y CONFIG_MMC_SDHCI_F_SDH30=y -CONFIG_MMC_ARMMMCI=y +CONFIG_MMC_MESON_GX=y +CONFIG_MMC_SDHCI_MSM=y +CONFIG_MMC_SPI=y CONFIG_MMC_DW=y -CONFIG_MMC_DW_ROCKCHIP=y CONFIG_MMC_DW_K3=y -CONFIG_MMC_MESON_GX=y +CONFIG_MMC_DW_ROCKCHIP=y CONFIG_MMC_SUNXI=m -CONFIG_MMC_SPI=y CONFIG_MMC_HSQ=y - -# --- Filesystems --- +CONFIG_MMC_SDHCI_XENON=y +CONFIG_RTC_CLASS=y +CONFIG_RTC_DRV_EFI=y +CONFIG_DMADEVICES=y +CONFIG_PL330_DMA=y +CONFIG_VFIO=y +CONFIG_VFIO_PCI=y +CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_BALLOON=y +CONFIG_VIRTIO_INPUT=y +CONFIG_VIRTIO_MMIO=y +CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y +CONFIG_VHOST_NET=m +CONFIG_ARM_SMMU=y +CONFIG_ARM_SMMU_V3=y +CONFIG_TEE=y +CONFIG_OPTEE=y CONFIG_EXT4_FS=y CONFIG_XFS_FS=m +CONFIG_FUSE_FS=y +CONFIG_CUSE=y +CONFIG_OVERLAY_FS=y +CONFIG_ISO9660_FS=y +CONFIG_JOLIET=y +CONFIG_UDF_FS=y +CONFIG_MSDOS_FS=y CONFIG_VFAT_FS=y +CONFIG_FAT_DEFAULT_IOCHARSET="utf8" +CONFIG_NTFS_FS=m CONFIG_TMPFS=y CONFIG_TMPFS_POSIX_ACL=y -CONFIG_PROC_FS=y -CONFIG_SYSFS=y -CONFIG_OVERLAY_FS=y +CONFIG_HUGETLBFS=y +CONFIG_EFIVAR_FS=y CONFIG_SQUASHFS=y CONFIG_SQUASHFS_XATTR=y CONFIG_SQUASHFS_LZ4=y @@ -310,206 +345,17 @@ CONFIG_NFS_FS=m CONFIG_NFS_V4=m CONFIG_NFS_V4_1=y CONFIG_NFS_V4_2=y -CONFIG_FUSE_FS=y -CONFIG_CUSE=y -CONFIG_EFIVAR_FS=y -CONFIG_ISO9660_FS=y -CONFIG_JOLIET=y -CONFIG_UDF_FS=y -CONFIG_MSDOS_FS=y -CONFIG_FAT_DEFAULT_IOCHARSET="utf8" -CONFIG_NTFS_FS=m -CONFIG_TMPFS_XATTR=y CONFIG_NLS_CODEPAGE_437=y CONFIG_NLS_ASCII=y CONFIG_NLS_ISO8859_1=y CONFIG_NLS_UTF8=y - -# --- Virtio (built-in for reliable boot) --- -CONFIG_VIRTIO=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BLK=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_INPUT=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y -CONFIG_HW_RANDOM_VIRTIO=y - -# --- Console / Serial --- -CONFIG_SERIAL_AMBA_PL011=y -CONFIG_SERIAL_AMBA_PL011_CONSOLE=y -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_VT=y -CONFIG_VT_CONSOLE=y -CONFIG_HW_CONSOLE=y -CONFIG_DUMMY_CONSOLE=y - -# --- Security --- -CONFIG_SECCOMP=y -CONFIG_SECCOMP_FILTER=y CONFIG_SECURITY=y -CONFIG_KEYS=y - -# --- Crypto --- -CONFIG_CRYPTO=y CONFIG_CRYPTO_AES=y +CONFIG_CRYPTO_XTS=y CONFIG_CRYPTO_SHA256=y CONFIG_CRYPTO_SHA512=y -CONFIG_CRYPTO_XTS=y -CONFIG_ARM64_CRYPTO=y CONFIG_CRYPTO_AES_ARM64_CE=y -CONFIG_CRYPTO_SHA2_ARM64_CE=y - -# --- BPF --- -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y -CONFIG_XDP_SOCKETS=y - -# --- ARM64 SoC/platform support --- -CONFIG_ARCH_BCM2835=y -CONFIG_ARCH_ROCKCHIP=y -CONFIG_ARCH_MESON=y -CONFIG_ARCH_SUNXI=y -CONFIG_ARCH_TEGRA=y -CONFIG_ARCH_MVEBU=y -CONFIG_ARCH_HISI=y -CONFIG_ARCH_QCOM=y -CONFIG_ARCH_EXYNOS=y -CONFIG_ARCH_VEXPRESS=y -CONFIG_ARCH_XGENE=y -CONFIG_ARCH_THUNDER=y -CONFIG_ARCH_THUNDER2=y -CONFIG_ARCH_SEATTLE=y -CONFIG_ARCH_SYNQUACER=y -CONFIG_ARCH_UNIPHIER=y -# ACPI / UEFI -CONFIG_ACPI=y -CONFIG_ACPI_DOCK=y -CONFIG_ACPI_IPMI=y -CONFIG_ACPI_APEI=y -CONFIG_ACPI_APEI_GHES=y -CONFIG_IPMI_HANDLER=m -CONFIG_IPMI_DMI_DECODE=y -CONFIG_IPMI_DEVICE_INTERFACE=m -CONFIG_IPMI_SI=m -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_HOTPLUG_PCI=y -CONFIG_PCI_HOST_GENERIC=y -CONFIG_PCIE_ROCKCHIP_HOST=y -CONFIG_PCIE_ARMADA_8K=y -CONFIG_PCIE_TEGRA194_HOST=y -# Broadcom STB PCIe controller (Raspberry Pi 4B — needed for VL805 USB 3.0) -CONFIG_PCIE_BRCMSTB=y -CONFIG_PCIE_ALTERA=y -CONFIG_PCI_AARDVARK=y -CONFIG_PCI_TEGRA=y -CONFIG_PCI_XGENE=y -CONFIG_PCI_HOST_THUNDER_PEM=y -CONFIG_PCI_HOST_THUNDER_ECAM=y -CONFIG_PCIE_QCOM=y -CONFIG_PCIE_AL=y -CONFIG_PCI_MESON=y -CONFIG_PCI_HISI=y -CONFIG_PCIE_KIRIN=y -CONFIG_PCIE_HISI_STB=y -CONFIG_PCI_IOV=y -CONFIG_PCI_PASID=y -CONFIG_HOTPLUG_PCI_ACPI=y -# IOMMU -CONFIG_ARM_SMMU=y -CONFIG_ARM_SMMU_V3=y -CONFIG_FW_LOADER=y -# Raspberry Pi firmware mailbox (required for GENET MAC address, clocks, etc.) -CONFIG_RASPBERRYPI_FIRMWARE=y -CONFIG_RASPBERRYPI_POWER=y -CONFIG_ARM_SCMI_PROTOCOL=y -CONFIG_ARM_SCPI_PROTOCOL=y -CONFIG_OF=y -CONFIG_USE_OF=y -# RTC -CONFIG_RTC_CLASS=y -CONFIG_RTC_DRV_EFI=y -# DMA -CONFIG_DMADEVICES=y -CONFIG_PL330_DMA=y -CONFIG_PRINTK=y CONFIG_PRINTK_TIME=y -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_PANIC_TIMEOUT=10 -CONFIG_PANIC_ON_OOPS=y CONFIG_MAGIC_SYSRQ=y -CONFIG_RD_GZIP=y -CONFIG_RD_XZ=y -CONFIG_RD_ZSTD=y -CONFIG_UEVENT_HELPER=y -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_CONNECTOR=y -CONFIG_DMI_SYSFS=y -CONFIG_EFI=y -CONFIG_EFI_STUB=y - -# --- Framebuffer/Console (required for EFI boot video output) --- -CONFIG_DRM=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_DRM_VIRTIO_GPU=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_FRAMEBUFFER_CONSOLE=y -CONFIG_SYSFB_SIMPLEFB=y -# Virtualization -CONFIG_VIRTUALIZATION=y -CONFIG_KVM=y -CONFIG_VFIO=y -CONFIG_VFIO_PCI=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m - -# --- Module compression --- -CONFIG_MODULE_COMPRESS_ZSTD=y -CONFIG_MODULE_DECOMPRESS=y - -# --- I2C (SBC board management, EEPROM, sensors) --- -CONFIG_I2C=y -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MV64XXX=y -CONFIG_I2C_RK3X=y -CONFIG_I2C_TEGRA=y -CONFIG_I2C_DESIGNWARE_PLATFORM=y -# SPI -CONFIG_SPI=y -CONFIG_SPI_ROCKCHIP=y -CONFIG_SPI_PL022=y -CONFIG_SPI_ORION=y -# GPIO -CONFIG_GPIOLIB=y -# Hardware watchdog -CONFIG_WATCHDOG=y -CONFIG_SOFTLOCKUP_DETECTOR=y -CONFIG_WQ_WATCHDOG=y -# Hardware RNG -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_TIMERIOMEM=y -# TPM -CONFIG_TCG_TIS=y -CONFIG_TCG_FTPM_TEE=y -CONFIG_TEE=y -CONFIG_OPTEE=y - -# --- Disable unnecessary subsystems --- -# CONFIG_SOUND is not set -# CONFIG_WIRELESS is not set -# CONFIG_WLAN is not set -# CONFIG_BLUETOOTH is not set -# CONFIG_NFC is not set -# CONFIG_INFINIBAND is not set -# CONFIG_MEDIA_SUPPORT is not set -CONFIG_INPUT_EVDEV=y -# CONFIG_INPUT_JOYSTICK is not set -# CONFIG_INPUT_TOUCHSCREEN is not set +CONFIG_PANIC_ON_OOPS=y +CONFIG_PANIC_TIMEOUT=10 From 2487744972fde738dc752d3f6cb8907f7b78fbe3 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 13 Apr 2026 21:00:43 +0200 Subject: [PATCH 092/143] kernel: 6.18.y.arm64: defconfig: `CONFIG_MODULE_COMPRESS_ZSTD=y` Signed-off-by: Ricardo Pardini --- kernel.configs/6.18.y.arm64 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kernel.configs/6.18.y.arm64 b/kernel.configs/6.18.y.arm64 index 5f7cff8f..0df517a5 100644 --- a/kernel.configs/6.18.y.arm64 +++ b/kernel.configs/6.18.y.arm64 @@ -63,6 +63,8 @@ CONFIG_VIRTUALIZATION=y CONFIG_KVM=y CONFIG_MODULES=y CONFIG_MODULE_UNLOAD=y +CONFIG_MODULE_COMPRESS=y +CONFIG_MODULE_COMPRESS_ZSTD=y CONFIG_BLK_DEV_THROTTLING=y CONFIG_BLK_CGROUP_IOLATENCY=y CONFIG_MEMORY_HOTPLUG=y From 4ef4c6dce3edb1ff3da22c25ee4ea2a3b751ed95 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 13 Apr 2026 22:52:35 +0200 Subject: [PATCH 093/143] gha: reintroduce kernel build jobs - using the Docker builder, so depend on that - abuse GHA caches and artifacts - the whole matrix will wait on kernel builds - but only a few will actually download and use the artifact Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 75 +++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ea951b4..999e075f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,22 +113,72 @@ jobs: - name: Build Dockerfile and push run: uv run captain builder --push + # ------------------------------------------------------------------- + # Download tools (containerd, runc, nerdctl, CNI plugins) + # ------------------------------------------------------------------- + build-captainos-kernel: + needs: [ build-dockerfile ] + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + arch: [ amd64, arm64 ] + env: + ARCH: ${{ matrix.arch }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Restore kernel cache + id: kernel-cache + uses: actions/cache/restore@v5 + with: + path: | + mkosi.output/kernel/${{ matrix.arch }} + key: kernel-${{ matrix.arch }}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time + restore-keys: | + kernel-${{ matrix.arch }} + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Build kernel + run: uv run captain --verbose kernel + + - name: Save kernel cache + if: github.ref == 'refs/heads/main' && steps.tools-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: | + mkosi.output/kernel/${{ matrix.arch }} + key: kernel-${{ matrix.arch }}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + + - name: Upload kernel artifacts + uses: actions/upload-artifact@v6 + with: + name: kernel-${{ matrix.arch }} + path: | + mkosi.output/kernel/${{ matrix.arch }} + retention-days: 1 + + # ------------------------------------------------------------------- # Build initramfs via mkosi (depends on tools) # ------------------------------------------------------------------- - build-all: + build-captainos: runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [ download-tools, build-dockerfile ] + needs: [ download-tools, build-dockerfile, build-captainos-kernel ] strategy: fail-fast: false matrix: include: - - { arch: amd64, output_arch: x86_64, iso: true, FLAVOR_ID: "trixie-full" } - - { arch: arm64, output_arch: aarch64, iso: true, FLAVOR_ID: "trixie-full" } - - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-rockchip64" } - - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-rockchip64-vendor" } - - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-meson64" } - - { arch: arm64, output_arch: aarch64, iso: false, FLAVOR_ID: "trixie-armbian-rpi" } + - { arch: amd64, output_arch: x86_64, captainos_kernel: true, iso: true, FLAVOR_ID: "trixie-full" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: true, iso: true, FLAVOR_ID: "trixie-full" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-rockchip64" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-rockchip64-vendor" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-meson64" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-armbian-rpi" } env: ARCH: ${{ matrix.arch }} MKOSI_MODE: docker @@ -144,6 +194,13 @@ jobs: name: tools-${{ matrix.arch }} path: mkosi.output/tools/${{ matrix.arch }} + - name: Download kernel artifacts + uses: actions/download-artifact@v8 + if: ${{ matrix.captainos_kernel }} # only if matrix entry had captainos_kernel: true + with: + name: kernel-${{ matrix.arch }} + path: mkosi.output/kernel/${{ matrix.arch }} + - name: Restore tool binary permissions run: | # GitHub Actions artifact upload/download strips execute permissions. @@ -205,7 +262,7 @@ jobs: if: github.ref == 'refs/heads/main' name: "publish-combined" runs-on: ubuntu-latest - needs: [ build-all ] + needs: [ build-captainos ] env: ARCH: amd64 TARGET: combined From 872b0682f4c1b6fb176bf51bc08673f5f36d5094 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 13 Apr 2026 23:44:27 +0200 Subject: [PATCH 094/143] gha: split matrix to add kernel job dependency for trixie-slim jobs - add gha builds for `trixie-slim` flavors (using captainos-kernel) - we've two kinds of build-captainos jobs: - ones that depend on kernel-build (trixie-slim) - ones that don't (all the others) - use YAML anchor trick to avoid duplicating 95% of code (envs: and steps: are defined only once) - matrix and needs (dependencies) are the only difference - artifact download is common but conditional on matrix values Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 999e075f..d5b4d010 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,28 +163,21 @@ jobs: retention-days: 1 - # ------------------------------------------------------------------- - # Build initramfs via mkosi (depends on tools) - # ------------------------------------------------------------------- - build-captainos: + build-captainos-with-kernel: # only for flavors that require the custom CaptainOS kernel; otherwise, just build-captainos runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} needs: [ download-tools, build-dockerfile, build-captainos-kernel ] strategy: fail-fast: false matrix: include: - - { arch: amd64, output_arch: x86_64, captainos_kernel: true, iso: true, FLAVOR_ID: "trixie-full" } - - { arch: arm64, output_arch: aarch64, captainos_kernel: true, iso: true, FLAVOR_ID: "trixie-full" } - - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-rockchip64" } - - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-rockchip64-vendor" } - - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-meson64" } - - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-armbian-rpi" } - env: + - { arch: amd64, output_arch: x86_64, captainos_kernel: true, iso: true, FLAVOR_ID: "trixie-slim" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: true, iso: true, FLAVOR_ID: "trixie-slim" } + env: &build_env ARCH: ${{ matrix.arch }} MKOSI_MODE: docker ISO_MODE: docker FLAVOR_ID: ${{ matrix.FLAVOR_ID }} - steps: + steps: &build_steps - name: Checkout code uses: actions/checkout@v6 @@ -194,7 +187,7 @@ jobs: name: tools-${{ matrix.arch }} path: mkosi.output/tools/${{ matrix.arch }} - - name: Download kernel artifacts + - name: Download kernel artifacts == ${{ matrix.captainos_kernel }} uses: actions/download-artifact@v8 if: ${{ matrix.captainos_kernel }} # only if matrix entry had captainos_kernel: true with: @@ -255,6 +248,22 @@ jobs: TARGET: ${{ matrix.arch }} run: uv run captain release-publish + build-captainos: # those use regular Debian apt repos for kernels + steps: *build_steps + env: *build_env + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + needs: [ download-tools, build-dockerfile ] + strategy: + fail-fast: false + matrix: + include: + - { arch: amd64, output_arch: x86_64, captainos_kernel: false, iso: true, FLAVOR_ID: "trixie-full" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: true, FLAVOR_ID: "trixie-full" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-rockchip64" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-rockchip64-vendor" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-meson64" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-armbian-rpi" } + # ------------------------------------------------------------------- # Publish combined multi-arch image (reuses per-arch registry blobs) # ------------------------------------------------------------------- From fa0cfd33b404f4fc7410d277ea02d677a633b915 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 14 Apr 2026 01:56:38 +0200 Subject: [PATCH 095/143] gha: make `trixie-slim` the default flavor - so it is the one included in the combined images - this does not change the CLI defaults, ofc - for trixie-slim, combined should depend on `build-captainos-with-kernel` - better names for matrix jobs Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5b4d010..bfabd80f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ permissions: packages: write env: - DEFAULT_FLAVOR_ID: "trixie-full" + DEFAULT_FLAVOR_ID: "trixie-slim" FORCE_COLOR: "1" jobs: @@ -164,6 +164,7 @@ jobs: build-captainos-with-kernel: # only for flavors that require the custom CaptainOS kernel; otherwise, just build-captainos + name: "build-captainos (with kernel) ${{ matrix.FLAVOR_ID }} (${{ matrix.arch }})" runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} needs: [ download-tools, build-dockerfile, build-captainos-kernel ] strategy: @@ -249,6 +250,7 @@ jobs: run: uv run captain release-publish build-captainos: # those use regular Debian apt repos for kernels + name: "build-captainos ${{ matrix.FLAVOR_ID }} (${{ matrix.arch }})" steps: *build_steps env: *build_env runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} @@ -271,7 +273,8 @@ jobs: if: github.ref == 'refs/heads/main' name: "publish-combined" runs-on: ubuntu-latest - needs: [ build-captainos ] + needs: [ build-captainos-with-kernel ] # if trixie-slim is the default + #needs: [ build-captainos ] # if trixie-full is the default env: ARCH: amd64 TARGET: combined From 869e822f35968024ead2e96e4425b20e53d1caf4 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 14 Apr 2026 02:02:24 +0200 Subject: [PATCH 096/143] gha: don't push OCI images for pull request workflows - (Docker) builder will end up being built multiple times for PR workflows, but such is life - it's also too huge for GHA artifact or GHA cache - don't push any captainos artifacts either (simple or combined) - also: only push those when building `main` branch Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfabd80f..0b8c2e67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,8 +110,15 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Build Dockerfile + run: uv run captain builder + # only if a PR, since we don't want to push images for PRs from forks + if: github.event_name == 'pull_request' + - name: Build Dockerfile and push run: uv run captain builder --push + # only if not a PR, since we don't want to push images for PRs from forks (no perms to push) + if: github.event_name != 'pull_request' # ------------------------------------------------------------------- # Download tools (containerd, runc, nerdctl, CNI plugins) @@ -244,7 +251,8 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Publish artifacts to GHCR - if: github.ref == 'refs/heads/main' + # only pushes to main, and not PR workflows, should try to push to OCI registry + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' env: TARGET: ${{ matrix.arch }} run: uv run captain release-publish @@ -322,6 +330,8 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Publish combined image to GHCR + # only pushes to main, and not PR workflows, should try to push to OCI registry + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' env: FLAVOR_ID: "${{ env.DEFAULT_FLAVOR_ID }}" run: uv run captain release-publish From a74e805ffeee5e609ebd51e1ff3071fd7eeabfc5 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 15 Apr 2026 21:11:53 +0200 Subject: [PATCH 097/143] cli: bring back `qemu` cli, with extra support for arm64 & UEFI/OVMF - arm64 qemu requires OVMF/AAVMF firmware (edk2) via a pflash - use `q35` machine type for amd64 (it is still not a default) - add kvm (Linux) and hvf (Darwin) acceleration if present and matching host/target arch - add `systemd.journald.forward_to_console=1` as default prefix kernel cmdline - remove `audit=0` as cmdline prefix default - run with eg `uv run captain --verbose --arch=amd64 --flavor-id=trixie-slim qemu -- tink_worker_image=... <...>` - extra args after `--` are passed directly as kernel cmdline - maybe restore option/env parsing with Click later (kept TODO'ed `_TINK_PARAMS`) for ergonomics - qemu: more systemd logging in default kernel cmdline - qemu: run x86 also under OVMF (UEFI), not BIOS emulation - this more accurately emulates real environment - real EFI implementation, with efivars etc - unify accel handling (accel only when host arch matches target arch) - but then it's the same across arches: hcf for Apple, kvm for Linux - refactor find_ovmf_firmware() as it's common Signed-off-by: Ricardo Pardini --- captain/cli/_main.py | 1 + captain/cli/_qemu.py | 36 ++++++++ captain/config.py | 1 - captain/qemu.py | 212 +++++++++++++++++++++++++++++++------------ 4 files changed, 193 insertions(+), 57 deletions(-) create mode 100644 captain/cli/_qemu.py diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 3a433577..a8a3ca9d 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -194,6 +194,7 @@ def main() -> None: _builder, _iso, _kernel, + _qemu, _release_publish, _shell, _tools, diff --git a/captain/cli/_qemu.py b/captain/cli/_qemu.py new file mode 100644 index 00000000..44913b01 --- /dev/null +++ b/captain/cli/_qemu.py @@ -0,0 +1,36 @@ +"""``captain tools`` — download tools (containerd, runc, nerdctl, CNI plugins).""" + +from __future__ import annotations + +import logging + +import click +from click import Context + +from captain.cli._main import CliContext, cli +from captain.qemu import run_qemu + +log = logging.getLogger(__name__) + + +@cli.command( + "qemu", + context_settings={ + "ignore_unknown_options": True, + "allow_extra_args": True, + }, + short_help="Run the built captainos kernel+initramfs in QEMU.", +) +@click.pass_obj +@click.pass_context +def shell_cmd(ctx: Context, cli_ctx: CliContext) -> None: + log.debug("Running captainos in QEMU...") + + cfg = cli_ctx.make_config() + + extra_args: list[str] = ctx.args + log.debug(f"Extra args passed to qemu: {extra_args}") + + run_qemu(cfg, extra_args) + + log.info("qemu exited.") diff --git a/captain/config.py b/captain/config.py index 87897649..281d0cd1 100644 --- a/captain/config.py +++ b/captain/config.py @@ -63,7 +63,6 @@ class Config: force_iso: bool = False # QEMU - qemu_append: str = "" qemu_mem: str = "2G" qemu_smp: str = "2" diff --git a/captain/qemu.py b/captain/qemu.py index ae0da4a0..3a2971a7 100644 --- a/captain/qemu.py +++ b/captain/qemu.py @@ -2,18 +2,20 @@ from __future__ import annotations -import argparse +import atexit import logging +import shutil import sys +import tempfile +from pathlib import Path from captain.config import Config -from captain.util import run +from captain.util import detect_current_machine_arch, run log = logging.getLogger(__name__) -# Tinkerbell kernel cmdline parameters. -# Maps the argparse dest name → kernel cmdline key. -# Insertion order is preserved. +# @TODO: not used, qemu cmd passes everything after -- as raw kernel cmdline. +# @TODO: maybe move those to Click params in _qemu.py and remove this list? _TINK_PARAMS: list[tuple[str, str]] = [ # (namespace_attr, cmdline_key) ("tink_worker_image", "tink_worker_image"), @@ -30,44 +32,17 @@ ] -def _tink_cmdline(args: argparse.Namespace) -> str: - """Build tinkerbell kernel cmdline fragment from parsed *args*.""" - parts: list[str] = [] - for attr, cmdline_key in _TINK_PARAMS: - value = getattr(args, attr, "") or "" - if not value: - continue - # Kernel cmdline is space-delimited; whitespace in values would - # split them into multiple arguments and silently change meaning. - if any(ch.isspace() for ch in value): - log.error( - "--%s must not contain whitespace; cannot safely add it to the kernel cmdline.", - attr.replace("_", "-"), - ) - sys.exit(1) - parts.append(f"{cmdline_key}={value}") - - # Static networking via ipam= parameter - ipam = getattr(args, "ipam", "") or "" - if ipam: - if any(ch.isspace() for ch in ipam): - log.error("--ipam must not contain whitespace.") - sys.exit(1) - parts.append(f"ipam={ipam}") - - return " ".join(parts) - - -def run_qemu(cfg: Config, args: argparse.Namespace | None = None) -> None: - """Boot the built image in QEMU for quick testing. - - *args* is the parsed :class:`argparse.Namespace` produced by - :mod:`configargparse`. When provided, Tinkerbell kernel cmdline - parameters are drawn from it instead of the environment. - """ +def run_qemu(cfg: Config, args: list[str] | None = None) -> None: + """Boot the built image in QEMU for quick testing.""" kernel = cfg.output_dir / f"vmlinuz-{cfg.flavor_id}-{cfg.arch_info.output_arch}" initrd = cfg.output_dir / f"initramfs-{cfg.flavor_id}-{cfg.arch_info.output_arch}" + log.debug("Looking for kernel at %s", kernel) + log.debug("Looking for initramfs at %s", initrd) + + log.debug("Current machine arch: %s", detect_current_machine_arch()) + log.debug("Current machine platform: %s", sys.platform) + missing: list[str] = [] if not kernel.is_file(): missing.append(str(kernel)) @@ -79,24 +54,43 @@ def run_qemu(cfg: Config, args: argparse.Namespace | None = None) -> None: log.error(" %s", m) sys.exit(1) - tink = _tink_cmdline(args) if args is not None else "" - if args is not None and not any( - getattr(args, v, None) for v in ("tink_worker_image", "tink_docker_registry") - ): - log.warning( - "Neither --tink-worker-image nor --tink-docker-registry is set. " - "tink-agent services will not start." - ) - - log.info("Booting CaptainOS in QEMU (Ctrl-A X to exit)...") - - qemu_cmd = cfg.arch_info.qemu_binary - append = f"console=ttyS0 audit=0 {tink} {cfg.qemu_append}".strip() - - log.info("Kernel cmdline: %s", append) + tink = " ".join(args) if args is not None else "" + log.info("Booting CaptainOS in QEMU ('Ctrl-A x' to exit)...") + + cmdline_compos: list[str] = [ + "systemd.journald.forward_to_console=1", + # "systemd.log_level=debug", + # "systemd.log_target=console", + f"{tink}", + ] + + per_arch_extra_cmds: list[str] = [] + match cfg.arch: + # direct kernel boot for x86 + case "amd64": + qemu_amd64_params(cfg, cmdline_compos, per_arch_extra_cmds) + + # indirect boot via OVMF for arm64 + case "arm64": + qemu_arm64_params(cfg, cmdline_compos, per_arch_extra_cmds) + + # If matching the host architecture, enable hardware acceleration via KVM or HVF + if cfg.arch == detect_current_machine_arch(): + # if on Darwin and arm64 (Apple Silicon), enable hvf accel. + if sys.platform == "darwin": + log.info("Native run on Apple, enabling HVF acceleration") + per_arch_extra_cmds += ["-accel", "hvf"] + + if sys.platform == "linux" and Path("/dev/kvm").is_char_device(): + log.info("Native run on Linux with KVM available, enabling KVM acceleration") + per_arch_extra_cmds += ["-accel", "kvm"] + + append = " ".join(cmdline_compos).strip() + log.info("Kernel cmdline: '%s'", append) run( [ - qemu_cmd, + cfg.arch_info.qemu_binary, + *per_arch_extra_cmds, "-kernel", str(kernel), "-initrd", @@ -113,3 +107,109 @@ def run_qemu(cfg: Config, args: argparse.Namespace | None = None) -> None: "-no-reboot", ], ) + + +def qemu_amd64_params(cfg: Config, cmdline_compos: list[str], per_arch_extra_cmds: list[str]): + cmdline_compos.insert(0, "console=ttyS0") + per_arch_extra_cmds += ["-machine", "q35"] + per_arch_extra_cmds += ["-cpu", "max"] + find_ovmf_firmware( + cfg, + per_arch_extra_cmds, + [ + "edk2-x86_64-code.fd", # Darwin/homebrew + "OVMF_CODE_4M.fd", # Debian's 'ovmf' package - recent + "OVMF_CODE.fd", # Debian's 'ovmf' package - older + ], + [ + "edk2-i386-vars.fd", # Darwin/homebrew + "OVMF_VARS_4M.fd", # Debian's 'ovmf' package - recent + "OVMF_VARS.fd", # Debian's 'ovmf' package - older + ], + ) + + +def qemu_arm64_params(cfg: Config, cmdline_compos: list[str], per_arch_extra_cmds: list[str]): + cmdline_compos.insert(0, "console=ttyAMA0") + per_arch_extra_cmds += ["-machine", "virt"] + per_arch_extra_cmds += ["-cpu", "host"] + find_ovmf_firmware( + cfg, + per_arch_extra_cmds, + [ + "edk2-aarch64-code.fd", # Darwin/homebrew + "AAVMF_CODE.no-secboot.fd", # Debian's qemu-efi-aarch64 - recent + "AAVMF_CODE.fd", # Debian's qemu-efi-aarch64 - older + ], + [ + "edk2-arm-vars.fd", # Darwin/homebrew + "AAVMF_VARS.fd", # Debian's qemu-efi-aarch64 + ], + ) + + +def find_ovmf_firmware( + cfg: Config, per_arch_extra_cmds: list[str], fw_paths: list[str], vars_paths: list[str] +): + base_dirs: list[Path] = [] + match sys.platform: + case "darwin": + qemu_brew_prefix = run(["brew", "--prefix", "qemu"], capture=True).stdout.strip() + base_dirs.append(Path(qemu_brew_prefix) / "share" / "qemu") + case "linux": + base_dirs += [Path("/usr/share/OVMF"), Path("/usr/share/AAVMF")] + + # keep only the base_dirs that actually exist and are directories + valid_base_dirs = [d for d in base_dirs if d.is_dir()] + if not valid_base_dirs: + log.error( + "No valid base directories found for OVMF firmware on platform %s: %s", + sys.platform, + base_dirs, + ) + log.error( + "Install the appropriate OVMF package for arch %s your distribution and try again.", + cfg.arch, + ) + sys.exit(1) + + # Now just combinatorics: for each base dir, check if any of the expected firmware paths exist. + combined_full_fw_paths = [base / f for base in valid_base_dirs for f in fw_paths] + # find the first that exists and is a file; if none, edk2_path is None + edk2_path = next((p for p in combined_full_fw_paths if p.is_file()), None) + + if edk2_path is None: + log.error( + "OVMF code binary for arch %s not found in any of the expected paths: %s", + cfg.arch, + combined_full_fw_paths, + ) + sys.exit(1) + + # Again, but for the vars binary, which is also required. + combined_full_vars_paths = [base / f for base in valid_base_dirs for f in vars_paths] + vars_path_ro = next((p for p in combined_full_vars_paths if p.is_file()), None) + if vars_path_ro is None: + log.error( + "OVMF vars binary for arch %s not found in any of the expected paths: %s", + cfg.arch, + combined_full_vars_paths, + ) + sys.exit(1) + + # create a temporary file and copy the vars_path_ro there; clean it up on exit + vars_path = Path(tempfile.mkstemp(suffix=f"captain.qemu.efivars.{vars_path_ro.name}.fd")[1]) + log.info("Copying OVMF vars binary from %s to temporary file %s", vars_path_ro, vars_path) + shutil.copy2(vars_path_ro, vars_path) + atexit.register( + lambda: ( + log.info("Cleaning up temporary OVMF vars file at %s", vars_path), + vars_path.unlink(missing_ok=True), + ) + ) + + log.info("Using OVMF firmware code binary at %s", edk2_path) + per_arch_extra_cmds += ["-drive", f"if=pflash,format=raw,readonly=on,file={edk2_path!s}"] + + log.info("Using OVMF firmware vars binary at %s from %s", vars_path, vars_path_ro) + per_arch_extra_cmds += ["-drive", f"if=pflash,format=raw,file={vars_path!s}"] From b999587860e0b4ca01c6263847ddcff65a643bb4 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 15 Apr 2026 22:45:50 +0200 Subject: [PATCH 098/143] kernel: hash defconfig + kernel.py code and use as cache key; clean output when cache missed - that way any changes to defconfig or kernel-building logic should force a new build - on new builds, clean up old artifacts so caches are kept clean (eg: GHA's) Signed-off-by: Ricardo Pardini --- captain/cli/stages.py | 5 +++++ captain/kernel.py | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/captain/cli/stages.py b/captain/cli/stages.py index a44863ec..cbd51948 100644 --- a/captain/cli/stages.py +++ b/captain/cli/stages.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import shutil from captain import docker, iso, kernel, tools from captain.config import Config @@ -27,6 +28,10 @@ def build_kernel_stage(cfg: Config) -> None: log.info("Kernel already built (use --force-kernel to rebuild)") return + # Cleanup any existing outputs so that caches don't grow forever. + if cfg.kernel_output.is_dir(): + shutil.rmtree(cfg.kernel_output) + # --- native ------------------------------------------------------- if cfg.kernel_mode == "native": missing = check_kernel_dependencies(cfg.arch) diff --git a/captain/kernel.py b/captain/kernel.py index 41046063..48b6aaff 100644 --- a/captain/kernel.py +++ b/captain/kernel.py @@ -8,6 +8,7 @@ from __future__ import annotations +import hashlib import logging import os import re @@ -141,7 +142,7 @@ def configure_kernel(cfg: Config, src_dir: Path) -> None: shutil.copy2(defconfig, src_dir / ".config") run(["make", "olddefconfig"], env=make_env, cwd=src_dir) - log.warning( + log.debug( "Considering whether to launch menuconfig for manual config tweaks... %s", cfg.kernel_menuconfig, ) @@ -220,9 +221,21 @@ def build_kernel(cfg: Config, src_dir: Path) -> str: def obtain_target_artifact_path(cfg, ensure_parent: bool = False) -> Path: if ensure_parent: ensure_dir(cfg.kernel_output) + + # let's hash: + # 1) the input defconfig + defconfig = _find_defconfig(cfg) + hex_digest_defconfig = hashlib.sha256(defconfig.read_bytes()).hexdigest() + # 2) the contents of this script (as an imperfect proxy for the build logic) + script_path = Path(__file__) + hex_digest_script = hashlib.sha256(script_path.read_bytes()).hexdigest() + # Combine and shorten to 8 chars for the final version hash + version_hash = hashlib.sha256((hex_digest_defconfig + hex_digest_script).encode()).hexdigest() + version_hash_shorten = version_hash[:8] + return ( cfg.kernel_output / f"linux-image-{cfg.kernel_version}-captainos" - f"_{cfg.kernel_version}-1_" + f"_{cfg.kernel_version}-1-{version_hash_shorten}_" f"{cfg.arch_info.kernel_arch}.deb" ) From 3de686b7de485f2f0fbb1c844affa870bfb19c6d Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 16 Apr 2026 10:46:49 +0200 Subject: [PATCH 099/143] gha: drop `DEFAULT_FLAVOR_ID`; make publish-combined a matrix (for both trixie-slim and trixie-full) - so we've two combined images now, and no longer a default one Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b8c2e67..a40577d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,6 @@ permissions: packages: write env: - DEFAULT_FLAVOR_ID: "trixie-slim" FORCE_COLOR: "1" jobs: @@ -279,11 +278,17 @@ jobs: # ------------------------------------------------------------------- publish-combined: if: github.ref == 'refs/heads/main' - name: "publish-combined" + strategy: + fail-fast: false + matrix: + include: + - { combined_flavor: "trixie-full" } + - { combined_flavor: "trixie-slim" } + name: "publish-combined ${{ matrix.combined_flavor }}" runs-on: ubuntu-latest - needs: [ build-captainos-with-kernel ] # if trixie-slim is the default - #needs: [ build-captainos ] # if trixie-full is the default + needs: [ build-captainos, build-captainos-with-kernel ] # depend on all flavors being built env: + FLAVOR_ID: ${{ matrix.combined_flavor }} ARCH: amd64 TARGET: combined steps: @@ -298,25 +303,25 @@ jobs: - name: Download initramfs artifacts (amd64) uses: actions/download-artifact@v8 with: - name: initramfs-${{ env.DEFAULT_FLAVOR_ID }}-amd64 + name: initramfs-${{ matrix.combined_flavor }}-amd64 path: out - name: Download ISO artifact (amd64) uses: actions/download-artifact@v8 with: - name: iso-${{ env.DEFAULT_FLAVOR_ID }}-amd64 + name: iso-${{ matrix.combined_flavor }}-amd64 path: out - name: Download initramfs artifacts (arm64) uses: actions/download-artifact@v8 with: - name: initramfs-${{ env.DEFAULT_FLAVOR_ID }}-arm64 + name: initramfs-${{ matrix.combined_flavor }}-arm64 path: out - name: Download ISO artifact (arm64) uses: actions/download-artifact@v8 with: - name: iso-${{ env.DEFAULT_FLAVOR_ID }}-arm64 + name: iso-${{ matrix.combined_flavor }}-arm64 path: out - name: Install uv @@ -332,6 +337,4 @@ jobs: - name: Publish combined image to GHCR # only pushes to main, and not PR workflows, should try to push to OCI registry if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - env: - FLAVOR_ID: "${{ env.DEFAULT_FLAVOR_ID }}" run: uv run captain release-publish From e901f4795f594ecd44018e1016c71abcbb5890d9 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 16 Apr 2026 14:21:31 +0200 Subject: [PATCH 100/143] timesyncd: retry connections every 2s - seems like timesyncd is interrupted by networking changes constantly during bootup - each change triggers a retry timeout, which is 30s by default Signed-off-by: Ricardo Pardini --- .../flavors/common_debian/mkosi.extra/etc/systemd/timesyncd.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/captain/flavors/common_debian/mkosi.extra/etc/systemd/timesyncd.conf b/captain/flavors/common_debian/mkosi.extra/etc/systemd/timesyncd.conf index ff03bad4..42d82897 100644 --- a/captain/flavors/common_debian/mkosi.extra/etc/systemd/timesyncd.conf +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/timesyncd.conf @@ -1,2 +1,3 @@ [Time] FallbackNTP=time.cloudflare.com time.google.com time.nist.gov +ConnectionRetrySec=2 From bf11ff8a4d8c322dc7247ee4bcfc9c237a6faf5e Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 16 Apr 2026 14:24:05 +0200 Subject: [PATCH 101/143] util: debug: quote run() params with spaces or specials in them - handle capture=True case a bit better (shows invocation debug, but no ruler) Signed-off-by: Ricardo Pardini --- captain/util.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/captain/util.py b/captain/util.py index 97d81a17..6c3927c3 100644 --- a/captain/util.py +++ b/captain/util.py @@ -92,9 +92,14 @@ def run( run_env = {**os.environ, **env} # If not capturing, and debugging, emit a Rich separator line, for visual clarity. - if not capture and log.isEnabledFor(logging.DEBUG): + if log.isEnabledFor(logging.DEBUG): + # join cmd into a single string; elements with spaces or special chars should be quoted + special = ('"', "'", "\\", "$", ",", "=", "/") + cmd_joined = " ".join( + f'"{c}"' if " " in c or any(s in c for s in special) else c for c in cmd + ) syntax = Syntax( - " ".join(cmd), "bash", theme="monokai", word_wrap=True, background_color="default" + cmd_joined, "bash", theme="monokai", word_wrap=True, background_color="default" ) panel = Panel( syntax, @@ -111,7 +116,8 @@ def run( table.add_row(k, v) captain.console.print(table) - captain.console.print(Rule(f"⮕ Starting subprocess: {cmd} ⮕", style="green")) + if not capture: + captain.console.print(Rule(f"⮕ Starting subprocess: {cmd} ⮕", style="green")) proc = subprocess.run( cmd, From 85c7924d275501a970aed48e3e64bb5dafd163d2 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 16 Apr 2026 17:34:00 +0200 Subject: [PATCH 102/143] flavors: flavor_packages() hierarchy, include_apt(), include_hwdb() - `include_apt()`: if true, includes a working apt install - `include_hwdb()`: if true, leaves hwdb data in - move `acpid` package to common_acpi flavor - also always include `curl`, `pciutils`, `pv` Signed-off-by: Ricardo Pardini --- captain/flavors/common_acpi/__init__.py | 3 +++ captain/flavors/common_armbian/__init__.py | 3 +++ captain/flavors/common_debian/__init__.py | 10 ++++++++-- captain/flavors/common_debian/mkosi.conf.j2 | 15 ++++++++++++--- captain/flavors/trixie_armbian_rpi/__init__.py | 4 ++-- captain/flavors/trixie_full/__init__.py | 16 ++++++++++++++-- captain/flavors/trixie_meson64/__init__.py | 4 ++-- captain/flavors/trixie_rockchip64/__init__.py | 4 ++-- .../flavors/trixie_rockchip64_vendor/__init__.py | 4 ++-- captain/flavors/trixie_slim/__init__.py | 4 ++-- 10 files changed, 50 insertions(+), 17 deletions(-) diff --git a/captain/flavors/common_acpi/__init__.py b/captain/flavors/common_acpi/__init__.py index b9e35eb6..89946165 100644 --- a/captain/flavors/common_acpi/__init__.py +++ b/captain/flavors/common_acpi/__init__.py @@ -26,3 +26,6 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: # This flavor can produce working ISO images (generic UEFI/ACPI) def has_iso(self) -> bool: return True + + def flavor_packages(self) -> set[str]: + return {"acpid", "efibootmgr"}.union(super().flavor_packages()) diff --git a/captain/flavors/common_armbian/__init__.py b/captain/flavors/common_armbian/__init__.py index 388cf31b..da549320 100644 --- a/captain/flavors/common_armbian/__init__.py +++ b/captain/flavors/common_armbian/__init__.py @@ -22,3 +22,6 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: def has_iso(self) -> bool: return False + + def flavor_packages(self) -> set[str]: + return {"tiny-initramfs"}.union(super().flavor_packages()) diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py index e81da689..0b859e33 100644 --- a/captain/flavors/common_debian/__init__.py +++ b/captain/flavors/common_debian/__init__.py @@ -39,8 +39,14 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: self.add_static_dir("mkosi.skeleton", this_flavor_dir) @abstractmethod - def kernel_packages(self) -> set[str]: - pass + def flavor_packages(self) -> set[str]: + return set({}) def package_directories(self) -> set[str]: return set({}) + + def include_working_apt(self) -> bool: + return False + + def include_hwdb(self) -> bool: + return False diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index a957732a..cf2916a0 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -15,7 +15,11 @@ CacheDirectory=mkosi.cache.for.incremental [Content] Bootable=no +{% if flavor.include_working_apt() %} +CleanPackageMetadata=no +{% else %} CleanPackageMetadata=yes +{% endif %} Autologin=yes # Skips installing documentation/man pages WithDocs=no @@ -30,8 +34,7 @@ PackageDirectories= # pivot_root(2) work for container runtimes (runc). Packages= - {{ ' '.join(flavor.kernel_packages()) }} - tiny-initramfs # a tiny initrd builder to account for the linux-image dependency + {{ "\n ".join(flavor.flavor_packages()) }} # systemd and core systemd systemd-sysv # Provides poweroff, shutdown, reboot, halt, etc. @@ -41,7 +44,6 @@ Packages= dbus kmod dmsetup - acpid polkitd # Logging rsyslog @@ -74,6 +76,9 @@ Packages= #zstd #xz-utils fastfetch + pciutils # for listing PCI devices + curl + pv RemoveFiles= /etc/initrd-release @@ -83,11 +88,15 @@ RemoveFiles= /usr/share/locale /usr/share/i18n /usr/share/zoneinfo + {% if not flavor.include_working_apt() %} /var/cache + {% endif %} /var/log /usr/share/bash-completion /usr/share/zsh + {% if not flavor.include_hwdb() %} /usr/lib/udev/hwdb.d + {% endif %} /usr/share/lintian /usr/share/bug /usr/share/pixmaps diff --git a/captain/flavors/trixie_armbian_rpi/__init__.py b/captain/flavors/trixie_armbian_rpi/__init__.py index a5c42572..ff8bf554 100644 --- a/captain/flavors/trixie_armbian_rpi/__init__.py +++ b/captain/flavors/trixie_armbian_rpi/__init__.py @@ -18,5 +18,5 @@ class TrixieArmbianRPiFlavor(ArmbianCommonFlavor): description = "Debian Trixie based on Armbian's rockchip64-edge kernel" supported_architectures = frozenset(["arm64"]) # does NOT support amd64 - def kernel_packages(self) -> set[str]: - return {"linux-image-current-bcm2711"} + def flavor_packages(self) -> set[str]: + return {"linux-image-current-bcm2711"}.union(super().flavor_packages()) diff --git a/captain/flavors/trixie_full/__init__.py b/captain/flavors/trixie_full/__init__.py index 9b81c31e..cc369bd5 100644 --- a/captain/flavors/trixie_full/__init__.py +++ b/captain/flavors/trixie_full/__init__.py @@ -18,5 +18,17 @@ class TrixieFullFlavor(TrixieACPIFlavor): description = "Debian Trixie based with linux-image-generic standard Debian kernel" supported_architectures = frozenset(["amd64", "arm64"]) - def kernel_packages(self) -> set[str]: - return {"linux-image-generic"} + def flavor_packages(self) -> set[str]: + pkgs = { + "linux-image-generic", # Debian's standard kernel (arm64/amd64) + "tiny-initramfs", # A tiny initramfs builder; required for kernel image + } + if self.include_working_apt(): + pkgs.add("apt") + return pkgs.union(super().flavor_packages()) + + def include_hwdb(self) -> bool: + return True + + def include_working_apt(self) -> bool: + return True diff --git a/captain/flavors/trixie_meson64/__init__.py b/captain/flavors/trixie_meson64/__init__.py index f3dd09e5..ab598542 100644 --- a/captain/flavors/trixie_meson64/__init__.py +++ b/captain/flavors/trixie_meson64/__init__.py @@ -18,5 +18,5 @@ class TrixieMeson64Flavor(ArmbianCommonFlavor): description = "Debian Trixie based with Armbian's meson64-edge kernel" supported_architectures = frozenset(["arm64"]) # does NOT support amd64 - def kernel_packages(self) -> set[str]: - return {"linux-image-edge-meson64"} + def flavor_packages(self) -> set[str]: + return {"linux-image-edge-meson64"}.union(super().flavor_packages()) diff --git a/captain/flavors/trixie_rockchip64/__init__.py b/captain/flavors/trixie_rockchip64/__init__.py index c49b5ab7..7e5fa946 100644 --- a/captain/flavors/trixie_rockchip64/__init__.py +++ b/captain/flavors/trixie_rockchip64/__init__.py @@ -18,5 +18,5 @@ class TrixieRockchip64Flavor(ArmbianCommonFlavor): description = "Debian Trixie based with Armbian's rockchip64-edge kernel" supported_architectures = frozenset(["arm64"]) # does NOT support amd64 - def kernel_packages(self) -> set[str]: - return {"linux-image-edge-rockchip64"} + def flavor_packages(self) -> set[str]: + return {"linux-image-edge-rockchip64"}.union(super().flavor_packages()) diff --git a/captain/flavors/trixie_rockchip64_vendor/__init__.py b/captain/flavors/trixie_rockchip64_vendor/__init__.py index 0b9213f6..30eceb8f 100644 --- a/captain/flavors/trixie_rockchip64_vendor/__init__.py +++ b/captain/flavors/trixie_rockchip64_vendor/__init__.py @@ -18,5 +18,5 @@ class TrixieRockchip64VendorFlavor(ArmbianCommonFlavor): description = "Debian Trixie based with Armbian's rk35xx-vendor kernel" supported_architectures = frozenset(["arm64"]) # does NOT support amd64 - def kernel_packages(self) -> set[str]: - return {"linux-image-vendor-rk35xx"} + def flavor_packages(self) -> set[str]: + return {"linux-image-vendor-rk35xx"}.union(super().flavor_packages()) diff --git a/captain/flavors/trixie_slim/__init__.py b/captain/flavors/trixie_slim/__init__.py index 9af4a51f..e3832649 100644 --- a/captain/flavors/trixie_slim/__init__.py +++ b/captain/flavors/trixie_slim/__init__.py @@ -25,8 +25,8 @@ def pre_mkosi_stage(self): # call the kernel build stage, to ensure kernel .deb is in mkosi.output/kernel/ stages.build_kernel_stage(self.cfg) - def kernel_packages(self) -> set[str]: - return {f"linux-image-{self.cfg.kernel_version}-captainos"} + def flavor_packages(self) -> set[str]: + return {f"linux-image-{self.cfg.kernel_version}-captainos"}.union(super().flavor_packages()) def package_directories(self) -> set[str]: return {str(obtain_target_artifact_path(self.cfg).parent.relative_to(self.cfg.project_dir))} From 5a61f89245ec6b48ae3ad3ee56351bf5df2a91a3 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 17 Apr 2026 20:00:56 +0200 Subject: [PATCH 103/143] flavors: introduce `trixie-genio` for Armbian's linux-image-edge-genio Signed-off-by: Ricardo Pardini --- captain/flavors/trixie_genio/__init__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 captain/flavors/trixie_genio/__init__.py diff --git a/captain/flavors/trixie_genio/__init__.py b/captain/flavors/trixie_genio/__init__.py new file mode 100644 index 00000000..494fcfe7 --- /dev/null +++ b/captain/flavors/trixie_genio/__init__.py @@ -0,0 +1,22 @@ +import logging +from dataclasses import dataclass + +from captain.flavor import BaseFlavor +from captain.flavors.common_armbian import ArmbianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieGenioFlavor() + + +@dataclass +class TrixieGenioFlavor(ArmbianCommonFlavor): + id = "trixie-genio" + name = "Trixie for Mediatek Genio 64-bit ARM machines" + description = "Debian Trixie with Armbian's genio-edge kernel" + supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + + def flavor_packages(self) -> set[str]: + return {"linux-image-edge-genio"}.union(super().flavor_packages()) From 9dc365867e5e3436fca5a2016b819bdadd06be64 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 17 Apr 2026 20:01:32 +0200 Subject: [PATCH 104/143] gha: add trixie-genio build Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a40577d2..b3a543c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -270,6 +270,7 @@ jobs: - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: true, FLAVOR_ID: "trixie-full" } - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-rockchip64" } - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-rockchip64-vendor" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-genio" } - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-meson64" } - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-armbian-rpi" } From 07f6c43335722137a169b5bc614cc8b9f889c6bc Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 20 Apr 2026 14:45:19 +0200 Subject: [PATCH 105/143] kernel: 6.18.y amd64: CONFIG_BE2NET=y (from #59) - via `uv run captain --arch amd64 kernel --config`, then pick `CONFIG_BE2NET` Signed-off-by: Ricardo Pardini --- kernel.configs/6.18.y.amd64 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel.configs/6.18.y.amd64 b/kernel.configs/6.18.y.amd64 index 36f25819..9814e51d 100644 --- a/kernel.configs/6.18.y.amd64 +++ b/kernel.configs/6.18.y.amd64 @@ -503,7 +503,7 @@ CONFIG_BNXT=y # CONFIG_NET_VENDOR_CORTINA is not set # CONFIG_NET_VENDOR_DEC is not set # CONFIG_NET_VENDOR_DLINK is not set -# CONFIG_NET_VENDOR_EMULEX is not set +CONFIG_BE2NET=y # CONFIG_NET_VENDOR_EZCHIP is not set CONFIG_GVE=m # CONFIG_NET_VENDOR_HUAWEI is not set From 4700a2ab794da6dfb1321868de12b05398aac270 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 20 Apr 2026 14:52:38 +0200 Subject: [PATCH 106/143] docker: pass `--load` to `docker buildx build` - from https://github.com/tinkerbell/captain/pull/64 Signed-off-by: Ricardo Pardini --- captain/docker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/captain/docker.py b/captain/docker.py index 20584955..acdaebe0 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -97,6 +97,7 @@ def obtain_builder(cfg: Config) -> None: "docker", "buildx", "build", + "--load", # for older Docker versions "--progress=plain", "-t", local_tagged_image, From e939389335f115de0e503f97a602aa3539aae2e4 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 21 Apr 2026 13:59:10 +0200 Subject: [PATCH 107/143] release-publish: fix to use run_captain_in_builder() Signed-off-by: Ricardo Pardini --- captain/cli/_release_publish.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/captain/cli/_release_publish.py b/captain/cli/_release_publish.py index 6561cd00..6e9617c7 100644 --- a/captain/cli/_release_publish.py +++ b/captain/cli/_release_publish.py @@ -128,8 +128,6 @@ def release_publish_cmd( sha = _resolve_git_sha(git_sha, cfg.project_dir) env_args: list[str] = [ - "-e", - f"FLAVOR_ID={cfg.flavor_id}", "-e", f"REGISTRY={registry}", "-e", @@ -146,23 +144,7 @@ def release_publish_cmd( if force: env_args += ["-e", "FORCE=true"] - inner_cmd = ["captain", "release-publish"] - - try: - docker.run_in_builder( - cfg, - command_and_args=[ - "--entrypoint", - "/usr/bin/uv", - cfg.builder_image, - *(["--verbose"] if cfg.verbose_uv else ["--quiet"]), - "run", - *inner_cmd, - ], - extra_docker_args=env_args, - ) - except subprocess.CalledProcessError as exc: - raise SystemExit(exc.returncode) from None + docker.run_captain_in_builder(cfg, ["release-publish"], extra_docker_args=env_args) docker.fix_docker_ownership(cfg, ["/work/out"]) return From 34d0d125efa3c8f03c6426e8ae7f637a6fa8699e Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 21 Apr 2026 14:04:46 +0200 Subject: [PATCH 108/143] initramfs/iso: introduce content-hashing for idempotency - flavor can produce a hash of contents - incl static + templated files - does NOT include extratrees or packagedirs yet (kernel/tools) - does NOT clean up leftovers yet Signed-off-by: Ricardo Pardini --- captain/cli/_iso.py | 3 +- captain/cli/stages.py | 5 ++- captain/config.py | 23 +++++++++-- captain/flavor.py | 46 ++++++++++++++++++--- captain/flavors/common_debian/__init__.py | 3 ++ captain/flavors/common_debian/mkosi.conf.j2 | 2 +- captain/oci/_publish.py | 2 +- 7 files changed, 71 insertions(+), 13 deletions(-) diff --git a/captain/cli/_iso.py b/captain/cli/_iso.py index 631d2409..c320f87a 100644 --- a/captain/cli/_iso.py +++ b/captain/cli/_iso.py @@ -51,7 +51,8 @@ def build_cmd( ) # Instantiate the flavor - captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + flavor.generate(hash_only=True) # Just populates cfg.flavor_hash so we can find the initramfs _build_iso_stage(cfg) artifacts.collect_iso(cfg) diff --git a/captain/cli/stages.py b/captain/cli/stages.py index cbd51948..00a028f0 100644 --- a/captain/cli/stages.py +++ b/captain/cli/stages.py @@ -4,6 +4,7 @@ import logging import shutil +from pathlib import Path from captain import docker, iso, kernel, tools from captain.config import Config @@ -145,8 +146,8 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: # --- docker ------------------------------------------------------- docker.obtain_builder(cfg) log.info("Building initrd with mkosi (docker)...") - tools_tree = f"/work/mkosi.output/tools/{cfg.arch}" - output_dir = f"/work/mkosi.output/initramfs/{cfg.flavor_id}/{cfg.arch}" + tools_tree = f"/work/mkosi.output/tools/{cfg.arch}" # @TODO ooops + output_dir = cfg.calc_initramfs_output(Path("/work"), "initramfs") docker.run_mkosi_in_builder( cfg, f"--extra-tree={tools_tree}", diff --git a/captain/config.py b/captain/config.py index 281d0cd1..22f5c56c 100644 --- a/captain/config.py +++ b/captain/config.py @@ -31,11 +31,14 @@ class Config: # Paths project_dir: Path - output_dir: Path + + # "across invocations" / "GHA interface dir"; project_dir / "out" + output_dir: Path # target of collect_xx() stuff, coming from mkosi.output # Target arch: str = "amd64" flavor_id: str = DEFAULT_FLAVOR_ID + flavor_hash: str | None = None # resolved at runtime # For kernel build build_kernel: bool = False @@ -117,12 +120,26 @@ def mkosi_output(self) -> Path: @property def initramfs_output(self) -> Path: """Per-version, per-arch directory for mkosi initramfs output.""" - return self.project_dir / "mkosi.output" / "initramfs" / self.flavor_id / self.arch + base_dir: Path = self.project_dir + return self.calc_initramfs_output(base_dir, "initramfs") @property def iso_output(self) -> Path: """Per-version, per-arch directory for the built ISO image.""" - return self.project_dir / "mkosi.output" / "iso" / self.flavor_id / self.arch + base_dir: Path = self.project_dir + return self.calc_initramfs_output(base_dir, "iso") + + def calc_initramfs_output(self, base_dir: Path, which: str) -> Path: + log.debug("Asked for %s output dir; flavor_hash=%r", which, self.flavor_hash) + # To know the output dir, we must know the flavor_hash; make sure. + if self.flavor_hash is None: + log.error("flavor_hash is not set; cannot determine initramfs output directory") + raise ValueError("flavor_hash is not set; cannot determine initramfs output directory") + + flavor_id = ( + base_dir / "mkosi.output" / which / self.flavor_id / self.arch / self.flavor_hash + ) + return flavor_id @property def iso_staging(self) -> Path: diff --git a/captain/flavor.py b/captain/flavor.py index b02c08aa..2b9eaa6c 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import logging import shutil from abc import abstractmethod @@ -9,7 +10,9 @@ from typing import Protocol, runtime_checkable import jinja2 +from rich.table import Table +import captain from captain.config import Config log = logging.getLogger(__name__) @@ -42,7 +45,7 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: log.debug("Called BaseFlavor.setup()...") pass - def generate(self): + def generate(self, hash_only: bool = False): log.debug("Called BaseFlavor.generate()...") # Before generating, cleanup known targets. @TODO make dir disposable instead log.debug("Cleaning up old generated files in %s", self.cfg.project_dir) @@ -53,8 +56,28 @@ def generate(self): shutil.rmtree(self.cfg.project_dir / "mkosi.sandbox", ignore_errors=True) shutil.rmtree(self.cfg.project_dir / "mkosi.skeleton", ignore_errors=True) - self.copy_static_files(self.cfg.project_dir) - self.render_templates(self.cfg.project_dir) # For compatibility + hashes: dict[str, str] = {} + self.copy_static_files(self.cfg.project_dir, hash_only, hashes) + self.render_templates(self.cfg.project_dir, hash_only, hashes) # For compatibility + + self.cfg.flavor_hash = hashlib.sha256( + "".join(f"{k}={v}" for k, v in (sorted(hashes.items()))).encode() + ).hexdigest()[:16] + log.info( + "Generated flavor_hash: %s (based on %s hashes)", self.cfg.flavor_hash, len(hashes) + ) + + # Emit a Rich Table of the generated files and their hashes + if log.isEnabledFor(logging.DEBUG): + table = Table( + title="Generated Files and Hashes", show_header=True, header_style="bold magenta" + ) + table.add_column("Relative Path", style="dim", width=40) + table.add_column("SHA256 Hash", style="dim", width=64) + for relative_path, hash_value in hashes.items(): + table.add_row(relative_path, hash_value) + captain.console.print(table) + pass def specific_flavor_dir(self, flavor_id: str) -> Path: @@ -78,7 +101,9 @@ def add_static_dir(self, dir_to_include: str, flavor_dir: Path): relative_path = extra_file.relative_to(flavor_dir) self.static_map[str(relative_path)] = extra_file - def render_templates(self, output_dir: Path): + def render_templates( + self, output_dir: Path, hash_only: bool = False, hashes: dict[str, str] | None = None + ): log.debug("Called BaseFlavor.render_templates() with output_dir: %s", output_dir) # Use jinja2 to render all templates in self.template_map, writing output to output_dir # The keys of self.template_map are the relative output paths (e.g. "mkosi.conf"), and the @@ -103,6 +128,11 @@ def render_templates(self, output_dir: Path): ).get_template(template_path.name) rendered_content += template.render(cfg=self.cfg, flavor=self) + if hashes is not None: + hashes[relative_output_path] = hashlib.sha256(rendered_content.encode()).hexdigest() + if hash_only: + continue # Skip writing if we're only interested in hashes + output_file_path = output_dir / relative_output_path log.debug("Writing rendered content to %s", output_file_path) output_file_path.parent.mkdir(parents=True, exist_ok=True) @@ -111,10 +141,16 @@ def render_templates(self, output_dir: Path): # Make output_file executable @TODO: we will need a way to tell output_file_path.chmod(output_file_path.stat().st_mode | 0o111) - def copy_static_files(self, project_dir): + def copy_static_files( + self, project_dir, hash_only: bool = False, hashes: dict[str, str] | None = None + ): # Do a plain copy of all files in self.static_map to project_dir / relative_path, where # relative_path is the key in self.static_map for relative_path, source_path in self.static_map.items(): + if hashes is not None: + hashes[relative_path] = hashlib.sha256(source_path.read_bytes()).hexdigest() + if hash_only: + continue # Skip copying if we're only interested in hashes destination_path = project_dir / relative_path log.debug("Copying static file from '%s' to '%s'", source_path, destination_path) destination_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py index 0b859e33..35cda527 100644 --- a/captain/flavors/common_debian/__init__.py +++ b/captain/flavors/common_debian/__init__.py @@ -42,6 +42,9 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: def flavor_packages(self) -> set[str]: return set({}) + def sorted_flavor_packages(self) -> list[str]: + return sorted(self.flavor_packages()) + def package_directories(self) -> set[str]: return set({}) diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index cf2916a0..efa118f2 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -34,7 +34,7 @@ PackageDirectories= # pivot_root(2) work for container runtimes (runc). Packages= - {{ "\n ".join(flavor.flavor_packages()) }} + {{ "\n ".join(flavor.sorted_flavor_packages()) }} # systemd and core systemd systemd-sysv # Provides poweroff, shutdown, reboot, halt, etc. diff --git a/captain/oci/_publish.py b/captain/oci/_publish.py index eebf5205..33cb545b 100644 --- a/captain/oci/_publish.py +++ b/captain/oci/_publish.py @@ -203,7 +203,7 @@ def publish( arch_layer_tars[arch] = [_deterministic_tar(f, out) for f in files] # A single layer for all DTBs, if any; those are highly compressible together. - dtb_dir_in = out / f"dtb-{cfg.flavor_id}-{get_arch_info(arch).output_arch}" + dtb_dir_in = out / f"dtb-{cfg.flavor_id}-{get_arch_info(arch).output_arch}" # @TODO "out" if not dtb_dir_in.is_dir(): log.warning("No dtbs directory found for %s: %s", arch, dtb_dir_in) else: From cab586dbc3c73bb5ec3f6879d3ceb7380888a6e3 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 21 Apr 2026 14:15:15 +0200 Subject: [PATCH 109/143] gha: set CAPTAIN_VERBOSE=1 & allow workflow_dispatch Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3a543c6..a7975bd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,6 @@ name: For each commit and PR on: + workflow_dispatch: push: branches: - "main" @@ -11,6 +12,7 @@ permissions: env: FORCE_COLOR: "1" + CAPTAIN_VERBOSE: "1" jobs: # ------------------------------------------------------------------- From d6331e5724717d134bc31e26f34f84a3b0b22ac5 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 21 Apr 2026 14:15:40 +0200 Subject: [PATCH 110/143] gha: add GHA caches for initramfs and iso Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7975bd4..e17f87e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -210,12 +210,51 @@ jobs: chmod +x mkosi.output/tools/${{ matrix.arch }}/usr/local/bin/* chmod +x mkosi.output/tools/${{ matrix.arch }}/opt/cni/bin/* + - name: Restore initramfs cache + id: initramfs-cache + uses: actions/cache/restore@v5 + with: + path: | + mkosi.output/initramfs + key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time + restore-keys: | + initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}- + + - name: Restore iso cache + id: iso-cache + if: ${{ matrix.iso }} # only if matrix entry had iso: true + uses: actions/cache/restore@v5 + with: + path: | + mkosi.output/iso + key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time + restore-keys: | + iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}- + - name: Install uv uses: astral-sh/setup-uv@v7 - name: Build initramfs run: uv run captain build # full build, incl initramfs and iso when appropriate + - name: Save initramfs cache + if: github.ref == 'refs/heads/main' && steps.initramfs-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: | + mkosi.output/initramfs + key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + + - name: Save iso cache + if: matrix.iso && github.ref == 'refs/heads/main' && steps.iso-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: | + mkosi.output/iso + key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + - name: Upload initramfs artifacts uses: actions/upload-artifact@v6 with: From c9985911b3b70da41645dca869f092fba6e2cb99 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 21 Apr 2026 23:32:24 +0200 Subject: [PATCH 111/143] tools: default to `native` mode (avoid pulling builder for cache hits) Signed-off-by: Ricardo Pardini --- captain/cli/_build.py | 2 +- captain/cli/stages.py | 1 + captain/config.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/captain/cli/_build.py b/captain/cli/_build.py index 2a251948..e7912088 100644 --- a/captain/cli/_build.py +++ b/captain/cli/_build.py @@ -34,7 +34,7 @@ @click.option( "--tools-mode", envvar="TOOLS_MODE", - default="docker", + default="native", show_default=True, type=click.Choice(["docker", "native", "skip"], case_sensitive=False), metavar="MODE", diff --git a/captain/cli/stages.py b/captain/cli/stages.py index 00a028f0..357422ef 100644 --- a/captain/cli/stages.py +++ b/captain/cli/stages.py @@ -63,6 +63,7 @@ def build_kernel_stage(cfg: Config) -> None: def _build_tools_stage(cfg: Config) -> None: """Run the tools download stage according to *cfg.tools_mode*.""" + log.debug("Building tools stage in mode %s", cfg.tools_mode) # --- skip --------------------------------------------------------- if cfg.tools_mode == "skip": diff --git a/captain/config.py b/captain/config.py index 22f5c56c..b02c6d1b 100644 --- a/captain/config.py +++ b/captain/config.py @@ -55,7 +55,7 @@ class Config: verbose_docker: bool = False # Pass --verbose to captain when re-launched in Docker # Per-stage mode: "docker" | "native" | "skip" - tools_mode: str = "docker" + tools_mode: str = "native" mkosi_mode: str = "docker" iso_mode: str = "docker" release_mode: str = "docker" From 4a38c1be538f6d1f09f88247678f49163f9b7e34 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 22 Apr 2026 00:21:18 +0200 Subject: [PATCH 112/143] publish: more determinism - deterministic tar for dtb layer too - use epoch as `created` date, so manifests also hit Signed-off-by: Ricardo Pardini --- captain/oci/_build.py | 14 ++++++++++++++ captain/oci/_publish.py | 18 +++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/captain/oci/_build.py b/captain/oci/_build.py index ac846d62..b7c3369f 100644 --- a/captain/oci/_build.py +++ b/captain/oci/_build.py @@ -35,6 +35,20 @@ def _deterministic_tar(file_path: Path, output_dir: Path) -> Path: return tar_path +def _deterministic_tar_multiple(all_dtb_files: list[Path], dtb_tar_path, out: Path): + with tarfile.open(dtb_tar_path, "w") as tf: + for f in all_dtb_files: + info = tf.gettarinfo(str(f), arcname=str(f.relative_to(out))) + info.uid = 0 + info.gid = 0 + info.uname = "" + info.gname = "" + info.mtime = 0 + info.mode = 0o644 + with open(f, "rb") as fh: + tf.addfile(info, fh) + + def _collect_arch_artifacts( project_dir: Path, out: Path, diff --git a/captain/oci/_publish.py b/captain/oci/_publish.py index 33cb545b..ec89a668 100644 --- a/captain/oci/_publish.py +++ b/captain/oci/_publish.py @@ -4,7 +4,6 @@ import contextlib import logging -import tarfile from datetime import datetime, timezone from pathlib import Path from uuid import uuid4 @@ -14,7 +13,12 @@ from captain.flavor import BaseFlavor from captain.util import ensure_dir, get_arch_info -from ._build import _build_platform_image, _collect_arch_artifacts, _deterministic_tar +from ._build import ( + _build_platform_image, + _collect_arch_artifacts, + _deterministic_tar, + _deterministic_tar_multiple, +) from ._common import _ARCHES, _image_ref log = logging.getLogger(__name__) @@ -187,7 +191,9 @@ def publish( return out = ensure_dir(cfg.output_dir) - created = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + # Set created to the epoch so it is always the same + created = datetime(1970, 1, 1, tzinfo=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") # Collect artifacts for every requested architecture. arch_files: dict[str, list[Path]] = {} @@ -209,10 +215,8 @@ def publish( else: log.info(f"Found DTB directory for {arch}: {dtb_dir_in}") all_dtb_files: list[Path] = sorted(dtb_dir_in.glob("**/*.dtb*")) - dtb_tar_path = out / f"dtbs-{cfg.flavor_id}-{arch}.tar" - with tarfile.open(dtb_tar_path, "w") as tar: - for f in all_dtb_files: - tar.add(f, arcname=f.relative_to(out)) + dtb_tar_path = out / f".layer-dtbs-{cfg.flavor_id}-{arch}.tar" + _deterministic_tar_multiple(all_dtb_files, dtb_tar_path, out) arch_layer_tars[arch].append(dtb_tar_path) pushed = True From a38e8577d898a801e923f448a9ade2b3ab817f06 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 22 Apr 2026 12:18:43 +0200 Subject: [PATCH 113/143] flavors: common-debian: reduce postinst spurious debug logging Signed-off-by: Ricardo Pardini --- captain/flavors/common_debian/mkosi.postinst.sh.j2 | 6 ------ 1 file changed, 6 deletions(-) diff --git a/captain/flavors/common_debian/mkosi.postinst.sh.j2 b/captain/flavors/common_debian/mkosi.postinst.sh.j2 index 0dd8da04..d4b72230 100755 --- a/captain/flavors/common_debian/mkosi.postinst.sh.j2 +++ b/captain/flavors/common_debian/mkosi.postinst.sh.j2 @@ -5,12 +5,6 @@ # creating directories and dropping config files. set -euo pipefail -log info "CaptainOS post-install: dump env at $(hostname)..." -env - -log info "CaptainOS post-install: show mounts at $(hostname)..." -mount || true - # Ensure CA certificate bundle is generated (dpkg triggers may not # fire reliably in mkosi's chroot). log info "Generating fresh CA certificate bundle..." From f80c8f2fc511ce2e30ccf5e7ddef7fbc88a6f912 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 22 Apr 2026 12:24:26 +0200 Subject: [PATCH 114/143] captain: split mkosi.input from mkosi.output - `tools` and `kernel` are really inputs to mkosi, not outputs Signed-off-by: Ricardo Pardini --- .dockerignore | 1 + .gitignore | 1 + captain/cli/_tools.py | 2 +- captain/cli/stages.py | 8 ++++---- captain/config.py | 8 ++------ captain/docker.py | 1 + captain/flavors/trixie_slim/__init__.py | 2 +- captain/tools.py | 2 +- pyproject.toml | 2 +- 9 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.dockerignore b/.dockerignore index b46e3e81..cf9fbee7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ # Exclude build artifacts from Docker build context mkosi.output/ +mkosi.input/ mkosi.cache/ mkosi.tools/ mkosi.tools.manifest diff --git a/.gitignore b/.gitignore index 4098d42b..8fb78ba4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build outputs mkosi.output/ +mkosi.input/ mkosi.cache/ mkosi.tools/ mkosi.tools.manifest diff --git a/captain/cli/_tools.py b/captain/cli/_tools.py index d5211f3e..7e5968e0 100644 --- a/captain/cli/_tools.py +++ b/captain/cli/_tools.py @@ -42,7 +42,7 @@ def tools_cmd( """Download tools (containerd, runc, nerdctl, CNI plugins). Fetches pre-built binaries for the target architecture and stages - them under ``mkosi.output/tools/{arch}/``. The tools are later + them under ``mkosi.input/tools/{arch}/``. The tools are later merged into the initramfs by mkosi via ``--extra-tree``. \b diff --git a/captain/cli/stages.py b/captain/cli/stages.py index 357422ef..fb280d99 100644 --- a/captain/cli/stages.py +++ b/captain/cli/stages.py @@ -15,7 +15,7 @@ def build_kernel_stage(cfg: Config) -> None: """Run the kernel build stage according to *cfg.kernel_mode*.""" - log.warning("Building kernel stage in mode %s", cfg.kernel_mode) + log.debug("Building kernel stage in mode %s", cfg.kernel_mode) # --- skip --------------------------------------------------------- if cfg.kernel_mode == "skip": @@ -55,7 +55,7 @@ def build_kernel_stage(cfg: Config) -> None: docker.fix_docker_ownership( cfg, [ - f"/work/mkosi.output/kernel/{cfg.kernel_version}/{cfg.arch}", + f"/work/mkosi.input/kernel/{cfg.kernel_version}/{cfg.arch}", "/work/out", ], ) @@ -80,7 +80,7 @@ def _build_tools_stage(cfg: Config) -> None: docker.obtain_builder(cfg) log.info("Downloading tools (nerdctl, containerd, etc.) docker...") docker.run_captain_in_builder(cfg, ["tools"]) - docker.fix_docker_ownership(cfg, ["/work/mkosi.output"]) + docker.fix_docker_ownership(cfg, ["/work/mkosi.input"]) def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: @@ -147,7 +147,7 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: # --- docker ------------------------------------------------------- docker.obtain_builder(cfg) log.info("Building initrd with mkosi (docker)...") - tools_tree = f"/work/mkosi.output/tools/{cfg.arch}" # @TODO ooops + tools_tree = f"/work/mkosi.input/tools/{cfg.arch}" # @TODO ooops output_dir = cfg.calc_initramfs_output(Path("/work"), "initramfs") docker.run_mkosi_in_builder( cfg, diff --git a/captain/config.py b/captain/config.py index b02c6d1b..951ca9f5 100644 --- a/captain/config.py +++ b/captain/config.py @@ -100,7 +100,7 @@ def tools_output(self) -> Path: Passed to mkosi via ``--extra-tree=`` to merge into the initramfs. Kernel modules are stored separately under :attr:`kernel_output`. """ - return self.project_dir / "mkosi.output" / "tools" / self.arch + return self.project_dir / "mkosi.input" / "tools" / self.arch @property def kernel_output(self) -> Path: @@ -111,11 +111,7 @@ def kernel_output(self) -> Path: (``usr/lib/modules/{kver}/``) so it can be passed directly as an ``--extra-tree=`` to mkosi. """ - return self.project_dir / "mkosi.output" / "kernel" / self.arch - - @property - def mkosi_output(self) -> Path: - return self.project_dir / "mkosi.output" + return self.project_dir / "mkosi.input" / "kernel" / self.arch @property def initramfs_output(self) -> Path: diff --git a/captain/docker.py b/captain/docker.py index acdaebe0..d96e2848 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -209,6 +209,7 @@ def run_in_builder( docker_args += ["-v", f"{cfg.project_dir}/kernel.configs:/work/kernel.configs"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.output:/work/mkosi.output"] + docker_args += ["-v", f"{cfg.project_dir}/mkosi.input:/work/mkosi.input"] docker_args += ["-v", f"{cfg.project_dir}/out:/work/out"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.extra:/work/mkosi.extra"] diff --git a/captain/flavors/trixie_slim/__init__.py b/captain/flavors/trixie_slim/__init__.py index e3832649..bd9f6042 100644 --- a/captain/flavors/trixie_slim/__init__.py +++ b/captain/flavors/trixie_slim/__init__.py @@ -22,7 +22,7 @@ class TrixieSlimFlavor(TrixieACPIFlavor): def pre_mkosi_stage(self): log.debug("Flavor delegating to build_kernel_stage to ensure kernel .deb is present") - # call the kernel build stage, to ensure kernel .deb is in mkosi.output/kernel/ + # call the kernel build stage, to ensure kernel .deb is in mkosi.input/kernel/ stages.build_kernel_stage(self.cfg) def flavor_packages(self) -> set[str]: diff --git a/captain/tools.py b/captain/tools.py index d4f6facf..ac2a182e 100644 --- a/captain/tools.py +++ b/captain/tools.py @@ -158,7 +158,7 @@ def download_tool(tool: ToolSpec, arch: str, output_base: Path, force: bool) -> def download_all(cfg: Config) -> None: - """Download all tools into mkosi.output/tools/{arch}/.""" + """Download all tools into mkosi.input/tools/{arch}/.""" arch = cfg.arch_info.dl_arch output_base = cfg.tools_output diff --git a/pyproject.toml b/pyproject.toml index 3ecbea2a..daefe5c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,5 +40,5 @@ select = [ [tool.pyright] pythonVersion = "3.13" typeCheckingMode = "standard" -exclude = ["mkosi.tools", "mkosi.output", "__pycache__"] +exclude = ["mkosi.tools", "mkosi.output", "mkosi.input", "__pycache__"] include = ["captain", "build.py"] From ffe55c116bf02954faa76827d9408295fe717db7 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 22 Apr 2026 12:24:36 +0200 Subject: [PATCH 115/143] gha: split mkosi.input from mkosi.output - `tools` and `kernel` are really inputs to mkosi, not outputs Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e17f87e1..7a31c417 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,8 +56,8 @@ jobs: uses: actions/cache/restore@v5 with: path: | - mkosi.output/tools/${{ matrix.arch }}/usr/local/bin - mkosi.output/tools/${{ matrix.arch }}/opt/cni + mkosi.input/tools/${{ matrix.arch }}/usr/local/bin + mkosi.input/tools/${{ matrix.arch }}/opt/cni key: tools-${{ matrix.arch }}-${{ hashFiles('captain/tools.py') }} - name: Install uv @@ -73,8 +73,8 @@ jobs: uses: actions/cache/save@v5 with: path: | - mkosi.output/tools/${{ matrix.arch }}/usr/local/bin - mkosi.output/tools/${{ matrix.arch }}/opt/cni + mkosi.input/tools/${{ matrix.arch }}/usr/local/bin + mkosi.input/tools/${{ matrix.arch }}/opt/cni key: tools-${{ matrix.arch }}-${{ hashFiles('captain/tools.py') }} - name: Upload tools artifacts @@ -82,8 +82,8 @@ jobs: with: name: tools-${{ matrix.arch }} path: | - mkosi.output/tools/${{ matrix.arch }}/usr/local/bin - mkosi.output/tools/${{ matrix.arch }}/opt/cni + mkosi.input/tools/${{ matrix.arch }}/usr/local/bin + mkosi.input/tools/${{ matrix.arch }}/opt/cni retention-days: 1 # ------------------------------------------------------------------- @@ -142,7 +142,7 @@ jobs: uses: actions/cache/restore@v5 with: path: | - mkosi.output/kernel/${{ matrix.arch }} + mkosi.input/kernel/${{ matrix.arch }} key: kernel-${{ matrix.arch }}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time restore-keys: | @@ -159,7 +159,7 @@ jobs: uses: actions/cache/save@v5 with: path: | - mkosi.output/kernel/${{ matrix.arch }} + mkosi.input/kernel/${{ matrix.arch }} key: kernel-${{ matrix.arch }}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} - name: Upload kernel artifacts @@ -167,7 +167,7 @@ jobs: with: name: kernel-${{ matrix.arch }} path: | - mkosi.output/kernel/${{ matrix.arch }} + mkosi.input/kernel/${{ matrix.arch }} retention-days: 1 @@ -194,21 +194,21 @@ jobs: uses: actions/download-artifact@v8 with: name: tools-${{ matrix.arch }} - path: mkosi.output/tools/${{ matrix.arch }} + path: mkosi.input/tools/${{ matrix.arch }} - name: Download kernel artifacts == ${{ matrix.captainos_kernel }} uses: actions/download-artifact@v8 if: ${{ matrix.captainos_kernel }} # only if matrix entry had captainos_kernel: true with: name: kernel-${{ matrix.arch }} - path: mkosi.output/kernel/${{ matrix.arch }} + path: mkosi.input/kernel/${{ matrix.arch }} - name: Restore tool binary permissions run: | # GitHub Actions artifact upload/download strips execute permissions. # Restore +x on all tool binaries so they work inside the initramfs. - chmod +x mkosi.output/tools/${{ matrix.arch }}/usr/local/bin/* - chmod +x mkosi.output/tools/${{ matrix.arch }}/opt/cni/bin/* + chmod +x mkosi.input/tools/${{ matrix.arch }}/usr/local/bin/* + chmod +x mkosi.input/tools/${{ matrix.arch }}/opt/cni/bin/* - name: Restore initramfs cache id: initramfs-cache From 0e49ac8caf10484af6429cdef48a62f6467b5261 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Fri, 24 Apr 2026 12:38:27 +0200 Subject: [PATCH 116/143] publish: let flavor decide which files/dirs are published in OCI images - BaseFlavor: - `list_arch_artifacts()` handles vmlinuz, initramfs, and iso directly - delegates to overridable `add_arch_dtb_artifacts()` - defaults to including dtbs for aarch64 - but not for trixie-slim (to keep it, well, "slim") - introduce artifacts.py `OutputArchArtifactType` and `OutputArchArtifact` - just to type-enforce - `_collect_arch_artifacts()` is dead, long live `_checksum_files()` - rework logic in `oci._publish()` - this is to prepare for extra per-flavor assets, eg "rpi eeprom firmware" for rpi flavor Signed-off-by: Ricardo Pardini --- captain/artifacts.py | 14 +++++ captain/flavor.py | 37 +++++++++++++ captain/flavors/trixie_slim/__init__.py | 5 ++ captain/oci/_build.py | 23 +------- captain/oci/_publish.py | 73 +++++++++++++++++++------ 5 files changed, 114 insertions(+), 38 deletions(-) diff --git a/captain/artifacts.py b/captain/artifacts.py index 82b07abe..d962e368 100644 --- a/captain/artifacts.py +++ b/captain/artifacts.py @@ -5,6 +5,8 @@ import hashlib import logging import shutil +from dataclasses import dataclass +from enum import StrEnum from pathlib import Path from captain.config import Config @@ -132,3 +134,15 @@ def collect(cfg: Config) -> None: collect_initramfs(cfg) collect_kernel(cfg) collect_iso(cfg) + + +class OutputArchArtifactType(StrEnum): + FILE = "file" + DIRECTORY = "directory" + + +@dataclass +class OutputArchArtifact: + # type is either File or Directory + type: OutputArchArtifactType + name: str diff --git a/captain/flavor.py b/captain/flavor.py index 2b9eaa6c..8826d02d 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -13,6 +13,7 @@ from rich.table import Table import captain +from captain.artifacts import OutputArchArtifact, OutputArchArtifactType from captain.config import Config log = logging.getLogger(__name__) @@ -163,6 +164,42 @@ def has_iso(self) -> bool: def pre_mkosi_stage(self): pass + def list_arch_artifacts(self, output_arch: str) -> list[OutputArchArtifact]: + artifacts: list[OutputArchArtifact] = [ + OutputArchArtifact( + type=OutputArchArtifactType.FILE, name=f"vmlinuz-{self.cfg.flavor_id}-{output_arch}" + ), + OutputArchArtifact( + type=OutputArchArtifactType.FILE, + name=f"initramfs-{self.cfg.flavor_id}-{output_arch}", + ), + ] + # include .iso if the flavor supports it. + if self.has_iso(): + artifacts += [ + OutputArchArtifact( + type=OutputArchArtifactType.FILE, + name=f"captainos-{self.cfg.flavor_id}-{output_arch}.iso", + ) + ] + + self.add_arch_dtb_artifacts(artifacts, output_arch) + + log.info( + "Artifacts for flavor %s for arch '%s': %s", self.cfg.flavor_id, output_arch, artifacts + ) + return artifacts + + def add_arch_dtb_artifacts(self, artifacts: list[OutputArchArtifact], output_arch: str): + # if arm64, include DTBs. + if output_arch == "aarch64": + artifacts.append( + OutputArchArtifact( + type=OutputArchArtifactType.DIRECTORY, + name=f"dtb-{self.cfg.flavor_id}-{output_arch}", + ) + ) + def list_available_flavors() -> list[str]: import importlib diff --git a/captain/flavors/trixie_slim/__init__.py b/captain/flavors/trixie_slim/__init__.py index bd9f6042..881160ba 100644 --- a/captain/flavors/trixie_slim/__init__.py +++ b/captain/flavors/trixie_slim/__init__.py @@ -1,6 +1,7 @@ import logging from dataclasses import dataclass +from captain.artifacts import OutputArchArtifact from captain.cli import stages from captain.flavor import BaseFlavor from captain.flavors.common_acpi import TrixieACPIFlavor @@ -30,3 +31,7 @@ def flavor_packages(self) -> set[str]: def package_directories(self) -> set[str]: return {str(obtain_target_artifact_path(self.cfg).parent.relative_to(self.cfg.project_dir))} + + def add_arch_dtb_artifacts(self, artifacts: list[OutputArchArtifact], output_arch: str): + # no dtb artifacts for trixie-slim flavor + pass diff --git a/captain/oci/_build.py b/captain/oci/_build.py index b7c3369f..3349efc7 100644 --- a/captain/oci/_build.py +++ b/captain/oci/_build.py @@ -9,7 +9,6 @@ from pathlib import Path from captain import artifacts, buildah -from captain.util import get_arch_info log = logging.getLogger(__name__) @@ -49,27 +48,7 @@ def _deterministic_tar_multiple(all_dtb_files: list[Path], dtb_tar_path, out: Pa tf.addfile(info, fh) -def _collect_arch_artifacts( - project_dir: Path, - out: Path, - arch: str, - flavor_id: str, - has_iso: bool, -) -> list[Path]: - """Collect and return the artifact files for a single architecture. - - Returns [vmlinuz, initramfs, iso, checksums] paths in *out*. - """ - oarch = get_arch_info(arch).output_arch - - arch_files = [ - out / f"vmlinuz-{flavor_id}-{oarch}", - out / f"initramfs-{flavor_id}-{oarch}", - ] - - if has_iso: - arch_files += [out / f"captainos-{flavor_id}-{oarch}.iso"] - +def _checksum_files(arch_files: list[Path], flavor_id: str, oarch: str, out: Path) -> list[Path]: checksums_path = out / f"sha256sums-{flavor_id}-{oarch}.txt" artifacts.collect_checksums(arch_files, checksums_path) diff --git a/captain/oci/_publish.py b/captain/oci/_publish.py index ec89a668..ec8618fb 100644 --- a/captain/oci/_publish.py +++ b/captain/oci/_publish.py @@ -9,13 +9,14 @@ from uuid import uuid4 from captain import buildah, skopeo +from captain.artifacts import OutputArchArtifactType from captain.config import Config from captain.flavor import BaseFlavor from captain.util import ensure_dir, get_arch_info from ._build import ( _build_platform_image, - _collect_arch_artifacts, + _checksum_files, _deterministic_tar, _deterministic_tar_multiple, ) @@ -197,27 +198,65 @@ def publish( # Collect artifacts for every requested architecture. arch_files: dict[str, list[Path]] = {} + arch_directories: dict[str, list[Path]] = {} for arch in arches: - arch_files[arch] = _collect_arch_artifacts( - cfg.project_dir, out, arch, cfg.flavor_id, has_iso=flavor.has_iso() + output_arch = get_arch_info(arch).output_arch + arch_artifacts = flavor.list_arch_artifacts(output_arch) + + # First, pick all the type=file artifacts for this arch. + file_arch_artifacts = [a for a in arch_artifacts if a.type == OutputArchArtifactType.FILE] + if not file_arch_artifacts: + log.warning("No file artifacts found for %s/%s", flavor.id, output_arch) + continue + + file_arch_artifacts_in_out = [] + for a in file_arch_artifacts: + path_in_out = out / a.name + if not path_in_out.is_file(): + log.error("Expected artifact file not found in output: %s", path_in_out) + raise SystemExit(1) + file_arch_artifacts_in_out.append(path_in_out) + + # Calculate the checksums & add to arch_files + arch_files[arch] = _checksum_files( + file_arch_artifacts_in_out, cfg.flavor_id, output_arch, out ) + # Also add directories; those are not checksummed + dir_arch_artifacts_in_out = [] + dir_arch_artifacts = [ + a for a in arch_artifacts if a.type == OutputArchArtifactType.DIRECTORY + ] + if dir_arch_artifacts: + for a in dir_arch_artifacts: + dir_in_out = out / a.name + if not dir_in_out.is_dir(): + log.error("Expected artifact directory not found in output: %s", dir_in_out) + raise SystemExit(1) + dir_arch_artifacts_in_out.append(dir_in_out) + arch_directories[arch] = dir_arch_artifacts_in_out + # Create deterministic layer tars (shared across manifest pushes). arch_layer_tars: dict[str, list[Path]] = {} - for arch, files in arch_files.items(): - log.info("Creating layer tars for %s... files: %s", arch, files) - arch_layer_tars[arch] = [_deterministic_tar(f, out) for f in files] - # A single layer for all DTBs, if any; those are highly compressible together. - dtb_dir_in = out / f"dtb-{cfg.flavor_id}-{get_arch_info(arch).output_arch}" # @TODO "out" - if not dtb_dir_in.is_dir(): - log.warning("No dtbs directory found for %s: %s", arch, dtb_dir_in) - else: - log.info(f"Found DTB directory for {arch}: {dtb_dir_in}") - all_dtb_files: list[Path] = sorted(dtb_dir_in.glob("**/*.dtb*")) - dtb_tar_path = out / f".layer-dtbs-{cfg.flavor_id}-{arch}.tar" - _deterministic_tar_multiple(all_dtb_files, dtb_tar_path, out) - arch_layer_tars[arch].append(dtb_tar_path) + # Add the files first, each in a deterministic tar + for arch, files in arch_files.items(): + log.info("Creating layer tars (single-file) for %s... files: %s", arch, files) + arch_layer_tars[arch] = [] + for f in files: + log.info("Creating layer tar (single-file) for %s... file: %s", arch, f) + arch_layer_tars[arch].append(_deterministic_tar(f, out)) + + # Add all directories (with every file within); single tar for better compression + for arch, dirs in arch_directories.items(): + for directory in dirs: + log.info("Creating layer tar (directory) for %s... directory: %s", arch, directory) + all_dir_files: list[Path] = sorted(directory.glob("**/*")) + all_dir_files = [f for f in all_dir_files if f.is_file()] # only files, not dirs + dir_tar_path = out / f".layer-dir-{cfg.flavor_id}-{arch}-{directory.name}.tar" + log.debug("Adding %d files to layer tar for arch %s", len(all_dir_files), arch) + _deterministic_tar_multiple(all_dir_files, dir_tar_path, out) + arch_layer_tars[arch].append(dir_tar_path) pushed = True try: @@ -254,6 +293,8 @@ def publish( artifact_names: list[str] = [] for arch in arches: artifact_names.extend(f.name for f in arch_files.get(arch, [])) + artifact_names.extend(d.name for d in arch_directories.get(arch, [])) + platforms = [f"linux/{a}" for a in _ARCHES] log.info("") log.info("Publish complete") From 193b7c21503b23763c44905478dbad80cf2fe712 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 26 Apr 2026 10:05:55 +0200 Subject: [PATCH 117/143] flavor: introduce BaseFlavor::post_mkosi_stage() and BaseFlavor::post_artifact_collect() - for custom stuff done after initramfs build / after artifact collection (mkosi.output/... -> out/) Signed-off-by: Ricardo Pardini --- captain/cli/_build.py | 6 ++++++ captain/flavor.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/captain/cli/_build.py b/captain/cli/_build.py index e7912088..df30e49a 100644 --- a/captain/cli/_build.py +++ b/captain/cli/_build.py @@ -125,6 +125,8 @@ def build_cmd( artifacts.collect_dtbs(cfg) log.info("Initramfs build complete.") + flavor.post_mkosi_stage() + # ISO stage (if the flavor supports it). if flavor.has_iso(): _build_iso_stage(cfg) @@ -133,4 +135,8 @@ def build_cmd( # Final artifact collection. artifacts.collect(cfg) + + # Flavor-driven artifact collection. + flavor.post_artifact_collect() + log.info("Build complete!") diff --git a/captain/flavor.py b/captain/flavor.py index 8826d02d..062fd771 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -164,6 +164,12 @@ def has_iso(self) -> bool: def pre_mkosi_stage(self): pass + def post_mkosi_stage(self): + pass + + def post_artifact_collect(self): + pass + def list_arch_artifacts(self, output_arch: str) -> list[OutputArchArtifact]: artifacts: list[OutputArchArtifact] = [ OutputArchArtifact( From 6b44c12a970cc45d6156fb490a61c378f07007db Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 26 Apr 2026 10:34:09 +0200 Subject: [PATCH 118/143] trixie-armbian-rpi: implement ready-to-go RPi firmware in `firmware-rpi` dir - this is targetting (eeprom-based) RPi Netboot (without u-boot) - implement list_arch_artifacts(), post_mkosi_stage(), post_artifact_collect() Signed-off-by: Ricardo Pardini --- .../flavors/trixie_armbian_rpi/__init__.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/captain/flavors/trixie_armbian_rpi/__init__.py b/captain/flavors/trixie_armbian_rpi/__init__.py index ff8bf554..b2d22350 100644 --- a/captain/flavors/trixie_armbian_rpi/__init__.py +++ b/captain/flavors/trixie_armbian_rpi/__init__.py @@ -1,8 +1,14 @@ import logging +import os +import shutil from dataclasses import dataclass +from pathlib import Path +from captain.artifacts import OutputArchArtifact, OutputArchArtifactType from captain.flavor import BaseFlavor from captain.flavors.common_armbian import ArmbianCommonFlavor +from captain.tools import _download_binary +from captain.util import ensure_dir log: logging.Logger = logging.getLogger(__name__) @@ -17,6 +23,105 @@ class TrixieArmbianRPiFlavor(ArmbianCommonFlavor): name = "Trixie for Raspberry Pi - Armbian bcm2711-current Kernel" description = "Debian Trixie based on Armbian's rockchip64-edge kernel" supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + rpi_fw_tag = "1.20260408" def flavor_packages(self) -> set[str]: return {"linux-image-current-bcm2711"}.union(super().flavor_packages()) + + def list_arch_artifacts(self, output_arch: str) -> list[OutputArchArtifact]: + artifacts = super().list_arch_artifacts(output_arch) + artifacts.append( + OutputArchArtifact( + type=OutputArchArtifactType.DIRECTORY, name=self.firmware_out_dirname() + ) + ) + return artifacts + + def post_mkosi_stage(self): + self.download_rpi_firmware() + + def post_artifact_collect(self): + out: Path = ensure_dir(self.cfg.output_dir) + log.debug("Copying firmware directory from %s to %s", self.firmware_output(), out) + target_fw_dir = out / self.firmware_out_dirname() + if target_fw_dir.exists(): + shutil.rmtree(target_fw_dir) + shutil.copytree(self.firmware_output(), target_fw_dir) + log.info("Copied firmware directory: %s", target_fw_dir) + + # Symlink all dtbs directly in the fw dir + out_dtbs = out / f"dtb-{self.cfg.flavor_id}-{self.cfg.arch_info.output_arch}" + broadcom_dtbs = out_dtbs / "broadcom" + if not broadcom_dtbs.is_dir(): + log.error( + "Expected dtb directory %s does not exist, skipping dtb symlinks", broadcom_dtbs + ) + raise ValueError(f"Expected dtb directory {broadcom_dtbs} does not exist") + for dtb in broadcom_dtbs.glob("*.dtb"): + target_dtb = target_fw_dir / dtb.name + log.debug("Symlinking dtb: %s to %s", dtb, target_dtb) + target_dtb.symlink_to(Path(os.path.relpath(dtb, target_dtb.parent))) + + # Symlink all overlays (which are in the same level as "broadcom") into an "overlays" subdir + broadcom_overlays = out_dtbs / "overlays" + if not broadcom_overlays.is_dir(): + log.error("Expected dtb overlays directory %s does not exist", broadcom_overlays) + raise ValueError(f"Expected dtb overlays directory {broadcom_overlays} does not exist") + target_overlays = ensure_dir(target_fw_dir / "overlays") + for overlay in broadcom_overlays.glob("*.dtbo"): + log.debug("Symlinking dtb overlay %s to firmware overlays directory", overlay) + target_overlay = target_overlays / overlay.name + target_overlay.symlink_to(Path(os.path.relpath(overlay, target_overlay.parent))) + + def firmware_out_dirname(self) -> str: + # return f"firmware-{self.cfg.flavor_id}-{self.cfg.arch_info.output_arch}" + return "firmware-rpi" + + def firmware_output(self) -> Path: + return ensure_dir(self.cfg.initramfs_output / "firmware") + + def rpi_firmware_base_url(self) -> str: + return ( + "https://raw.githubusercontent.com/raspberrypi/firmware/refs/tags/" + + self.rpi_fw_tag + + "/boot/" + ) + + def download_rpi_firmware(self): + rpi_fw_files = [ + "bootcode.bin", + "fixup4cd.dat", + "fixup4.dat", + "fixup4db.dat", + "fixup4x.dat", + "fixup_cd.dat", + "fixup.dat", + "fixup_db.dat", + "fixup_x.dat", + "LICENCE.broadcom", + "start4cd.elf", + "start4db.elf", + "start4.elf", + "start4x.elf", + "start_cd.elf", + "start_db.elf", + "start.elf", + "start_x.elf", + ] + for filename in rpi_fw_files: + url = self.rpi_firmware_base_url() + filename + output_path = self.firmware_output() / filename + if output_path.is_file(): + log.debug( + "Raspberry Pi firmware file %s already exists at %s, skipping download", + url, + output_path, + ) + continue + log.info( + "Raspberry Pi firmware file %s does not exist at %s, downloading...", + url, + output_path, + ) + log.info("Downloading Raspberry Pi firmware file %s to %s", url, output_path) + _download_binary(url, output_path) From de6c658ab79b1453b0390894e07d8d7e0a0f5494 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 27 Apr 2026 15:24:12 +0200 Subject: [PATCH 119/143] util: introduce symlink_relative() - with mixes Path from pathlib and os.path for non-subpaths Signed-off-by: Ricardo Pardini --- captain/util.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/captain/util.py b/captain/util.py index 6c3927c3..26432a60 100644 --- a/captain/util.py +++ b/captain/util.py @@ -232,3 +232,9 @@ def check_dependencies(arch: str) -> list[str]: Returns a list of missing command names (empty if all found). """ return check_kernel_dependencies(arch) + check_mkosi_dependencies() + + +def symlink_relative(initramfs_dst: Path, initramfs_src: Path): + relative = Path(os.path.relpath(initramfs_src, initramfs_dst.parent)) + log.debug("Symlinking %s to %s (relative path: %s)", initramfs_dst, initramfs_src, relative) + initramfs_dst.symlink_to(relative) From 42682222633744135a21f0108bebe8d46fb535bc Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 27 Apr 2026 15:24:50 +0200 Subject: [PATCH 120/143] trixie-armbian-rpi: include kernel8.img and initramfs8 symlinks - refactor to use util.symlink_relative() Signed-off-by: Ricardo Pardini --- .../flavors/trixie_armbian_rpi/__init__.py | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/captain/flavors/trixie_armbian_rpi/__init__.py b/captain/flavors/trixie_armbian_rpi/__init__.py index b2d22350..006cff0f 100644 --- a/captain/flavors/trixie_armbian_rpi/__init__.py +++ b/captain/flavors/trixie_armbian_rpi/__init__.py @@ -1,5 +1,4 @@ import logging -import os import shutil from dataclasses import dataclass from pathlib import Path @@ -8,7 +7,7 @@ from captain.flavor import BaseFlavor from captain.flavors.common_armbian import ArmbianCommonFlavor from captain.tools import _download_binary -from captain.util import ensure_dir +from captain.util import ensure_dir, symlink_relative log: logging.Logger = logging.getLogger(__name__) @@ -49,7 +48,7 @@ def post_artifact_collect(self): shutil.copytree(self.firmware_output(), target_fw_dir) log.info("Copied firmware directory: %s", target_fw_dir) - # Symlink all dtbs directly in the fw dir + ## Symlink all dtbs directly in the fw dir out_dtbs = out / f"dtb-{self.cfg.flavor_id}-{self.cfg.arch_info.output_arch}" broadcom_dtbs = out_dtbs / "broadcom" if not broadcom_dtbs.is_dir(): @@ -60,7 +59,7 @@ def post_artifact_collect(self): for dtb in broadcom_dtbs.glob("*.dtb"): target_dtb = target_fw_dir / dtb.name log.debug("Symlinking dtb: %s to %s", dtb, target_dtb) - target_dtb.symlink_to(Path(os.path.relpath(dtb, target_dtb.parent))) + symlink_relative(target_dtb, dtb) # Symlink all overlays (which are in the same level as "broadcom") into an "overlays" subdir broadcom_overlays = out_dtbs / "overlays" @@ -71,7 +70,34 @@ def post_artifact_collect(self): for overlay in broadcom_overlays.glob("*.dtbo"): log.debug("Symlinking dtb overlay %s to firmware overlays directory", overlay) target_overlay = target_overlays / overlay.name - target_overlay.symlink_to(Path(os.path.relpath(overlay, target_overlay.parent))) + symlink_relative(target_overlay, overlay) + + ## Symlink the kernel and initramfs + # kernel as kernel8.img + # see https://www.raspberrypi.com/documentation/computers/config_txt.html#kernel + kernel_src = out / f"vmlinuz-{self.cfg.flavor_id}-{self.cfg.arch_info.output_arch}" + if not kernel_src.is_file(): + log.error( + "Expected kernel image %s does not exist, cannot symlink to firmware dir", + kernel_src, + ) + raise ValueError(f"Expected kernel image {kernel_src} does not exist") + kernel_dst = target_fw_dir / "kernel8.img" + log.debug("Symlinking kernel from %s to %s", kernel_src, kernel_dst) + symlink_relative(kernel_dst, kernel_src) + + # initramfs as initramfs8 + # see https://www.raspberrypi.com/documentation/computers/config_txt.html#initramfs + initramfs_src = out / f"initramfs-{self.cfg.flavor_id}-{self.cfg.arch_info.output_arch}" + if not initramfs_src.is_file(): + log.error( + "Expected initramfs image %s does not exist, cannot symlink to firmware dir", + initramfs_src, + ) + raise ValueError(f"Expected initramfs image {initramfs_src} does not exist") + initramfs_dst = target_fw_dir / "initramfs8" + log.debug("Symlinking initramfs from %s to %s", initramfs_src, initramfs_dst) + symlink_relative(initramfs_dst, initramfs_src) def firmware_out_dirname(self) -> str: # return f"firmware-{self.cfg.flavor_id}-{self.cfg.arch_info.output_arch}" From 49831bcf6da27bf6b692022189aec538c91ffda1 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 30 Apr 2026 12:28:19 +0200 Subject: [PATCH 121/143] trixie-slim: bump kernel 6.18.22 -> 6.18.26 (copy.fail etc) - comment-change in mkosi.conf to force hash to change and thus all to rebuild Signed-off-by: Ricardo Pardini --- captain/config.py | 2 +- captain/flavors/common_debian/mkosi.conf.j2 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/captain/config.py b/captain/config.py index 951ca9f5..64b14bd3 100644 --- a/captain/config.py +++ b/captain/config.py @@ -22,7 +22,7 @@ # The latest version can be obtained and passed as --kernel-version/KERNEL_VERSION # via <@TODO cmd for listing from cached releases.json # Override at runtime via --kernel-version or KERNEL_VERSION env var. -DEFAULT_KERNEL_VERSION = "6.18.22" +DEFAULT_KERNEL_VERSION = "6.18.26" @dataclass(slots=True) diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index efa118f2..624394db 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -34,6 +34,7 @@ PackageDirectories= # pivot_root(2) work for container runtimes (runc). Packages= + # flavor-specific packages {{ "\n ".join(flavor.sorted_flavor_packages()) }} # systemd and core systemd From dff67c04fa3d4821a4bb644b53783a878f659681 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 5 May 2026 14:18:06 +0200 Subject: [PATCH 122/143] common-debian: enable Networking for scripts; fix postinst docs and use mkosi-chroot - set `[Build]`.`WithNetwork=yes` so scripts can hit the network if needed - mkosi.postinst is _not_ mkosi.postinst.chroot - See https://github.com/systemd/mkosi/blob/main/mkosi/resources/man/mkosi.1.md#scripts Signed-off-by: Ricardo Pardini --- captain/flavors/common_debian/mkosi.conf.j2 | 1 + .../flavors/common_debian/mkosi.postinst.sh.j2 | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index 624394db..5bcee990 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -11,6 +11,7 @@ OutputDirectory=mkosi.output [Build] Incremental=yes +WithNetwork=yes CacheDirectory=mkosi.cache.for.incremental [Content] diff --git a/captain/flavors/common_debian/mkosi.postinst.sh.j2 b/captain/flavors/common_debian/mkosi.postinst.sh.j2 index d4b72230..1a52990b 100755 --- a/captain/flavors/common_debian/mkosi.postinst.sh.j2 +++ b/captain/flavors/common_debian/mkosi.postinst.sh.j2 @@ -1,14 +1,15 @@ #!/bin/bash # mkosi.postinst — Post-installation script for the CaptainOS initrd. -# Runs inside the image chroot after packages are installed. -# NOTE: The filesystem is mostly read-only here. Use mkosi.extra/ for -# creating directories and dropping config files. +# Runs *outside* the image chroot after packages are installed. +# The target filesystem is at /buildroot; use 'mkosi-chroot' to run commands inside the chroot environment. +# You can also use mkosi.extra/ for creating directories and dropping config files. set -euo pipefail -# Ensure CA certificate bundle is generated (dpkg triggers may not -# fire reliably in mkosi's chroot). +log info "Regular CaptainOS post-install starting..." + +# Ensure CA certificate bundle is generated. log info "Generating fresh CA certificate bundle..." -update-ca-certificates --fresh 2>/dev/null || true +mkosi-chroot update-ca-certificates --fresh # Service enablement/disablement is handled via systemd preset files: # - mkosi.extra/etc/systemd/system-preset/10-captainos-tink.preset (tink-agent and CaptainOS services) @@ -21,6 +22,6 @@ update-ca-certificates --fresh 2>/dev/null || true # Set default target to multi-user (no graphical) log info "Setting default systemd target to multi-user.target (no graphical)..." -systemctl set-default multi-user.target || true +mkosi-chroot systemctl set-default multi-user.target || true log info "CaptainOS post-install complete." From 988d4f78974ceb2225f46daeeec2d75c048754f9 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Sun, 3 May 2026 14:15:00 +0200 Subject: [PATCH 123/143] release: rework into click Group; bring back pull and tag - publish is a release subcommand now - logging fixes, don't show locals, suppress Docker traces - TODO: BUILDAH_INSECURE etc - center on Config (release_ prefix), Docker repasses - drop `git describe`, always use `v0.0.0-{sha}` Signed-off-by: Ricardo Pardini --- captain/__init__.py | 6 +- captain/buildah.py | 2 +- captain/cli/_main.py | 3 +- captain/cli/_release.py | 259 ++++++++++++++++++++++++++++++++ captain/cli/_release_publish.py | 193 ------------------------ captain/config.py | 11 ++ captain/docker.py | 31 +++- captain/oci/__init__.py | 2 - captain/oci/_common.py | 44 +----- captain/oci/_pull.py | 45 ++++-- captain/skopeo.py | 14 +- 11 files changed, 347 insertions(+), 263 deletions(-) create mode 100644 captain/cli/_release.py delete mode 100644 captain/cli/_release_publish.py diff --git a/captain/__init__.py b/captain/__init__.py index c9620023..d9d8362f 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -22,10 +22,10 @@ # Install Rich traceback handler globally (once, at import time). if os.environ.get("FORCE_COLOR", "0") == "1": console: Console = Console(stderr=True, color_system="standard", width=env_columns) - _install_rich_traceback(console=console, show_locals=True, width=env_columns, suppress=[click]) + _install_rich_traceback(console=console, show_locals=False, width=env_columns, suppress=[click]) else: console: Console = Console(stderr=True) - _install_rich_traceback(console=console, show_locals=True, width=None, suppress=[click]) + _install_rich_traceback(console=console, show_locals=False, width=None, suppress=[click]) class _StageFormatter(logging.Formatter): @@ -44,7 +44,7 @@ def format(self, record: logging.LogRecord) -> str: show_path=True, markup=True, # interprets [braket]stuff[/bracket] in log messages, beware rich_tracebacks=True, - tracebacks_show_locals=True, + tracebacks_show_locals=False, tracebacks_suppress=[click], # don't wanna see click infra code in traces ) _handler.setFormatter(_StageFormatter("[bold][cyan]%(stage)s[/cyan][/bold]: %(message)s")) diff --git a/captain/buildah.py b/captain/buildah.py index e595069b..aec2d36e 100644 --- a/captain/buildah.py +++ b/captain/buildah.py @@ -26,7 +26,7 @@ def from_image( *, platform: str | None = None, ) -> str: - cmd: list[str] = ["buildah", "from"] + cmd: list[str] = ["buildah", "from", "--tls-verify=false"] # @TODO BUILDAH_INSECURE if platform: cmd += ["--platform", platform] cmd.append(image) diff --git a/captain/cli/_main.py b/captain/cli/_main.py index a8a3ca9d..87ed0ffe 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -170,6 +170,7 @@ def cli( return # Build the shared context object for subcommands. + log.debug("Main CLI creating CliContext...") ctx.obj = CliContext( project_dir=resolve_project_dir(project_dir), arch=arch, @@ -195,7 +196,7 @@ def main() -> None: _iso, _kernel, _qemu, - _release_publish, + _release, _shell, _tools, ) diff --git a/captain/cli/_release.py b/captain/cli/_release.py new file mode 100644 index 00000000..96c3eb78 --- /dev/null +++ b/captain/cli/_release.py @@ -0,0 +1,259 @@ +"""``captain release-publish`` — publish artifacts as OCI images via buildah.""" + +from __future__ import annotations + +import logging +import subprocess +from pathlib import Path + +import click + +import captain.flavor +from captain import oci +from captain.cli._main import CliContext, cli +from captain.config import Config +from captain.util import check_release_dependencies + +log = logging.getLogger(__name__) + + +@cli.group() +@click.option( + "--release-mode", + envvar="RELEASE_MODE", + default="native", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Release stage execution mode (docker, native, skip).", +) +@click.option( + "--registry", + envvar="REGISTRY", + default="ghcr.io", + show_default=True, + help="OCI registry hostname.", +) +@click.option( + "--repository", + envvar="GITHUB_REPOSITORY", + default="tinkerbell/captain", + show_default=True, + help="Repository path (owner/name).", +) +@click.option( + "--oci-artifact-name", + envvar="OCI_ARTIFACT_NAME", + default="artifacts", + show_default=True, + help="OCI artifact image name.", +) +@click.option( + "--target", + envvar="TARGET", + default=None, + type=click.Choice(["amd64", "arm64", "combined"], case_sensitive=False), + metavar="TARGET", + help="Artifact target: 'amd64', 'arm64', or 'combined' (default: value of --arch); " + "'combined' requires an ACPI flavor with both arch's outputs present.", +) +@click.option( + "--git-sha", + envvar="GIT_SHA", + default=None, + help="Git commit SHA (default: auto-detected via git rev-parse HEAD).", +) +@click.option( + "--force-release", + envvar="FORCE_RELEASE", + is_flag=True, + default=False, + help="Publish even if the image already exists in the registry.", +) +@click.option( + "--src-tag", + envvar="SRC_TAG", + default=None, + help="Override specific tag to use; auto-determined if omitted.", +) +@click.pass_context +def release_group( + ctx: click.Context, + *, + release_mode: str, + registry: str, + repository: str, + oci_artifact_name: str, + target: str | None, + git_sha: str | None, + force_release: bool, + src_tag: str | None, +) -> None: + """Release-related operations: publish, pull, tag. Operates on OCI images in a registry.""" + cli_ctx: CliContext = ctx.obj + + if target is None: + target = cli_ctx.arch + assert isinstance(target, str) + + log.debug("Creating Release group Config and putting into Context...") + + cfg = cli_ctx.make_config( + release_mode=release_mode, + release_registry=registry, + release_repository=repository, + release_oci_artifact_name=oci_artifact_name, + release_target=target, + release_git_sha=git_sha, + force_release=force_release, + release_src_tag=src_tag, + ) + + # resolve the SHA before launching Docker, as we've the .git here and not there. + if cfg.release_git_sha is None: + log.debug( + "Auto-detecting git SHA for release since not provided via --git-sha or GITHUB_SHA..." + ) + cfg.release_git_sha = _autodetect_git_sha(cfg.project_dir) + log.debug("Git SHA at group level: %s", cfg.release_git_sha) + + if cfg.release_src_tag is None: + log.debug( + "Auto-computing version tag for release since not provided via --src-tag or SRC_TAG..." + ) + tag = f"v0.0.0-{str(cfg.release_git_sha)[:7]}" + cfg.release_src_tag = f"{tag}-{cfg.flavor_id}" + log.debug("Tag at group level: %s", cfg.release_src_tag) + + # pass it down via Context // pass_obj + ctx.obj = cfg + + +@release_group.command("pull") +@click.option( + "--pull-output", + envvar="PULL_OUTPUT", + required=True, + type=click.Path(), + help="Output directory for pulled artifacts, relative to 'out/'.", +) +@click.pass_obj +def pull_command(cfg: Config, pull_output: Path) -> None: + """Pull release artifacts as OCI images from a registry and extract to disk.""" + log.debug("Release Pull with Config %s and pull_output %s", cfg, pull_output) + + cfg.release_pull_output = pull_output + + if skip_or_relaunch_in_docker(cfg, "pull"): + return + + oci.pull( + registry=str(cfg.release_registry), + repository=str(cfg.release_repository), + artifact_name=str(cfg.release_oci_artifact_name), + tag=str(cfg.release_src_tag), + target=str(cfg.release_target), + output_dir=cfg.output_dir / cfg.release_pull_output, + ) + + +@release_group.command("tag") +@click.option( + "--new-tag", + envvar="NEW_TAG", + required=True, + help="New tag to apply to the existing release image (e.g. v1.0.0).", +) +@click.pass_obj +def tag_command(cfg: Config, new_tag: str) -> None: + """Tag an existing release image with a new version.""" + log.debug("Release tag with Config: %s and new_tag %s", cfg, new_tag) + + cfg.release_new_tag = new_tag + + if skip_or_relaunch_in_docker(cfg, "tag"): + return + + flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + + oci.tag_all( + registry=str(cfg.release_registry), + repository=str(cfg.release_repository), + artifact_name=str(cfg.release_oci_artifact_name), + src_tag=str(cfg.release_src_tag), + new_tag=str(cfg.release_new_tag), + arches=list(flavor.supported_architectures), + ) + + +@release_group.command("publish") +@click.pass_obj +def publish_command(cfg: Config) -> None: + """Publish build artifacts as a multi-arch OCI image to a registry. + + Uses buildah to construct OCI images from the build artifacts (kernel, + initramfs, ISO, DTBs) and pushes them to the specified registry. + + Each artifact file becomes its own layer. Deterministic tar generation + ensures byte-identical layers across runs so that registries can + deduplicate blobs. + """ + log.debug("Got into release publish with Config: %s", cfg) + + if skip_or_relaunch_in_docker(cfg, "publish"): + return + + flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + + oci.publish( + cfg, + flavor, + target=str(cfg.release_target), + registry=str(cfg.release_registry), + repository=str(cfg.release_repository), + artifact_name=str(cfg.release_oci_artifact_name), + tag=str(cfg.release_src_tag), + sha=str(cfg.release_git_sha), + force=cfg.force_release, + ) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def skip_or_relaunch_in_docker(cfg: Config, subcommand: str) -> bool: + # --- skip mode -------------------------------------------------------- + if cfg.release_mode == "skip": + log.info("RELEASE_MODE=skip — nothing to do.") + return True + + # --- docker mode ------------------------------------------------------ + if cfg.release_mode == "docker": + from captain import docker + + docker.obtain_builder(cfg) + docker.run_captain_in_builder(cfg, ["release", subcommand]) + docker.fix_docker_ownership(cfg, ["/work/out"]) + return True + + # --- native mode ------------------------------------------------------ + missing = check_release_dependencies() + if missing: + log.error("Missing release tools: %s", ", ".join(missing)) + log.error("Install them or set --release-mode=docker.") + raise SystemExit(1) + + return False + + +def _autodetect_git_sha(project_dir: Path) -> str: + """Auto-detect via ``git rev-parse HEAD``.""" + return subprocess.run( + ["git", "rev-parse", "HEAD"], + capture_output=True, + text=True, + check=True, + cwd=project_dir, + ).stdout.strip() diff --git a/captain/cli/_release_publish.py b/captain/cli/_release_publish.py deleted file mode 100644 index 6e9617c7..00000000 --- a/captain/cli/_release_publish.py +++ /dev/null @@ -1,193 +0,0 @@ -"""``captain release-publish`` — publish artifacts as OCI images via buildah.""" - -from __future__ import annotations - -import logging -import subprocess -from pathlib import Path - -import click - -import captain.flavor -from captain import oci -from captain.cli._main import CliContext, cli -from captain.util import check_release_dependencies - -log = logging.getLogger(__name__) - - -@cli.command( - "release-publish", - short_help="Publish build artifacts as a multi-arch OCI image.", -) -@click.option( - "--release-mode", - envvar="RELEASE_MODE", - default="native", - show_default=True, - type=click.Choice(["docker", "native", "skip"], case_sensitive=False), - metavar="MODE", - help="Release stage execution mode (docker, native, skip).", -) -@click.option( - "--registry", - envvar="REGISTRY", - default="ghcr.io", - show_default=True, - help="OCI registry hostname.", -) -@click.option( - "--repository", - envvar="GITHUB_REPOSITORY", - default="tinkerbell/captain", - show_default=True, - help="Repository path (owner/name).", -) -@click.option( - "--oci-artifact-name", - envvar="OCI_ARTIFACT_NAME", - default="artifacts", - show_default=True, - help="OCI artifact image name.", -) -@click.option( - "--target", - envvar="TARGET", - default=None, - type=click.Choice(["amd64", "arm64", "combined"], case_sensitive=False), - metavar="TARGET", - help="Artifact target: amd64, arm64, or combined (default: value of --arch); " - "combined requires trixie-full or equivalent flavor with both arch's outputs present.", -) -@click.option( - "--git-sha", - envvar="GITHUB_SHA", - default=None, - help="Git commit SHA (default: auto-detected via git rev-parse HEAD).", -) -@click.option( - "--version-exclude", - envvar="VERSION_EXCLUDE", - default=None, - help="Tag to exclude from git-describe version lookup.", -) -@click.option( - "--force", - "force", - is_flag=True, - default=False, - help="Publish even if the image already exists in the registry.", -) -@click.pass_obj -def release_publish_cmd( - cli_ctx: CliContext, - *, - release_mode: str, - registry: str, - repository: str, - oci_artifact_name: str, - target: str | None, - git_sha: str | None, - version_exclude: str | None, - force: bool, -) -> None: - """Publish build artifacts as a multi-arch OCI image to a registry. - - Uses buildah to construct OCI images from the build artifacts (kernel, - initramfs, ISO, DTBs) and pushes them to the specified registry. - - Each artifact file becomes its own layer. Deterministic tar generation - ensures byte-identical layers across runs so that registries can - deduplicate blobs. - - \b - Examples - -------- - captain release-publish - captain release-publish --arch arm64 --target arm64 - captain release-publish --target combined --force - captain release-publish --registry ghcr.io --repository tinkerbell/captain - """ - - if target is None: - target = cli_ctx.arch - assert isinstance(target, str) - - cfg = cli_ctx.make_config(release_mode=release_mode) - - # --- skip mode -------------------------------------------------------- - if cfg.release_mode == "skip": - log.info("RELEASE_MODE=skip — nothing to do.") - return - - # --- docker mode ------------------------------------------------------ - if cfg.release_mode == "docker": - from captain import docker - - docker.obtain_builder(cfg) - sha = _resolve_git_sha(git_sha, cfg.project_dir) - - env_args: list[str] = [ - "-e", - f"REGISTRY={registry}", - "-e", - f"GITHUB_REPOSITORY={repository}", - "-e", - f"OCI_ARTIFACT_NAME={oci_artifact_name}", - "-e", - f"GITHUB_SHA={sha}", - "-e", - f"TARGET={target}", - ] - if version_exclude: - env_args += ["-e", f"VERSION_EXCLUDE={version_exclude}"] - if force: - env_args += ["-e", "FORCE=true"] - - docker.run_captain_in_builder(cfg, ["release-publish"], extra_docker_args=env_args) - docker.fix_docker_ownership(cfg, ["/work/out"]) - return - - # --- native mode ------------------------------------------------------ - missing = check_release_dependencies() - if missing: - log.error("Missing release tools: %s", ", ".join(missing)) - log.error("Install them or set --release-mode=docker.") - raise SystemExit(1) - - sha = _resolve_git_sha(git_sha, cfg.project_dir) - tag = oci.compute_version_tag(cfg.project_dir, sha, exclude=version_exclude) - tag = f"{tag}-{cfg.flavor_id}" - - flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) - - oci.publish( - cfg, - flavor, - target=target, - registry=registry, - repository=repository, - artifact_name=oci_artifact_name, - tag=tag, - sha=sha, - force=force, - ) - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - - -def _resolve_git_sha(sha: str | None, project_dir: Path) -> str: - """Return the provided SHA or auto-detect via ``git rev-parse HEAD``.""" - if sha: - return sha - result = subprocess.run( - ["git", "rev-parse", "HEAD"], - capture_output=True, - text=True, - check=True, - cwd=project_dir, - ) - return result.stdout.strip() diff --git a/captain/config.py b/captain/config.py index 64b14bd3..665d4253 100644 --- a/captain/config.py +++ b/captain/config.py @@ -64,11 +64,22 @@ class Config: # Force flags force_tools: bool = False force_iso: bool = False + force_release: bool = False # QEMU qemu_mem: str = "2G" qemu_smp: str = "2" + # Release related + release_registry: str | None = None + release_repository: str | None = None + release_oci_artifact_name: str | None = None + release_target: str | None = None + release_git_sha: str | None = None + release_src_tag: str | None = None + release_pull_output: Path | None = None + release_new_tag: str | None = None + # mkosi passthrough mkosi_args: list[str] = field(default_factory=list) diff --git a/captain/docker.py b/captain/docker.py index d96e2848..f73bf4d8 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -7,6 +7,7 @@ import os import platform from pathlib import Path +from subprocess import CalledProcessError from rich.table import Table @@ -153,32 +154,45 @@ def run_in_builder( """ docker_envs: dict[str, str] = { - "CAPTAIN_IN_DOCKER": "docker", + # common to all commands "ARCH": cfg.arch, "FLAVOR_ID": cfg.flavor_id, "KERNEL_VERSION": cfg.kernel_version, "FORCE_TOOLS": str(int(cfg.force_tools)), "FORCE_ISO": str(int(cfg.force_iso)), "FORCE_KERNEL": f"{int(cfg.force_kernel)!s}", + "FORCE_RELEASE": str(cfg.force_release), + "CAPTAIN_VERBOSE": "1" if cfg.verbose_docker else "0", + "CONFIG_KERNEL": "1" if cfg.kernel_menuconfig else "0", + # publish-related env vars "BUILDAH_ISOLATION": "chroot", "BUILDAH_INSECURE": os.environ.get("BUILDAH_INSECURE", ""), + "REGISTRY": str(cfg.release_registry), + "GITHUB_REPOSITORY": str(cfg.release_repository), + "OCI_ARTIFACT_NAME": str(cfg.release_oci_artifact_name), + "TARGET": str(cfg.release_target), + "GIT_SHA": str(cfg.release_git_sha), + "SRC_TAG": str(cfg.release_src_tag), + "PULL_OUTPUT": str(cfg.release_pull_output), + "NEW_TAG": str(cfg.release_new_tag), + # set all modes to native under Docker "KERNEL_MODE": "native", "RELEASE_MODE": "native", "TOOLS_MODE": "native", "MKOSI_MODE": "native", "ISO_MODE": "native", + # terminal control / gha stuff etc "TERM": os.environ.get("TERM", "xterm-256color"), "FORCE_COLOR": "1", "COLUMNS": str(captain.env_columns), "GITHUB_ACTIONS": os.environ.get("GITHUB_ACTIONS", ""), + "CAPTAIN_IN_DOCKER": "docker", # Forward host registry credentials so buildah/skopeo can authenticate. # The caller sets these env vars on the host (e.g. via docker login or # CI secrets); they are passed through to the container as-is. "REGISTRY_AUTH_FILE": os.environ.get("REGISTRY_AUTH_FILE", ""), "REGISTRY_USERNAME": os.environ.get("REGISTRY_USERNAME", ""), "REGISTRY_PASSWORD": os.environ.get("REGISTRY_PASSWORD", ""), - "CAPTAIN_VERBOSE": "1" if cfg.verbose_docker else "0", - "CONFIG_KERNEL": "1" if cfg.kernel_menuconfig else "0", } docker_args: list[str] = [ @@ -232,7 +246,16 @@ def run_in_builder( docker_args += [cfg.builder_image] docker_args.extend(command_and_args) - run(docker_args) + + # Handle exceptions: when re-launching in Docker, the inner instance already showed + # the full error with traceback, so we can suppress the redundant outer traceback. + try: + run(docker_args) + except CalledProcessError as e: + log.error( + "Command '%s' failed with exit code %d.", " ".join(command_and_args), e.returncode + ) + raise SystemExit(e.returncode) from None def run_captain_in_builder( diff --git a/captain/oci/__init__.py b/captain/oci/__init__.py index ed1ece90..81ae1217 100644 --- a/captain/oci/__init__.py +++ b/captain/oci/__init__.py @@ -27,12 +27,10 @@ * ``skopeo`` extracts individual files for release workflows. """ -from captain.oci._common import compute_version_tag from captain.oci._publish import publish from captain.oci._pull import pull, tag_all, tag_image __all__ = [ - "compute_version_tag", "publish", "pull", "tag_all", diff --git a/captain/oci/_common.py b/captain/oci/_common.py index 9bb5afc2..6c68ecf6 100644 --- a/captain/oci/_common.py +++ b/captain/oci/_common.py @@ -2,51 +2,13 @@ from __future__ import annotations -import subprocess -from pathlib import Path +import logging _ARCHES = ("amd64", "arm64") +log = logging.getLogger(__name__) + def _image_ref(registry: str, repository: str, artifact_name: str, tag: str) -> str: """Build a fully-qualified OCI image reference.""" return f"{registry}/{repository}/{artifact_name}:{tag}" - - -def compute_version_tag( - project_dir: Path, - sha: str, - *, - exclude: str | None = None, -) -> str: - """Compute the version tag from git describe + short SHA. - - Mirrors the CI logic:: - - VERSION=$(git describe --tags --first-parent --abbrev=0 \\ - --match 'v[0-9]*' 2>/dev/null || echo "v0.0.0") - TAG="${VERSION}-${SHA::7}" - """ - cmd = [ - "git", - "describe", - "--tags", - "--first-parent", - "--abbrev=0", - "--match", - "v[0-9]*", - ] - if exclude: - cmd += [f"--exclude={exclude}"] - try: - result = subprocess.run( - cmd, - capture_output=True, - text=True, - check=True, - cwd=project_dir, - ) - version = result.stdout.strip() - except subprocess.CalledProcessError: - version = "v0.0.0" - return f"{version}-{sha[:7]}" diff --git a/captain/oci/_pull.py b/captain/oci/_pull.py index 13165f6a..f8238795 100644 --- a/captain/oci/_pull.py +++ b/captain/oci/_pull.py @@ -7,7 +7,7 @@ from captain import skopeo -from ._common import _ARCHES, _image_ref +from ._common import _image_ref log = logging.getLogger(__name__) @@ -64,32 +64,45 @@ def tag_all( artifact_name: str, src_tag: str, new_tag: str, - arches: list[str] | None = None, + arches: list[str], ) -> None: """Tag all artifact images (per-arch + combined) with a new version.""" - arches = arches or list(_ARCHES) + all_tags = [] for a in arches: + arch_orig_tag = f"{src_tag}-{a}" + arch_new_tag = f"{new_tag}-{a}" + + if len(arches) == 1: + arch_new_tag = new_tag + + log.info("Tagging for arch %s → %s", arch_orig_tag, arch_new_tag) tag_image( registry=registry, repository=repository, artifact_name=artifact_name, - src_tag=f"{src_tag}-{a}", - new_tag=f"{new_tag}-{a}", + src_tag=arch_orig_tag, + new_tag=arch_new_tag, ) - # Tag the combined image (no arch suffix). - tag_image( - registry=registry, - repository=repository, - artifact_name=artifact_name, - src_tag=src_tag, - new_tag=new_tag, - ) + all_tags.append((arch_orig_tag, arch_new_tag)) + + # if both arm64 and amd64 arches, also tag the combined image. + if set(arches) >= {"amd64", "arm64"}: + # Tag the combined image (no arch suffix). + log.info("Tagging combined image %s → %s", src_tag, new_tag) + tag_image( + registry=registry, + repository=repository, + artifact_name=artifact_name, + src_tag=src_tag, + new_tag=new_tag, + ) + all_tags.append((src_tag, new_tag)) # Recap image = f"{registry}/{repository}/{artifact_name}" log.info("") log.info("Tag complete") log.info(" Image: %s", image) - for a in arches: - log.info(" %s-%s → %s-%s", src_tag, a, new_tag, a) - log.info(" %s → %s", src_tag, new_tag) + # log all_tags with arrows + for src, dest in all_tags: + log.info(" %s → %s", src, dest) diff --git a/captain/skopeo.py b/captain/skopeo.py index a3b5712d..ea3f42f0 100644 --- a/captain/skopeo.py +++ b/captain/skopeo.py @@ -53,7 +53,17 @@ def copy(src: str, dest: str) -> None: destination differ only in the tag component. """ log.info("skopeo copy %s → %s", src, dest) - run(["skopeo", "copy", "--all", f"docker://{src}", f"docker://{dest}"]) + run( + [ + "skopeo", + "copy", + "--src-tls-verify=false", # @TODO BUILDAH_INSECURE + "--dest-tls-verify=false", # @TODO BUILDAH_INSECURE + "--all", + f"docker://{src}", + f"docker://{dest}", + ] + ) def copy_to_dir( @@ -70,7 +80,7 @@ def copy_to_dir( Returns *output_dir*. """ output_dir.mkdir(parents=True, exist_ok=True) - cmd: list[str] = ["skopeo", "copy"] + cmd: list[str] = ["skopeo", "copy", "--src-tls-verify=false"] # @TODO BUILDAH_INSECURE if platform: parts = platform.split("/") if len(parts) == 2: From 50d96a2ed5947cf3431e8e05d27d26c490687043 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 5 May 2026 08:47:33 +0200 Subject: [PATCH 124/143] gha: adapt Release GHA Workflow - publish all flavors - last-minute tarball of directories (dtbs, firmware) for GH Release upload - bump actions, drop unused envs, etc - ci: don't fetch-depth:0 as we don't do git describe anymore Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 6 ++-- .github/workflows/release.yml | 56 +++++++++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a31c417..2cb8d1e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -295,7 +295,7 @@ jobs: if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' env: TARGET: ${{ matrix.arch }} - run: uv run captain release-publish + run: uv run captain release publish build-captainos: # those use regular Debian apt repos for kernels name: "build-captainos ${{ matrix.FLAVOR_ID }} (${{ matrix.arch }})" @@ -336,8 +336,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 - with: - fetch-depth: 0 - name: Load shared config run: cat .github/config.env >> "$GITHUB_ENV" @@ -379,4 +377,4 @@ jobs: - name: Publish combined image to GHCR # only pushes to main, and not PR workflows, should try to push to OCI registry if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - run: uv run captain release-publish + run: uv run captain release publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e249c28..6bc2aaa4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,10 @@ permissions: contents: write packages: write +env: + FORCE_COLOR: "1" + CAPTAIN_VERBOSE: "1" + jobs: release: runs-on: ubuntu-latest @@ -30,21 +34,55 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v8.1.0 - - name: Pull release artifacts (combined) - env: - VERSION_EXCLUDE: ${{ github.ref_name }} - run: uv run ./build.py release pull --target combined --pull-output artifacts/combined + - name: Pull release artifacts + run: | + uv run captain --flavor-id trixie-full release --target combined pull --pull-output release-artifacts + uv run captain --flavor-id trixie-slim release --target combined pull --pull-output release-artifacts + uv run captain --flavor-id trixie-armbian-rpi release --target arm64 pull --pull-output release-artifacts + uv run captain --flavor-id trixie-rockchip64 release --target arm64 pull --pull-output release-artifacts + uv run captain --flavor-id trixie-meson64 release --target arm64 pull --pull-output release-artifacts + uv run captain --flavor-id trixie-rockchip64-vendor release --target arm64 pull --pull-output release-artifacts + uv run captain --flavor-id trixie-genio release --target arm64 pull --pull-output release-artifacts + + - name: "directories -> tarball" + working-directory: out/release-artifacts + run: | + for dir in */; do + # if not a directory, skip + [[ ! -d "$dir" || "$dir" == "." || "$dir" == ".." ]] && continue + tarball="${dir%/}.tar.gz" + echo "Creating tarball for '$dir' -> '$tarball'" + tar -czf "${dir%/}.tar.gz" "$dir" + echo "Removing directory '$dir'" + rm -rf "$dir" + done - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} run: | gh release create "${{ github.ref_name }}" \ - artifacts/combined/* \ + out/release-artifacts/* \ --generate-notes \ --title "${{ github.ref_name }}" - - name: Tag OCI artifacts with version - env: - VERSION_EXCLUDE: ${{ github.ref_name }} - run: uv run ./build.py release tag ${{ github.ref_name }} + - name: Tag (trixie-full) + run: uv run captain --flavor-id trixie-full release tag --new-tag "${{ github.ref_name }}" + + - name: Tag (trixie-slim) + run: uv run captain --flavor-id trixie-slim release tag --new-tag "${{ github.ref_name }}-slim" + + - name: Tag (trixie-armbian-rpi) + run: uv run captain --flavor-id trixie-armbian-rpi --arch arm64 release tag --new-tag "${{ github.ref_name }}-rpi" + + - name: Tag (trixie-rockchip64) + run: uv run captain --flavor-id trixie-rockchip64 --arch arm64 release tag --new-tag "${{ github.ref_name }}-rockchip64" + + - name: Tag (trixie-meson64) + run: uv run captain --flavor-id trixie-meson64 --arch arm64 release tag --new-tag "${{ github.ref_name }}-meson64" + + - name: Tag (trixie-rockchip64-vendor) + run: uv run captain --flavor-id trixie-rockchip64-vendor --arch arm64 release tag --new-tag "${{ github.ref_name }}-rockchip64-vendor" + + - name: Tag (trixie-genio) + run: uv run captain --flavor-id trixie-genio --arch arm64 release tag --new-tag "${{ github.ref_name }}-genio" From 5a015ab5147a7597cdc3ec21a6717849b031d403 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 5 May 2026 14:43:39 +0200 Subject: [PATCH 125/143] captain: implement `REGISTRY_INSECURE=1` env var for talking to insecure registries - nee `BUILDAH_INSECURE`, also used for skopeo - mostly for development vs `registry:2` - eg `docker run -p 5001:5000 -it registry:2` - and `REGISTRY_INSECURE=1 uv run captain ... --registry 192.168.66.10:5001 publish` Signed-off-by: Ricardo Pardini --- captain/buildah.py | 8 ++++++-- captain/docker.py | 2 +- captain/skopeo.py | 14 +++++++++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/captain/buildah.py b/captain/buildah.py index aec2d36e..1ac63838 100644 --- a/captain/buildah.py +++ b/captain/buildah.py @@ -26,7 +26,11 @@ def from_image( *, platform: str | None = None, ) -> str: - cmd: list[str] = ["buildah", "from", "--tls-verify=false"] # @TODO BUILDAH_INSECURE + cmd: list[str] = [ + "buildah", + "from", + *(["--tls-verify=false"] if os.environ.get("REGISTRY_INSECURE") == "1" else []), + ] if platform: cmd += ["--platform", platform] cmd.append(image) @@ -125,7 +129,7 @@ def manifest_push( "buildah", "manifest", "push", - *(("--tls-verify=false",) if os.environ.get("BUILDAH_INSECURE") == "1" else ()), + *(["--tls-verify=false"] if os.environ.get("REGISTRY_INSECURE") == "1" else []), "--all", manifest, f"docker://{dest}", diff --git a/captain/docker.py b/captain/docker.py index f73bf4d8..c06915c5 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -166,7 +166,7 @@ def run_in_builder( "CONFIG_KERNEL": "1" if cfg.kernel_menuconfig else "0", # publish-related env vars "BUILDAH_ISOLATION": "chroot", - "BUILDAH_INSECURE": os.environ.get("BUILDAH_INSECURE", ""), + "REGISTRY_INSECURE": os.environ.get("REGISTRY_INSECURE", ""), "REGISTRY": str(cfg.release_registry), "GITHUB_REPOSITORY": str(cfg.release_repository), "OCI_ARTIFACT_NAME": str(cfg.release_oci_artifact_name), diff --git a/captain/skopeo.py b/captain/skopeo.py index ea3f42f0..23af61da 100644 --- a/captain/skopeo.py +++ b/captain/skopeo.py @@ -9,6 +9,7 @@ import json import logging +import os import tarfile from pathlib import Path @@ -57,8 +58,11 @@ def copy(src: str, dest: str) -> None: [ "skopeo", "copy", - "--src-tls-verify=false", # @TODO BUILDAH_INSECURE - "--dest-tls-verify=false", # @TODO BUILDAH_INSECURE + *( + ["--src-tls-verify=false", "--dest-tls-verify=false"] + if os.environ.get("REGISTRY_INSECURE") == "1" + else [] + ), "--all", f"docker://{src}", f"docker://{dest}", @@ -80,7 +84,11 @@ def copy_to_dir( Returns *output_dir*. """ output_dir.mkdir(parents=True, exist_ok=True) - cmd: list[str] = ["skopeo", "copy", "--src-tls-verify=false"] # @TODO BUILDAH_INSECURE + cmd: list[str] = [ + "skopeo", + "copy", + *(["--src-tls-verify=false"] if os.environ.get("REGISTRY_INSECURE") == "1" else []), + ] if platform: parts = platform.split("/") if len(parts) == 2: From eb80b6994c79d13dd203fa4860963fe4a21bc02e Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 5 May 2026 12:52:10 +0200 Subject: [PATCH 126/143] captain: simple end-user customization via `custom` folder - allows for - `custom/packages.txt`: extra packages to be installed - `custom/mkosi-postinst` and `custom/mkosi-finalize` folders: extra scripts - `custom/files`: extra files in root filesystem - when rendering templates, force newline at the end of each - pass `custom` dir down to Docker Signed-off-by: Ricardo Pardini --- captain/config.py | 5 +++ captain/docker.py | 3 ++ captain/flavor.py | 6 ++- captain/flavors/common_debian/__init__.py | 40 ++++++++++++++++++- .../common_debian/mkosi.finalize.sh.j2 | 5 +-- .../common_debian/mkosi.postinst.sh.j2 | 2 +- 6 files changed, 53 insertions(+), 8 deletions(-) diff --git a/captain/config.py b/captain/config.py index 665d4253..032ff056 100644 --- a/captain/config.py +++ b/captain/config.py @@ -152,3 +152,8 @@ def calc_initramfs_output(self, base_dir: Path, which: str) -> Path: def iso_staging(self) -> Path: """Per-version, per-arch staging directory for assembling the ISO filesystem.""" return self.iso_output / "staging" + + @property + def custom_dir(self) -> Path: + """Directory for custom user files""" + return self.project_dir / "custom" diff --git a/captain/docker.py b/captain/docker.py index c06915c5..5e3ac776 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -221,6 +221,9 @@ def run_in_builder( for k, v in docker_envs.items(): docker_args += ["-e", f"{k}={v}"] + if cfg.custom_dir.exists() and cfg.custom_dir.is_dir(): + docker_args += ["-v", f"{cfg.custom_dir}:/work/custom"] + docker_args += ["-v", f"{cfg.project_dir}/kernel.configs:/work/kernel.configs"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.output:/work/mkosi.output"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.input:/work/mkosi.input"] diff --git a/captain/flavor.py b/captain/flavor.py index 062fd771..0320dfd4 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -94,12 +94,14 @@ def specific_flavor_dir(self, flavor_id: str) -> Path: raise SystemExit(1) return flavor_dir - def add_static_dir(self, dir_to_include: str, flavor_dir: Path): + def add_static_dir(self, dir_to_include: str, flavor_dir: Path, prefix_with: str | None = None): extra_dir = flavor_dir / dir_to_include if extra_dir.exists() and extra_dir.is_dir(): for extra_file in extra_dir.rglob("*"): if extra_file.is_file(): relative_path = extra_file.relative_to(flavor_dir) + if prefix_with is not None: + relative_path = Path(prefix_with) / extra_file.relative_to(extra_dir) self.static_map[str(relative_path)] = extra_file def render_templates( @@ -127,7 +129,7 @@ def render_templates( loader=jinja2.FileSystemLoader(template_path.parent), undefined=jinja2.StrictUndefined, ).get_template(template_path.name) - rendered_content += template.render(cfg=self.cfg, flavor=self) + rendered_content += template.render(cfg=self.cfg, flavor=self) + "\n" if hashes is not None: hashes[relative_output_path] = hashlib.sha256(rendered_content.encode()).hexdigest() diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py index 35cda527..5423c6cc 100644 --- a/captain/flavors/common_debian/__init__.py +++ b/captain/flavors/common_debian/__init__.py @@ -38,9 +38,47 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: self.add_static_dir("mkosi.sandbox", this_flavor_dir) self.add_static_dir("mkosi.skeleton", this_flavor_dir) + # Allow for easily user-customizable dirs and scripts + # those apply to all flavors that inherit from this common-debian base. + # Use-cases: + # - add CA root certificate to global trust store (via ca-certificates package) + # - add some Debian package (eg firmware) - [handled in flavor_packages() below] + # - configure containerd to use specific mirrors + # - run some postinst or finalize script to do some custom configuration + self.add_static_dir("files", cfg.custom_dir, "mkosi.extra") + self.add_custom_scripts(cfg, "mkosi-postinst", "mkosi.postinst") + self.add_custom_scripts(cfg, "mkosi-finalize", "mkosi.finalize") + + def add_custom_scripts(self, cfg: Config, in_dir: str, out_script: str): + custom_postinst_dir = cfg.custom_dir / in_dir + if custom_postinst_dir.exists() and custom_postinst_dir.is_dir(): + files_in_order = sorted(custom_postinst_dir.iterdir()) + log.debug( + "Adding custom %s scripts from %s: %s", + out_script, + custom_postinst_dir, + files_in_order, + ) + for file in files_in_order: + if file.is_file(): + log.debug("Adding custom %s script: %s", out_script, file) + self.template_map[out_script].append(file) + @abstractmethod def flavor_packages(self) -> set[str]: - return set({}) + packages = set({}) + # look into cfg.custom_dir/packages.txt, if it exists, read each non-comment line + custom_packages_file = self.cfg.custom_dir / "packages.txt" + if custom_packages_file.exists(): + log.debug("Reading custom packages from %s", custom_packages_file) + packages = set( + { + line.strip() + for line in (custom_packages_file.read_text().splitlines()) + if line.strip() and not line.strip().startswith("#") + } + ) + return packages def sorted_flavor_packages(self) -> list[str]: return sorted(self.flavor_packages()) diff --git a/captain/flavors/common_debian/mkosi.finalize.sh.j2 b/captain/flavors/common_debian/mkosi.finalize.sh.j2 index c0c67d41..5888729d 100755 --- a/captain/flavors/common_debian/mkosi.finalize.sh.j2 +++ b/captain/flavors/common_debian/mkosi.finalize.sh.j2 @@ -3,9 +3,6 @@ # The root filesystem is writable here. set -euo pipefail -log info "Showing full contents of environment variables for debugging:" -env - log info "CaptainOS finalize: unlocking root account..." # Unlock root with empty password @@ -132,4 +129,4 @@ fi log info "Final contents of root filesystem (up to depth 4, top 30):" du -h -x -d 4 "$BUILDROOT" | sort -h | tail -n30 || true -log info "CaptainOS finalize complete." +log info "Regular CaptainOS finalize complete." diff --git a/captain/flavors/common_debian/mkosi.postinst.sh.j2 b/captain/flavors/common_debian/mkosi.postinst.sh.j2 index 1a52990b..627bc223 100755 --- a/captain/flavors/common_debian/mkosi.postinst.sh.j2 +++ b/captain/flavors/common_debian/mkosi.postinst.sh.j2 @@ -24,4 +24,4 @@ mkosi-chroot update-ca-certificates --fresh log info "Setting default systemd target to multi-user.target (no graphical)..." mkosi-chroot systemctl set-default multi-user.target || true -log info "CaptainOS post-install complete." +log info "Regular CaptainOS post-install complete." From dc68ef9b164096ba11f37088ccb7708511d2f6b7 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 6 May 2026 16:11:22 +0200 Subject: [PATCH 127/143] docker: bind-mount Docker's config.json when launching Docker under GHA - so publish can use GHA-provided docker authentication vs ghcr.io Signed-off-by: Ricardo Pardini --- captain/docker.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/captain/docker.py b/captain/docker.py index 5e3ac776..be933fb7 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -221,6 +221,16 @@ def run_in_builder( for k, v in docker_envs.items(): docker_args += ["-e", f"{k}={v}"] + if os.environ.get("GITHUB_ACTIONS") == "true": + log.debug("Running in GitHub Actions, adding bind mount for Docker credentials...") + github_docker_config = Path.home() / ".docker" / "config.json" + if github_docker_config.exists() and github_docker_config.is_file(): + log.debug( + "Under GHA, Found Docker config file at %s, adding to bind mounts.", + github_docker_config, + ) + docker_args += ["-v", f"{github_docker_config}:/root/.docker/config.json"] + if cfg.custom_dir.exists() and cfg.custom_dir.is_dir(): docker_args += ["-v", f"{cfg.custom_dir}:/work/custom"] From 8be8ea6c2f53e5db8e2ab08978f2f031f81b274a Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 5 May 2026 12:42:52 +0200 Subject: [PATCH 128/143] custom: example customization possibilities (packages, scripts, extra files) Signed-off-by: Ricardo Pardini --- custom/files/etc/custom.example | 1 + custom/mkosi-finalize/example-finalize.sh | 2 + custom/mkosi-postinst/example-postinst.sh | 58 +++++++++++++++++++++++ custom/packages.txt | 2 + 4 files changed, 63 insertions(+) create mode 100644 custom/files/etc/custom.example create mode 100644 custom/mkosi-finalize/example-finalize.sh create mode 100644 custom/mkosi-postinst/example-postinst.sh create mode 100644 custom/packages.txt diff --git a/custom/files/etc/custom.example b/custom/files/etc/custom.example new file mode 100644 index 00000000..5ef9fff6 --- /dev/null +++ b/custom/files/etc/custom.example @@ -0,0 +1 @@ +This is an example; this file will end up in /etc/custom.example on the built CaptainOS filesystem. diff --git a/custom/mkosi-finalize/example-finalize.sh b/custom/mkosi-finalize/example-finalize.sh new file mode 100644 index 00000000..ea1c09c3 --- /dev/null +++ b/custom/mkosi-finalize/example-finalize.sh @@ -0,0 +1,2 @@ +log warn "Example custom finalize script (custom/mkosi-finalize/example-finalize.sh) running in mkosi finalize phase. This runs after the image is built and before it is packaged." +# Keep in mind that the target filesystem is at /buildroot at this stage, and commands ran here are all powerful ref the host system. diff --git a/custom/mkosi-postinst/example-postinst.sh b/custom/mkosi-postinst/example-postinst.sh new file mode 100644 index 00000000..fdcc0ef5 --- /dev/null +++ b/custom/mkosi-postinst/example-postinst.sh @@ -0,0 +1,58 @@ +log warn "Example custom post-install script (custom/mkosi-postinst/example-postinst.sh) running in mkosi post-install phase. This runs after packages are installed." +# Runs *outside* the image chroot after packages are installed. +# The target filesystem is at /buildroot; use 'mkosi-chroot' to run commands inside the chroot environment. + +declare -i ENABLE_EXAMPLE_CONTAINERD_MIRRORS=0 # flip to 1 to enable +declare -i ENABLE_EXAMPLE_CA_CERTS=0 # flip to 1 to enable + +### Example: configure containerd registry mirrors for a few common registries. +# Each target registry is a directory under /etc/containerd/certs.d with a hosts.toml file. +# You could also do this with individual files and directories in custom/files/ dir. + +if [[ ${ENABLE_EXAMPLE_CONTAINERD_MIRRORS} -gt 0 ]]; then + + declare -A registry_mirrors=( + ["docker.io"]="http://harbor.app.192.168.66.171.nip.io/v2/docker.io" + ["quay.io"]="http://harbor.app.192.168.66.171.nip.io/v2/quay.io" + ["ghcr.io"]="http://harbor.app.192.168.66.171.nip.io/v2/ghcr.io" + ["registry.k8s.io"]="http://harbor.app.192.168.66.171.nip.io/v2/registry.k8s.io" + ) + + log info "Configuring containerd registry mirrors..." + mkdir -pv "${BUILDROOT}/etc/containerd/certs.d" + + for registry in "${!registry_mirrors[@]}"; do + mirror="${registry_mirrors[$registry]}" + log info "Configuring mirror for registry '$registry' pointing to target '$mirror'" + mkdir -pv "${BUILDROOT}/etc/containerd/certs.d/${registry}" + + cat <<- ONE_HOSTS_TOML > "${BUILDROOT}/etc/containerd/certs.d/${registry}/hosts.toml" +server = "${mirror}" +[host."${mirror}"] +capabilities = ["pull", "resolve"] +override_path = true +ONE_HOSTS_TOML + done + + log info "Containerd registry mirrors configuration complete." +fi + +### Example: add a custom root CA certificate to the image's trust store. +# You could also do this by dropping .crt files in custom/files/usr/local/share/ca-certificates +if [[ ${ENABLE_EXAMPLE_CA_CERTS} -gt 0 ]]; then + + log info "Adding custom root CA certificate to image trust store..." + + curl https://letsencrypt.org/certs/staging/letsencrypt-stg-root-x1.pem \ + -o "${BUILDROOT}/usr/local/share/ca-certificates/letsencrypt-stg-root-x1.crt" + + curl https://letsencrypt.org/certs/staging/letsencrypt-stg-root-x2.pem \ + -o "${BUILDROOT}/usr/local/share/ca-certificates/letsencrypt-stg-root-x2.crt" + + # update the CA certificate bundle to include the new certs. + # this is not needed when using .crt files in the custom/files/ directory, since the regular + # (non-custom) postinst already does this. + mkosi-chroot update-ca-certificates --fresh --verbose + + log info "Custom root CA certificate added and CA bundle updated." +fi diff --git a/custom/packages.txt b/custom/packages.txt new file mode 100644 index 00000000..da97e4cd --- /dev/null +++ b/custom/packages.txt @@ -0,0 +1,2 @@ +# Add (Debian) package names, one per line, to be included in CaptainOS. +cowsay From e316c1177bc68dd93f7dde8045dae343042f75f8 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 6 May 2026 09:57:05 +0200 Subject: [PATCH 129/143] docs: update Markdown documentation Signed-off-by: Ricardo Pardini --- README.md | 307 ++++++++---------- .../oci-tooling-buildah-skopeo.md | 95 ++++-- docs/kernel-build.md | 26 +- 3 files changed, 213 insertions(+), 215 deletions(-) diff --git a/README.md b/README.md index 799ab842..baa42165 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,22 @@ A minimal, systemd-based in-memory OS for [Tinkerbell](https://tinkerbell.org) bare-metal provisioning. -CaptainOS boots via PXE/iPXE, runs entirely from RAM as a compressed CPIO initramfs, and provides a container runtime environment for the [tink-agent](https://github.com/tinkerbell/tinkerbell) — the component that drives hardware provisioning workflows. +CaptainOS boots via PXE/iPXE, runs entirely from RAM as a compressed CPIO initramfs, and provides a container runtime +environment for the [tink-agent](https://github.com/tinkerbell/tinkerbell) — the component that drives hardware +provisioning workflows. ## Why does CaptainOS exist? -CaptainOS is the next generation of Tinkerbell's in-memory OS, building on years of experience building, maintaining, and operating [HookOS](https://github.com/tinkerbell/hook) in production. -It is built with [mkosi](https://github.com/systemd/mkosi), producing a minimal systemd-based Debian OS that runs entirely from RAM. +CaptainOS is the next generation of Tinkerbell's in-memory OS, building on years of experience building, maintaining, +and operating [HookOS](https://github.com/tinkerbell/hook) in production. +It is built with [mkosi](https://github.com/systemd/mkosi), producing a minimal systemd-based Debian OS that runs +entirely from RAM. - **Significantly smaller initramfs** — small enough to boot comfortably on resource-constrained single-board computers. -- **No Docker-in-Docker** — tink-agent runs directly on the host with containerd, giving it native access to the container runtime without any nesting. -- **Familiar operations** — systemd foundation with journalctl, networkd, and standard service management make debugging and troubleshooting straightforward. +- **No Docker-in-Docker** — tink-agent runs directly on the host with containerd, giving it native access to the + container runtime without any nesting. +- **Familiar operations** — systemd foundation with journalctl, networkd, and standard service management make debugging + and troubleshooting straightforward. - **Simpler architecture** — fewer layers between hardware and workload, easier to develop against and extend. ## How it works @@ -19,234 +25,175 @@ It is built with [mkosi](https://github.com/systemd/mkosi), producing a minimal 1. The machine PXE boots the kernel (`vmlinuz`) and initramfs (`initramfs`) or runs the UEFI-bootable ISO image 2. A custom `/init` script transitions the rootfs to tmpfs, then exec's systemd 3. systemd-networkd configures DHCP on all ethernet interfaces -4. containerd starts, then `tink-agent-setup` pulls the tink-agent container image (configured via kernel cmdline), extracts the binary, and runs it as a host process +4. containerd starts, then `tink-agent-setup` pulls the tink-agent container image (configured via kernel cmdline), + extracts the binary, and runs it as a host process 5. tink-agent connects to the Tinkerbell server and executes provisioning workflows -## Architecture +## Build model -The build has four stages: +The current CLI is click-based (`captain`) with flavor-driven image generation. -1. **Kernel compilation** (`./build.py kernel`) — builds a Linux kernel from source using defconfigs from `kernel.configs/` -2. **Tool download** (`./build.py tools`) — fetches pinned binary releases of the container runtime stack -3. **Initramfs build** (`./build.py initramfs`) — assembles a Debian Trixie CPIO initramfs with systemd, injecting the kernel, modules, and tools using `mkosi` -4. **ISO assembly** (`./build.py iso`) — builds a UEFI-bootable ISO with GRUB via `grub-mkrescue` +- `captain build` runs: **tools -> initramfs (mkosi) -> iso (if flavor supports it) -> collect artifacts** +- `captain kernel` is a separate stage used when building kernel artifacts directly (for example `trixie-slim` + workflows) +- Flavor templates and static overlays are rendered into generated `mkosi.*` files +- `mkosi.output/` keeps hashed per-flavor build outputs; `out/` gets stable publishable filenames -## Usage +## Build system Usage -**Prerequisites:** `uv` (Python), Docker. +**Prerequisites:** `uv` (Python), Docker (with buildx/BuildKit). + +- On Debian, the regular Debian-supplied `docker.io` and `docker-cli`/`docker-buildx` packages work fine. You can also + use Docker CE from Docker Inc if you prefer. +- On MacOS with brew, `colima` is a good alternative to Docker Desktop or similar. ```bash # Install Astral's `uv` if you don't have it: https://docs.astral.sh/uv/getting-started/installation/ curl -LsSf https://astral.sh/uv/install.sh | sh # then re-log in -# Build with defaults (amd64, kernel 6.18.16) -uv run ./build.py --help - -usage: build.py [flags] - -Build CaptainOS images. Stages: kernel → tools → initramfs → iso. - -options: - -h, --help show this help message and exit - -build configuration: - --arch {amd64,arm64} target architecture (default: amd64) - --builder-image IMAGE Docker builder image name (default: captainos-builder) - --no-cache rebuild builder image without Docker cache +# top-level help +uv run captain --help +``` -kernel: - --flavor-id VER kernel version to build (default: 6.18.16) +### Core commands -tools: - --tools-mode {docker,native,skip} tools stage execution mode (default: docker) - --force-tools re-download tools even if outputs exist +**Important**: please pay attention to the level flags are defined. For example, `--flavor-id` is a global option and +needs to be passed after `uv run captain` but before the subcommand (`build`, `kernel`, etc). +We're investigating ways to make this more flexible using Python's Click library, but for now please follow the +documented usage. -initramfs (mkosi): - --mkosi-mode {docker,native,skip} mkosi stage execution mode (default: docker) - --force passed through to mkosi as --force +```bash +uv run captain builder --help +uv run captain build --help +uv run captain kernel --help +uv run captain tools --help +uv run captain iso --help +uv run captain qemu --help +uv run captain release --help +uv run captain shell --help +``` -iso: - --iso-mode {docker,native,skip} iso stage execution mode (default: docker) - --force-iso force ISO rebuild even if outputs exist +## Flavors -commands: - build Run all build stages: kernel → tools → initramfs → iso (default) - kernel Build only the kernel + modules - tools Download tools (containerd, runc, nerdctl, CNI) - initramfs Build only the initramfs via mkosi - iso Build a UEFI-bootable ISO image - checksums Compute SHA-256 checksums for specified files - shell Interactive shell inside the builder container - clean Remove all build artifacts - summary Print mkosi configuration summary - qemu-test Boot the image in QEMU for testing +Available flavors are discovered dynamically from `captain/flavors/`. Here is a current list: -``` +| Flavor | Architectures | ISO | Description | +|----------------------------|------------------|-----|-----------------------------------------------------------------------------------------------------------------------| +| `trixie-full` | `amd64`, `arm64` | yes | Debian Trixie, standard Debian linux-generic kernel, working `apt` package manager | +| `trixie-slim` | `amd64`, `arm64` | yes | Kernel built from source, slimmed down Debian userspace, ACPI/UEFI only | +| `trixie-armbian-rpi` | `arm64` | no | For RaspberryPi's, using Armbian's bcm2711 kernel; produces `rpi-firmware` which is ready-to-netboot on RPi's | +| `trixie-meson64` | `arm64` | no | For Amlogic boards, using Armbian's meson64 kernel | +| `trixie-rockchip64` | `arm64` | no | For Rockchip boards (rk35xx, rk33xx, etc) using Armbian's _mainline_ rockchip64 kernel | +| `trixie-rockchip64-vendor` | `arm64` | no | For Rockchip boards (rk35xx) using the Rockchip vendor kernel from Armbian. Prefer the non-vendor flavor if possible. | +| `trixie-genio` | `arm64` | no | For MediaTek Genioboards, using Armbian's genio kernel | -Output artifacts are placed in `out/`: +Use `--flavor-id` (or environment `FLAVOR_ID`) to select one. -- `out/vmlinuz--` — the kernel -- `out/initramfs--` — the initramfs -- `out/captainos--.iso` — UEFI-bootable ISO image -- `out/sha256sums--.txt` — SHA-256 checksums +## Build modes -Here `` is the kernel version (e.g. `6.18.16`) and `` is the Linux architecture name (`x86_64` or `aarch64`). +Most stages support `docker | native | skip`: -## Release +- `--tools-mode` / `TOOLS_MODE` +- `--mkosi-mode` / `MKOSI_MODE` +- `--iso-mode` / `ISO_MODE` +- `--kernel-mode` / `KERNEL_MODE` +- `captain release --release-mode` / `RELEASE_MODE` -CI publishes build artifacts as OCI images on every push to `main`. Pushing a version tag (`v*`) creates a GitHub Release with downloadable files and tags the OCI images with the release version. +Defaults are command-specific (for example, `captain build` defaults to `--tools-mode native`, while `captain tools` +defaults to `--tools-mode docker`). Use `--help` on each command for current defaults. -### OCI artifact images +## Customization via `custom/` -Three multi-arch OCI indexes are published per build: +The `custom/` directory is mounted automatically in docker-mode runs and applied by Debian-based flavors. -| Image | Tag | Contents | -| --- | --- | --- | -| amd64-only | `vX.Y.Z--amd64` | vmlinuz, initramfs, ISO, checksums (x86_64) | -| arm64-only | `vX.Y.Z--arm64` | vmlinuz, initramfs, ISO, checksums (aarch64) | -| combined | `vX.Y.Z-` | all artifacts from both architectures | +- `custom/packages.txt`: one package per line (comments allowed) +- `custom/files/`: copied into the image (mapped under `mkosi.extra/`) +- `custom/mkosi-postinst/`: appended scripts for post-install phase +- `custom/mkosi-finalize/`: appended scripts for finalize phase -Each artifact file is pushed as its own OCI layer. Deterministic tar creation (zeroed metadata) ensures identical layer digests across per-arch and combined images, so registries deduplicate shared blobs — the combined image adds zero additional storage. +See examples under `custom/` in this repository. -All three images are multi-arch OCI indexes with `linux/amd64` and `linux/arm64` platform entries pointing to the same content, so any platform can pull them. Images are compatible with: +## Output artifacts -- **containerd** — valid `rootfs.diff_ids` in the config; Kubernetes image-volume mounts work -- **skopeo** — extracts individual artifact files for release workflows +Final artifacts are copied to `out/` with flavor-aware names: -### GitHub Release +- `out/vmlinuz--` +- `out/initramfs--` +- `out/captainos--.iso` (only for ISO-capable flavors) +- `out/dtb--/` (directory, arm64 flavors that export DTBs) -When a `v*` tag is pushed, the release workflow: +`` uses Linux output architecture names (`x86_64`, `aarch64`). -1. Pulls the combined OCI image (both architectures) -2. Attaches all artifacts as downloadable files on the GitHub Release page: - - `vmlinuz--x86_64`, `initramfs--x86_64`, `captainos--x86_64.iso`, `sha256sums--x86_64.txt` - - `vmlinuz--aarch64`, `initramfs--aarch64`, `captainos--aarch64.iso`, `sha256sums--aarch64.txt` -3. Tags all three OCI images with the clean release version (`vX.Y.Z`, `vX.Y.Z-amd64`, `vX.Y.Z-arm64`) +## Release (OCI artifacts) -### Release subcommands +Release operations are grouped under `captain release`: ```bash -# Publish artifacts as a multi-arch OCI image -./build.py release publish --target amd64 +# publish +uv run captain release --target amd64 publish -# Pull and extract artifacts -./build.py release pull --target combined --pull-output ./out/release/ +# pull artifacts into out/release/ +uv run captain release --target combined pull --pull-output release -# Tag all artifact images with a release version -./build.py release tag v1.0.0 +# retag existing images +uv run captain release --src-tag v0.0.0-abcdef1-trixie-full tag --new-tag v1.0.0 ``` -Run `./build.py release -h` for full flag reference. +Notes: -## Build modes +- Base source tag defaults to `v0.0.0--`. +- Per-arch publish refs append `-amd64` or `-arm64`; combined uses the bare source tag. +- `publish` computes `sha256sums--.txt` and includes checksums as OCI layers. +- `REGISTRY_INSECURE=1` can be used for insecure registries (propagated to buildah/skopeo wrappers). -Each stage can be executed in one of three modes: +For the rationale behind the Buildah + Skopeo approach, see `docs/design-decisions/oci-tooling-buildah-skopeo.md`. -- `docker` (default) — runs the stage inside a Docker container, providing a consistent build environment regardless of host OS. -- `native` — runs the stage directly on the host machine. Requires all build dependencies to be installed and configured correctly. -- `skip` — skips the stage entirely. +## Testing with QEMU + +`captain qemu` boots artifacts from `out/` and forwards additional arguments to the kernel cmdline. -### Setting modes +```bash +# boot with defaults +uv run captain --arch amd64 --flavor-id trixie-full qemu -| Mode | CLI flag | Env var | Example | -| --- | --- | --- | --- | -| `docker` | `--{stage}-mode docker` | `{STAGE}_MODE=docker` | `--kernel-mode docker` | -| `native` | `--{stage}-mode native` | `{STAGE}_MODE=native` | `--tools-mode native` | -| `skip` | `--{stage}-mode skip` | `{STAGE}_MODE=skip` | `--iso-mode skip` | +# pass extra kernel cmdline args after -- +uv run captain --arch amd64 --flavor-id trixie-full qemu -- tink_worker_image=ghcr.io/tinkerbell/tink-agent:latest +``` -### Included tools +You will need OVMF firmware for the architecture installed on the host system: -| Component | Version | Purpose | -| --- | --- | --- | -| containerd | 2.2.1 | Container runtime | -| nerdctl | 2.2.1 | Container CLI (Docker-compatible) | -| runc | 1.4.0 | OCI runtime | -| CNI plugins | 1.6.0 | Container networking (bridge, host-local, loopback, portmap, firewall, tuning) | +- On MacOS with brew, OVMF builds for both x86 and arm64 is included with the `qemu` package. +- On Linux, you can install `ovmf` from your package manager: + - Debian: x86: `ovmf`, arm64: `qemu-efi-aarch64` + - Fedora: `edk2-ovmf` (both architectures) -### Key design decisions +On native-host architecture runs, acceleration is enabled when available (`hvf` on macOS, `kvm` on Linux). -- **Custom `/init` instead of `MakeInitrd`** — systemd's initrd mode expects to switch-root to a real rootfs. CaptainOS runs entirely from RAM, so a custom init transitions rootfs → tmpfs before exec'ing systemd. This makes `pivot_root(2)` work for container runtimes. -- **No UPX compression** — the final image is compressed with zstd level 19. Raw ELF binaries compress better under zstd than UPX-packed ones (UPX output looks like random data to zstd). -- **iptables-nft backend** — uses the nftables-backed iptables for container networking, with the necessary `CONFIG_NF_TABLES_*` kernel options enabled. -- **IP forwarding via sysctl** — enabled at boot for container network traffic. +Exit QEMU with `Ctrl-a x` (that's `Ctrl` + `a`, release, then press `x`). ## Project layout -```bash +```text . -├── build.py # Main build entry point (Python >= 3.13; use `uv run build.py`) -├── captain/ # Build system package (stdlib only) -│ ├── __init__.py # Package init incl logging -│ ├── cli.py # CLI subcommands (argparse) -│ ├── config.py # Configuration from environment -│ ├── docker.py # Docker builder management -│ ├── kernel.py # Kernel compilation logic -│ ├── tools.py # Binary tool downloader -│ ├── artifacts.py # Artifact collection & checksums -│ ├── oci.py # OCI artifact publish/pull/tag -│ ├── buildah.py # buildah CLI wrapper (image construction) -│ ├── skopeo.py # skopeo CLI wrapper (inspect/copy/export) -│ ├── iso.py # ISO image assembly -│ ├── qemu.py # QEMU boot testing -│ └── util.py # Shared helpers & arch mapping -├── Dockerfile # Builder container definition -├── Dockerfile.release # Lightweight container for OCI release ops -├── mkosi.conf # mkosi image configuration -├── mkosi.postinst # Post-install hooks (symlinks, cleanup) -├── mkosi.finalize # Final image adjustments -├── config/ -│ ├── defconfig.amd64 # Kernel config for x86_64 -│ └── defconfig.arm64 # Kernel config for aarch64 -└── mkosi.extra/ # Files overlaid into the image - ├── init # Custom PID 1 (rootfs → tmpfs → systemd) - └── etc/ - ├── containerd/ # containerd configuration - ├── systemd/system/ # systemd units - ├── acpi/ # ACPI power button handler - ├── sysctl.d/ # Kernel tunables - └── os-release # OS identification +|- build.py +|- captain/ +| |- cli/ # click CLI groups/commands +| |- flavors/ # flavor definitions and mkosi overlays +| |- oci/ # OCI publish/pull/tag logic +| |- artifacts.py # copy outputs into out/ +| |- docker.py # builder image + docker relaunch helpers +| |- iso.py # ISO assembly +| |- kernel.py # kernel build logic +| |- qemu.py # qemu launcher +| `- tools.py # pinned tool downloads +|- custom/ # user customizations +|- docs/ # documentation +|- kernel.configs/ # kernel configuration (defconfig) for trixie-slim kernel builds +|- mkosi.input/ # build inputs (generated and cached) +|- mkosi.output/ # per-flavor hashed mkosi outputs +`- out/ # final artifacts for release/use ``` -## Testing with QEMU - -```bash -./build.py qemu-test -h - -usage: build.py qemu-test [flags] - -Boot the image in QEMU for testing - -options: - -h, --help show this help message and exit - -build configuration: - --arch {amd64,arm64} target architecture (default: amd64) - --builder-image IMAGE Docker builder image name (default: captainos-builder) - --no-cache rebuild builder image without Docker cache - -qemu: - --qemu-append ARGS extra kernel cmdline args for qemu-test - --qemu-mem SIZE QEMU RAM size (default: 2G) - --qemu-smp N QEMU CPU count (default: 2) - -tinkerbell: - --tink-worker-image IMAGE tink-agent container image reference (default: ghcr.io/tinkerbell/tink- - agent:latest) - --tink-docker-registry HOST registry host (triggers tink-agent services) - --tink-grpc-authority ADDR tink-server gRPC endpoint (host:port) - --tink-worker-id ID machine / worker ID - --tink-tls BOOL enable TLS to tink-server (default: false) - --tink-insecure-tls BOOL allow insecure TLS (default: true) - --tink-insecure-registries LIST comma-separated insecure registries - --tink-registry-username USER registry auth username - --tink-registry-password PASS registry auth password - --tink-syslog-host HOST remote syslog host - --tink-facility CODE facility code - --ipam PARAM static networking IPAM parameter -``` - -This boots the image in QEMU with a virtio NIC and serial console. `console=ttyS0 audit=0` is always appended to the kernel cmdline. Press `Ctrl-A X` to exit or run `poweroff` inside the VM. - ## License See [Tinkerbell](https://github.com/tinkerbell/captain/blob/main/LICENSE) for license information. diff --git a/docs/design-decisions/oci-tooling-buildah-skopeo.md b/docs/design-decisions/oci-tooling-buildah-skopeo.md index 9cb984d0..1d1b724e 100644 --- a/docs/design-decisions/oci-tooling-buildah-skopeo.md +++ b/docs/design-decisions/oci-tooling-buildah-skopeo.md @@ -12,7 +12,8 @@ We iterated through three OCI client tools before arriving at the current approa ## Tool #1: ORAS -The initial implementation (`b34c119`..`90e6f75`) used [ORAS](https://oras.land/) directly in CI workflows to push artifact files to GHCR: +The initial implementation (`b34c119`..`90e6f75`) used [ORAS](https://oras.land/) directly in CI workflows to push +artifact files to GHCR: ```bash oras push \ @@ -21,61 +22,89 @@ oras push \ vmlinuz-x86_64 initramfs-x86_64 captainos-x86_64.iso ... ``` -**Problem:** `oras push` creates **OCI artifacts** (with an artifact manifest), not **OCI images**. Containerd cannot pull OCI artifacts because the layer count and `rootfs.diff_ids` in the config don't match — the image simply isn't a valid container image. This meant **Kubernetes image volumes didn't work**, which was a hard requirement. +**Problem:** `oras push` creates **OCI artifacts** (with an artifact manifest), not **OCI images**. Containerd cannot +pull OCI artifacts because the layer count and `rootfs.diff_ids` in the config don't match — the image simply isn't a +valid container image. This meant **Kubernetes image volumes didn't work**, which was a hard requirement. -The fix wasn't a flag or config change — ORAS is designed for arbitrary artifact storage, not for producing container images that a runtime can unpack. +The fix wasn't a flag or config change — ORAS is designed for arbitrary artifact storage, not for producing container +images that a runtime can unpack. ## Tool #2: Crane -We replaced ORAS with [crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane) (`58f25cf`), wrapping it in a Python module (`captain/crane.py`). Crane produces valid OCI images with correct `rootfs.diff_ids`, so containerd could pull them and Kubernetes image volumes worked. +We replaced ORAS with [crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane) (`58f25cf`), wrapping +it in a Python module (`captain/crane.py`). Crane produces valid OCI images with correct `rootfs.diff_ids`, so +containerd could pull them and Kubernetes image volumes worked. -The crane-based approach used `crane append` to add layers, `crane mutate` to set platform metadata and annotations, `crane index append` for multi-arch indexes, and `crane export` to extract files during release. +The crane-based approach used `crane append` to add layers, `crane mutate` to set platform metadata and annotations, +`crane index append` for multi-arch indexes, and `crane export` to extract files during release. -**Problem:** `crane append` is a **remote-first** operation — each call pushes a manifest to the registry. When building an image with four artifact layers, each `crane append` overwrites the tag with a new manifest, leaving the previous manifests as **untagged ("orphaned") images** in the registry. For every publish cycle we created several untagged manifests per architecture. +**Problem:** `crane append` is a **remote-first** operation — each call pushes a manifest to the registry. When building +an image with four artifact layers, each `crane append` overwrites the tag with a new manifest, leaving the previous +manifests as **untagged ("orphaned") images** in the registry. For every publish cycle we created several untagged +manifests per architecture. We tried multiple mitigations: -- **WIP tags** (`6cca281`): Push layers to a temporary `-wip` tag, then retag to the final ref. But GHCR requires `packages:delete` permission to remove tags, so WIP tag cleanup silently failed, leaving orphaned `-wip` tags instead. -- **Single-layer bundling** (`2cb2206`): Bundle all artifact files into one tar so `crane append` only runs once per platform manifest. This reduced but didn't eliminate the problem — `crane mutate` (for setting platform, annotations, labels) and `crane set_created` each rewrite the manifest, still producing untagged intermediates. -- **Digest refs** (`f1c8e8a`): Capture digests after each platform push and pass digest refs to `crane index append` instead of tags. This avoided some tag churn but didn't solve the fundamental issue. +- **WIP tags** (`6cca281`): Push layers to a temporary `-wip` tag, then retag to the final ref. But GHCR requires + `packages:delete` permission to remove tags, so WIP tag cleanup silently failed, leaving orphaned `-wip` tags instead. +- **Single-layer bundling** (`2cb2206`): Bundle all artifact files into one tar so `crane append` only runs once per + platform manifest. This reduced but didn't eliminate the problem — `crane mutate` (for setting platform, annotations, + labels) and `crane set_created` each rewrite the manifest, still producing untagged intermediates. +- **Digest refs** (`f1c8e8a`): Capture digests after each platform push and pass digest refs to `crane index append` + instead of tags. This avoided some tag churn but didn't solve the fundamental issue. -The root cause is architectural: crane operates directly on the registry for every mutation. There is no local staging area — every `append`, `mutate`, or `edit config` call creates a new manifest on the remote, and the old one becomes untagged. Over time this accumulated significant registry garbage. +The root cause is architectural: crane operates directly on the registry for every mutation. There is no local staging +area — every `append`, `mutate`, or `edit config` call creates a new manifest on the remote, and the old one becomes +untagged. Over time this accumulated significant registry garbage. ## Tool #3: Buildah + Skopeo (current) The current approach splits responsibilities: -- **Buildah** handles image *construction*: creating containers from scratch, adding layers, setting metadata (OS, arch, annotations, labels), committing images, and managing manifest lists. All operations happen **locally** — nothing touches the registry until the final `buildah manifest push --all`. -- **Skopeo** handles image *read operations*: inspecting digests, checking if images exist, copying/retagging between refs, and downloading images for artifact extraction. +- **Buildah** handles image *construction*: creating containers from scratch, adding layers, setting metadata (OS, arch, + annotations, labels), committing images, and managing manifest lists. All operations happen **locally** — nothing + touches the registry until the final `buildah manifest push --all`. +- **Skopeo** handles image *read operations*: inspecting digests, checking if images exist, copying/retagging between + refs, and downloading images for artifact extraction. + +Current CLI entry points are grouped under: + +```bash +uv run captain release --help +uv run captain release publish --help +uv run captain release pull --help +uv run captain release tag --help +``` ### Why this solves the problems -1. **No untagged manifests.** Buildah builds the complete image locally (all layers, all metadata, correct timestamps) and pushes the finished manifest in one shot. There are no intermediate remote manifests to orphan. +1. **No untagged manifests.** Buildah builds the complete image locally (all layers, all metadata, correct timestamps) + and pushes the finished manifest in one shot. There are no intermediate remote manifests to orphan. -2. **Containerd compatibility.** Buildah produces standard OCI images with valid `rootfs.diff_ids` — Kubernetes image volumes work correctly. +2. **Containerd compatibility.** Buildah produces standard OCI images with valid `rootfs.diff_ids` — Kubernetes image + volumes work correctly. -3. **Multi-arch indexes.** `buildah manifest create/add/push` manages OCI indexes natively, pushing the index and all referenced platform manifests in a single `push --all` operation. +3. **Multi-arch indexes.** `buildah manifest create/add/push` manages OCI indexes natively, pushing the index and all + referenced platform manifests in a single `push --all` operation. -4. **No external binary downloads.** Both buildah and skopeo are available as standard distro packages (`apt-get install buildah skopeo`), eliminating the need to download and verify release tarballs from GitHub as we did with crane. +4. **No external binary downloads.** Both buildah and skopeo are available as standard distro packages ( + `apt-get install buildah skopeo`), eliminating the need to download and verify release tarballs from GitHub as we did + with crane. 5. **Rootless operation.** Both tools work without a container runtime or root privileges when using `fuse-overlayfs`. ### Trade-offs -- Buildah's local storage model uses more disk than crane's append-directly-to-registry approach, though this is negligible for CI runners with ephemeral storage. -- Two tools instead of one, but the separation of concerns (write vs. read) maps cleanly to the codebase: `captain/buildah.py` for construction, `captain/skopeo.py` for inspection/extraction. - -## Timeline - -| Commit | Change | -|--------|--------| -| `b34c119` | Initial release workflow with ORAS | -| `4d40c3d` | Add multi-arch OCI index via `oras manifest index create` | -| `65a15a7` | Fix `oras push` flag (`--artifact-platform`) | -| `58f25cf` | Replace ORAS with crane; add Python `crane.py` module | -| `2de90bf` | Fix `crane mutate` (`--set-platform` vs `--platform`) | -| `2cb2206` | Bundle artifacts into single tar to reduce untagged images | -| `f1c8e8a` | Use digest refs for `crane index append` | -| `40e5690` | Per-artifact layers for dedup with multi-arch indexes | -| `dcc3fb2` | Add OCI metadata, created timestamps, simplify layout | -| current | Replace crane with buildah + skopeo | +- Buildah's local storage model uses more disk than crane's append-directly-to-registry approach, though this is + negligible for CI runners with ephemeral storage. +- Two tools instead of one, but the separation of concerns (write vs. read) maps cleanly to the codebase: + `captain/buildah.py` for construction, `captain/skopeo.py` for inspection/extraction. + +### Operational notes + +- Release commands are exposed as a click command group (`captain release ...`) with shared options (`--registry`, + `--repository`, `--target`, `--src-tag`, and `--release-mode`). +- Insecure registries can be supported via `REGISTRY_INSECURE=1` (translated to `--tls-verify=false` in wrapper + commands). +- Per-arch and combined images still use deterministic layers to preserve blob deduplication behavior. + diff --git a/docs/kernel-build.md b/docs/kernel-build.md index 0f5fb217..46aa975d 100644 --- a/docs/kernel-build.md +++ b/docs/kernel-build.md @@ -1,5 +1,27 @@ # Kernel Build Process -CaptainOS uses standard or custom Debian-packaged kernels. +CaptainOS now has two kernel paths, selected by flavor and command: -The default flavor, `trixie-full`, uses `linux-image-generic` default Debian kernel. +1. **Flavor-provided distro kernels** (for example `trixie-full` and several Armbian-based arm64 flavors). +2. **Captain-built kernels** via `captain kernel` (notably `trixie-slim` workflows). + +## Commands + +```bash +uv run captain kernel --help +``` + +Important options: + +- `--kernel-mode {docker,native,skip}` +- `--force-kernel` +- `--kernel-version` +- `--config` (interactive `menuconfig` flow; implies rebuild) + +## Build outputs and handoff + +- Kernel build artifacts are staged under `mkosi.input/kernel//`. +- Flavor generation + mkosi consume the staged kernel/modules. +- Final published kernel artifact is copied to `out/vmlinuz--`. + +For the end-to-end pipeline using kernels and tools together, see `README.md` (`captain build`). From 122ad2de40e1b6b37ce42abd0c97522d9b6bc05b Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 6 May 2026 15:44:49 +0200 Subject: [PATCH 130/143] release: switch release default mode to docker - GHA has skopeo and buildah available in its default runners, but they're old versions - use Docker mode by default also simplifies end-user usage Signed-off-by: Ricardo Pardini --- captain/cli/_release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/captain/cli/_release.py b/captain/cli/_release.py index 96c3eb78..1856fad3 100644 --- a/captain/cli/_release.py +++ b/captain/cli/_release.py @@ -21,7 +21,7 @@ @click.option( "--release-mode", envvar="RELEASE_MODE", - default="native", + default="docker", show_default=True, type=click.Choice(["docker", "native", "skip"], case_sensitive=False), metavar="MODE", From 1c85d2b6a7cb8da0bc82a41f0352bbc05bdfe747 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 6 May 2026 15:01:23 +0200 Subject: [PATCH 131/143] release: consolidate OCI metadata/annotations creation and add to manifests - makes browsing ghcr.io a bit easier - drop sha/tag from manifest metadata to up SHA-based reuse Signed-off-by: Ricardo Pardini --- captain/buildah.py | 15 +++++- captain/cli/_release.py | 1 - captain/oci/_build.py | 16 +----- captain/oci/_publish.py | 112 +++++++++++++++++++++++++++++++--------- 4 files changed, 105 insertions(+), 39 deletions(-) diff --git a/captain/buildah.py b/captain/buildah.py index 1ac63838..96299594 100644 --- a/captain/buildah.py +++ b/captain/buildah.py @@ -96,9 +96,22 @@ def push( def manifest_create( ref: str, + oci_metadata: dict[str, str], ) -> str: log.info("buildah manifest create %s", ref) - result = run(["buildah", "manifest", "create", ref], capture=True) + cmd = ["buildah", "manifest", "create"] + for key, value in oci_metadata.items(): + cmd += ["--annotation", f"{key}={value}"] + cmd += [ref] + result = run(cmd, capture=True, check=False) + # handle and show both stdout and stderr on failure + if result.returncode != 0: + log.error( + "buildah manifest create failed: stdout: '%s' stderr: '%s'", + result.stdout.strip(), + result.stderr.strip(), + ) + raise RuntimeError("buildah manifest create failed") return result.stdout.strip() diff --git a/captain/cli/_release.py b/captain/cli/_release.py index 1856fad3..f75f0b3a 100644 --- a/captain/cli/_release.py +++ b/captain/cli/_release.py @@ -213,7 +213,6 @@ def publish_command(cfg: Config) -> None: repository=str(cfg.release_repository), artifact_name=str(cfg.release_oci_artifact_name), tag=str(cfg.release_src_tag), - sha=str(cfg.release_git_sha), force=cfg.force_release, ) diff --git a/captain/oci/_build.py b/captain/oci/_build.py index 3349efc7..4dbcce77 100644 --- a/captain/oci/_build.py +++ b/captain/oci/_build.py @@ -63,12 +63,9 @@ def _checksum_files(arch_files: list[Path], flavor_id: str, oarch: str, out: Pat def _build_platform_image( layer_tars: list[Path], platform: str, - sha: str, - repository: str, *, created: str, - tag: str, - artifact_name: str, + oci_metadata: dict[str, str], base: str = "scratch", ) -> str: """Build an OCI image locally for *platform*. @@ -87,16 +84,6 @@ def _build_platform_image( """ os_name, arch = platform.split("/") epoch = int(datetime.fromisoformat(created.replace("Z", "+00:00")).timestamp()) - oci_metadata = { - "org.opencontainers.image.created": created, - "org.opencontainers.image.source": f"https://github.com/{repository}", - "org.opencontainers.image.revision": sha, - "org.opencontainers.image.version": tag, - "org.opencontainers.image.title": artifact_name, - "org.opencontainers.image.description": "CaptainOS build artifacts", - "org.opencontainers.image.vendor": "Tinkerbell", - "org.opencontainers.image.licenses": "Apache-2.0", - } # Build one layer per tar: from base → add file → commit → repeat. # Track intermediate image IDs so they can be cleaned up afterwards; @@ -120,6 +107,7 @@ def _build_platform_image( if prev != base: intermediates.append(prev) + log.debug("Cleaning up intermediate images: %s", intermediates) for img in intermediates: with contextlib.suppress(Exception): buildah.rmi(img) diff --git a/captain/oci/_publish.py b/captain/oci/_publish.py index ec8618fb..fa0343ed 100644 --- a/captain/oci/_publish.py +++ b/captain/oci/_publish.py @@ -28,6 +28,7 @@ def _create_push_cleanup( image_ids: list[str], dest_ref: str, + oci_metadata: dict[str, str], ) -> None: """Create a manifest list from *image_ids*, push it to *dest_ref*, and clean up. @@ -38,14 +39,16 @@ def _create_push_cleanup( temp_name = f"captain-local-{uuid4().hex[:12]}" manifest_id: str | None = None try: - manifest_id = buildah.manifest_create(temp_name) + manifest_id = buildah.manifest_create(temp_name, oci_metadata) for image_id in image_ids: buildah.manifest_add(manifest_id, image_id) buildah.manifest_push(manifest_id, dest_ref) finally: if manifest_id is not None: + log.debug("Cleaning up manifest: %s", manifest_id) with contextlib.suppress(Exception): buildah.rmi(manifest_id) + log.debug("Cleaning up intermediate images: %s", image_ids) for image_id in image_ids: with contextlib.suppress(Exception): buildah.rmi(image_id) @@ -53,12 +56,10 @@ def _create_push_cleanup( def _publish_single_arch( *, + flavor: BaseFlavor, layer_tars: list[Path], ref: str, - tag: str, - sha: str, repository: str, - artifact_name: str, created: str, ) -> None: """Build a per-arch multi-arch index and push it. @@ -71,25 +72,36 @@ def _publish_single_arch( image_id = _build_platform_image( layer_tars, f"linux/{platform_arch}", - sha, - repository, created=created, - tag=tag, - artifact_name=artifact_name, + oci_metadata=create_oci_metadata( + flavor=flavor, + image_type="inner-arch", + arch=platform_arch, + repository=repository, + ), ) image_ids.append(image_id) - _create_push_cleanup(image_ids, ref) + _create_push_cleanup( + image_ids=image_ids, + dest_ref=ref, + oci_metadata=create_oci_metadata( + flavor=flavor, + image_type="outer-arch", + arch=None, + repository=repository, + ), + ) def _publish_combined( *, + flavor: BaseFlavor, arch_layer_tars: dict[str, list[Path]], registry: str, repository: str, artifact_name: str, tag: str, - sha: str, created: str, force: bool = False, ) -> bool: @@ -125,12 +137,10 @@ def _publish_combined( per_arch_ref, ) _publish_single_arch( + flavor=flavor, layer_tars=arch_layer_tars[arch], ref=per_arch_ref, - tag=per_arch_tag, - sha=sha, repository=repository, - artifact_name=artifact_name, created=created, ) @@ -143,16 +153,28 @@ def _publish_combined( image_id = _build_platform_image( arch_layer_tars[other], f"linux/{arch}", - sha, - repository, created=created, - tag=tag, - artifact_name=artifact_name, + oci_metadata=create_oci_metadata( + flavor=flavor, + image_type="inner-combined", + arch=arch, + repository=repository, + ), base=f"docker://{per_arch_ref}", ) image_ids.append(image_id) - _create_push_cleanup(image_ids, combined_ref) + _create_push_cleanup( + image_ids=image_ids, + dest_ref=combined_ref, + oci_metadata=create_oci_metadata( + flavor=flavor, + image_type="outer-combined", + arch=None, + repository=repository, + ), + ) + return True @@ -165,7 +187,6 @@ def publish( repository: str, artifact_name: str, tag: str, - sha: str, force: bool = False, ) -> None: """Collect artifacts and publish a multi-arch OCI index. @@ -262,23 +283,21 @@ def publish( try: if target == "combined": pushed = _publish_combined( + flavor=flavor, arch_layer_tars=arch_layer_tars, registry=registry, repository=repository, artifact_name=artifact_name, tag=tag, - sha=sha, created=created, force=force, ) else: _publish_single_arch( + flavor=flavor, layer_tars=arch_layer_tars[target], ref=final_ref, - tag=full_tag, - sha=sha, repository=repository, - artifact_name=artifact_name, created=created, ) finally: @@ -305,3 +324,50 @@ def publish( log.info(" Artifacts:") for name in artifact_names: log.info(" - %s", name) + + +def create_oci_metadata( + *, + flavor: BaseFlavor, + image_type: str, + arch: str | None = None, + repository: str, +) -> dict[str, str]: + title_parts = ["CaptainOS"] + title_parts += [f"{flavor.id}"] + title_parts += [f"{image_type}"] + if arch is not None: + title_parts += [f"{arch}"] + + desc_parts = ["Tinkerbell CaptainOS"] + + # Add flavor info + desc_parts += [f"flavor {flavor.id}"] + desc_parts += [f"({flavor.description})"] + + if arch is not None: + desc_parts += [f"{arch}"] + + match image_type: + case "inner-arch": + desc_parts += ["single-platform inner image"] + case "inner-combined": + desc_parts += ["combined inner image"] + case "outer-combined": + desc_parts += ["combined multi-platform image"] + case "outer-arch": + desc_parts += ["single-platform image"] + case _: + log.warning("Unknown OCI metadata type: %s", image_type) + + description = " ".join(desc_parts) + title = " ".join(title_parts) + + oci_metadata = { + "org.opencontainers.image.vendor": "Tinkerbell", + "org.opencontainers.image.licenses": "Apache-2.0", + "org.opencontainers.image.source": f"https://github.com/{repository}", + "org.opencontainers.image.title": title, + "org.opencontainers.image.description": description, + } + return oci_metadata From 73df51754d4687e9680daa36a59d19669dc7943a Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 7 May 2026 10:15:32 +0200 Subject: [PATCH 132/143] buildah: omit history for further determinism at the manifest level - otherwise, history between otherwise identical manifests differs for little reason Signed-off-by: Ricardo Pardini --- captain/buildah.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/captain/buildah.py b/captain/buildah.py index 96299594..4820ff3e 100644 --- a/captain/buildah.py +++ b/captain/buildah.py @@ -34,7 +34,7 @@ def from_image( if platform: cmd += ["--platform", platform] cmd.append(image) - log.info("buildah from %s", image) + log.info("buildah from %s (platform %s)", image, platform) result = run(cmd, capture=True) return result.stdout.strip() @@ -78,7 +78,7 @@ def commit( timestamp: int | None = None, ) -> str: log.info("buildah commit %s", container) - cmd: list[str] = ["buildah", "commit", "--rm"] + cmd: list[str] = ["buildah", "commit", "--rm", "--omit-history=true"] if timestamp is not None: cmd += ["--timestamp", str(timestamp)] cmd.append(container) From 42b4c8b40bd62b8fc5bb3b0a8ba4e508a958bcdb Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 7 May 2026 11:19:08 +0200 Subject: [PATCH 133/143] buildah: strip digest annotation auto-added by `buildah from` (more determinism) - otherwise, history between otherwise identical manifests differs for little reason Signed-off-by: Ricardo Pardini --- captain/buildah.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/captain/buildah.py b/captain/buildah.py index 4820ff3e..ff400da0 100644 --- a/captain/buildah.py +++ b/captain/buildah.py @@ -36,7 +36,13 @@ def from_image( cmd.append(image) log.info("buildah from %s (platform %s)", image, platform) result = run(cmd, capture=True) - return result.stdout.strip() + + imageid = result.stdout.strip() + # Use buildah config to strip the annotation added by buildah from + log.debug("Stripping digest annotation from base image %s", imageid) + run(["buildah", "config", "--annotation", "org.opencontainers.image.base.digest=", imageid]) + + return imageid def add( From f7d04900e2a6c5216a8741e52cd1b3e8bc1c74ff Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 7 May 2026 12:29:19 +0200 Subject: [PATCH 134/143] gha: ci: fix: avoid cache mis-hit due to unintended prefix match - eg `rockchip64-*` matches `rockchip64-vendor` - show sha256's after restore & before save Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 48 ++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cb8d1e7..c2a52df0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,24 +214,31 @@ jobs: id: initramfs-cache uses: actions/cache/restore@v5 with: - path: | - mkosi.output/initramfs - key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + path: mkosi.output/initramfs + key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time - restore-keys: | - initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}- + restore-keys: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-- + + - name: Show initramfs cache restore SHA256's + run: | + echo "initramfs cache hit: ${{ steps.initramfs-cache.outputs.cache-hit }}" + sha256sum $(find mkosi.output/initramfs -type f || true) || true - name: Restore iso cache id: iso-cache if: ${{ matrix.iso }} # only if matrix entry had iso: true uses: actions/cache/restore@v5 with: - path: | - mkosi.output/iso - key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + path: mkosi.output/iso + key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time - restore-keys: | - iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}- + restore-keys: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-- + + - name: Show iso cache restore SHA256's + if: ${{ matrix.iso }} # only if matrix entry had iso: true + run: | + echo "iso cache hit: ${{ steps.iso-cache.outputs.cache-hit }}" + sha256sum $(find mkosi.output/iso -type f || true) || true - name: Install uv uses: astral-sh/setup-uv@v7 @@ -239,21 +246,30 @@ jobs: - name: Build initramfs run: uv run captain build # full build, incl initramfs and iso when appropriate + - name: Show initramfs cache save SHA256's + run: | + echo "initramfs cache hit: ${{ steps.initramfs-cache.outputs.cache-hit }}" + sha256sum $(find mkosi.output/initramfs -type f || true) || true + - name: Save initramfs cache if: github.ref == 'refs/heads/main' && steps.initramfs-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v5 with: - path: | - mkosi.output/initramfs - key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + path: mkosi.output/initramfs + key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + + - name: Show iso cache save SHA256's + if: ${{ matrix.iso }} # only if matrix entry had iso: true + run: | + echo "iso cache hit: ${{ steps.iso-cache.outputs.cache-hit }}" + sha256sum $(find mkosi.output/iso -type f || true) || true - name: Save iso cache if: matrix.iso && github.ref == 'refs/heads/main' && steps.iso-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v5 with: - path: | - mkosi.output/iso - key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + path: mkosi.output/iso + key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} - name: Upload initramfs artifacts uses: actions/upload-artifact@v6 From 271bcc018f23b9ec422d763b9d8bd05680d9f5b2 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 18 May 2026 21:45:04 +0200 Subject: [PATCH 135/143] captain: bump trixie-slim's kernel 6.18.26 -> 6.18.39 Signed-off-by: Ricardo Pardini --- captain/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/captain/config.py b/captain/config.py index 032ff056..8aaea9d0 100644 --- a/captain/config.py +++ b/captain/config.py @@ -22,7 +22,7 @@ # The latest version can be obtained and passed as --kernel-version/KERNEL_VERSION # via <@TODO cmd for listing from cached releases.json # Override at runtime via --kernel-version or KERNEL_VERSION env var. -DEFAULT_KERNEL_VERSION = "6.18.26" +DEFAULT_KERNEL_VERSION = "6.18.39" @dataclass(slots=True) From b73fb917532ed662515f621331ee8c97f6aadd82 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 19 May 2026 12:03:35 +0200 Subject: [PATCH 136/143] trixie-meson64: use Armbian's oldlts instead of edge - edge is at 7.0.y; oldlts is at 6.12.y - after 6.18.y, meson64 with PCIe is really not stable Signed-off-by: Ricardo Pardini --- captain/flavors/trixie_meson64/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/captain/flavors/trixie_meson64/__init__.py b/captain/flavors/trixie_meson64/__init__.py index ab598542..6f5a3176 100644 --- a/captain/flavors/trixie_meson64/__init__.py +++ b/captain/flavors/trixie_meson64/__init__.py @@ -15,8 +15,8 @@ def create_flavor() -> BaseFlavor: class TrixieMeson64Flavor(ArmbianCommonFlavor): id = "trixie-meson64" name = "Trixie for Meson (Amlogic) 64-bit ARM machines" - description = "Debian Trixie based with Armbian's meson64-edge kernel" + description = "Debian Trixie based with Armbian's meson64-oldlts kernel" supported_architectures = frozenset(["arm64"]) # does NOT support amd64 def flavor_packages(self) -> set[str]: - return {"linux-image-edge-meson64"}.union(super().flavor_packages()) + return {"linux-image-oldlts-meson64"}.union(super().flavor_packages()) From ebbd9249dc7ab2dd7722aa37c9ad6c488d554213 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 9 Jun 2026 15:59:55 +0200 Subject: [PATCH 137/143] common-armbian: enable hwdb & working apt Signed-off-by: Ricardo Pardini --- captain/flavors/common_armbian/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/captain/flavors/common_armbian/__init__.py b/captain/flavors/common_armbian/__init__.py index da549320..03c0d86a 100644 --- a/captain/flavors/common_armbian/__init__.py +++ b/captain/flavors/common_armbian/__init__.py @@ -25,3 +25,9 @@ def has_iso(self) -> bool: def flavor_packages(self) -> set[str]: return {"tiny-initramfs"}.union(super().flavor_packages()) + + def include_hwdb(self) -> bool: + return True + + def include_working_apt(self) -> bool: + return True From 0f04372d56ee844bf7c341821b4503574bb55ce5 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 15 Jun 2026 14:21:15 +0200 Subject: [PATCH 138/143] flavors: move include_working_apt()'s apt package handling up to common-debian Signed-off-by: Ricardo Pardini --- captain/flavors/common_debian/__init__.py | 2 ++ captain/flavors/trixie_full/__init__.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py index 5423c6cc..65099d24 100644 --- a/captain/flavors/common_debian/__init__.py +++ b/captain/flavors/common_debian/__init__.py @@ -78,6 +78,8 @@ def flavor_packages(self) -> set[str]: if line.strip() and not line.strip().startswith("#") } ) + if self.include_working_apt(): + packages.add("apt") return packages def sorted_flavor_packages(self) -> list[str]: diff --git a/captain/flavors/trixie_full/__init__.py b/captain/flavors/trixie_full/__init__.py index cc369bd5..713a8ae7 100644 --- a/captain/flavors/trixie_full/__init__.py +++ b/captain/flavors/trixie_full/__init__.py @@ -23,8 +23,6 @@ def flavor_packages(self) -> set[str]: "linux-image-generic", # Debian's standard kernel (arm64/amd64) "tiny-initramfs", # A tiny initramfs builder; required for kernel image } - if self.include_working_apt(): - pkgs.add("apt") return pkgs.union(super().flavor_packages()) def include_hwdb(self) -> bool: From ac9e1fef382369a0f4a571bc0f7751b7affb12d4 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Wed, 17 Jun 2026 10:44:12 +0200 Subject: [PATCH 139/143] pyproject.toml: bump deps, loosen bounds, update uv.lock Signed-off-by: Ricardo Pardini --- pyproject.toml | 8 +- uv.lock | 235 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 200 insertions(+), 43 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index daefe5c7..f4330ca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,16 +8,16 @@ name = "captain" requires-python = ">=3.13" dependencies = [ "click>=8.1", - "rich>=14.3.3", - "jinja2>=3.1.6", - "trogon>=0.6.0", + "rich>=15.0", + "jinja2>=3.1", + "trogon>=0.6", ] [project.scripts] captain = "build:main" [project.optional-dependencies] -dev = ["ruff>=0.15.9", "pyright>=1.1"] +dev = ["ruff>=0.15", "pyright>=1.1"] [tool.ruff] target-version = "py310" diff --git a/uv.lock b/uv.lock index b436ceb9..e0f3177f 100644 --- a/uv.lock +++ b/uv.lock @@ -5,10 +5,12 @@ requires-python = ">=3.13" [[package]] name = "captain" version = "0.0.1" -source = { virtual = "." } +source = { editable = "." } dependencies = [ - { name = "configargparse" }, + { name = "click" }, + { name = "jinja2" }, { name = "rich" }, + { name = "trogon" }, ] [package.optional-dependencies] @@ -19,32 +21,139 @@ dev = [ [package.metadata] requires-dist = [ - { name = "configargparse", specifier = ">=1.7" }, + { name = "click", specifier = ">=8.1" }, + { name = "jinja2", specifier = ">=3.1" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1" }, - { name = "rich", specifier = ">=14.3.3" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9" }, + { name = "rich", specifier = ">=15.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15" }, + { name = "trogon", specifier = ">=0.6" }, ] provides-extras = ["dev"] [[package]] -name = "configargparse" -version = "1.7.5" +name = "click" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/0b/30328302903c55218ffc5199646d0e9d28348ff26c02ba77b2ffc58d294a/configargparse-1.7.5.tar.gz", hash = "sha256:e3f9a7bb6be34d66b2e3c4a2f58e3045f8dfae47b0dc039f87bcfaa0f193fb0f", size = 53548, upload-time = "2026-03-11T02:19:38.144Z" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/19/3ba5e1b0bcc7b91aeab6c258afd70e4907d220fed3972febe38feb40db30/configargparse-1.7.5-py3-none-any.whl", hash = "sha256:1e63fdffedf94da9cd435fc13a1cd24777e76879dd2343912c1f871d4ac8c592", size = 27692, upload-time = "2026-03-11T02:19:36.442Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, ] [[package]] @@ -65,6 +174,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "platformdirs" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/cd/4f25b2f95b23f5d2c9c1fe43e49841bff5800562149b2666afc09309aa8f/platformdirs-4.10.1.tar.gz", hash = "sha256:ceab4084426fe6319ce18e86deada8ab1b7487c7aee7040c55e277c9ae793695", size = 31678, upload-time = "2026-07-18T03:53:43.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/73/6fd0bb9ce84138c3857f12e9de63bc901852975a092d545f18087a204aa2/platformdirs-4.10.1-py3-none-any.whl", hash = "sha256:0e4eff26be2d75293977f7cddc153fd9b8eaa7fb0c7b64ffe4076cb443117443", size = 22906, upload-time = "2026-07-18T03:53:42.576Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -76,15 +194,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.408" +version = "1.1.411" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, + { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, ] [[package]] @@ -102,34 +220,73 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, - { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, - { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, - { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, - { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, - { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, - { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +] + +[[package]] +name = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + +[[package]] +name = "trogon" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/ae/7367acac2194a215b092ba3fccde3b558272702110b8bb9bea164ab4ac42/trogon-0.6.0.tar.gz", hash = "sha256:fd1abfeb7b15d79d6e6cfc9e724aad2a2728812e4713a744d975f133e7ec73a4", size = 22902, upload-time = "2024-10-02T13:38:12.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/30/33035d5796a3b8b9624997fec7545e3febd2268c7b48df38a715a95cb5e4/trogon-0.6.0-py3-none-any.whl", hash = "sha256:fb5b6c25acd7a0eaba8d2cd32a57f1d80c26413cea737dad7a4eebcda56060e0", size = 26077, upload-time = "2024-10-02T13:38:11.09Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, ] From 2e1947059a94818e3ea94667b97786dde434c4d3 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 23 Jul 2026 13:35:24 +0200 Subject: [PATCH 140/143] oci: recap release pull from image contents, not disk export_image() now returns the top-level entries (files and dirs) the pulled layers actually contained; pull() recaps from that instead of scanning output_dir with iterdir(), which listed stale on-disk files and dropped directory artifacts (DTBs) via its is_file() filter. Signed-off-by: Ricardo Pardini --- captain/oci/_pull.py | 5 ++--- captain/skopeo.py | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/captain/oci/_pull.py b/captain/oci/_pull.py index f8238795..4c8cc6d5 100644 --- a/captain/oci/_pull.py +++ b/captain/oci/_pull.py @@ -29,10 +29,9 @@ def pull( """ tag_suffix = "" if target == "combined" else f"-{target}" ref = _image_ref(registry, repository, artifact_name, f"{tag}{tag_suffix}") - skopeo.export_image(ref, output_dir) + extracted = skopeo.export_image(ref, output_dir) - # Recap - extracted = sorted(f.name for f in Path(output_dir).iterdir() if f.is_file()) + # Recap — list what the pulled image contained, not what is on disk. log.info("") log.info("Pull complete") log.info(" Image: %s", ref) diff --git a/captain/skopeo.py b/captain/skopeo.py index 23af61da..925e257f 100644 --- a/captain/skopeo.py +++ b/captain/skopeo.py @@ -104,17 +104,23 @@ def export_image( output_dir: Path, *, platform: str | None = None, -) -> None: +) -> list[str]: """Download and extract all layers from *image_ref* into *output_dir*. Uses ``skopeo copy`` to download the image to a temporary directory, parses the manifest to find layer blobs, and extracts each layer tar with path-traversal protection. + + Returns the sorted list of top-level entries (files and directories) + that the pulled image's layers contained. This reflects the image + contents, not whatever happens to be on disk in *output_dir*. """ import tempfile output_dir.mkdir(parents=True, exist_ok=True) + extracted: set[str] = set() + with tempfile.TemporaryDirectory(prefix="skopeo-export-") as tmp: tmp_dir = Path(tmp) copy_to_dir(image_ref, tmp_dir, platform=platform) @@ -137,4 +143,11 @@ def export_image( log.info("Extracting layer %s…", digest_str[:20]) with tarfile.open(blob_file, "r:*") as tf: - safe_extractall(tf, output_dir) + members = tf.getmembers() + for m in members: + parts = Path(m.name).parts + if parts and parts[0] not in (".", ""): + extracted.add(parts[0]) + safe_extractall(tf, output_dir, members=members) + + return sorted(extracted) From ad0a9068734d77b4ed79e2ca85e300ee433344a8 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Thu, 23 Jul 2026 13:48:23 +0200 Subject: [PATCH 141/143] ci: --drop-old-caches to keep only the freshly-built flavor hash initramfs/iso outputs live under ///, but the caches persist the whole mkosi.output/{initramfs,iso} tree. A restore-key (prefix) cache hit brings in the previous hash's dir, which then gets re-saved alongside the new one and accumulates forever. Add an opt-in --drop-old-caches flag (env CAPTAIN_DROP_OLD_CACHES) that prunes sibling hash dirs after a build, keeping only the current hash; wired into the CI flavor-build jobs via the shared build env. Default (local) behaviour is unchanged, so prior versions are kept. Signed-off-by: Ricardo Pardini --- .github/workflows/ci.yml | 3 +++ captain/cli/_build.py | 4 ++++ captain/cli/_iso.py | 2 ++ captain/cli/_main.py | 14 ++++++++++++++ captain/cli/stages.py | 26 ++++++++++++++++++++++++++ captain/config.py | 4 ++++ 6 files changed, 53 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2a52df0..fca2fbd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -186,6 +186,9 @@ jobs: MKOSI_MODE: docker ISO_MODE: docker FLAVOR_ID: ${{ matrix.FLAVOR_ID }} + # Keep the initramfs/iso caches to only the freshly-built flavor hash; a + # restore-key (prefix) cache hit otherwise leaves the previous hash behind. + CAPTAIN_DROP_OLD_CACHES: "1" steps: &build_steps - name: Checkout code uses: actions/checkout@v6 diff --git a/captain/cli/_build.py b/captain/cli/_build.py index df30e49a..080aba7c 100644 --- a/captain/cli/_build.py +++ b/captain/cli/_build.py @@ -13,6 +13,7 @@ _build_iso_stage, _build_mkosi_stage, _build_tools_stage, + _prune_stale_output_dirs, ) log = logging.getLogger(__name__) @@ -139,4 +140,7 @@ def build_cmd( # Flavor-driven artifact collection. flavor.post_artifact_collect() + # Keep caches/artifacts lean when asked (CI). + _prune_stale_output_dirs(cfg) + log.info("Build complete!") diff --git a/captain/cli/_iso.py b/captain/cli/_iso.py index c320f87a..56590b33 100644 --- a/captain/cli/_iso.py +++ b/captain/cli/_iso.py @@ -11,6 +11,7 @@ from captain.cli._main import CliContext, cli from captain.cli.stages import ( _build_iso_stage, + _prune_stale_output_dirs, ) log = logging.getLogger(__name__) @@ -56,4 +57,5 @@ def build_cmd( _build_iso_stage(cfg) artifacts.collect_iso(cfg) + _prune_stale_output_dirs(cfg) log.info("ISO build complete!!!") diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 87ed0ffe..c3c2212f 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -34,6 +34,7 @@ class CliContext: builder_repository: str | None builder_image: str verbose_docker: bool + drop_old_caches: bool def make_config(self, **overrides: Any) -> Config: """Build a :class:`Config` from the common options plus per-command *overrides*.""" @@ -46,6 +47,7 @@ def make_config(self, **overrides: Any) -> Config: builder_repository=self.builder_repository, builder_image=self.builder_image, verbose_docker=self.verbose_docker, + drop_old_caches=self.drop_old_caches, **overrides, ) @@ -145,6 +147,16 @@ def resolve_project_dir(project_dir: str | None) -> Path: show_default=True, help="Local name/tag of Docker builder image name", ) +@click.option( + "--drop-old-caches", + envvar="CAPTAIN_DROP_OLD_CACHES", + is_flag=True, + default=False, + help=( + "After a build, remove sibling flavor-hash output dirs so caches/artifacts " + "only keep the freshly-built version (env: CAPTAIN_DROP_OLD_CACHES)." + ), +) @click.pass_context def cli( ctx: click.Context, @@ -156,6 +168,7 @@ def cli( builder_registry: str | None, builder_repository: str | None, builder_image: str, + drop_old_caches: bool, ) -> None: """CaptainOS build system — click CLI.""" # Configure log level based on --verbose. @@ -179,6 +192,7 @@ def cli( builder_repository=builder_repository, builder_image=builder_image, verbose_docker=verbose, + drop_old_caches=drop_old_caches, ) diff --git a/captain/cli/stages.py b/captain/cli/stages.py index fb280d99..7eea15b4 100644 --- a/captain/cli/stages.py +++ b/captain/cli/stages.py @@ -166,6 +166,32 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: ) +def _prune_stale_output_dirs(cfg: Config) -> None: + """Drop sibling flavor-hash output dirs, keeping only the current hash. + + Caches (``mkosi.output/initramfs`` / ``mkosi.output/iso``) are keyed on the + whole tree but outputs live under ``...///``. + A restore-key (prefix) cache hit brings in the *previous* hash's dir, which + would otherwise be re-saved alongside the freshly-built one and accumulate + forever. When enabled, prune everything but the current hash so the cache + only contains the just-built version. + + Opt-in via ``--drop-old-caches`` / ``CAPTAIN_DROP_OLD_CACHES``; a no-op + otherwise so local builds keep prior versions around. + """ + if not cfg.drop_old_caches: + return + + for current in (cfg.initramfs_output, cfg.iso_output): + parent = current.parent # .../// + if not parent.is_dir(): + continue + for child in parent.iterdir(): + if child.is_dir() and child.name != current.name: + log.info("Dropping stale output dir %s", child) + shutil.rmtree(child) + + def _build_iso_stage(cfg: Config) -> None: """Run the ISO build stage according to *cfg.iso_mode*.""" diff --git a/captain/config.py b/captain/config.py index 8aaea9d0..fd35b014 100644 --- a/captain/config.py +++ b/captain/config.py @@ -66,6 +66,10 @@ class Config: force_iso: bool = False force_release: bool = False + # Prune sibling flavor_hash output dirs after a build, so caches/artifacts + # only contain the freshly-built version (opt-in; used in CI). + drop_old_caches: bool = False + # QEMU qemu_mem: str = "2G" qemu_smp: str = "2" From 229fdf4b26a4854cab1d92793a4fd719e8cac69f Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Mon, 18 May 2026 21:42:44 +0200 Subject: [PATCH 142/143] ci: resolve latest kernel & armbian versions in a prepare job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 'captain prepare-versions' helper that resolves two dynamic tokens and prints them as name=value for $GITHUB_OUTPUT: - kernel_version: latest kernel.org point release of the configured branch (from releases.json), so the from-source kernel is never pinned; falls back to the hardcoded default off-CI. - armbian_version: a token derived from the armbian-next repo Release ETag, folded into armbian flavors' mkosi.conf (hence flavor_hash) so they rebuild when armbian publishes a newer kernel under the same package name — replacing the hand-edited 'force a cache miss' comment. The CI 'lint' job is renamed 'prepare' and now emits both as outputs; build jobs consume them via KERNEL_VERSION/ARMBIAN_VERSION env and fold them into the kernel/initramfs/iso cache keys so caches track versions. --kernel-version becomes a global option (shared by kernel + build), and the kernel save-gate now checks the correct cache-hit id. Signed-off-by: Ricardo Pardini Assisted-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 43 ++++++++++----- captain/armbian.py | 49 +++++++++++++++++ captain/cli/_kernel.py | 13 +---- captain/cli/_main.py | 27 +++++++++- captain/cli/_prepare.py | 33 ++++++++++++ captain/config.py | 5 ++ captain/flavor.py | 9 ++++ captain/flavors/common_armbian/__init__.py | 5 ++ captain/flavors/common_debian/mkosi.conf.j2 | 2 + captain/kernel.py | 58 ++++++++++++++++++++- 10 files changed, 216 insertions(+), 28 deletions(-) create mode 100644 captain/armbian.py create mode 100644 captain/cli/_prepare.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fca2fbd2..c61e3903 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,16 @@ env: jobs: # ------------------------------------------------------------------- - # Lint — ruff (lint + format) and pyright (type checking) + # Lint — ruff (lint + format) and pyright (type checking). + # Doubles as the "prepare" job: resolves dynamic version tokens once + # (latest kernel.org release of the branch; armbian-next Release ETag) + # and exposes them as outputs consumed by the build jobs. # ------------------------------------------------------------------- - lint: + prepare: runs-on: ubuntu-latest + outputs: + kernel_version: ${{ steps.versions.outputs.kernel_version }} + armbian_version: ${{ steps.versions.outputs.armbian_version }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -33,11 +39,15 @@ jobs: - name: Lint run: make lint + - name: Resolve dynamic version tokens + id: versions + run: uv run captain prepare-versions >> "$GITHUB_OUTPUT" + # ------------------------------------------------------------------- # Download tools (containerd, runc, nerdctl, CNI plugins) # ------------------------------------------------------------------- download-tools: - needs: [ lint ] + needs: [ prepare ] runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} strategy: fail-fast: false @@ -90,7 +100,7 @@ jobs: # Build Docker builder image, per-arch; pushes to ghcr.io. # ------------------------------------------------------------------- build-dockerfile: - needs: [ lint ] + needs: [ prepare ] runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} strategy: fail-fast: false @@ -125,7 +135,7 @@ jobs: # Download tools (containerd, runc, nerdctl, CNI plugins) # ------------------------------------------------------------------- build-captainos-kernel: - needs: [ build-dockerfile ] + needs: [ prepare, build-dockerfile ] runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} strategy: fail-fast: false @@ -133,6 +143,7 @@ jobs: arch: [ amd64, arm64 ] env: ARCH: ${{ matrix.arch }} + KERNEL_VERSION: ${{ needs.prepare.outputs.kernel_version }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -143,7 +154,7 @@ jobs: with: path: | mkosi.input/kernel/${{ matrix.arch }} - key: kernel-${{ matrix.arch }}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + key: kernel-${{ matrix.arch }}-${{ needs.prepare.outputs.kernel_version }}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time restore-keys: | kernel-${{ matrix.arch }} @@ -155,12 +166,12 @@ jobs: run: uv run captain --verbose kernel - name: Save kernel cache - if: github.ref == 'refs/heads/main' && steps.tools-cache.outputs.cache-hit != 'true' + if: github.ref == 'refs/heads/main' && steps.kernel-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v5 with: path: | mkosi.input/kernel/${{ matrix.arch }} - key: kernel-${{ matrix.arch }}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + key: kernel-${{ matrix.arch }}-${{ needs.prepare.outputs.kernel_version }}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} - name: Upload kernel artifacts uses: actions/upload-artifact@v6 @@ -174,7 +185,7 @@ jobs: build-captainos-with-kernel: # only for flavors that require the custom CaptainOS kernel; otherwise, just build-captainos name: "build-captainos (with kernel) ${{ matrix.FLAVOR_ID }} (${{ matrix.arch }})" runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [ download-tools, build-dockerfile, build-captainos-kernel ] + needs: [ prepare, download-tools, build-dockerfile, build-captainos-kernel ] strategy: fail-fast: false matrix: @@ -189,6 +200,10 @@ jobs: # Keep the initramfs/iso caches to only the freshly-built flavor hash; a # restore-key (prefix) cache hit otherwise leaves the previous hash behind. CAPTAIN_DROP_OLD_CACHES: "1" + # Latest-of-branch kernel (trixie-slim) and armbian-next repo token + # (armbian flavors), resolved once by the prepare job. + KERNEL_VERSION: ${{ needs.prepare.outputs.kernel_version }} + ARMBIAN_VERSION: ${{ needs.prepare.outputs.armbian_version }} steps: &build_steps - name: Checkout code uses: actions/checkout@v6 @@ -218,7 +233,7 @@ jobs: uses: actions/cache/restore@v5 with: path: mkosi.output/initramfs - key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--k${{ needs.prepare.outputs.kernel_version }}-a${{ needs.prepare.outputs.armbian_version }}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time restore-keys: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-- @@ -233,7 +248,7 @@ jobs: uses: actions/cache/restore@v5 with: path: mkosi.output/iso - key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--k${{ needs.prepare.outputs.kernel_version }}-a${{ needs.prepare.outputs.armbian_version }}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time restore-keys: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-- @@ -259,7 +274,7 @@ jobs: uses: actions/cache/save@v5 with: path: mkosi.output/initramfs - key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--k${{ needs.prepare.outputs.kernel_version }}-a${{ needs.prepare.outputs.armbian_version }}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} - name: Show iso cache save SHA256's if: ${{ matrix.iso }} # only if matrix entry had iso: true @@ -272,7 +287,7 @@ jobs: uses: actions/cache/save@v5 with: path: mkosi.output/iso - key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--k${{ needs.prepare.outputs.kernel_version }}-a${{ needs.prepare.outputs.armbian_version }}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} - name: Upload initramfs artifacts uses: actions/upload-artifact@v6 @@ -321,7 +336,7 @@ jobs: steps: *build_steps env: *build_env runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [ download-tools, build-dockerfile ] + needs: [ prepare, download-tools, build-dockerfile ] strategy: fail-fast: false matrix: diff --git a/captain/armbian.py b/captain/armbian.py new file mode 100644 index 00000000..046fd5c5 --- /dev/null +++ b/captain/armbian.py @@ -0,0 +1,49 @@ +"""Helpers for the Armbian-next apt repository. + +Armbian flavors pull stable meta-package names (``linux-image-edge-rockchip64`` +etc.) whose underlying kernel version changes whenever armbian-next republishes. +The repo's ``Release`` file ETag is a cheap proxy for "the repo changed": folding +it into the flavor hash makes CI rebuild (and ship the newer kernel) instead of +reusing a stale cached build. +""" + +from __future__ import annotations + +import hashlib +import logging +import urllib.error +import urllib.request + +log = logging.getLogger(__name__) + +# The `armbian` dist (where the linux-image kernel packages live). A single +# repo-wide token from this Release file applies to all armbian flavors. +ARMBIAN_NEXT_RELEASE_URL = "https://apt-test.next.armbian.com/dists/armbian/Release" + +_FETCH_TIMEOUT = 30 # seconds + + +def release_etag(url: str = ARMBIAN_NEXT_RELEASE_URL) -> str: + """Return a short, stable cache-bust token from *url*'s ETag response header. + + Returns ``""`` on any failure or when the server sends no ETag — an empty + token means "no cache-bust", preserving current behaviour. + """ + try: + req = urllib.request.Request(url) + with urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT) as resp: + etag = resp.headers.get("ETag", "") + except (urllib.error.URLError, OSError) as exc: + log.warning("Could not fetch ETag from %s (%s); no armbian cache-bust", url, exc) + return "" + + # Normalise weak-validator prefix and surrounding quotes, then hash to a + # fixed-width token that is safe to embed in filenames / cache keys. + etag = etag.strip().removeprefix("W/").strip('"') + if not etag: + log.warning("No ETag header from %s; no armbian cache-bust", url) + return "" + + token = hashlib.sha256(etag.encode()).hexdigest()[:16] + log.info("Armbian repo Release ETag %r → cache-bust token %s", etag, token) + return token diff --git a/captain/cli/_kernel.py b/captain/cli/_kernel.py index 9f8a519b..1f25dffe 100644 --- a/captain/cli/_kernel.py +++ b/captain/cli/_kernel.py @@ -4,7 +4,6 @@ import click -from captain import config from captain.cli._main import CliContext, cli from captain.cli.stages import build_kernel_stage @@ -31,13 +30,6 @@ default=False, help="Build kernel even if outputs already exist.", ) -@click.option( - "--kernel-version", - envvar="KERNEL_VERSION", - default=config.DEFAULT_KERNEL_VERSION, - show_default=True, - help="Kernel version to build. Must match an official tarball.", -) @click.option( "--config", envvar="CONFIG_KERNEL", @@ -46,14 +38,11 @@ help="Skip building and instead do interactive kernel configuration (menuconfig).", ) @click.pass_obj -def kernel_cmd( - cli_ctx: CliContext, *, kernel_mode: str, force_kernel: bool, kernel_version: str, config: bool -) -> None: +def kernel_cmd(cli_ctx: CliContext, *, kernel_mode: str, force_kernel: bool, config: bool) -> None: log.debug("CLI kernel mode: %s", kernel_mode) cfg = cli_ctx.make_config( force_kernel=force_kernel or config, - kernel_version=kernel_version, kernel_mode=kernel_mode, build_kernel=True, kernel_menuconfig=config, diff --git a/captain/cli/_main.py b/captain/cli/_main.py index c3c2212f..da16de37 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -11,7 +11,7 @@ import click from trogon import tui -from captain.config import DEFAULT_FLAVOR_ID, Config +from captain.config import DEFAULT_FLAVOR_ID, DEFAULT_KERNEL_VERSION, Config from captain.flavor import list_available_flavors from captain.util import detect_current_machine_arch @@ -35,6 +35,8 @@ class CliContext: builder_image: str verbose_docker: bool drop_old_caches: bool + kernel_version: str + armbian_version: str | None def make_config(self, **overrides: Any) -> Config: """Build a :class:`Config` from the common options plus per-command *overrides*.""" @@ -48,6 +50,8 @@ def make_config(self, **overrides: Any) -> Config: builder_image=self.builder_image, verbose_docker=self.verbose_docker, drop_old_caches=self.drop_old_caches, + kernel_version=self.kernel_version, + armbian_version=self.armbian_version, **overrides, ) @@ -157,6 +161,22 @@ def resolve_project_dir(project_dir: str | None) -> Path: "only keep the freshly-built version (env: CAPTAIN_DROP_OLD_CACHES)." ), ) +@click.option( + "--kernel-version", + envvar="KERNEL_VERSION", + default=DEFAULT_KERNEL_VERSION, + show_default=True, + help="Kernel version to build/reference. Must match an official tarball (env: KERNEL_VERSION).", +) +@click.option( + "--armbian-version", + envvar="ARMBIAN_VERSION", + default=None, + help=( + "Armbian repo version token folded into armbian flavors' hash so they rebuild " + "when armbian-next updates (env: ARMBIAN_VERSION). Empty = no cache-bust." + ), +) @click.pass_context def cli( ctx: click.Context, @@ -169,6 +189,8 @@ def cli( builder_repository: str | None, builder_image: str, drop_old_caches: bool, + kernel_version: str, + armbian_version: str | None, ) -> None: """CaptainOS build system — click CLI.""" # Configure log level based on --verbose. @@ -193,6 +215,8 @@ def cli( builder_image=builder_image, verbose_docker=verbose, drop_old_caches=drop_old_caches, + kernel_version=kernel_version, + armbian_version=armbian_version, ) @@ -209,6 +233,7 @@ def main() -> None: _builder, _iso, _kernel, + _prepare, _qemu, _release, _shell, diff --git a/captain/cli/_prepare.py b/captain/cli/_prepare.py new file mode 100644 index 00000000..6484ea1c --- /dev/null +++ b/captain/cli/_prepare.py @@ -0,0 +1,33 @@ +"""``captain prepare-versions`` — resolve dynamic version tokens for CI. + +Fetches the latest kernel.org point release of the configured branch and the +armbian-next repo Release ETag, then emits them as ``name=value`` lines on +stdout (logging goes to stderr) suitable for appending to ``$GITHUB_OUTPUT``: + + kernel_version=6.18.40 + armbian_version= +""" + +from __future__ import annotations + +import logging + +import click + +from captain import armbian, kernel +from captain.cli._main import cli + +log = logging.getLogger(__name__) + + +@cli.command( + "prepare-versions", + short_help="Emit latest kernel/armbian version tokens as name=value (for $GITHUB_OUTPUT).", +) +def prepare_versions_cmd() -> None: + """Resolve dynamic version tokens and print them to stdout.""" + kernel_version = kernel.latest_branch_version() + armbian_version = armbian.release_etag() + + click.echo(f"kernel_version={kernel_version}") + click.echo(f"armbian_version={armbian_version}") diff --git a/captain/config.py b/captain/config.py index fd35b014..690b91a3 100644 --- a/captain/config.py +++ b/captain/config.py @@ -47,6 +47,11 @@ class Config: kernel_clean: bool = False kernel_menuconfig: bool = False + # Armbian repo version token (ETag proxy) folded into armbian flavors' hash + # so they rebuild when armbian-next publishes a newer kernel. Empty/None = + # no cache-bust (default; stable hash for local builds). + armbian_version: str | None = None + # Docker builder_registry: str | None = None builder_repository: str | None = None diff --git a/captain/flavor.py b/captain/flavor.py index 0320dfd4..ce63a441 100644 --- a/captain/flavor.py +++ b/captain/flavor.py @@ -159,6 +159,15 @@ def copy_static_files( destination_path.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source_path, destination_path) + def cache_bust_token(self) -> str: + """Token rendered into mkosi.conf so a flavor's hash changes on demand. + + Base flavors return an empty string (stable hash). Flavors whose upstream + packages move under a stable name (e.g. armbian meta-packages) override + this to fold a repo version token in, forcing a rebuild when it changes. + """ + return "" + @abstractmethod def has_iso(self) -> bool: return False diff --git a/captain/flavors/common_armbian/__init__.py b/captain/flavors/common_armbian/__init__.py index 03c0d86a..29d9a8c2 100644 --- a/captain/flavors/common_armbian/__init__.py +++ b/captain/flavors/common_armbian/__init__.py @@ -23,6 +23,11 @@ def setup(self, cfg: Config, flavor_dir: Path) -> None: def has_iso(self) -> bool: return False + def cache_bust_token(self) -> str: + # Fold the armbian-next repo version token in so these flavors rebuild + # when armbian publishes a newer kernel under the same package name. + return self.cfg.armbian_version or "" + def flavor_packages(self) -> set[str]: return {"tiny-initramfs"}.union(super().flavor_packages()) diff --git a/captain/flavors/common_debian/mkosi.conf.j2 b/captain/flavors/common_debian/mkosi.conf.j2 index 5bcee990..4501935b 100644 --- a/captain/flavors/common_debian/mkosi.conf.j2 +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -1,3 +1,5 @@ +# cache-bust: {{ flavor.cache_bust_token() }} + [Distribution] Distribution=debian Release=trixie diff --git a/captain/kernel.py b/captain/kernel.py index 48b6aaff..1456f5d2 100644 --- a/captain/kernel.py +++ b/captain/kernel.py @@ -9,6 +9,7 @@ from __future__ import annotations import hashlib +import json import logging import os import re @@ -28,16 +29,71 @@ ) from captain import console -from captain.config import Config +from captain.config import DEFAULT_KERNEL_VERSION, Config from captain.util import ensure_dir, run, safe_extractall KERNEL_BUILD_BASE_PATH = "/work/kernel-build" +RELEASES_JSON_URL = "https://www.kernel.org/releases.json" + log = logging.getLogger(__name__) _DOWNLOAD_TIMEOUT = 60 # seconds +def _version_tuple(version: str) -> tuple[int, ...]: + """Parse a dotted kernel version into an int tuple for comparison.""" + parts: list[int] = [] + for piece in version.split("."): + # stop at the first non-numeric component (e.g. rc suffixes) + if not piece.isdigit(): + break + parts.append(int(piece)) + return tuple(parts) + + +def latest_branch_version(branch: str | None = None) -> str: + """Return the newest kernel.org point release of *branch* (e.g. ``"6.18"``). + + Consults kernel.org's ``releases.json``, which lists the latest release of + each active branch. *branch* defaults to the ``.`` of + :data:`DEFAULT_KERNEL_VERSION`. Falls back to ``DEFAULT_KERNEL_VERSION`` on + any network/parse failure or when the branch is not listed — never returns + an empty string, so callers/CI always get a buildable version. + """ + if branch is None: + branch = ".".join(DEFAULT_KERNEL_VERSION.split(".")[:2]) + + try: + req = urllib.request.Request(RELEASES_JSON_URL) + with urllib.request.urlopen(req, timeout=_DOWNLOAD_TIMEOUT) as resp: + data = json.loads(resp.read().decode()) + except (urllib.error.URLError, json.JSONDecodeError, OSError) as exc: + log.warning( + "Could not fetch %s (%s); using default %s", + RELEASES_JSON_URL, + exc, + DEFAULT_KERNEL_VERSION, + ) + return DEFAULT_KERNEL_VERSION + + prefix = f"{branch}." + matches = [ + r["version"] + for r in data.get("releases", []) + if isinstance(r.get("version"), str) and r["version"].startswith(prefix) + ] + if not matches: + log.warning( + "No kernel.org release for branch %s; using default %s", branch, DEFAULT_KERNEL_VERSION + ) + return DEFAULT_KERNEL_VERSION + + latest = max(matches, key=_version_tuple) + log.info("Latest kernel for branch %s is %s", branch, latest) + return latest + + def _download_with_progress(url: str, filename: Path) -> None: """Download *url* to *filename* with a Rich progress bar.""" req = urllib.request.Request(url) From 703940537f312f9b9c85f0b20da33c9c0f98ed78 Mon Sep 17 00:00:00 2001 From: Ricardo Pardini Date: Tue, 28 Jul 2026 17:46:10 +0200 Subject: [PATCH 143/143] kernel: enable CONFIG_HP_ILO=m - from Hook https://github.com/tinkerbell/hook/pull/331 --- kernel.configs/6.18.y.amd64 | 1 + kernel.configs/6.18.y.arm64 | 1 + 2 files changed, 2 insertions(+) diff --git a/kernel.configs/6.18.y.amd64 b/kernel.configs/6.18.y.amd64 index 9814e51d..68c5b6fb 100644 --- a/kernel.configs/6.18.y.amd64 +++ b/kernel.configs/6.18.y.amd64 @@ -420,6 +420,7 @@ CONFIG_BLK_DEV_RAM_SIZE=65536 CONFIG_ATA_OVER_ETH=m CONFIG_VIRTIO_BLK=y CONFIG_BLK_DEV_NVME=y +CONFIG_HP_ILO=m # CONFIG_SCSI_PROC_FS is not set CONFIG_BLK_DEV_SD=y CONFIG_BLK_DEV_SR=y diff --git a/kernel.configs/6.18.y.arm64 b/kernel.configs/6.18.y.arm64 index 0df517a5..d5291bc5 100644 --- a/kernel.configs/6.18.y.arm64 +++ b/kernel.configs/6.18.y.arm64 @@ -186,6 +186,7 @@ CONFIG_BLK_DEV_RAM=y CONFIG_BLK_DEV_RAM_SIZE=65536 CONFIG_VIRTIO_BLK=y CONFIG_BLK_DEV_NVME=y +CONFIG_HP_ILO=m CONFIG_BLK_DEV_SD=y CONFIG_CHR_DEV_SG=y CONFIG_SCSI_HPSA=y