From d50e36fbdb2107d47caeaed0b778a9d7bf3cfe4a Mon Sep 17 00:00:00 2001 From: D-Sector Hater Date: Mon, 7 Sep 2026 14:15:52 +0500 Subject: [PATCH 1/3] Fix CI compile error and unbreak Nix builds from Windows clones Two defects that keep this branch from working outside the cloud sandbox: 1. `.github/workflows/ci.yml` never compiled. A step's own `shell:` accepts no contexts at all, so `shell: ${{ matrix.shell }}` made GitHub reject the whole workflow: run 34096684564 failed in 0s with no jobs, and the workflow is still registered under its path instead of its `name:`. Job-level `defaults.run` does accept the matrix context, so the value moves there. Verified with actionlint, which flags the original and passes the fix. 2. There was no `.gitattributes`. Git for Windows defaults to core.autocrlf=true, so a Windows clone gets CRLF in every `.nix` file. Nix embeds those contents verbatim into derivation build scripts, bash then reads the CR as a command, and the build dies: qubix-openbox-rc.xml.drv: builder failed with exit code 127 > /build/.attr-...: line 1: $'\r': command not found That made `nix build .#spotibox-release` impossible from a Windows clone, and made the "manifest.json matches Nix" check report a false mismatch on every line. Normalising to LF in the working tree fixes both; the CMD entry points stay CRLF because cmd.exe needs it for multi-line blocks. After this, on a Windows host: flake check passes, the release bundle builds, manifest.json matches Nix, 42/42 controller checks pass under both PowerShell 5.1 and pwsh 7, and PSScriptAnalyzer is clean. Co-Authored-By: Claude Opus 5 --- .gitattributes | 11 +++++++++++ .github/workflows/ci.yml | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..905b17e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Nix embeds the contents of these files verbatim into derivation build +# scripts. A CR that survives into such a script reaches bash as a literal +# $'\r' command and the build dies with exit code 127, so a Windows clone made +# with the Git default core.autocrlf=true cannot build the images at all. +# Normalise everything to LF in the working tree, not just in the blobs. +* text=auto eol=lf + +# The CMD entry points are the exception: cmd.exe parses multi-line blocks +# reliably only with CRLF. +*.cmd text eol=crlf +*.bat text eol=crlf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36d19c1..558eaaf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,11 @@ jobs: # Windows PowerShell 5.1 is what qubix-up.cmd runs; pwsh is the # developer console. Both must stay happy. shell: [powershell, pwsh] + defaults: + run: + # A step's own `shell:` accepts no contexts at all, so the matrix + # value has to arrive through the job defaults instead. + shell: ${{ matrix.shell }} steps: - uses: actions/checkout@v4 @@ -51,7 +56,6 @@ jobs: if ($results.Count -gt 0) { exit 1 } - name: unit checks - shell: ${{ matrix.shell }} run: | & ./tests/qubixctl.Tests.ps1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } From 9d80ce099c58445a295f0f1d1749dbd74bb4b7b1 Mon Sep 17 00:00:00 2001 From: D-Sector Hater Date: Mon, 7 Sep 2026 17:31:02 +0500 Subject: [PATCH 2/3] Build xrdp without MP3/Opus so mstsc actually plays the audio Audio redirection was silent on Windows for a reason that hides well: nixpkgs builds xrdp with `--enable-mp3lame` and `--enable-opus`, mstsc then negotiates WAVE_FORMAT_MPEGLAYER3, and drops the stream. Everything inside the guest looks healthy while this happens, which is why it reads as a configuration problem and is not one: - pulseaudio xrdp-sink loaded - pulseaudio xrdp-sink set as default Connected ok fd 17 sound_process_training: round trip time 0 Socket 19: connection accepted from AF_UNIX The sink is default, unmuted, at 100%, and cycles IDLE -> RUNNING in step with the track. Only the host is silent, and the Windows mixer shows an mstsc slider that never moves. chansrv names the culprit outright: sound_process_output_format: wFormatNo 2 wFormatTag WAVE_FORMAT_MPEGLAYER3 Dropping both encoders leaves PCM, which every RDP client decodes; ~176 kB/s is nothing beside the video channel this appliance already pushes. The override is an overlay rather than `services.xrdp.package`, because that option does not work: the NixOS module declares it, then hardcodes `pkgs.xrdp` in the ExecStart of xrdp.service and xrdp-sesman.service. Setting the option rebuilds confDir only, and the daemons keep running the unmodified build -- confirmed on the host, where the override built as expected and the guest still executed /nix/store/1fsy7...-xrdp. Worth reporting upstream separately. Verified end to end on a Windows 11 host with Hyper-V: `nix flake check` passes, the rebuilt xrdp links neither libmp3lame nor libopus, xrdp-sesman resolves to the overridden package, chansrv offers WAVE_FORMAT_PCM only with zero MPEGLAYER3 lines in its log, and audio is audible in the mstsc window. README gains a Continuous Integration section (triggers, jobs, gh commands, and the actionlint tip for workflows GitHub refuses to compile) plus a "Why PCM-only audio" design note. Co-Authored-By: Claude Opus 5 --- README.md | 59 +++++++++++++++++++++++++++++- profiles/audio/pulseaudio-xrdp.nix | 23 ++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ef71905..67ab277 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,9 @@ WSL is not required on the host. It stays available as the developer loop `tools/qubix-up.cmd`, the double-click launcher that elevates itself. - GitHub Actions: `ci` (flake check, manifest drift, PowerShell lint + unit checks) and `release` (builds and attaches the images to a tagged release). -- A stable xrdp audio baseline using PulseAudio, not PipeWire. +- A stable xrdp audio baseline using PulseAudio, not PipeWire, and xrdp built + without the MP3/Opus encoders so that mstsc negotiates PCM and actually + plays it (see *Why PCM-only audio*). - Separate `user` and `rdp` accounts (pinned UIDs) to avoid session cross-contamination. - A NixOS smoke test for users, xrdp, Spotify, Openbox, Avahi and the kiosk @@ -135,6 +137,40 @@ C:\HyperV\Qubix\ - User IDs are pinned (`user` = 1000, `rdp` = 1001) so a persistent home never changes owner when the user list changes. +## Continuous Integration + +`ci` runs on pull requests, on pushes to `main`, and on manual dispatch. It +does **not** run on a push to a feature branch, so work on a branch stays +unchecked until a PR exists - open one early if you want the signal. + +| Job | Runner | What it does | +| --- | --- | --- | +| `flake check, manifest sync, home seed` | ubuntu | `nix flake check`, fails on `manifest.json` drift, builds the home seed | +| `controller lint + unit checks (powershell)` | windows | unit checks under Windows PowerShell 5.1, the shell `qubix-up.cmd` actually uses | +| `controller lint + unit checks (pwsh)` | windows | the same checks under pwsh 7, plus PSScriptAnalyzer | + +Driving it from the terminal with the GitHub CLI: + +```bash +gh run list --limit 10 # recent runs, newest first +gh run watch # follow the run for the current branch +gh run view --log-failed # only the failing steps +gh run rerun --failed # retry just the failed jobs +gh workflow run ci.yml --ref # manual dispatch +gh pr checks # per-check status for a PR +``` + +A run that fails in 0s with no jobs, and a workflow listed under its file path +instead of its `name:`, means GitHub could not compile the YAML - the run log +is empty in that case, so lint locally instead: + +```bash +nix run nixpkgs#actionlint +``` + +`actionlint` catches the whole class of errors GitHub reports only as a failed +run, such as using a context where none is allowed. + ## Publishing A Release ```bash @@ -296,6 +332,27 @@ PipeWire -> disabled EasyEffects is intentionally not the active DSP baseline here. It is PipeWire-oriented, while this xrdp audio path expects PulseAudio. +### Why PCM-only audio + +nixpkgs builds xrdp with `--enable-mp3lame` and `--enable-opus`. With those +available, Windows' `mstsc` negotiates `WAVE_FORMAT_MPEGLAYER3` and then plays +nothing at all, while every diagnostic inside the guest looks perfectly +healthy: `xrdp-sink` is the default sink, it is not muted, it sits at 100%, it +moves between IDLE and RUNNING in time with the track, chansrv accepts the +socket and logs `round trip time 0`. Only the host is silent, and the Windows +volume mixer shows the mstsc slider with no level on it. See +[neutrinolabs/xrdp#965](https://github.com/neutrinolabs/xrdp/issues/965). + +`profiles/audio/pulseaudio-xrdp.nix` therefore drops both encoders, which +leaves PCM as the only negotiable format. PCM is ~176 kB/s - irrelevant next to +the video channel. + +The override has to be a `nixpkgs.overlays` entry rather than the obvious +`services.xrdp.package`. The NixOS module declares that option but then +hardcodes `pkgs.xrdp` in the `ExecStart` of both `xrdp.service` and +`xrdp-sesman.service`, so setting it rebuilds `confDir` only and the daemons +keep running the untouched build - the option silently does nothing. + ### Nix-Generated JSON Nix is the source of truth for the manifest. `manifest.json` is the output of diff --git a/profiles/audio/pulseaudio-xrdp.nix b/profiles/audio/pulseaudio-xrdp.nix index 5b0c2e1..b99eca9 100644 --- a/profiles/audio/pulseaudio-xrdp.nix +++ b/profiles/audio/pulseaudio-xrdp.nix @@ -12,6 +12,29 @@ lib.mkIf (config.qubix.audio == "pulseaudio-xrdp") { # profile only wires the audio path into it. services.xrdp.audio.enable = true; + # mstsc negotiates WAVE_FORMAT_MPEGLAYER3 whenever xrdp offers it and then + # plays nothing at all. Everything inside the guest looks healthy while this + # happens - the sink runs, it is not muted, chansrv accepts the socket and + # reports a round trip time - but the host stays silent, because mstsc simply + # drops the MP3 stream (neutrinolabs/xrdp#965). Building xrdp without the MP3 + # and Opus encoders leaves PCM as the only negotiable format, and every RDP + # client decodes that. PCM costs ~176 kB/s, which is nothing next to the + # video channel this appliance already pushes. + # + # This must be an overlay, not services.xrdp.package: the NixOS xrdp module + # declares that option but hardcodes pkgs.xrdp in the ExecStart lines of both + # xrdp.service and xrdp-sesman.service, so setting the option rebuilds only + # confDir while the daemons keep running the unmodified build. + nixpkgs.overlays = [ + (_final: prev: { + xrdp = prev.xrdp.overrideAttrs (old: { + configureFlags = builtins.filter + (f: f != "--enable-mp3lame" && f != "--enable-opus") + old.configureFlags; + }); + }) + ]; + security.rtkit.enable = true; services.pulseaudio.enable = true; From d84cb92abfde5fa2be084b6f042782f592256370 Mon Sep 17 00:00:00 2001 From: D-Sector Hater Date: Mon, 7 Sep 2026 17:44:45 +0500 Subject: [PATCH 3/3] Point at the upstream fix for the services.xrdp.package bug The overlay here works around a nixpkgs bug, and that bug already has an open fix: NixOS/nixpkgs#452303, "nixos/xrdp: actually use cfg.package parameter", two approvals, unmerged since October 2025. It covers one more call site than this repo had found -- xrdp-keygen in ExecStartPre -- besides the two ExecStart lines. Record the link in both the profile and the design note so that whoever revisits this can drop the overlay for a plain services.xrdp.package assignment once the fix lands, instead of rediscovering why it is an overlay at all. Co-Authored-By: Claude Opus 5 --- README.md | 4 +++- profiles/audio/pulseaudio-xrdp.nix | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 67ab277..938c1c0 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,9 @@ The override has to be a `nixpkgs.overlays` entry rather than the obvious `services.xrdp.package`. The NixOS module declares that option but then hardcodes `pkgs.xrdp` in the `ExecStart` of both `xrdp.service` and `xrdp-sesman.service`, so setting it rebuilds `confDir` only and the daemons -keep running the untouched build - the option silently does nothing. +keep running the untouched build - the option silently does nothing. A fix is open +upstream as [nixpkgs#452303](https://github.com/NixOS/nixpkgs/pull/452303); when it +lands, this overlay can become a plain `services.xrdp.package` assignment. ### Nix-Generated JSON diff --git a/profiles/audio/pulseaudio-xrdp.nix b/profiles/audio/pulseaudio-xrdp.nix index b99eca9..70d216e 100644 --- a/profiles/audio/pulseaudio-xrdp.nix +++ b/profiles/audio/pulseaudio-xrdp.nix @@ -24,7 +24,9 @@ lib.mkIf (config.qubix.audio == "pulseaudio-xrdp") { # This must be an overlay, not services.xrdp.package: the NixOS xrdp module # declares that option but hardcodes pkgs.xrdp in the ExecStart lines of both # xrdp.service and xrdp-sesman.service, so setting the option rebuilds only - # confDir while the daemons keep running the unmodified build. + # confDir while the daemons keep running the unmodified build. A fix is + # already open upstream as https://github.com/NixOS/nixpkgs/pull/452303; + # once it lands this can go back to a plain services.xrdp.package assignment. nixpkgs.overlays = [ (_final: prev: { xrdp = prev.xrdp.overrideAttrs (old: {