[PATCH v2 03/10] bootlinux: add direct-boot build path
Daniel Gomez <[email protected]> Fri, 12 Jun 2026 12:55:15 +0200
| Newsgroups | dev.linux.lists.kdevops |
|---|---|
| Message-ID | <[email protected]> |
From: Daniel Gomez <[email protected]> Build the kernel out-of-tree on the controller into a destdir and stage the resulting image plus modules for backends that boot via QEMU's -kernel argument. Lets a guest run a kdevops-built kernel without an in-guest distro toolchain. The image is copied straight into the destdir rather than via `make install`: the kernel's scripts/install.sh searches for an installkernel hook and on most distros finds /sbin/installkernel, which rebuilds the initramfs and runs depmod -- side effects the controller does not want. Generated-by: Claude AI Signed-off-by: Daniel Gomez <[email protected]> --- .gitignore | 5 + MAINTAINERS | 8 + kconfigs/Kconfig.kdevops | 15 ++ playbooks/bootlinux.yml | 25 +++ .../roles/bootlinux/tasks/build/direct-boot.yml | 207 +++++++++++++++++++++ playbooks/roles/bootlinux/tasks/config.yml | 1 + workflows/linux/Kconfig | 69 +++++++ workflows/linux/Makefile | 12 +- 8 files changed, 341 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 89c2898e..396872d4 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,11 @@ archive/ # NixOS generated files nixos/generated/ +# Controller-side data path (KDEVOPS_CONTROLLER_DATA_PATH default). +# Local kernel checkouts, out-of-tree build trees, install destdirs, +# workflow scratch space. +data/ + # Dyanmic cloud kconfig files terraform/datacrunch/kconfigs/Kconfig.compute.generated terraform/datacrunch/kconfigs/Kconfig.images.generated diff --git a/MAINTAINERS b/MAINTAINERS index 65bf5716..5b9a1f23 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -79,6 +79,14 @@ F: playbooks/roles/ansible_cfg F: scripts/ansible_av.py F: scripts/ansible.Makefile +BOOTLINUX DIRECT BOOT +M: Daniel Gomez <[email protected]> +L: [email protected] +S: Maintained +T: git https://github.com/linux-kdevops/kdevops.git +F: playbooks/roles/bootlinux/tasks/build/direct-boot.yml +N: BOOTLINUX_DIRECT_BOOT + KDEVOPS CI M: Daniel Gomez <[email protected]> L: [email protected] diff --git a/kconfigs/Kconfig.kdevops b/kconfigs/Kconfig.kdevops index 4872fdc3..fee93f68 100644 --- a/kconfigs/Kconfig.kdevops +++ b/kconfigs/Kconfig.kdevops @@ -78,6 +78,21 @@ config KDEVOPS_CUSTOM_SSH_KEXALGORITHMS Some distributions, such as older distributions, may require a custom ssh configuration entry for the KexAlgorithms parameter. +config KDEVOPS_CONTROLLER_DATA_PATH + string "Controller-side data path" + output yaml + default "$(TOPDIR_PATH)/data" + help + Absolute path on the control node where kdevops places + controller-side artefacts: source trees for local builds + (bootlinux controller mode clones Linux under here), + out-of-tree build directories, install destdirs, and + workflow scratch space. This is the controller-side parallel + of data_path, which is the target-guest view of the same + concept. Defaults to a data/ sibling of the kdevops checkout + so kdevops does not assume a distro-wide /data mount on the + control host. + config KDEVOPS_MAKE_VERBOSE bool "Enable verbose make output (V=1)" default n diff --git a/playbooks/bootlinux.yml b/playbooks/bootlinux.yml index b2844e4d..35d53882 100644 --- a/playbooks/bootlinux.yml +++ b/playbooks/bootlinux.yml @@ -1,5 +1,30 @@ --- # SPDX-License-Identifier: copyleft-next-0.3.1 +- name: Bootlinux — controller-side preparation + hosts: localhost + connection: local + gather_facts: true + tasks: + - name: Import optional extra_args file + ansible.builtin.include_vars: "{{ item }}" + failed_when: false + with_first_found: + - files: + - "../extra_vars.yml" + - "../extra_vars.yaml" + - "../extra_vars.json" + skip: true + tags: [always] + + - name: Build the kernel on the controller into a destdir + ansible.builtin.include_role: + name: bootlinux + tasks_from: build/direct-boot.yml + apply: + tags: [bootlinux_direct_boot_build] + tags: [never, bootlinux_direct_boot_build] + when: bootlinux_direct_boot | default(false) | bool + - name: Bootlinux hosts: all roles: diff --git a/playbooks/roles/bootlinux/tasks/build/direct-boot.yml b/playbooks/roles/bootlinux/tasks/build/direct-boot.yml new file mode 100644 index 00000000..39263570 --- /dev/null +++ b/playbooks/roles/bootlinux/tasks/build/direct-boot.yml @@ -0,0 +1,207 @@ +--- +# SPDX-License-Identifier: copyleft-next-0.3.1 +# +# Out-of-tree kernel build into a controller-side destdir for QEMU's +# -kernel direct boot path. See linuxboot.rst in QEMU's tree. + +- name: Compose the direct-boot build environment + ansible.builtin.set_fact: + bootlinux_direct_boot_build_environment: "{{ bootlinux_build_environment | combine({'LOCALVERSION': target_linux_localversion | default('', true)}) }}" + run_once: true + delegate_to: localhost + tags: [always] + +- name: Install kernel build dependencies on the controller + ansible.builtin.import_tasks: "{{ role_path }}/tasks/install-deps/main.yml" + run_once: true + delegate_to: localhost + +- name: Wipe the controller build directory when a clean build was requested + ansible.builtin.file: + path: "{{ bootlinux_direct_boot_builddir }}" + state: absent + when: + - bootlinux_clean_before_build | default(false) | bool + run_once: true + delegate_to: localhost + tags: [build-linux] + +- name: Ensure the controller build and install directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ bootlinux_direct_boot_builddir }}" + - "{{ bootlinux_direct_boot_destdir }}" + - "{{ bootlinux_direct_boot_destdir }}/boot" + run_once: true + delegate_to: localhost + tags: [always] + +# If the tree exists (manual clone, worktree, symlink, prior bringup), +# build against its current HEAD. No fetch, no checkout. +- name: Detect a pre-existing kernel tree under bootlinux_direct_boot_tree_path + ansible.builtin.stat: + path: "{{ bootlinux_direct_boot_tree_path }}/.git" + register: bootlinux_direct_boot_tree_git + run_once: true + delegate_to: localhost + tags: [clone, always] + +- name: Note that the controller tree is user-managed (skip clone/checkout) + ansible.builtin.debug: + msg: | + {{ bootlinux_direct_boot_tree_path }} already contains a git + checkout. kdevops will not clone, fetch, or checkout — building + against whatever HEAD is currently in that tree. + when: + - bootlinux_direct_boot_tree_git.stat.exists | default(false) | bool + run_once: true + delegate_to: localhost + tags: [clone, always] + +- name: Git clone kernel on the controller + ansible.builtin.git: + repo: "{{ target_linux_git }}" + dest: "{{ bootlinux_direct_boot_tree_path }}" + version: "{{ target_linux_ref }}" + depth: "{{ target_linux_shallow_depth | default(omit) }}" + when: + - not bootlinux_direct_boot_tree_git.stat.exists | default(false) | bool + run_once: true + delegate_to: localhost + tags: [clone] + +- name: Select the .config for the controller build + ansible.builtin.include_tasks: "{{ role_path }}/tasks/config.yml" + tags: [build-linux] + +- name: Stage the resolved .config into the out-of-tree build directory + ansible.builtin.copy: + src: "{{ role_path }}/templates/{{ linux_config }}" + dest: "{{ bootlinux_direct_boot_builddir }}/.config" + mode: "0644" + force: false + run_once: true + delegate_to: localhost + tags: [build-linux] + +- name: Resolve .config against the tree with olddefconfig + community.general.make: + chdir: "{{ bootlinux_direct_boot_tree_path }}" + target: olddefconfig + params: + O: "{{ bootlinux_direct_boot_builddir }}" + environment: "{{ bootlinux_direct_boot_build_environment }}" + run_once: true + delegate_to: localhost + tags: [build-linux] + +- name: Sync include/config/auto.conf to the refreshed .config + community.general.make: + chdir: "{{ bootlinux_direct_boot_tree_path }}" + target: syncconfig + params: + O: "{{ bootlinux_direct_boot_builddir }}" + environment: "{{ bootlinux_direct_boot_build_environment }}" + run_once: true + delegate_to: localhost + tags: [build-linux] + +- name: Compute the expected kernel release from the resolved build tree + community.general.make: + chdir: "{{ bootlinux_direct_boot_tree_path }}" + target: kernelrelease + params: + O: "{{ bootlinux_direct_boot_builddir }}" + environment: "{{ bootlinux_direct_boot_build_environment }}" + register: bootlinux_direct_boot_kernelrelease_make + changed_when: false + run_once: true + delegate_to: localhost + tags: [build-linux] + +- name: Record the expected kernel release + ansible.builtin.set_fact: + bootlinux_direct_boot_kernel_release: "{{ bootlinux_direct_boot_kernelrelease_make.stdout_lines[-1] }}" + run_once: true + delegate_to: localhost + tags: [build-linux] + +- name: Get nproc on the controller + ansible.builtin.command: nproc + register: bootlinux_direct_boot_nproc + changed_when: false + run_once: true + delegate_to: localhost + tags: [build-linux] + +- name: Build kernel on the controller + community.general.make: + jobs: "{{ bootlinux_direct_boot_nproc.stdout }}" + chdir: "{{ bootlinux_direct_boot_tree_path }}" + params: "{{ bootlinux_make_params | combine({'O': bootlinux_direct_boot_builddir}) }}" + environment: "{{ bootlinux_direct_boot_build_environment }}" + run_once: true + delegate_to: localhost + tags: [build-linux] + +# `make install` runs /sbin/installkernel against the controller's +# /boot and may need root. Copy the image into the destdir by hand. +- name: Resolve the built kernel image path + community.general.make: + chdir: "{{ bootlinux_direct_boot_tree_path }}" + target: image_name + params: + O: "{{ bootlinux_direct_boot_builddir }}" + environment: "{{ bootlinux_direct_boot_build_environment }}" + register: bootlinux_direct_boot_image_name_make + changed_when: false + run_once: true + delegate_to: localhost + tags: [install-linux] + +- name: Install the kernel image into <destdir>/boot + ansible.builtin.copy: + src: "{{ bootlinux_direct_boot_builddir }}/{{ bootlinux_direct_boot_image_name_make.stdout_lines[-1] | trim }}" + dest: "{{ bootlinux_direct_boot_destdir }}/boot/vmlinuz-{{ bootlinux_direct_boot_kernel_release }}" + mode: "0644" + remote_src: true + run_once: true + delegate_to: localhost + tags: [install-linux] + +- name: Install kernel modules into <destdir>/lib/modules + community.general.make: + chdir: "{{ bootlinux_direct_boot_tree_path }}" + target: modules_install + params: + O: "{{ bootlinux_direct_boot_builddir }}" + INSTALL_MOD_PATH: "{{ bootlinux_direct_boot_destdir }}" + environment: "{{ bootlinux_direct_boot_build_environment }}" + run_once: true + delegate_to: localhost + tags: [install-linux] + +# modules_install creates build/ but not source/. Tools that chase +# include/linux/kconfig.h (bpftrace, perf probe, libbpf-tools, any +# kbuild module build) need source/. Dangling if the tree isn't +# reachable from the guest is no worse than the prior state. +- name: Add canonical "source" symlink alongside "build" in <destdir>/lib/modules/<ver>/ + ansible.builtin.file: + src: "{{ bootlinux_direct_boot_tree_path }}" + dest: "{{ bootlinux_direct_boot_destdir }}/lib/modules/{{ bootlinux_direct_boot_kernel_release }}/source" + state: link + force: true + run_once: true + delegate_to: localhost + tags: [install-linux] + +- name: Publish controller-install facts for downstream roles + ansible.builtin.set_fact: + bootlinux_direct_boot_kernel_image: "{{ bootlinux_direct_boot_destdir }}/boot/vmlinuz-{{ bootlinux_direct_boot_kernel_release }}" + bootlinux_direct_boot_modules_dir: "{{ bootlinux_direct_boot_destdir }}/lib/modules/{{ bootlinux_direct_boot_kernel_release }}" + run_once: true + delegate_to: localhost + tags: [install-linux] diff --git a/playbooks/roles/bootlinux/tasks/config.yml b/playbooks/roles/bootlinux/tasks/config.yml index 6d892744..a54db514 100644 --- a/playbooks/roles/bootlinux/tasks/config.yml +++ b/playbooks/roles/bootlinux/tasks/config.yml @@ -39,6 +39,7 @@ when: - bootlinux_clean_before_build|default(false)|bool - not bootlinux_9p|bool + - not bootlinux_direct_boot|default(false)|bool - name: Set the .config file for building the test kernel ansible.builtin.set_fact: diff --git a/workflows/linux/Kconfig b/workflows/linux/Kconfig index 51dcb0f0..4f8694d9 100644 --- a/workflows/linux/Kconfig +++ b/workflows/linux/Kconfig @@ -92,8 +92,77 @@ config BOOTLINUX_BUILDER This choice is best when the test runners are resource- limited or vastly different than the controller host. +config BOOTLINUX_DIRECT_BOOT + bool "Direct kernel boot (destdir, no in-guest install)" + output yaml + help + Choosing this option builds the test kernel out-of-tree on + the Ansible controller and installs the image plus modules + into a local destdir. Nothing is copied onto any guest + filesystem; a consumer feeds the kernel to QEMU via the + -kernel direct boot path (QEMU's term, see + docs/system/linuxboot.rst upstream) and virtiofs-shares + the modules tree at boot. + + The wired consumer today is kdevops's nixosfi backend (imageless NixOS): its + systemd template units run each guest as a systemd-machined + machine and invoke qemu-system-* with -kernel pointing at + this destdir. + Pairs with the nixos-flake backends.imageless module, + which describes a NixOS closure that boots without a disk + image and expects the kernel to arrive via QEMU's direct + boot path. + + No playbook-side cache: Make's source-level dependency + tracking is the cache. A no-op rebuild walks the dep graph + and exits in seconds; a real source change rebuilds only + what actually changed. + endchoice +if BOOTLINUX_DIRECT_BOOT + +config BOOTLINUX_DIRECT_BOOT_TREE_PATH + string "Linux source tree path on the controller" + output yaml + default "{{ kdevops_controller_data_path }}/linux" + help + Absolute path on the controller where the Linux source + tree lives. Defaults to a linux/ subdirectory of + KDEVOPS_CONTROLLER_DATA_PATH so it sits alongside the + other controller-side artefacts kdevops produces. + + kdevops manages the tree only when this path is empty: the + initial bringup clones the configured tree at the configured + ref into the path, then leaves it alone. If the path + already contains a git checkout — manually cloned, a git + worktree, a symlink to another checkout, or a previous + kdevops bringup — kdevops detects it and skips clone, + fetch, and checkout entirely. + +config BOOTLINUX_DIRECT_BOOT_BUILDDIR + string "Out-of-tree controller-side kernel build directory" + output yaml + default "{{ kdevops_controller_data_path }}/linux-build" + help + Absolute path on the controller for the out-of-tree kernel + build (passed to make as O=...). Kept separate from + BOOTLINUX_DIRECT_BOOT_TREE_PATH so the source tree stays clean. + +config BOOTLINUX_DIRECT_BOOT_DESTDIR + string "Controller-side kernel install destdir" + output yaml + default "{{ kdevops_controller_data_path }}/linux-destdir" + help + Absolute path on the controller that receives the kernel + image (under boot/) and modules tree (under lib/modules/) + from make install + make modules_install. The nixosfi backend reads + vmlinuz-* from boot/ for its QEMU -kernel argv and + virtiofs-shares lib/modules into the imageless nixos-flake + closure at runtime. + +endif # BOOTLINUX_DIRECT_BOOT + if BOOTLINUX_9P menu "Modify default 9p configuration" diff --git a/workflows/linux/Makefile b/workflows/linux/Makefile index a6b8f2ea..38167634 100644 --- a/workflows/linux/Makefile +++ b/workflows/linux/Makefile @@ -106,7 +106,9 @@ linux-help-end: LINUX_HELP_EXTRA := PHONY += linux -ifeq (y,$(CONFIG_KDEVOPS_BASELINE_AND_DEV)) +ifeq (y,$(CONFIG_BOOTLINUX_DIRECT_BOOT)) +linux: linux-direct-boot +else ifeq (y,$(CONFIG_KDEVOPS_BASELINE_AND_DEV)) ifeq (y,$(CONFIG_BOOTLINUX_AB_DIFFERENT_REF)) linux: linux-baseline linux-dev else @@ -158,6 +160,14 @@ linux-mount: --tags vars,9p_mount \ --extra-vars="$(BOOTLINUX_ARGS)" $(LIMIT_HOSTS) +PHONY += linux-direct-boot +linux-direct-boot: + $(Q)ansible-playbook \ + --connection=local --inventory localhost, \ + $(KDEVOPS_PLAYBOOKS_DIR)/bootlinux.yml \ + --tags bootlinux_direct_boot_build \ + --extra-vars="$(BOOTLINUX_ARGS)" + PHONY += linux-deploy linux-deploy: $(Q)ansible-playbook \ -- 2.54.0