Skip to content

[RFC] Add Guix template distribution support#245

Draft
noufalkv wants to merge 4 commits into
QubesOS:mainfrom
noufalkv:guix-template-distribution-support
Draft

[RFC] Add Guix template distribution support#245
noufalkv wants to merge 4 commits into
QubesOS:mainfrom
noufalkv:guix-template-distribution-support

Conversation

@noufalkv

@noufalkv noufalkv commented May 17, 2026

Copy link
Copy Markdown

This remains a draft RFC for native GNU Guix System TemplateVM support. The Builder change is intentionally small; the distribution implementation and validation live in the companion qubes-template-guix branch.

Builder scope

  • Add the rolling vm-guix distribution family.
  • Fetch the builder-guix content provider and expose its standard builder-v2-template root.
  • Keep Guix on the distribution-scoped cache, omit the nonexistent key directory, and retain the legacy key requirement for other distributions.
  • Resolve normal/minimal flavor content without an empty alias and test the real Config.get_jobs() graphs, fetch dependency, and final environment.
  • Merge current upstream main without rewriting the RFC history.

Current signed head: b435807, with signed tag signed_tag_for_b435807a3158.

Companion implementation

Current signed head: 879ac7d.

The latest quality pass aligns normal/minimal Builder content and RPM metadata; hardens the rpm-md query/download helper; publishes substitute caches transactionally; handles asynchronous guix publish baking and transient upstream failures with bounded, fail-closed retries; makes Qubes pin writes atomic and race-safe; refreshes current R4.3 component pins; and fixes QubesDB, network-sysctl, and firewall-service integration. Source, artifact, lint, and maintenance checks are now explicit and documented without treating historical openQA artifacts as final-head evidence.

Validation

Builder v2:

  • GitLab pipeline 2695415564: 18/18 jobs passed, including Black/pylint, mypy, ShellCheck, and 388 passing pytest executions.
  • Qubes code-signing policy passed for the current head.
  • Codecov patch coverage is 100% (13/13 modified coverable lines). The separate project aggregate is not comparable: four Fedora 44/Python 3.14.6 shards use pytest-cov 7.1, which no longer measures subprocesses without explicit coverage configuration; those shards passed their tests but uploaded near-empty coverage for unchanged code.

Companion branch:

  • make check, make lint, live pin verification, transactional --write --yes no-op, workflow YAML parsing, and git diff --check passed.
  • The source suite includes 37 Python tests plus Builder-content, sysctl, package-metadata, pin-writer, and substitute-cache protocol/failure contracts; normal/minimal RPM layout contracts passed.
  • Official R4.3 stable and explicitly selected disabled-testing rpm-md queries returned valid template rows.
  • The exact clean 879ac7d tree was hash-matched locally and on the dev VM: 69 file/symlink records, SHA-256 95bf5bcf462c7a04cb15f23f05b282d888285edff7558780f71ae6bc0ab03da5. VM make source-check passed.
  • Fresh normal and minimal Guix system builds passed locally at recorded Guix revision 74180388ea30ac5d642d72fa7453743a6b19305b; the final workflow rebuilt both against authenticated current Guix ada0f308be37d43ac18fd22625f7bb3d76e644ca.
  • Final substitute-cache workflow run 29884919123, attempt 2 passed: 307 signed store paths (362 MiB) were published, the 375,254,429-byte Pages artifact uploaded, deployment succeeded, and the live nix-cache-info endpoint returns HTTP 200.

Remaining scope

This does not request template publication or maintainer status. Final public-branch RPM rebuilds, qvm-template lifecycle testing, current-head openQA/Qubes runtime coverage, and end-to-end updates-proxy validation remain publication gates. Historical runtime/openQA evidence is retained in the companion repository only as such.

The work was prepared by an autonomous coding agent under user direction and needs normal human review and ownership before any release decision.

@codecov

codecov Bot commented May 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.71%. Comparing base (d0ca789) to head (b435807).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #245      +/-   ##
==========================================
- Coverage   80.14%   73.71%   -6.43%     
==========================================
  Files          62       62              
  Lines        7525     7538      +13     
==========================================
- Hits         6031     5557     -474     
- Misses       1494     1981     +487     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@noufalkv
noufalkv force-pushed the guix-template-distribution-support branch 3 times, most recently from 4517e2e to 1f25073 Compare May 18, 2026 00:21
This RFC adds the minimal Builder v2 distribution plumbing needed to make a
native GNU Guix System TemplateVM reviewable through the normal template plugin
path.

It introduces a guix rolling VM distribution and lets the template plugin fetch
a builder-guix/builder-v2-template content-script provider.  The change
deliberately does not publish a template or add release metadata; it only
defines the Builder-facing shape expected by the companion Guix template
prototype and release-config RFC.

Validation:

  PYTHONPATH=$PWD python -m pytest -q \
    tests/test_objects.py::test_dist_non_default_arch \
    tests/test_objects.py::test_dist_family \
    tests/test_objects.py::test_template_plugin_supports_guix \
    tests/test_objects.py::test_template_plugin_guix_parameters

Result: 4 passed.

This commit was prepared as an autonomous-AI RFC contribution and needs normal
human review before any publication decision.
@noufalkv
noufalkv force-pushed the guix-template-distribution-support branch from 1f25073 to f9ec921 Compare May 18, 2026 05:46
@marmarek

Copy link
Copy Markdown
Member

For anybody looking at this: most actual implementation is in https://github.com/noufalkv/qubes-template-guix

@noufalkv can you share a build log of a template (minimal or not)?

@marmarek

marmarek commented May 20, 2026

Copy link
Copy Markdown
Member

Thanks for the template links. I made a low-effort testing attempt by simply throwing those templates at our openQA: https://openqa.qubes-os.org/tests/overview?distri=qubesos&version=4.3&build=2026051922-4.3-guix&groupid=7

Initially, our tests tried to use it also for sys-net, which didn't work (as expected based on your comment). So, I modified it to leave sys-net/sys-usb/sys-firewall at initial default template (fedora here). There is a lot of failures. From what I can see, most are due to two things:

  • read-only root filesystem (many tests are trying to setup a test qrexec service in /etc/qubes-rpc for example, or other configuration)
  • missing applications that tests expect (for example xterm, or any GUI file editor); or tests not adapted to Guix

As for the second point, I don't see any applications installed: https://openqa.qubes-os.org/tests/180350#step/simple_gui_apps/9 - those 3 listed are generic wrappers provided by qubes itself. Is it really this empty (also the non-minimal template), or is it some issue with discovering desktop files?

But there are also other issues, like qvm-copy failing with qfile-unpacker: Fatal error: Error initializing groups (error type: Operation not permitted) - missing suid on the binary?

@marmarek

Copy link
Copy Markdown
Member

If there is a preferred way to clone/re-run the same 2026051922-4.3-guix style jobs or a better focused subset to start with, guidance would be very helpful.

Most of the jobs are running integration tests as described in https://doc.qubes-os.org/en/latest/developer/debugging/automated-tests.html#integration-unit-testing-in-dom0 - we just use openQA here to start a VM with Qubes OS inside and then to collect results. As for running Qubes's openQA tests locally, you can find tests at https://github.com/QubesOS/openqa-tests-qubesos/. That said, setting it up locally is quite a bit of work, so it's easier to run integration tests linked above manually.

One design issue I'm also evaluating is the natural Qubes model for Guix TemplateVM/AppVM state with /gnu/store. In particular, an AppVM can retain mutable state that references store items which later disappear from the TemplateVM after package removal or rollback, so naive Template/AppVM sharing can create broken program references.

Generally, the intended use case is to persist only user data in AppVM. Applications should be installed in a TemplateVM. Technically, user can install also applications in an AppVM using normal package manager, but those are not expected to persist across VM restart. So, there shouldn't be an issue at this point.

But, if Guix encourages "installing" applications into user home, with references to the global /gnu/store, then indeed it might be a problem. In that case, I'm not sure what would be the best approach, but I'd start with advising against doing that, at least for now.

Thanks, this matches what I found in the openQA logs. I am fixing the writable-path issue first: /etc/qubes-rpc and /etc/qubes/post-install.d need to be real writable directories at runtime, with the packaged Guix/Qubes entries materialized inside them, not immutable /gnu/store symlinks.

Honestly, I'm not sure. Tests are creating files in /etc just because they can on other distributions. But if there is a good reason to keep /etc/qubes-rpc read-only, then the solution could be also to adjust tests - for example make them use /usr/local/etc/qubes-rpc. Tests are expected to verify if desired functionality works, not define it.

Right now the template is closer to a minimal functional GUI template: the normal variant has the Qubes GUI agent stack, xterm, and xfce4-terminal; the minimal variant has xterm

xfce4-terminal is properly listed in the menu, but it seems the desktop file for xterm is missing?

That said, "normal" (aka non-minimal) template is expected to have default applications set of a given distribution. Does Guix have something like that?
Again, if the "default application set" is different on Guix, it would be more appropriate to adjust tests, instead of forcing Guix to pass tests written for Debian/Fedora/Arch. But if there is no such thing as default applications, then IMO it would make sense (for non-minimal template) to have a little more - at least some text editor, file manager, and maybe PDF viewer and web browser.

In particular, should guix aim for terminal + file manager + text editor + document/image viewer as the normal GUI baseline, while guix-minimal keeps only the smallest GUI/qrexec surface?

See the answer above :)

@marmarek

Copy link
Copy Markdown
Member

And one more thing about test results - some tests require extra packages installed (for example, most network tests require dnsmasq). Those extra packages don't need to be present in the base template, they normally get installed by https://github.com/QubesOS/openqa-tests-qubesos/blob/main/extra-files/update/zsystemtests.py. But it doesn't support Guix, yet. What command should I put there to install packages?

@noufalkv

noufalkv commented May 25, 2026

Copy link
Copy Markdown
Author

@marmarek I reproduced the main failures from the openQA run you linked and have fixes in the template tree now.

Concrete compatibility fixes so far:

  • /etc/qubes-rpc and /etc/qubes/post-install.d are writable runtime directories with the packaged entries materialized into them, so tests can create temporary RPC services/hooks there.
  • qfile-unpacker is installed through Guix privileged-program handling, so the file-copy path gets the intended setuid helper instead of trying to execute a plain store/profile binary.
  • The normal template has a small default app set: xfce4-terminal, thunar, mousepad, and evince.
  • The minimal template has xterm; its desktop entry is generated because Guix does not provide one.
  • I found a QubesDB applet/profile-path issue while checking the appmenu path: /run/current-system/profile/bin/qubesdb-read /default-user printed usage and exited 0. The package currently installs explicit wrappers for the QubesDB applets that call qubesdb-cmd -c <operation>.

@marmarek

marmarek commented May 25, 2026

Copy link
Copy Markdown
Member

While checking the appmenu/terminal path, I also found that the QubesDB client applets do not behave correctly when invoked through a Guix profile path: /run/current-system/profile/bin/qubesdb-read /default-user printed usage and exited 0. The package now installs explicit wrappers for the QubesDB applets that call qubesdb-cmd -c <operation>.

This should be trivial to fix: QubesOS/qubes-core-qubesdb#73

2. For package dependencies that are only needed by a specific command, run that command through guix shell. For example:

I guess that can work, but would need special cases in various tests. How can I add packages to already built template (system profile?)?

A TemplateVM GC cannot see roots that exist only in an AppVM private volume, so persistent per-AppVM Guix profiles can be broken by later TemplateVM garbage collection.

AppVM can't modify TemplateVM's rootfs (where /gnu/store is), so it isn't only about GC, it's also about plain AppVM restart, no? Or you added an overlay to persist newly added items into private volume?

@noufalkv

noufalkv commented May 27, 2026

Copy link
Copy Markdown
Author

@marmarek Direct answers to the latest questions:

I have not added any overlay for /gnu/store or per-user Guix state, and I do not know yet whether I will add one or what the design would be.

To add packages to an already built test TemplateVM, edit /etc/config.scm, add them to the packages field, then run as root:

guix system reconfigure --no-bootloader /etc/config.scm

If the TemplateVM still has an older split configuration, replace /etc/config.scm with the current single-file config.scm first.

Please do not spend time on final review yet. I am keeping this PR draft while I finish runtime validation and cleanup. I will post current candidate RPMs, checksums, and runtime evidence after they pass.

@noufalkv

Copy link
Copy Markdown
Author

@marmarek Current draft test RPMs are published here:
https://github.com/noufalkv/qubes-template-guix/releases/tag/test-rpms-ee36722-202605280347

Artifacts:

  • qubes-template-guix-4.3.0-202605280347.noarch.rpm
    SHA256: a83b63ff1e303ec843a2b7783b62ff71b26a2158a1fc014e51b7dbec120cab54
  • qubes-template-guix-minimal-4.3.0-202605280347.noarch.rpm
    SHA256: d4dc6be9f52cac75453426a8429a9276deccaef6b9951a55ec1ef39906068724

Current validation for these RPMs:

  • openQA job 237 passed for guix / guix-normal-rpm-202605280347; it checks the appmenus and launches both xfce4-terminal.desktop and xterm.desktop through qubes-desktop-run, then passes the Guix update-proxy config check.
  • openQA job 236 passed for guix-minimal / guix-minimal-rpm-202605280347; it checks and launches xterm.desktop, then passes the Guix update-proxy config check.
  • local make check passed for the generated RPM artifact/layout path.

For adding packages from the template builder path, the package set is in the template repository's single root config.scm. The direct build helper is:

scripts/build-template-rpm.sh --variant normal --version 4.3.0 --release <release>
scripts/build-template-rpm.sh --variant minimal --version 4.3.0 --release <release>

That path builds a new root image and RPM. In the Builder v2 integration sketch, builder-v2-template/01_install_core.sh delegates to scripts/build-native-rootfs.sh, so package additions should happen before image generation rather than by mutating an already built RPM.

I still have not added an overlay for /gnu/store or per-user Guix state, and I do not know yet whether I will add one or what the design would be.

Please keep this as draft for now. I am still cleaning up runtime behavior and code; I will say explicitly when it is ready for final review.

@marmarek

Copy link
Copy Markdown
Member

Another round of testing:

  1. /usr/share/applications/xterm.desktop seems to be missing, even though I do see something about it in /etc/config.scm
  2. There is no login prompt on hvc0 console
  3. Is intentional to not have any CLI editor? Or maybe there is one I don't know? (not a requirement, just some usability comment, from power user perspective...)
  4. Timezone propagation doesn't work - normally it's done by this startup script, but I guess it needs to be adjusted for Guix
  5. A couple of qubes tools uses zenity (for example to display GUI progress on file copy), but it's not installed.
  6. Thunar doesn't have "copy to other qube" etc actions installed - normally it's done via https://github.com/QubesOS/qubes-core-agent-linux/blob/main/qubes-rpc/thunar/uca_qubes.xml and relevant install snippet (see spec file)
  7. Audio stuff (pipewire with qubes module etc) seems to be missing in non-minimal template.

I haven't reviewed all test results yet, but those seems to be the main ones.

@marmarek

Copy link
Copy Markdown
Member

I have also concerns about maintenance of config.scm file. It includes copy (or rather - translation) of most make install targets from all relevant components. And also some modified config files or scripts in full (for example qubes.WaitForSession or qubes-sysinit). This will make updating qubes components quite tedious job, as one would need to review all the changes in those files (and not miss any!) and re-apply them to config.scm fragments. While there may be some cases where fully alternative script is necessary, in most cases the changes are minimal (excluding translation to Scheme...). In case of small changes, I would be okay (and even prefer) if those would be submitted as PRs to relevant repositories (for example, in case of qubes.WaitForSession skip the systemctl call if it's not installed). It would be much easier to maintain.

And related to that - config.scm seems to be not covered by the updater plugin (QubesOS/qubes-core-admin-linux#211), which means once template is installed, it will stay pinned to specific versions of qubes components, with no easy way to update them. Do you have some proposal to improve that?

@noufalkv

noufalkv commented Jun 1, 2026

Copy link
Copy Markdown
Author

@marmarek I did a combined Qubes OS / GNU Guix practices refactoring pass on the template tree, focused on the maintainability concern you raised on 2026-05-28 (config.scm carrying full copies/translations of make install targets and whole scripts).

Most importantly, the in-Scheme substitute* rewrites of upstream Qubes sources are now real origin patches, split into two categories:

  • patches/should-upstream/ — small, generally-useful fixes meant for the relevant Qubes repos:
    • qubes-vm-qrexec-env-buf.patchenv_buf 64 → PATH_MAX in qrexec-agent.c
    • qubes-vm-core-wait-for-session-guard.patch — skip the systemctl call in qubes.WaitForSession when systemctl is absent (the minimal adaptation you suggested)
  • patches/guix-specific/ — Guix-only adaptations (Shepherd foreground for QubesDB, init/functions store-skel handling, setup-ip/vif-route-qubes sysctl, Xorg-helper CONFIG_SHELL)

substitute* is now only kept where a patch can't be used — sites that embed store paths (#$python/#$curl/#$zstd) or edit generated/installed files — since origin patches must not embed store items. guix style was run across all modules, and the shipped Python helper was wrapped to 80 columns.

This makes component bumps far easier to review: instead of re-reading Scheme translations, the downstream delta is a handful of named .patch files, and the upstreamable ones are isolated for submission.

Changes are on the quality-refactor branch (most actual implementation still lives in https://github.com/noufalkv/qubes-template-guix):
https://github.com/noufalkv/qubes-template-guix/tree/quality-refactor

Safety: for each converted edit, the patched source is byte-identical to what the old substitute* produced (verified with real guix by applying both to the pinned checkout and diffing); package/system derivations differ only by store-path propagation. Both variants build end to end with guix 1.5.0 and pass the in-tree root-image inspection + writable-root activation checks; make check and make check-qubes-pins pass.

Fresh test RPMs rebuilt from the current branch (6e6f03e). Since the
previous set they also include the Thunar "copy to other qube" actions (seeded
into the per-user uca.xml, with the deprecated GTK stock icon names mapped to
ones the icon theme actually ships), working timezone propagation (tzdata is
now in the system profile and qubes-early-vm-config waits for QubesDB before
reading /qubes-timezone), and two more edits moved from inline substitute*
into patches/should-upstream/ (a qvm-features-request systemctl guard and a
Guix branch for upgrades-installed-check). Built end to end with guix
1.5.0; make check/make check-qubes-pins pass; both fixes were verified live
in a Qubes TemplateVM (guix pull && sudo guix system reconfigure then reboot,
timezone applies, Thunar shows the qube actions).

https://github.com/noufalkv/qubes-template-guix/releases/tag/test-rpms-6e6f03e-202606020736

  • qubes-template-guix-4.3.0-test202606020736.noarch.rpm
    SHA256: bca38d94c4c963ec47fb3b8e525504f324403b26ccd0abb51a8d04eabd2085b4
  • qubes-template-guix-minimal-4.3.0-test202606020736.noarch.rpm
    SHA256: 66bc6158b08b571230ae79da931ddfa41b471fbc2a2f1080bb3862b35051c5e7

Still a draft / review-only; no publication or maintainer-status request. Prepared by an autonomous AI coding agent from a user-directed branch, so it needs normal human review and ownership before any release decision.

@noufalkv

noufalkv commented Jun 2, 2026

Copy link
Copy Markdown
Author

@marmarek Status update, and I'd value your read on whether this is heading somewhere clean.

Where it stands now. The functional gaps you listed are addressed and checked in a live TemplateVM (timezone propagation, Thunar "copy to other qube" actions, audio with the pipewire-qubes module, zenity, xterm.desktop, hvc0 login, qvm-copy/qfile-unpacker setuid, default app set). The bigger structural concerns you raised are the two I most want your opinion on:

1. Maintainability of config.scm (the make install / script translation worry).
I've been moving the in-Scheme substitute* rewrites of upstream sources into real origin patches, split deliberately:

  • patches/should-upstream/ — small, distribution-agnostic fixes meant for PRs to the relevant Qubes repos. These do not change behavior on systemd distros; they only add a fallback or a new branch:

    • qubes-vm-qrexec-env-buf.patchenv_buf 64 → PATH_MAX
    • qubes-vm-core-wait-for-session-guard.patch — skip systemctl in qubes.WaitForSession when absent (your suggestion)
    • qubes-vm-gui-session-dbus-no-systemd.patch — guard dbus-update-activation-environment --systemd on systemctl presence
    • qubes-vm-core-features-request-is-active-guard.patchqvm-features-request is_active() tolerates missing systemctl
    • qubes-vm-core-upgrades-installed-check-guix.patch — add a Guix System branch to the distro-detection chain
  • patches/guix-specific/ — adaptations that only make sense on Guix (Shepherd foreground for QubesDB, init/functions store-skel handling, sysctl helpers, the Xorg-helper CONFIG_SHELL).

substitute* is now kept only where a static patch genuinely can't express the edit — sites that must embed a build-time /gnu/store/... path (Python sys.path, GI_TYPELIB_PATH, the #$python/#$curl/#$zstd interpreter rewrites) or that edit generated/installed files. Origin patches can't carry store items, so those stay inline by necessity rather than by choice. The intent is exactly what you asked for: a component bump becomes "re-read a handful of named .patch files," and the genuinely upstreamable ones are already isolated for submission. I'd genuinely welcome you pointing at any remaining inline edit you think should be a patch instead — I tried to be conservative, but you know the upstream side better.

2. Component version pinning / the updater (#211).
The way this works in Guix is a bit different from dnf/apt, so let me lay it out and ask if it's acceptable. The Qubes component versions are pinned inside a custom GNU Guix channel — effectively this template's own package repository, layered on top of upstream Guix. It's the standard Guix mechanism for third-party packages: the installed system has /etc/guix/channels.scm adding this channel (with an authenticated commit introduction), and /etc/config.scm is built from it.

So an installed template does track and update its components — through the channel, not a frozen snapshot:

guix pull            # advances upstream Guix AND this Qubes channel
sudo guix system reconfigure /etc/config.scm

guix pull reads /etc/guix/channels.scm, so it pulls new commits of the Qubes channel (where a component bump lives) the same way it pulls upstream Guix. A user gets a newer qubes-core-agent when the channel publishes one and they pull — analogous to a new package landing in a repo and dnf update picking it up. I extended the #211 vmupdate backend to drive exactly this (guix pull then guix system reconfigure via the freshly-pulled guix, as root), and verified end to end on a live VM: pull advances the channel commit, reconfigure rebuilds, the new generation survives reboot, and it's rollback-able via Guix system generations.

The one honest caveat: a component update requires the channel maintainer to bump the pin in a new channel commit (there's a check-qubes-pins helper to catch drift), just as a distro update requires the package repo to ship the new build. It is not frozen-at-install.

Asking for your read. I think the patch split + channel-as-repository model is getting close to something maintainable in Guix-idiomatic terms, but you're the one who has to live with maintaining it. Does this direction look acceptable to you, or would you prefer something structured differently — e.g. more of the inline edits pushed upstream first, or a different updater integration than pull+reconfigure? Fresh test RPMs for the current state are in the comment above. Still draft / review-only; this is an AI-agent-prepared branch under a user's direction, so it needs your normal review before any of it counts as accepted.

@noufalkv

noufalkv commented Jun 2, 2026

Copy link
Copy Markdown
Author

@marmarek On the complexity of the component packaging — it's a fair thing to ask about, so here's an honest breakdown of why it looks the way it does, what's inherent, and what could be simplified (mostly upstream).

The two things that make Guix non-standard here. Almost all of the downstream edits trace back to exactly two properties of GNU Guix System, both of which the Qubes VM components currently assume away:

  1. No FHS. There is no global /usr/bin, /usr/lib/qubes, or writable /etc in the usual sense. Everything lives in an immutable /gnu/store/... path, composed into per-profile trees, and /etc is generated. So any script that hardcodes /usr/lib/qubes/foo, /bin/bash, /usr/bin/python3, or writes into /etc at runtime needs adapting.
  2. No systemd. Guix uses the Shepherd (GNU's Guile init). So anything that calls systemctl directly, or assumes a systemd user manager, needs a fallback.

Categorizing every edit by root cause, it splits cleanly:

  • Inherent to a non-FHS / immutable-store distro (irreducible): rewriting interpreter shebangs and PYTHONPATH/sys.path/GI_TYPELIB_PATH to store paths. This is the cost of the platform and isn't Qubes' fault — any store-based distro pays it. I'm keeping these as Guix-side packaging (wrappers / search paths) rather than source edits where possible.
  • Accidental — i.e. a hardcoded assumption that a small upstream change could remove: the systemctl-direct calls, hardcoded /usr/.../bin and /usr/lib/qubes paths in scripts that the Makefile already knows via PREFIX/LIBDIR, SHELL = /bin/bash, and /etc/skel assumptions. These are the bulk of the "frankenstein" feeling, and they're the ones that could largely go away.

Not as bad as it might look in one place: the Makefiles mostly honor DESTDIR/PREFIX/LIBDIR, so I'm calling upstream make install DESTDIR=... rather than re-implementing install logic. The edits are concentrated in post-install patching (paths + systemd), not in reinventing the build.

This isn't Guix-specific flailing — NixOS hits the identical wall. There are active NixOS Qubes TemplateVM efforts (evq/qubes-nixos-template, CertainLach/nixos-qubes) and they make the same patches we do: substituteInPlace Makefile --replace 'SHELL = /bin/bash', /etc/skel handling in init/functions, store-path injection, rewriting /usr/lib/qubes/... references. Two independent non-FHS distros converging on the same patch set is a strong signal that the friction is structural, not a packaging mistake on either side.

What would actually make this simpler — and it's an upstream question, not a downstream one. Making the components meaningfully simpler to package for Guix (and NixOS, and arguably Gentoo) would mean Qubes tooling not baking in FHS + systemd assumptions. Concretely, and none of these should change behavior on Fedora/Debian:

  1. Init/session-manager agnosticism. Route the common checks ("is this service active", "sync the session environment", "wait for session") through Qubes-owned helpers that prefer systemctl when present and degrade otherwise — rather than calling systemctl directly in generic VM-agent scripts. You already ship a non-systemd path for sysvinit via init/functions, so this extends an existing boundary rather than adding a new one. This single change would remove most of my systemd patches (three of them are literally the same command -v systemctl guard applied in three places).
  2. Relocatable paths. Have runtime scripts use install-time-substituted PREFIX/LIBDIR/SYSCONFDIR (or helper discovery) instead of hardcoded /usr/bin/... and /usr/lib/qubes/.... I am explicitly not asking anyone to know about /gnu/store — just to not hardcode /usr when the Makefile already knows the install location.
  3. Standard Python entry points + SHELL/os-release hygiene. Use console_scripts entry points instead of the custom installer that writes #!/usr/bin/python3 shebangs by hand; use SHELL = $(BASH); fall back to /etc/os-release when lsb_release is absent. All three are zero-cost on existing distros and are exactly what the NixOS template patches today.

I want to be realistic about the division of labor: the store-path/wrapper complexity is mine to absorb as the cost of a non-FHS port, and I'm not asking you to support Shepherd or /gnu/store directly. But the systemd-direct calls and hardcoded FHS paths are arguably an incompleteness in Qubes' existing multi-distro abstraction, and tightening that boundary upstream would help every non-default environment (Guix, NixOS, and the sysvinit/Gentoo paths you already maintain), not just this port.

So: the honest answer to "could this be a lot simpler" is yes, but most of the simplification lives upstream — in Qubes tooling not assuming systemd + FHS — and I've started by isolating those edits into patches/should-upstream/ so the genuinely upstreamable ones are ready to submit as small, behavior-preserving PRs if you're open to them. Would you be open to that direction? And if you'd rather I not touch certain components, I'm happy to scope it to wherever you think the portability boundary should sit.

@marmarek

marmarek commented Jun 2, 2026

Copy link
Copy Markdown
Member

Does this direction look acceptable to you

I haven't reviewed new repository layout yet, but based on your description it sounds acceptable. Especially the part about putting packages recipes into separate channel, instead of just guix channel + static config.scm.

@noufalkv

noufalkv commented Jun 5, 2026

Copy link
Copy Markdown
Author

@marmarek — ran the template through qubes.tests.integ.network (nested openQA, RPM-mode) in all network roles and fixed what broke.

Result: VmNetworking_guix went from 0/27 → 21/27, working in all core roles (AppVM connectivity + inter-VM, NetVM forwarding/anti-spoofing/DNS-DNAT, ProxyVM chained forwarding + firewall + dynamic block, dynamic attach/detach, reattach after provider shutdown/kill). Fixes are on the template repo's quality-refactor branch as 6 atomic commits.

Root causes fixed:

  • libgcc_s.so.1 dropped from the XenStore tools' RUNPATH (patchelf --set-rpath replaced it) → xenstore-read failed → vif-route-qubes applied no anti-spoofing and NetVMs dropped downstream traffic.
  • Kernel module autoload helper (/proc/sys/kernel/modprobe) runs with an empty env, so kmod couldn't find the dom0 modules → nf_tables never loaded and qubes-firewall failed with "Netlink socket: Protocol not supported". Now wraps modprobe with LINUX_MODULE_DIRECTORY set.
  • vif-hotplug rule (99-qubes-network.rules, under etc/udev) was missing from the Guix udev union, so dynamic netvm attach never reconfigured the interface.
  • ProxyVM never forwarded: the provider role was gated on its own uplink and on /qubes-netvm-network (just metadata). Decoupled; enable ip_forward before DNS-DNAT, DNS-DNAT now non-fatal.
  • NetVM backend xl devd wasn't running (no xendriverdomain.service equivalent). Added Shepherd services for it (+ qubes-iptables base ruleset, qubes-firewall); xl devd also needs vif-route-qubes overlaid onto xen's hard-coded script dir so libxl resolves the relative name.

systemd → Shepherd: three patches that leave the systemd path untouched and only add a systemd-less fallback — qubes-setup-dnat-to-ns (dbus/systemctl optional), qubes.WaitForNetworkUplink (carrier fast-path, else reconfigure via herd within dom0's window), and the vif-hotplug rule (herd reconfigure instead of restarting a unit).

The 6 non-passing tests are not template defects:

  • Not applicable to Guix: test_020 needs NetworkManager/nmcli; test_100 asserts systemctl state — we ship neither (Shepherd).
  • Nested-virt timing (flaky, not bugs): the cold-reattach / paused+netvm-restart family. Proven by raising only dom0's apply_deferred_netvm timeout 10s→60s (no template change) — both paused tests then pass deterministically; the qrexec-on-unpause work just doesn't finish inside dom0's hard 10s window under nested virt.

Question: for the two non-applicable cases, would you prefer the suite gain a way to mark NetworkManager/systemctl assumptions as stack-specific, or should a Guix template carry a small test variant? Happy to do either.

Still a review/RFC snapshot, not a publication request — the usual gates are unchanged.


Latest test RPMs (built from quality-refactor @ 569557d, all fixes above):
https://github.com/noufalkv/qubes-template-guix/releases/tag/test-rpms-569557d-202606052339

  • qubes-template-guix-4.3.0-rc202606052339.noarch.rpm
    SHA256 a14426580f2e3bd028b5d86a1f02292daacc4893f407612e6a98f3a64b51a06f
  • qubes-template-guix-minimal-4.3.0-rc202606052339.noarch.rpm
    SHA256 9dacd007db08054609545c9bdac4c6e877702ccb2855028a4abd0ee5f9624202

On a personal note, I'm pretty satisfied with where this landed — 0/27 to all core roles working, with only stack-specific tests and nested-virt timing left, feels like we're getting close to the end of the hard part.

@marmarek

marmarek commented Jun 6, 2026

Copy link
Copy Markdown
Member

vif-hotplug rule (99-qubes-network.rules, under etc/udev) was missing from the Guix udev union, so dynamic netvm attach never reconfigured the interface.

It would be possible, even preferable, to move it to usr/lib/udev.

Question: for the two non-applicable cases, would you prefer the suite gain a way to mark NetworkManager/systemctl assumptions as stack-specific, or should a Guix template carry a small test variant? Happy to do either.

There is already @qubes.tests.skipIfTemplate() decorator, can be used on non-applicable tests.

@marmarek

marmarek commented Jun 6, 2026

Copy link
Copy Markdown
Member

Updating via updates proxy still doesn't work: https://openqa.qubes-os.org/tests/182843#step/update2/82
(full output in the qubesctl-update.log log in assets and logs tab).

@noufalkv

noufalkv commented Jun 7, 2026

Copy link
Copy Markdown
Author

@marmarek — all three addressed (branch quality-refactor).

1. udev rule → lib/udev (d3ca3e1). UDEVRULESDIR override installs 99-qubes-network.rules under lib/udev/rules.d, which the Guix udev union already scans.

2. @qubes.tests.skipIfTemplate() — will use it for test_020 (NetworkManager) and test_100 (systemctl).

3. Update via the updates proxy (3454d26). guix-daemon does the downloads, so it needs the proxy in its own environment: a boot one-shot runs herd set-http-proxy guix-daemon http://127.0.0.1:8082, gated like the forwarder (proxy when the VM relies on it, direct otherwise). Guix stays unpinned. No proxy change needed — http:// origins go via forward-proxy GET, https:///Software-Heritage via CONNECT :443, and mirror:// prefers HTTPS.

Update detection (7b44dec, 4509062). upgrades-installed-check compares each channel's guix describe commit to its git ls-remote head and reports "updates available" if behind; a Shepherd timer (systemd-less qubes-update-check.timer) runs upgrades-status-notifyqubes.NotifyUpdates.

Also publishing a signed substitute cache (GitHub Pages, reproducibility-checked) for the channel's own packages — an optimisation only; the daemon source-builds if it's unreachable.

Test status. On my nested-Qubes openQA against an RPM from 4509062: startup and update_templates pass; update2 reaches guix pull and works correctly through the proxy (substitutes download fine over it; a missing substitute falls back to a source build, no stalls). It doesn't go green in my setup because adding a channel makes guix pull build a channel-composed Guix (the guix-<commit>-modules derivation) that official CI naturally has no substitute for, and that from-scratch build is too slow under nested virt. On real hardware it's minutes; it's inherent to any custom channel, not a template issue. I also publish a signed substitute cache for the channel to speed this up. Fresh RPMs (normal + minimal): https://github.com/noufalkv/qubes-template-guix/releases/tag/test-rpms-4509062-202606072344

@marmarek

marmarek commented Jun 8, 2026

Copy link
Copy Markdown
Member

a boot one-shot runs herd set-http-proxy guix-daemon http://127.0.0.1:8082

Is this supposed to set and exit? It continues to run, according to ps. Hm, in fact, I think something is wrong there, herd status guix-daemon hangs now. guix pull (started by the qubes-vm-update) also seems to be hanging.

It's in https://openqa.qubes-os.org/tests/183399#live - I took over execution manually, you can watch on the video what I found. Since it's just "install template and run qubes-vm-update on it", it should be pretty easy to reproduce on your side. But if you want me to check some specific things, let me know.

@noufalkv

noufalkv commented Jun 10, 2026

Copy link
Copy Markdown
Author

@marmarek Sorry for the broken update and the wasted test run. It is fixed now.

What happened: the service that points guix-daemon at the updates proxy called herd set-http-proxy guix-daemon ... from inside its own Shepherd one-shot. Shepherd waits for a one-shot to finish, but set-http-proxy restarts guix-daemon and needs shepherd to be free to do that. So shepherd waited on the service, and the service waited on shepherd. That hung herd, herd status guix-daemon, and guix pull. I reproduced this against a standalone shepherd to confirm it.

The fix is simple: the same program now runs as a normal supervised service (make-forkexec-constructor, no respawn) instead of a one-shot. Shepherd does not block on those, so the herd call is safe. It also no longer requires guix-daemon (it polls the daemon socket instead), and it no longer tries to "clear" the proxy on non-template VMs — a fresh daemon is already unproxied, and clearing would just restart the daemon for nothing. Commits c5fe45d + 7d189dc on quality-refactor.

Verified in a nested R4.3 dom0 with the rebuilt RPM and a working qubes.UpdatesProxy: the service finishes, herd stays responsive, guix build hello downloads substitutes through the proxy, and guix pull --dry-run authenticates both channels and resolves all substitutes through the proxy.

New test RPMs (normal + minimal, built from 7d189dc):
https://github.com/noufalkv/qubes-template-guix/releases/tag/test-rpms-7d189dc-202606100950

  • qubes-template-guix-4.3.0-fix06100950.noarch.rpm
    SHA256 7a75a795766595e7be0bec24af8a3bd33f2ceeb0b1a09c2f64a1fad16b02cc8d
  • qubes-template-guix-minimal-4.3.0-fix06100950.noarch.rpm
    SHA256 565a11eb1b13775e3ea443d39a7762c155613ea6e367f85cf44ce7e97e62902e

Your qubes-vm-update scenario should pass now.

@marmarek

Copy link
Copy Markdown
Member

Yes, updating now works. While browsing logs, I noticed this:

2026-06-12 00:00:00 localhost shepherd[1]: In call to 'fork+exec-command', value (list "/gnu/store/20kdmjdnfyyf97nd9vpzyava0rjpk840-qubes-update-check") of argument 'command' has the wrong type (it should match 'list-of-strings?').

@noufalkv

noufalkv commented Jun 29, 2026

Copy link
Copy Markdown
Author

@marmarek fixed. That fork+exec-command ... wrong type ... list-of-strings? was the update-check timer: its command must be a gexp that evaluates to a list of strings, but it was passed #~(list #$prog), so the literal symbol list ended up in argv. Changed to #~(#$prog) (commit d9b6fa8 on quality-refactor). Confirmed the timer now fires cleanly with no shepherd errors.

RPMs (normal + minimal), built from 567f3ae:
https://github.com/noufalkv/qubes-template-guix/releases/tag/test-rpms-567f3ae-202606261958

  • qubes-template-guix-4.3.0-tmr06261853.noarch.rpm
    SHA256 a0cdda4a8e83d8c392e3b107d502cfbd45a6f20c20590fb90cc855cf88a55a25
  • qubes-template-guix-minimal-4.3.0-tmr06261853.noarch.rpm
    SHA256 14a12c7c4be40d59f917d516f8eb6cc2eef22d86024b5d7c91f7c801d88b8ba5

marmarek added a commit to QubesOS/openqa-tests-qubesos that referenced this pull request Jul 15, 2026
This is just enough to let some of the tests running.
See discussion at QubesOS/qubes-builderv2#245 for
details.
Autonomous AI contribution added 3 commits July 21, 2026 23:36
Keep Guix on the inherited distribution-scoped cache, omit the nonexistent key directory, and make the legacy key requirement conditional without relaxing it for other distributions. Ignore empty flavor aliases so the normal template uses the content-root fallback.

Exercise the real normal and minimal job graphs through Config.get_jobs(), including source-fetch ordering and the final environment contract.
Format the Guix template content path to the project 80-column Black policy.
@noufalkv

Copy link
Copy Markdown
Author

Final quality-pass evidence for this draft:

  • Builder v2 is at signed head b435807, with signed tag signed_tag_for_b435807a3158. Pipeline 2695415564 passed all 18 jobs (388 pytest executions), and the Qubes code-signing policy passed.
  • Codecov patch coverage is 100% (13/13). The red project aggregate is a runner-instrumentation issue: four Fedora 44/Python 3.14.6 shards use pytest-cov 7, which no longer records subprocesses without explicit coverage configuration; those shards passed their tests but uploaded near-empty reports for unchanged code.
  • The companion branch is at signed head 879ac7d. make check, make lint, live pins, workflow parsing, and diff checks passed. Its exact 69-record tree matched locally and on the dev VM at SHA-256 95bf5bcf462c7a04cb15f23f05b282d888285edff7558780f71ae6bc0ab03da5; VM make source-check passed all 37 Python tests and the shell contracts.
  • Substitute-cache run 29884919123, attempt 2 passed against authenticated Guix ada0f308: both system variants built, 307 signed paths (362 MiB) were published, artifact 8517334401 uploaded, and Pages deployed successfully. The live nix-cache-info returns HTTP 200.

I am keeping the PR in draft. Public-branch RPM rebuilds, qvm-template lifecycle coverage, current-head openQA/Qubes runtime testing, and end-to-end updates-proxy validation remain publication gates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants