Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 }
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <run-id> --log-failed # only the failing steps
gh run rerun <run-id> --failed # retry just the failed jobs
gh workflow run ci.yml --ref <branch> # manual dispatch
gh pr checks <pr> # 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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions profiles/audio/pulseaudio-xrdp.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading