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 } diff --git a/README.md b/README.md index ef71905..938c1c0 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,29 @@ 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. 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 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..70d216e 100644 --- a/profiles/audio/pulseaudio-xrdp.nix +++ b/profiles/audio/pulseaudio-xrdp.nix @@ -12,6 +12,31 @@ 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. 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: { + configureFlags = builtins.filter + (f: f != "--enable-mp3lame" && f != "--enable-opus") + old.configureFlags; + }); + }) + ]; + security.rtkit.enable = true; services.pulseaudio.enable = true;