[PATCH v2 07/10] nixos_flake: per-package source overrides

Daniel Gomez <[email protected]> Fri, 12 Jun 2026 12:55:19 +0200
Newsgroups dev.linux.lists.kdevops
Message-ID <[email protected]>
From: Daniel Gomez <[email protected]>

Each per-VM flake can rebuild fio, xfstests, xfsprogs, libbpf,
libbpf-tools, damo, nfstest, pynfs, xnvme, or cpupower from a
user-supplied source instead of the bundled nixos-flake overlay.
Toggling NIXOS_FLAKE_OVERRIDE_<PKG>=y plus its _SRC (and optional
_REF) routes a path: or git: flake input into a default.nix overlay
that overrideAttrs the package. Values interpolated into Nix strings
are escaped for backslash, quote, and antiquotation so an
operator-supplied path or ref cannot break out of the double-quoted
context.

The OVERRIDE_* knobs live in the shared nixos_flake library menu, and
both backends carry the input block and the overlay in their own
flake.nix and default.nix templates. The nixosfi backend's
refresh-source-overrides.yml updates the active inputs before the
closure build; its imageless build and rebuild-test paths import it.
Has no effect when every override is off.

Generated-by: Claude AI
Signed-off-by: Daniel Gomez <[email protected]>
---
 docs/nixos-flake-backends.md                       |  11 ++
 kconfigs/Kconfig.nixos_flake                       | 193 +++++++++++++++++++++
 playbooks/roles/nixosfi/tasks/imageless-build.yml  |   5 +
 playbooks/roles/nixosfi/tasks/rebuild-test.yml     |   9 +
 .../nixosfi/tasks/refresh-source-overrides.yml     |  41 +++++
 playbooks/roles/nixosfi/templates/default.nix.j2   |  31 ++++
 playbooks/roles/nixosfi/templates/flake.nix.j2     |  38 +++-
 playbooks/roles/nixosfl/templates/default.nix.j2   |  31 ++++
 playbooks/roles/nixosfl/templates/flake.nix.j2     |  38 +++-
 9 files changed, 395 insertions(+), 2 deletions(-)

diff --git a/docs/nixos-flake-backends.md b/docs/nixos-flake-backends.md
index a679a91d..c5008a66 100644
--- a/docs/nixos-flake-backends.md
+++ b/docs/nixos-flake-backends.md
@@ -33,3 +33,14 @@ monitors. A nixos-flake guest is built declaratively, so once it boots
 there is no per-guest Ansible step left for kdevops to run; wiring those
 workflows needs a controller/guest setup separation that is not in place
 yet.
+
+## Source overrides
+
+Any guest can rebuild a tracked package — fio, xfstests, xfsprogs, libbpf,
+libbpf-tools, damo, nfstest, pynfs, xnvme, or cpupower — from your own
+source instead of the bundled overlay. Set `NIXOS_FLAKE_OVERRIDE_<PKG>=y`
+plus `_SRC` (a local path or a git URL) and an optional `_REF`; kdevops
+routes it in as a `<pkg>-src` flake input that a `default.nix` overlay
+`overrideAttrs` consumes. It applies to both backends and has no effect
+when every override is off. After changing a `_SRC`/`_REF`, refresh the
+pinned input with `nix flake update --update-input <pkg>-src`.
diff --git a/kconfigs/Kconfig.nixos_flake b/kconfigs/Kconfig.nixos_flake
index 7d1f867e..1c1df1e7 100644
--- a/kconfigs/Kconfig.nixos_flake
+++ b/kconfigs/Kconfig.nixos_flake
@@ -169,6 +169,199 @@ config NIXOS_FLAKE_TESTSUITE_SYSBENCH
 
 endmenu  # Test suites
 
+menu "Source overrides"
+
+# Per-package source overrides. Each <PKG> exposes a bool gate;
+# enabling it surfaces SRC (local path or git URL) and REF
+# (branch/tag/commit). When enabled and SRC is non-empty the
+# closure rebuilds the package from that source instead of the
+# nixpkgs / nixos-flake-overlay default. REF is ignored for
+# local paths. All bools off is a no-op.
+
+config NIXOS_FLAKE_OVERRIDE_FIO
+	bool "Override fio source"
+	output yaml
+	default n
+	help
+	  Rebuild fio from a user-supplied tree instead of the
+	  nixos-flake fio overlay (nixpkgs fio + liburing patches).
+if NIXOS_FLAKE_OVERRIDE_FIO
+config NIXOS_FLAKE_OVERRIDE_FIO_SRC
+	string "fio source (local path or git URL)"
+	output yaml
+	default ""
+config NIXOS_FLAKE_OVERRIDE_FIO_REF
+	string "fio ref"
+	output yaml
+	default ""
+endif # NIXOS_FLAKE_OVERRIDE_FIO
+
+config NIXOS_FLAKE_OVERRIDE_XFSTESTS
+	bool "Override xfstests source"
+	output yaml
+	default n
+	help
+	  Rebuild xfstests from a user-supplied tree instead of the
+	  nixos-flake xfstests overlay (xfstests 2026.03.20 with
+	  gdbm/liburing/file_attr patches).
+if NIXOS_FLAKE_OVERRIDE_XFSTESTS
+config NIXOS_FLAKE_OVERRIDE_XFSTESTS_SRC
+	string "xfstests source (local path or git URL)"
+	output yaml
+	default ""
+config NIXOS_FLAKE_OVERRIDE_XFSTESTS_REF
+	string "xfstests ref"
+	output yaml
+	default ""
+endif # NIXOS_FLAKE_OVERRIDE_XFSTESTS
+
+config NIXOS_FLAKE_OVERRIDE_XFSPROGS
+	bool "Override xfsprogs source"
+	output yaml
+	default n
+	help
+	  Rebuild xfsprogs from a user-supplied tree instead of the
+	  nixpkgs default. Use this to pick up xfs_healer or other
+	  for-next changes ahead of nixpkgs.
+if NIXOS_FLAKE_OVERRIDE_XFSPROGS
+config NIXOS_FLAKE_OVERRIDE_XFSPROGS_SRC
+	string "xfsprogs source (local path or git URL)"
+	output yaml
+	default ""
+config NIXOS_FLAKE_OVERRIDE_XFSPROGS_REF
+	string "xfsprogs ref"
+	output yaml
+	default ""
+endif # NIXOS_FLAKE_OVERRIDE_XFSPROGS
+
+config NIXOS_FLAKE_OVERRIDE_LIBBPF
+	bool "Override libbpf source"
+	output yaml
+	default n
+	help
+	  Rebuild libbpf from a user-supplied tree instead of the
+	  nixpkgs default.
+if NIXOS_FLAKE_OVERRIDE_LIBBPF
+config NIXOS_FLAKE_OVERRIDE_LIBBPF_SRC
+	string "libbpf source (local path or git URL)"
+	output yaml
+	default ""
+config NIXOS_FLAKE_OVERRIDE_LIBBPF_REF
+	string "libbpf ref"
+	output yaml
+	default ""
+endif # NIXOS_FLAKE_OVERRIDE_LIBBPF
+
+config NIXOS_FLAKE_OVERRIDE_LIBBPF_TOOLS
+	bool "Override libbpf-tools source"
+	output yaml
+	default n
+	help
+	  Rebuild libbpf-tools from a user-supplied BCC tree instead
+	  of the release pinned by nixos-flake/pkgs/libbpf-tools.nix.
+if NIXOS_FLAKE_OVERRIDE_LIBBPF_TOOLS
+config NIXOS_FLAKE_OVERRIDE_LIBBPF_TOOLS_SRC
+	string "libbpf-tools source (local path or BCC git URL)"
+	output yaml
+	default ""
+config NIXOS_FLAKE_OVERRIDE_LIBBPF_TOOLS_REF
+	string "libbpf-tools ref"
+	output yaml
+	default ""
+endif # NIXOS_FLAKE_OVERRIDE_LIBBPF_TOOLS
+
+config NIXOS_FLAKE_OVERRIDE_DAMO
+	bool "Override damo source"
+	output yaml
+	default n
+	help
+	  Rebuild damo from a user-supplied tree instead of
+	  nixos-flake/pkgs/damo.nix.
+if NIXOS_FLAKE_OVERRIDE_DAMO
+config NIXOS_FLAKE_OVERRIDE_DAMO_SRC
+	string "damo source (local path or git URL)"
+	output yaml
+	default ""
+config NIXOS_FLAKE_OVERRIDE_DAMO_REF
+	string "damo ref"
+	output yaml
+	default ""
+endif # NIXOS_FLAKE_OVERRIDE_DAMO
+
+config NIXOS_FLAKE_OVERRIDE_NFSTEST
+	bool "Override nfstest source"
+	output yaml
+	default n
+	help
+	  Rebuild nfstest from a user-supplied tree instead of
+	  nixos-flake/pkgs/nfstest.nix.
+if NIXOS_FLAKE_OVERRIDE_NFSTEST
+config NIXOS_FLAKE_OVERRIDE_NFSTEST_SRC
+	string "nfstest source (local path or git URL)"
+	output yaml
+	default ""
+config NIXOS_FLAKE_OVERRIDE_NFSTEST_REF
+	string "nfstest ref"
+	output yaml
+	default ""
+endif # NIXOS_FLAKE_OVERRIDE_NFSTEST
+
+config NIXOS_FLAKE_OVERRIDE_PYNFS
+	bool "Override pynfs source"
+	output yaml
+	default n
+	help
+	  Rebuild pynfs from a user-supplied tree instead of
+	  nixos-flake/pkgs/pynfs.nix.
+if NIXOS_FLAKE_OVERRIDE_PYNFS
+config NIXOS_FLAKE_OVERRIDE_PYNFS_SRC
+	string "pynfs source (local path or git URL)"
+	output yaml
+	default ""
+config NIXOS_FLAKE_OVERRIDE_PYNFS_REF
+	string "pynfs ref"
+	output yaml
+	default ""
+endif # NIXOS_FLAKE_OVERRIDE_PYNFS
+
+config NIXOS_FLAKE_OVERRIDE_XNVME
+	bool "Override xnvme source"
+	output yaml
+	default n
+	help
+	  Rebuild xnvme from a user-supplied tree instead of
+	  nixos-flake/pkgs/xnvme.nix.
+if NIXOS_FLAKE_OVERRIDE_XNVME
+config NIXOS_FLAKE_OVERRIDE_XNVME_SRC
+	string "xnvme source (local path or git URL)"
+	output yaml
+	default ""
+config NIXOS_FLAKE_OVERRIDE_XNVME_REF
+	string "xnvme ref"
+	output yaml
+	default ""
+endif # NIXOS_FLAKE_OVERRIDE_XNVME
+
+config NIXOS_FLAKE_OVERRIDE_CPUPOWER
+	bool "Override cpupower source"
+	output yaml
+	default n
+	help
+	  Rebuild cpupower from a user-supplied Linux tree
+	  (tools/power/cpupower) instead of nixos-flake/pkgs/cpupower.nix.
+if NIXOS_FLAKE_OVERRIDE_CPUPOWER
+config NIXOS_FLAKE_OVERRIDE_CPUPOWER_SRC
+	string "cpupower source (local path or Linux git URL)"
+	output yaml
+	default ""
+config NIXOS_FLAKE_OVERRIDE_CPUPOWER_REF
+	string "cpupower ref"
+	output yaml
+	default ""
+endif # NIXOS_FLAKE_OVERRIDE_CPUPOWER
+
+endmenu  # Source overrides
+
 endmenu  # NixOS Flake
 
 endif # KDEVOPS_ENABLE_NIXOS_FLAKE
diff --git a/playbooks/roles/nixosfi/tasks/imageless-build.yml b/playbooks/roles/nixosfi/tasks/imageless-build.yml
index 560a8c1b..7afe06a6 100644
--- a/playbooks/roles/nixosfi/tasks/imageless-build.yml
+++ b/playbooks/roles/nixosfi/tasks/imageless-build.yml
@@ -53,6 +53,11 @@
   loop: "{{ groups['all'] | reject('equalto', 'localhost') | list }}"
   changed_when: true
 
+- name: Refresh per-package source-override flake inputs
+  ansible.builtin.import_tasks: refresh-source-overrides.yml
+  vars:
+    refresh_target_vms: "{{ groups['all'] | reject('equalto', 'localhost') | list }}"
+
 - name: Prepare qsu imageless artefacts per VM
   ansible.builtin.include_tasks: imageless-build-per-vm.yml
   loop: "{{ groups['all'] | reject('equalto', 'localhost') | list }}"
diff --git a/playbooks/roles/nixosfi/tasks/rebuild-test.yml b/playbooks/roles/nixosfi/tasks/rebuild-test.yml
index 4a7b31ed..1356c073 100644
--- a/playbooks/roles/nixosfi/tasks/rebuild-test.yml
+++ b/playbooks/roles/nixosfi/tasks/rebuild-test.yml
@@ -4,6 +4,15 @@
 # Rebuild the per-VM imageless NixOS closure on the controller
 # and activate it on the running guest. See README.md.
 
+# Pick up Kconfig overrides toggled since bringup, like rebuild-boot.yml.
+- name: Re-render per-node imageless configs for rebuild-test
+  ansible.builtin.import_tasks: generate-configs.yml
+
+- name: Refresh per-package source-override flake inputs for {{ inventory_hostname }}
+  ansible.builtin.import_tasks: refresh-source-overrides.yml
+  vars:
+    refresh_target_vms: "{{ [inventory_hostname] }}"
+
 - name: Rebuild imageless NixOS toplevel on the controller for {{ inventory_hostname }}
   delegate_to: localhost
   ansible.builtin.command:
diff --git a/playbooks/roles/nixosfi/tasks/refresh-source-overrides.yml b/playbooks/roles/nixosfi/tasks/refresh-source-overrides.yml
new file mode 100644
index 00000000..43d8c0a1
--- /dev/null
+++ b/playbooks/roles/nixosfi/tasks/refresh-source-overrides.yml
@@ -0,0 +1,41 @@
+---
+# SPDX-License-Identifier: copyleft-next-0.3.1
+#
+# Nix path: inputs are pinned by narHash, so edits under SRC are not
+# picked up until the lock is re-pinned. Callers pass refresh_target_vms
+# to scope the work to all guests or a single one.
+
+- name: Refresh per-package source-override flake inputs
+  delegate_to: localhost
+  ansible.builtin.command:
+    argv:
+      - nix
+      - flake
+      - update
+      - --flake
+      - "path:{{ nixosfi_config_dir }}/{{ item.0 }}"
+      - "{{ item.1 }}-src"
+  environment:
+    PATH: "/nix/var/nix/profiles/default/bin:{{ lookup('env', 'PATH') | default('/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin') }}"
+  vars:
+    enabled_overrides: >-
+      {{
+        ([{'name': 'fio',          'gate': nixos_flake_override_fio          | default(false), 'src': nixos_flake_override_fio_src          | default('')}]
+       + [{'name': 'xfstests',     'gate': nixos_flake_override_xfstests     | default(false), 'src': nixos_flake_override_xfstests_src     | default('')}]
+       + [{'name': 'xfsprogs',     'gate': nixos_flake_override_xfsprogs     | default(false), 'src': nixos_flake_override_xfsprogs_src     | default('')}]
+       + [{'name': 'libbpf',       'gate': nixos_flake_override_libbpf       | default(false), 'src': nixos_flake_override_libbpf_src       | default('')}]
+       + [{'name': 'libbpf-tools', 'gate': nixos_flake_override_libbpf_tools | default(false), 'src': nixos_flake_override_libbpf_tools_src | default('')}]
+       + [{'name': 'damo',         'gate': nixos_flake_override_damo         | default(false), 'src': nixos_flake_override_damo_src         | default('')}]
+       + [{'name': 'nfstest',      'gate': nixos_flake_override_nfstest      | default(false), 'src': nixos_flake_override_nfstest_src      | default('')}]
+       + [{'name': 'pynfs',        'gate': nixos_flake_override_pynfs        | default(false), 'src': nixos_flake_override_pynfs_src        | default('')}]
+       + [{'name': 'xnvme',        'gate': nixos_flake_override_xnvme        | default(false), 'src': nixos_flake_override_xnvme_src        | default('')}]
+       + [{'name': 'cpupower',     'gate': nixos_flake_override_cpupower     | default(false), 'src': nixos_flake_override_cpupower_src     | default('')}])
+        | selectattr('gate')
+        | selectattr('src')
+        | map(attribute='name')
+        | list
+      }}
+  loop: "{{ refresh_target_vms | product(enabled_overrides) | list }}"
+  loop_control:
+    label: "{{ item.0 }} -> {{ item.1 }}-src"
+  changed_when: true
diff --git a/playbooks/roles/nixosfi/templates/default.nix.j2 b/playbooks/roles/nixosfi/templates/default.nix.j2
index 026275b2..db8d3dc8 100644
--- a/playbooks/roles/nixosfi/templates/default.nix.j2
+++ b/playbooks/roles/nixosfi/templates/default.nix.j2
@@ -72,4 +72,35 @@
     options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ];
   };
 {% endif %}
+{% set override_pkgs = [
+  ('fio',          nixos_flake_override_fio          | default(false), nixos_flake_override_fio_src          | default('')),
+  ('xfstests',     nixos_flake_override_xfstests     | default(false), nixos_flake_override_xfstests_src     | default('')),
+  ('xfsprogs',     nixos_flake_override_xfsprogs     | default(false), nixos_flake_override_xfsprogs_src     | default('')),
+  ('libbpf',       nixos_flake_override_libbpf       | default(false), nixos_flake_override_libbpf_src       | default('')),
+  ('libbpf-tools', nixos_flake_override_libbpf_tools | default(false), nixos_flake_override_libbpf_tools_src | default('')),
+  ('damo',         nixos_flake_override_damo         | default(false), nixos_flake_override_damo_src         | default('')),
+  ('nfstest',      nixos_flake_override_nfstest      | default(false), nixos_flake_override_nfstest_src      | default('')),
+  ('pynfs',        nixos_flake_override_pynfs        | default(false), nixos_flake_override_pynfs_src        | default('')),
+  ('xnvme',        nixos_flake_override_xnvme        | default(false), nixos_flake_override_xnvme_src        | default('')),
+  ('cpupower',     nixos_flake_override_cpupower     | default(false), nixos_flake_override_cpupower_src     | default('')),
+] %}
+{% set has_overrides = override_pkgs | selectattr(1) | selectattr(2) | list | length > 0 %}
+{% if has_overrides %}
+
+  # Per-package src overrides driven by Kconfig NIXOS_FLAKE_OVERRIDE_<PKG>.
+  # Each entry consumes the matching <pkg>-src flake input declared in
+  # flake.nix and rebuilds the package against that source. lib.mkAfter
+  # ensures kdevops overrides run after nixos-flake.overlays.default so
+  # they win when both target the same package.
+  nixpkgs.overlays = lib.mkAfter [
+    (final: prev: {
+{% for pkg, enabled, src in override_pkgs %}{% if enabled and src %}
+      {{ pkg }} = prev.{{ pkg }}.overrideAttrs (_: {
+        src = inputs.{{ pkg }}-src;
+        version = "kdevops-${inputs.{{ pkg }}-src.shortRev or "local"}";
+      });
+{% endif %}{% endfor %}
+    })
+  ];
+{% endif %}
 }
diff --git a/playbooks/roles/nixosfi/templates/flake.nix.j2 b/playbooks/roles/nixosfi/templates/flake.nix.j2
index fce2fe68..ee0ac698 100644
--- a/playbooks/roles/nixosfi/templates/flake.nix.j2
+++ b/playbooks/roles/nixosfi/templates/flake.nix.j2
@@ -6,12 +6,48 @@
   resolves the nixos-flake input at the vendored subtree. Builds the
   system toplevel closure (packages.<system>.toplevel) that qsu boots
   imagelessly: tmpfs root with /nix/store and /lib/modules over
-  virtiofs, no disk image.
+  virtiofs, no disk image. Conditionally adds one `<pkg>-src` flake
+  input per package whose NIXOS_FLAKE_OVERRIDE_<PKG> bool is set; the
+  matching overlay in default.nix consumes them.
+
+  Refresh a source override after a Kconfig change:
+    nix flake update --update-input <pkg>-src
 #}
+{%- macro nixstr(value) -%}
+{{ value | replace('\\', '\\\\') | replace('"', '\\"') | replace('${', '\\${') }}
+{%- endmacro -%}
+{%- set override_pkgs = [
+  ('fio',          nixos_flake_override_fio          | default(false), nixos_flake_override_fio_src          | default(''), nixos_flake_override_fio_ref          | default('')),
+  ('xfstests',     nixos_flake_override_xfstests     | default(false), nixos_flake_override_xfstests_src     | default(''), nixos_flake_override_xfstests_ref     | default('')),
+  ('xfsprogs',     nixos_flake_override_xfsprogs     | default(false), nixos_flake_override_xfsprogs_src     | default(''), nixos_flake_override_xfsprogs_ref     | default('')),
+  ('libbpf',       nixos_flake_override_libbpf       | default(false), nixos_flake_override_libbpf_src       | default(''), nixos_flake_override_libbpf_ref       | default('')),
+  ('libbpf-tools', nixos_flake_override_libbpf_tools | default(false), nixos_flake_override_libbpf_tools_src | default(''), nixos_flake_override_libbpf_tools_ref | default('')),
+  ('damo',         nixos_flake_override_damo         | default(false), nixos_flake_override_damo_src         | default(''), nixos_flake_override_damo_ref         | default('')),
+  ('nfstest',      nixos_flake_override_nfstest      | default(false), nixos_flake_override_nfstest_src      | default(''), nixos_flake_override_nfstest_ref      | default('')),
+  ('pynfs',        nixos_flake_override_pynfs        | default(false), nixos_flake_override_pynfs_src        | default(''), nixos_flake_override_pynfs_ref        | default('')),
+  ('xnvme',        nixos_flake_override_xnvme        | default(false), nixos_flake_override_xnvme_src        | default(''), nixos_flake_override_xnvme_ref        | default('')),
+  ('cpupower',     nixos_flake_override_cpupower     | default(false), nixos_flake_override_cpupower_src     | default(''), nixos_flake_override_cpupower_ref     | default('')),
+] -%}
 {
   inputs = {
     nixos-flake.url = "path:{{ topdir_path }}/scripts/nixos-flake";
     nixpkgs.follows = "nixos-flake/nixpkgs";
+{% for pkg, enabled, src, ref in override_pkgs %}{% if enabled and src %}
+
+    {{ pkg }}-src = {
+{% if src.startswith('/') %}
+      type = "path";
+      path = "{{ nixstr(src) }}";
+{% else %}
+      type = "git";
+      url = "{{ nixstr(src) }}";
+{% if ref %}
+      ref = "{{ nixstr(ref) }}";
+{% endif %}
+{% endif %}
+      flake = false;
+    };
+{% endif %}{% endfor %}
   };
 
   outputs =
diff --git a/playbooks/roles/nixosfl/templates/default.nix.j2 b/playbooks/roles/nixosfl/templates/default.nix.j2
index c09c4576..7dddfe7f 100644
--- a/playbooks/roles/nixosfl/templates/default.nix.j2
+++ b/playbooks/roles/nixosfl/templates/default.nix.j2
@@ -82,4 +82,35 @@
     options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ];
   };
 {% endif %}
+{% set override_pkgs = [
+  ('fio',          nixos_flake_override_fio          | default(false), nixos_flake_override_fio_src          | default('')),
+  ('xfstests',     nixos_flake_override_xfstests     | default(false), nixos_flake_override_xfstests_src     | default('')),
+  ('xfsprogs',     nixos_flake_override_xfsprogs     | default(false), nixos_flake_override_xfsprogs_src     | default('')),
+  ('libbpf',       nixos_flake_override_libbpf       | default(false), nixos_flake_override_libbpf_src       | default('')),
+  ('libbpf-tools', nixos_flake_override_libbpf_tools | default(false), nixos_flake_override_libbpf_tools_src | default('')),
+  ('damo',         nixos_flake_override_damo         | default(false), nixos_flake_override_damo_src         | default('')),
+  ('nfstest',      nixos_flake_override_nfstest      | default(false), nixos_flake_override_nfstest_src      | default('')),
+  ('pynfs',        nixos_flake_override_pynfs        | default(false), nixos_flake_override_pynfs_src        | default('')),
+  ('xnvme',        nixos_flake_override_xnvme        | default(false), nixos_flake_override_xnvme_src        | default('')),
+  ('cpupower',     nixos_flake_override_cpupower     | default(false), nixos_flake_override_cpupower_src     | default('')),
+] %}
+{% set has_overrides = override_pkgs | selectattr(1) | selectattr(2) | list | length > 0 %}
+{% if has_overrides %}
+
+  # Per-package src overrides driven by Kconfig NIXOS_FLAKE_OVERRIDE_<PKG>.
+  # Each entry consumes the matching <pkg>-src flake input declared in
+  # flake.nix and rebuilds the package against that source. lib.mkAfter
+  # ensures kdevops overrides run after nixos-flake.overlays.default so
+  # they win when both target the same package.
+  nixpkgs.overlays = lib.mkAfter [
+    (final: prev: {
+{% for pkg, enabled, src in override_pkgs %}{% if enabled and src %}
+      {{ pkg }} = prev.{{ pkg }}.overrideAttrs (_: {
+        src = inputs.{{ pkg }}-src;
+        version = "kdevops-${inputs.{{ pkg }}-src.shortRev or "local"}";
+      });
+{% endif %}{% endfor %}
+    })
+  ];
+{% endif %}
 }
diff --git a/playbooks/roles/nixosfl/templates/flake.nix.j2 b/playbooks/roles/nixosfl/templates/flake.nix.j2
index 9e1be527..f74822e7 100644
--- a/playbooks/roles/nixosfl/templates/flake.nix.j2
+++ b/playbooks/roles/nixosfl/templates/flake.nix.j2
@@ -4,12 +4,48 @@
   Per-VM libvirt flake rendered by kdevops. Mirrors upstream's starter
   at scripts/nixos-flake/templates/libvirt/flake.nix and resolves the
   nixos-flake input at the vendored subtree. Builds a qcow2 disk image
-  (packages.<system>.image) that libvirtd runs.
+  (packages.<system>.image) that libvirtd runs. Conditionally adds one
+  `<pkg>-src` flake input per package whose NIXOS_FLAKE_OVERRIDE_<PKG>
+  bool is set; the matching overlay in default.nix consumes them.
+
+  Refresh a source override after a Kconfig change:
+    nix flake update --update-input <pkg>-src
 #}
+{%- macro nixstr(value) -%}
+{{ value | replace('\\', '\\\\') | replace('"', '\\"') | replace('${', '\\${') }}
+{%- endmacro -%}
+{%- set override_pkgs = [
+  ('fio',          nixos_flake_override_fio          | default(false), nixos_flake_override_fio_src          | default(''), nixos_flake_override_fio_ref          | default('')),
+  ('xfstests',     nixos_flake_override_xfstests     | default(false), nixos_flake_override_xfstests_src     | default(''), nixos_flake_override_xfstests_ref     | default('')),
+  ('xfsprogs',     nixos_flake_override_xfsprogs     | default(false), nixos_flake_override_xfsprogs_src     | default(''), nixos_flake_override_xfsprogs_ref     | default('')),
+  ('libbpf',       nixos_flake_override_libbpf       | default(false), nixos_flake_override_libbpf_src       | default(''), nixos_flake_override_libbpf_ref       | default('')),
+  ('libbpf-tools', nixos_flake_override_libbpf_tools | default(false), nixos_flake_override_libbpf_tools_src | default(''), nixos_flake_override_libbpf_tools_ref | default('')),
+  ('damo',         nixos_flake_override_damo         | default(false), nixos_flake_override_damo_src         | default(''), nixos_flake_override_damo_ref         | default('')),
+  ('nfstest',      nixos_flake_override_nfstest      | default(false), nixos_flake_override_nfstest_src      | default(''), nixos_flake_override_nfstest_ref      | default('')),
+  ('pynfs',        nixos_flake_override_pynfs        | default(false), nixos_flake_override_pynfs_src        | default(''), nixos_flake_override_pynfs_ref        | default('')),
+  ('xnvme',        nixos_flake_override_xnvme        | default(false), nixos_flake_override_xnvme_src        | default(''), nixos_flake_override_xnvme_ref        | default('')),
+  ('cpupower',     nixos_flake_override_cpupower     | default(false), nixos_flake_override_cpupower_src     | default(''), nixos_flake_override_cpupower_ref     | default('')),
+] -%}
 {
   inputs = {
     nixos-flake.url = "path:{{ topdir_path }}/scripts/nixos-flake";
     nixpkgs.follows = "nixos-flake/nixpkgs";
+{% for pkg, enabled, src, ref in override_pkgs %}{% if enabled and src %}
+
+    {{ pkg }}-src = {
+{% if src.startswith('/') %}
+      type = "path";
+      path = "{{ nixstr(src) }}";
+{% else %}
+      type = "git";
+      url = "{{ nixstr(src) }}";
+{% if ref %}
+      ref = "{{ nixstr(ref) }}";
+{% endif %}
+{% endif %}
+      flake = false;
+    };
+{% endif %}{% endfor %}
   };
 
   outputs =

-- 
2.54.0