[PATCH 05/12] workflows/linux: add controller build mode
Daniel Gomez <[email protected]> Thu, 23 Apr 2026 13:30:57 +0200
| Newsgroups | dev.linux.lists.kdevops |
|---|---|
| Message-ID | <20260423-kdevops-series-c-qemu-system-units-v1-5-b7bab3225a36@samsung.com> |
From: Daniel Gomez <[email protected]> Build modes 9p, builder, and targets all assume the kernel is consumed by a guest that runs its own bootloader or imports modules at runtime. A qsu backend booting an imageless NixOS guest has neither: it needs the kernel image as a plain file on the controller (to pass as -kernel) and the modules tree as a directory on the controller (to share into the guest over virtiofs), both produced before any VM starts. Add a fourth mode that clones, builds with O=<builddir>, and runs make install + make modules_install into a local destdir under KDEVOPS_CONTROLLER_DATA_PATH. The role skips the target- host phases (create_data_partition, reboot, post-build uname check) and ends the play as soon as the controller finishes building, since there is no target-side rollout. The workflow Makefile narrows the bootlinux limit to localhost under controller mode so gather_facts does not try to reach guests that do not exist yet. Extend the resolved build environment with LOCALVERSION set to target_linux_localversion (empty by default). With CONFIG_LOCALVERSION_AUTO=n, scripts/setlocalversion still appends a short scm marker when LOCALVERSION is unset, producing releases like 7.0.0+; an empty LOCALVERSION silences the marker and keeps the release stable across rebuilds so downstream qsu env files and module tree paths do not churn. Invalidate the rebuild cache on .config content changes: compare SHA-256 of builddir/.config against the config-<release> snapshot make install leaves next to vmlinuz, and rebuild when they differ. A release-only probe would miss =m->=y flips and other pure content edits to the seed config. Default to this mode under QEMU_SYSTEM_UNITS so a user selecting the qsu backend does not have to re-derive the build-mode decision from scratch. Generated-by: Claude AI Signed-off-by: Daniel Gomez <[email protected]> --- .../roles/bootlinux/tasks/build/controller.yml | 207 +++++++++++++++++++++ playbooks/roles/bootlinux/tasks/main.yml | 12 ++ workflows/linux/Kconfig | 55 ++++++ workflows/linux/Makefile | 7 + 4 files changed, 281 insertions(+) diff --git a/playbooks/roles/bootlinux/tasks/build/controller.yml b/playbooks/roles/bootlinux/tasks/build/controller.yml new file mode 100644 index 00000000..a3d5999f --- /dev/null +++ b/playbooks/roles/bootlinux/tasks/build/controller.yml @@ -0,0 +1,207 @@ +--- +# SPDX-License-Identifier: copyleft-next-0.3.1 +# +# Out-of-tree kernel build with make install + make modules_install +# on the Ansible controller. Leaves a vmlinuz at +# <destdir>/boot/vmlinuz-<release> and a modules tree under +# <destdir>/lib/modules/<release>/ that a downstream bringup +# backend consumes in place (for example through -kernel and a +# virtiofs share of the modules directory). +# +# Idempotency strategy: always clone, copy the .config, and run +# olddefconfig so the build tree reflects the intended config, +# then ask make what the kernel release for that config would be +# and check whether the matching vmlinuz + modules tree already +# live under destdir. If they do, skip make + make install + make +# modules_install; otherwise run the full pipeline. This way a +# seed .config swap (for example CONFIG_LOCALVERSION_AUTO flip, a +# new CONFIG_LOCALVERSION, a new git HEAD) invalidates the cache +# on its own, without asking the operator to rm destdir first. + +- name: Compose the controller-mode build environment + ansible.builtin.set_fact: + bootlinux_controller_build_environment: "{{ bootlinux_build_environment | combine({'LOCALVERSION': target_linux_localversion | default('', true)}) }}" + run_once: true + delegate_to: localhost + +- name: Install dependencies to build the Linux kernel + delegate_to: localhost + run_once: true + ansible.builtin.import_tasks: + file: install-deps/main.yml + +- name: Ensure the controller build and install directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ bootlinux_controller_builddir }}" + - "{{ bootlinux_controller_destdir }}" + run_once: true + delegate_to: localhost + +- name: Git clone {{ target_linux_tree }} on the control node + ansible.builtin.git: + repo: "{{ target_linux_git }}" + dest: "{{ bootlinux_controller_tree_path }}" + version: "{{ target_linux_ref }}" + depth: "{{ target_linux_shallow_depth | default(omit) }}" + force: "{{ target_linux_force_checkout | default(false) }}" + run_once: true + delegate_to: localhost + tags: ["clone"] + +- name: Copy configuration for {{ target_linux_tree }} to the build directory + ansible.builtin.copy: + src: "{{ role_path }}/templates/{{ linux_config }}" + dest: "{{ bootlinux_controller_builddir }}/.config" + mode: '0644' + run_once: true + delegate_to: localhost + +- name: Resolve .config against the tree with olddefconfig + community.general.make: + chdir: "{{ bootlinux_controller_tree_path }}" + target: olddefconfig + params: + O: "{{ bootlinux_controller_builddir }}" + environment: "{{ bootlinux_controller_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_controller_tree_path }}" + target: syncconfig + params: + O: "{{ bootlinux_controller_builddir }}" + environment: "{{ bootlinux_controller_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_controller_tree_path }}" + target: kernelrelease + params: + O: "{{ bootlinux_controller_builddir }}" + environment: "{{ bootlinux_controller_build_environment }}" + register: bootlinux_controller_kernelrelease_make + changed_when: false + run_once: true + delegate_to: localhost + +- name: Record the expected kernel release + ansible.builtin.set_fact: + bootlinux_controller_kernel_release: "{{ bootlinux_controller_kernelrelease_make.stdout_lines[-1] }}" + run_once: true + delegate_to: localhost + +- name: Probe destdir for a kernel image matching the expected release + ansible.builtin.stat: + path: "{{ bootlinux_controller_destdir }}/boot/vmlinuz-{{ bootlinux_controller_kernel_release }}" + register: bootlinux_controller_vmlinuz_stat + run_once: true + delegate_to: localhost + +- name: Probe destdir for the modules tree matching the expected release + ansible.builtin.stat: + path: "{{ bootlinux_controller_destdir }}/lib/modules/{{ bootlinux_controller_kernel_release }}/modules.dep" + register: bootlinux_controller_modules_dep + run_once: true + delegate_to: localhost + +- name: Checksum the resolved build .config + ansible.builtin.stat: + path: "{{ bootlinux_controller_builddir }}/.config" + checksum_algorithm: sha256 + register: bootlinux_controller_builddir_config_stat + run_once: true + delegate_to: localhost + +- name: Checksum the installed config-<release> snapshot + ansible.builtin.stat: + path: "{{ bootlinux_controller_destdir }}/boot/config-{{ bootlinux_controller_kernel_release }}" + checksum_algorithm: sha256 + register: bootlinux_controller_destdir_config_stat + run_once: true + delegate_to: localhost + +- name: Flag the controller build as already cached + ansible.builtin.set_fact: + bootlinux_controller_already_built: >- + {{ bootlinux_controller_vmlinuz_stat.stat.exists + and bootlinux_controller_modules_dep.stat.exists + and bootlinux_controller_destdir_config_stat.stat.exists + and bootlinux_controller_builddir_config_stat.stat.checksum == + bootlinux_controller_destdir_config_stat.stat.checksum }} + run_once: true + delegate_to: localhost + +- name: Report the cached controller build we will reuse + ansible.builtin.debug: + msg: "Reusing existing controller build: vmlinuz-{{ bootlinux_controller_kernel_release }}" + when: + - bootlinux_controller_already_built | bool + run_once: true + delegate_to: localhost + +- name: Get nproc on the control node + ansible.builtin.command: nproc + register: nproc_controller + changed_when: false + run_once: true + delegate_to: localhost + tags: ["build-linux"] + when: + - not bootlinux_controller_already_built | bool + +- name: Build {{ target_linux_tree }} on the control node using {{ nproc_controller.stdout | default('nproc') }} threads + community.general.make: + jobs: "{{ nproc_controller.stdout }}" + chdir: "{{ bootlinux_controller_tree_path }}" + params: "{{ bootlinux_make_params | combine({'O': bootlinux_controller_builddir}) }}" + environment: "{{ bootlinux_controller_build_environment }}" + run_once: true + delegate_to: localhost + tags: ["build-linux"] + when: + - not bootlinux_controller_already_built | bool + +- name: Install the kernel image into <destdir>/boot + community.general.make: + chdir: "{{ bootlinux_controller_tree_path }}" + target: install + params: + O: "{{ bootlinux_controller_builddir }}" + INSTALL_PATH: "{{ bootlinux_controller_destdir }}/boot" + environment: "{{ bootlinux_controller_build_environment }}" + run_once: true + delegate_to: localhost + tags: ["install-linux"] + when: + - not bootlinux_controller_already_built | bool + +- name: Install kernel modules into <destdir>/lib/modules + community.general.make: + chdir: "{{ bootlinux_controller_tree_path }}" + target: modules_install + params: + O: "{{ bootlinux_controller_builddir }}" + INSTALL_MOD_PATH: "{{ bootlinux_controller_destdir }}" + environment: "{{ bootlinux_controller_build_environment }}" + run_once: true + delegate_to: localhost + tags: ["install-linux"] + when: + - not bootlinux_controller_already_built | bool + +- name: Publish controller-install facts for downstream roles + ansible.builtin.set_fact: + bootlinux_controller_kernel_image: "{{ bootlinux_controller_destdir }}/boot/vmlinuz-{{ bootlinux_controller_kernel_release }}" + bootlinux_controller_modules_dir: "{{ bootlinux_controller_destdir }}/lib/modules/{{ bootlinux_controller_kernel_release }}" + run_once: true + delegate_to: localhost diff --git a/playbooks/roles/bootlinux/tasks/main.yml b/playbooks/roles/bootlinux/tasks/main.yml index da42612e..7700fe8e 100644 --- a/playbooks/roles/bootlinux/tasks/main.yml +++ b/playbooks/roles/bootlinux/tasks/main.yml @@ -194,6 +194,7 @@ name: create_data_partition when: - not workflow_linux_packaged|bool + - not bootlinux_controller|default(false)|bool - name: Mount bootlinux 9p on each target node become: true @@ -243,6 +244,17 @@ when: - bootlinux_builder|bool +- name: Build the Linux kernel on the controller with destdir install + ansible.builtin.include_tasks: + file: "{{ role_path }}/tasks/build/controller.yml" + when: + - bootlinux_controller|bool + +- name: End the play after controller mode has built and installed to destdir + ansible.builtin.meta: end_play + when: + - bootlinux_controller|bool + - name: Run uname before ansible.builtin.command: "uname -r" register: uname_cmd_before diff --git a/workflows/linux/Kconfig b/workflows/linux/Kconfig index b86e73d1..54910203 100644 --- a/workflows/linux/Kconfig +++ b/workflows/linux/Kconfig @@ -36,6 +36,7 @@ endif # HAVE_SUPPORTS_PURE_IOMAP choice prompt "Kernel build location" + default BOOTLINUX_CONTROLLER if QEMU_SYSTEM_UNITS default BOOTLINUX_TARGETS if !LIBVIRT || GUESTFS_LACKS_9P default BOOTLINUX_9P if LIBVIRT && !GUESTFS_LACKS_9P help @@ -91,6 +92,25 @@ config BOOTLINUX_BUILDER This choice is best when the test runners are resource- limited or vastly different than the controller host. +config BOOTLINUX_CONTROLLER + bool "Controller node (destdir install)" + output yaml + help + Choosing this option clones and builds the test kernel on + the Ansible controller node using an out-of-tree build + directory, then runs make install and make modules_install + into a local destdir on the controller. The kernel image + and modules tree are not copied to any target node; a + bringup backend that boots with an external kernel (for + example qemu-system-units with the nixos-qemu imageless + module) consumes them in place, typically by passing the + kernel image as -kernel and sharing the modules tree with + the guest over virtiofs. + + This choice is best when the target node runs with an + external kernel supplied by the controller rather than an + in-guest bootloader. + endchoice if BOOTLINUX_9P @@ -151,6 +171,41 @@ endmenu endif # BOOTLINUX_9P +if BOOTLINUX_CONTROLLER + +config BOOTLINUX_CONTROLLER_TREE_PATH + string "Linux source tree path on the controller" + output yaml + default "{{ kdevops_controller_data_path }}/linux" + help + Absolute path on the control node where the Linux source + tree is cloned and made available to make. Defaults to a + linux/ subdirectory of KDEVOPS_CONTROLLER_DATA_PATH so it + sits alongside the other controller-side artefacts + kdevops produces (build/, destdir/, and future per-workflow + scratch directories). + +config BOOTLINUX_CONTROLLER_BUILDDIR + string "Out-of-tree build directory" + output yaml + default "{{ kdevops_controller_data_path }}/linux-build" + help + Absolute path to the kernel out-of-tree build directory, + passed to make as O=. Defaults to a sibling of the linux + source tree under KDEVOPS_CONTROLLER_DATA_PATH. + +config BOOTLINUX_CONTROLLER_DESTDIR + string "Install destdir" + output yaml + default "{{ kdevops_controller_data_path }}/linux-destdir" + help + Absolute path where make install and make modules_install + place the kernel image and modules tree. Consumers locate + the kernel at <destdir>/boot/vmlinuz-<release> and the + matching module tree at <destdir>/lib/modules/<release>. + +endif # BOOTLINUX_CONTROLLER + choice prompt "Compiler to use to build the kernel" default BOOTLINUX_COMPILER_GCC diff --git a/workflows/linux/Makefile b/workflows/linux/Makefile index a6b8f2ea..6198af01 100644 --- a/workflows/linux/Makefile +++ b/workflows/linux/Makefile @@ -79,6 +79,13 @@ ifeq (y,$(CONFIG_KDEVOPS_SETUP_NFSD)) BOOTLINUX_LIMIT := baseline:dev:nfsd endif +# Controller mode builds and installs on the control node before any +# target VM exists, so it must run against localhost instead of reaching +# baseline:dev guests that have no sshd yet. +ifeq (y,$(CONFIG_BOOTLINUX_CONTROLLER)) +BOOTLINUX_LIMIT := localhost +endif + PHONY += linux-help-menu linux-help-menu: @echo "Linux git kernel development options" -- 2.53.0