Commit b130508c by PLN (Algolia)

fix(portable): the rig stops assuming it lives at /home/pln — and three tilde traps on the way

SRE #84 phases 1 and 2. Five files, ~30 load-bearing lines, and the point is
not tidiness: on a box where the account is not `pln`, SuperDirt booted
completely cleanly with every one of the three sample roots silently absent,
and the LCXL painter units exec-failed while rig-doctor reported the LCXL
section green. Two silent failures, each standing behind something that looked
like a pass.

**Phase 1 — the three sample roots and the two same-repo loads.**
start_and_midi.scd and sample_watch.scd held /home/pln as literal strings.
The fix is not a blind sed, because a raw tilde expands nowhere by itself.
Three traps, all verified on sclang 3.14.1 before writing a line:

  - `"~/x".standardizePath` keeps a trailing glob intact, so the loadSoundFiles
    calls can stay one-liners. Good.
  - `File.exists("~/x")` returns FALSE. A naive tilde substitution would have
    left the preload and sample-watcher guards syntactically perfect and
    permanently false — a dead preload with no error, which is worse than the
    hardcoded path it replaced.
  - `thisProcess.nowExecutingPath` is nil inside s.waitForBoot's closure. The
    two same-repo loads live in there, so the repo root is captured at TOP
    level into ~pvRepoRoot and used later. Reading it in place would have
    produced exactly the same dead-guard failure as the tilde.

Deriving the repo root from the file's own location beats a home-relative
guess: the rig now runs from any clone path, not merely any username.

**Phase 2 — the two outlier units.** lcxl3-driver.service and
lcxl-leds-watch.service baked WorkingDirectory and an absolute ExecStart,
where every sibling unit (midiviz, perf-tray, parvagues-bridge,
midi-autoconnect, tidal-ardour-autoroute) already used %h. Now they do too,
verified with systemd-analyze --user verify.

**Phase 3, unplanned — the fix broke the check that guards the fix.**
rig-doctor's check_scd_sample_roots reads the roots OUT of start_and_midi.scd
rather than hardcoding them, which is the right design and is why it caught
this at all. But it then called Path.is_dir() on the extracted literal, and
Python does not expand a tilde either — the same trap as File.exists, one
language over. So the portability fix turned three PASSes into three false
FAILs on the box where all three roots exist. That is how a check gets
switched off. It now expands before testing, and reports the resolved path so
the reader can see what was actually stat'ed.

Validation: both .scd files compile (File.readAllString(f).compile, which
parses without executing); both units pass systemd-analyze verify; rig-doctor
goes from 3 fail / 36 pass to 0 fail / 39 pass with the same verdict logic;
901 passed, 2 skipped across the suite.

The lesson for the log is the third phase, not the first two. A checker that
derives its expectations from the source it checks is strictly better than one
with hardcoded copies — and it still shares the source's bugs, one runtime
removed.
parent 1d3bb807
......@@ -27,11 +27,14 @@
// proves the bank is playable.
(
// Tilde paths, standardized explicitly: File.exists() and the glob helpers do
// NOT expand a raw "~" (verified), so the tilde must be resolved here or every
// root silently reads as absent.
~pvSampleRoots = [
"/home/pln/.local/share/SuperCollider/downloaded-quarks/Dirt-Samples",
"/home/pln/Work/Sound/Samples/extra",
"/home/pln/Work/Sound/Samples/tidal-drum-machines/machines"
];
"~/.local/share/SuperCollider/downloaded-quarks/Dirt-Samples",
"~/Work/Sound/Samples/extra",
"~/Work/Sound/Samples/tidal-drum-machines/machines"
].collect(_.standardizePath);
~pvBankCount = { ~dirt.soundLibrary.buffers.size };
......
......@@ -2,6 +2,16 @@
var on, off, cc;
var osc;
// Repo root, resolved from THIS file's own location so the rig runs from any
// clone path, under any username. Captured HERE, at top level, on purpose:
// thisProcess.nowExecutingPath is nil inside s.waitForBoot's closure (verified
// on sclang 3.14.1), so reading it down there yields nothing and every
// File.exists below silently goes false — a dead preload with no error.
// The fallback keeps hand-evaluation in an editor working.
~pvRepoRoot = if(thisProcess.nowExecutingPath.notNil,
{ thisProcess.nowExecutingPath.dirname },
{ "~/Work/Sound/Tidal".standardizePath });
// Send OSC messages to Hydra
//var hydra = NetAddr.new("127.0.0.1", 3333);
......@@ -202,11 +212,11 @@ s.waitForBoot {
~dirt.doNotReadYet = true; // Lazy-loading https://club.tidalcycles.org/t/superdirt-lazy-samples-loading/3148
//~dirt.loadSoundFiles; // load samples (path containing a wildcard can be passed in)
// for example: ~dirt.loadSoundFiles("/Users/myUserName/Dirt/samples/*");
~dirt.loadSoundFiles("/home/pln/.local/share/SuperCollider/downloaded-quarks/Dirt-Samples/*");
~dirt.loadSoundFiles("/home/pln/Work/Sound/Samples/extra/*");
~dirt.loadSoundFiles("~/.local/share/SuperCollider/downloaded-quarks/Dirt-Samples/*".standardizePath);
~dirt.loadSoundFiles("~/Work/Sound/Samples/extra/*".standardizePath);
// DRUM MACHINES
~drumMachinesDir = PathName.new("/home/pln/Work/Sound/Samples/tidal-drum-machines/machines");
~drumMachinesDir = PathName.new("~/Work/Sound/Samples/tidal-drum-machines/machines".standardizePath);
~machines = ~drumMachinesDir.folders; //All drum machines
//*~machines = ['Linn9000','RolandTR909']; //Selected drum machines
......@@ -246,9 +256,9 @@ s.waitForBoot {
// Set-specific sample PRELOAD — generated by gig-up.sh (tools/setlist_samples.py)
// from the recent set's tracks, so those samples are warm instead of
// lazy-loading (crackle + xruns) on first play mid-gig. Absent = pure lazy.
if(File.exists("/home/pln/Work/Sound/Tidal/preload.scd"), {
if(File.exists(~pvRepoRoot +/+ "preload.scd"), {
"preload: warming the set's samples…".postln;
thisProcess.interpreter.executeFile("/home/pln/Work/Sound/Tidal/preload.scd");
thisProcess.interpreter.executeFile(~pvRepoRoot +/+ "preload.scd");
}, {
"preload: no preload.scd — lazy-loading samples on demand.".postln;
});
......@@ -327,8 +337,8 @@ s.waitForBoot {
// Sample watcher responder: lets tools/sample-watcher.py register a newly
// dropped pack into THIS running rig, no reboot. Non-essential — if the
// file is missing the rig boots exactly as before.
if(File.exists("/home/pln/Work/Sound/Tidal/sample_watch.scd")) {
"/home/pln/Work/Sound/Tidal/sample_watch.scd".load;
if(File.exists(~pvRepoRoot +/+ "sample_watch.scd")) {
(~pvRepoRoot +/+ "sample_watch.scd").load;
} {
"pv: sample_watch.scd not found — watcher responder not armed.".postln;
};
......
[Unit]
Description=LCXL LED watcher (track-aware surface paint, #78/#85)
Documentation=file:///home/pln/Work/Sound/Tidal/tools/lcxl-leds.py
Documentation=file://%h/Work/Sound/Tidal/tools/lcxl-leds.py
# The LCXL is hot-pluggable and often absent at boot. Rather than bind to a
# device unit (whose name changes with the USB topology), this unit simply
# retries forever. Plug the board in and it lights up within RestartSec.
......@@ -14,14 +14,14 @@ StartLimitIntervalSec=0
[Service]
Type=simple
WorkingDirectory=/home/pln/Work/Sound/Tidal
WorkingDirectory=%h/Work/Sound/Tidal
# Python BLOCK-buffers stdout when it is a pipe, and under systemd it always is.
# So this unit produced no journal output beyond systemd's own lines, and its
# track-change diagnostics never reached the journal — which is why take 94 could
# not be explained from the logs even though the watcher ran throughout it. A
# daemon whose diagnostics sit unflushed in a 4 KB buffer is a daemon with none.
Environment=PYTHONUNBUFFERED=1
ExecStart=/usr/bin/python3 /home/pln/Work/Sound/Tidal/tools/lcxl-leds.py --watch -q
ExecStart=/usr/bin/python3 %h/Work/Sound/Tidal/tools/lcxl-leds.py --watch -q
Restart=always
RestartSec=10
......
[Unit]
Description=LCXL3 translation driver (v3 DAW protocol -> v2 corpus CCs + paint + OLED)
Documentation=file:///home/pln/Work/Sound/Tidal/tools/lcxl3-driver.py
Documentation=file://%h/Work/Sound/Tidal/tools/lcxl3-driver.py
# Born 2026-09-05, the night the LCXL3 sat untranslated while every check was
# green: the driver only ran when a session remembered to start it, and the
# session that started it got reaped. Faders -> Ardour gains live on this path;
......@@ -20,7 +20,7 @@ Conflicts=lcxl-leds-watch.service
[Service]
Type=simple
WorkingDirectory=/home/pln/Work/Sound/Tidal
WorkingDirectory=%h/Work/Sound/Tidal
# Python block-buffers stdout under systemd; without this the journal shows
# nothing and the driver's diagnostics die unflushed in a 4 KB buffer.
Environment=PYTHONUNBUFFERED=1
......@@ -28,7 +28,7 @@ Environment=PYTHONUNBUFFERED=1
# so paint and OLED labels track what is actually loaded instead of a name
# frozen at unit start. --relative was settled with PLN 2026-08-29 (the driver
# integrates encoder deltas and owns the values — pot-pickup cannot exist).
ExecStart=/usr/bin/python3 /home/pln/Work/Sound/Tidal/tools/lcxl3-driver.py --relative --paint
ExecStart=/usr/bin/python3 %h/Work/Sound/Tidal/tools/lcxl3-driver.py --relative --paint
Restart=always
RestartSec=5
......
......@@ -466,9 +466,16 @@ def check_scd_sample_roots() -> None:
for lit in literals:
checkdir = lit[:-1] if lit.endswith("*") else lit # strip glob
checkdir = checkdir.rstrip("/") or "/"
# A leading "~" is expanded by NOBODY for free: not by Path.is_dir()
# here, and not by SuperCollider's File.exists() either (verified).
# start_and_midi.scd resolves its roots with .standardizePath, so this
# check must expand too — otherwise the very fix that made the rig
# portable makes this check report three false FAILs on a box where
# all three roots are present, and a check that cries wolf gets ignored.
checkdir = os.path.expanduser(checkdir)
exists = Path(checkdir).is_dir()
if exists:
add("SAMPLES", f"root: {lit}", PASS, "exists")
add("SAMPLES", f"root: {lit}", PASS, f"exists ({checkdir})")
elif lit.startswith("/home/") and not lit.startswith(home):
other_user = lit.split("/")[2] if len(lit.split("/")) > 2 else "?"
add("SAMPLES", f"root: {lit}", FAIL,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment