probe-platforms
GitHub在新操作系统(Windows/Linux)上启动测试探针或调试失败问题的技能。提供分步验证阶梯,规避系统差异陷阱,解决跨平台兼容性问题。
触发场景
安装
npx skills add nubjs/nub --skill probe-platforms -g -y
SKILL.md
Frontmatter
{
"name": "probe-platforms",
"description": "Bring a test harness or probe up on a NEW operating system (Windows or Linux), or debug one that fails there. Invoke before the first run on a platform, and whenever a harness works on one OS but not another. Carries the bring-up ladder that avoids spending hours learning single booleans, the Windows spawn\/path\/disk faults that each present as something else entirely, the Linux Landlock and node-layout traps, and the remote-shell mechanics (PowerShell-over-SSH quoting, pkill matching your own command) that waste a loop each time they are rediscovered."
}
Bringing a probe up on a new platform
A harness that works on one OS fails on another for reasons that are almost never the thing the error names. Expect a CHAIN of faults, each hidden by the one in front of it.
⛔ Debug with the SMALLEST payload — the single most expensive mistake here
Measured, bringing the build-jail probe up on Windows: six sequential faults, found using
puppeteer as the probe, at ~25 minutes per attempt. Every one was answerable by a question as
small as "does the binary spawn?" — seconds with is-odd. About two hours bought six booleans.
Climb this ladder in order. Do not skip to the end.
| # | Step | Proves |
|---|---|---|
| 1 | <binary> --version |
it exists, is executable, spawns |
| 2 | same, with the config/override env set | the feature is compiled in AND the override engages |
| 3 | install a trivial no-scripts package (is-odd) |
fixture, store, linker |
| 4 | one tiny package that DOES run a script | a child process spawns under confinement |
| 5 | the real canary (large, real download) | end-to-end — a FINAL GATE, never a debugging instrument |
Each step is seconds. Each isolates one layer. When one passes, expect the next to fail — do not read a green step as "the platform works."
The top fault usually accuses the wrong subsystem. On Windows the probe insisted the binary lacked a cargo feature; the truth four layers down was an unspawnable path, with every per-cell log at 0 bytes. When a diagnostic names a subsystem, verify that subsystem independently before acting on the accusation — and when you fix a misleading diagnostic, it pays for itself immediately.
Windows — all measured
-
for /fruns its command throughcmd /c, which STRIPS the outer quote pair when the string both starts and ends with a quote. So the natural spelling of a capture,for /f "usebackq delims=" %%i in (`"%EXE%" arg "%DIR%"`), degrades toC:\...\x.exe" arg "C:\...\dirand dies with "The filename, directory name, or volume label syntax is incorrect" — with or without spaces in the path. Fix: wrap the whole command in one MORE quote pair (`""%EXE%" arg "%DIR%""`). Measured against a temp-file-redirect alternative, which did not work. -
An undefined
%VAR%expands to its own literal text, so a missing variable is passed onward as the string%VAR%rather than being empty — silently handing a bogus path downstream. Guard withif not defined VAR set "VAR=%CD%", undersetlocalso the default does not leak to the caller. -
A
.cmdonPATHis invisible to whole classes of CI. In this reponative-deps.ymlis the only workflow touching node-gyp and it is ubuntu-only, so the Windows.cmdshim shipped broken and unnoticed for as long as another code path kept it from ever running. Green Windows jobs are not evidence for a Windows file nothing executes — check that some job actually runs it. -
Node aborts at startup inside nub's build jail on Windows:
Assertion failed: ncrypto::CSPRNG(nullptr, 0). Any jailed scenario whose script isnodetherefore cannot report on what it meant to test; skip it with the reason printed rather than recording a pass or a fail. -
spawnSynccannot runnpm/npx/pnpm. They are.cmdshims: the bare name gives ENOENT, the.cmdspelling gives EINVAL (Node has refused toCreateProcessa batch file since CVE-2024-27980).shell: trueworks but is DEP0190 — args are concatenated rather than escaped, so cmd.exe re-parses a spec like@scope/pkg@1.0.0. Run the bundled JS instead:node <path>/node_modules/npm/bin/npm-cli.js. -
Git Bash paths are not spawnable by native tools.
cd $(dirname) && pwdunder Git Bash yields/c/Users/…, handed to CreateProcess verbatim → ENOENT. Convert toC:\…; for any path passed to a Windows binary usecygpath -w./tmp/xis NOT where a Windowsnodewill look. -
A copied binary must keep its
.exe. Windows decides executability from the SUFFIX, so a content-addressed copy named by bare hash is ENOENT even at 1.2 GB and mode 0755. -
Removing a
node_modulestree fails with EPERM routinely — a lifecycle child still holds a handle, or an indexer/AV opens files behind you. UsemaxRetries/retryDelay, and never let a cleanup failure discard a measurement that already succeeded (a non-zero exit from tidy-up reads upstream as "no result"). -
Disk is a first-class hazard, and a full disk is SILENT AND EVIL. A debug Rust binary can be ~1.2 GB with a
target/of tens of GB; add leaked fixture trees and 80 GB vanishes. When it fills,git fetchfails and the probe runs STALE CODE while you believe you are testing the fix. Size 200 GB+, build--profile fastnotdebug, and check free space before trusting any run. Growing the GCE disk is not enough — the in-OS partition must be extended too, andResize-Partitioncan simply fail. -
Prefer a CI-built binary to provisioning MSVC. A
windows-latestworkflow that uploadstarget/debug/nub.exegives a real MSVC binary for one dispatch, versus installing the VSVCToolsworkload,cmake, and switching the rustup host triple by hand. Awindows-gnucross-compile is NOT a substitute when the thing under test is OS confinement behaviour. -
If you do build on the VM: rustup may default to
windows-gnu(nub needs-msvc), VS Build Tools can be present with no C++ workload so there is no linker at all, and/STACK:8388608is an MSVC-only flag that breaks a GNU link — it is required under MSVC because Windows gives the main thread 1 MB against Linux's 8 MB.
Windows filesystem speed — budget ~6x Linux, and beware the benchmark itself
MEASURED on matched GCE e2-standard-8 / pd-balanced VMs, 3,000 small file creates:
| time | |
|---|---|
| Linux (bash redirect) | 181 ms |
Windows ([System.IO.File]::WriteAllText) |
1,142 ms |
Windows (PowerShell Set-Content) |
3,156 ms |
~6x, not 17x. The first Windows figure was Set-Content, whose cmdlet/pipeline overhead is
roughly two-thirds of it — comparing that against a bash redirect is not varying one thing. Use a
raw write on both sides before quoting any cross-OS I/O ratio.
⛔ DO NOT EXTRAPOLATE THAT RATIO TO INSTALL TIME — I did, and it was wrong. A microbenchmark of raw file creation is not a workload. Measured with the SAME nub binary on matched VMs:
| install | Linux | Windows | ratio |
|---|---|---|---|
trivial (is-odd) |
493 ms | 744 ms | 1.5x |
file-heavier (typescript) |
1,675 ms | 3,218 ms | 1.9x |
So a real install is ~1.5–2x, not 6x: installs are dominated by network and archive work, and the file-create penalty is a minority of the total. I first wrote "budget an order of magnitude" here off the microbenchmark alone; that was a wrong planning number in a durable doc. Time the actual workload before sizing anything.
(Note the file COUNTS in that test are not comparable — Get-ChildItem -Recurse reported 9 where
find -L reported 264, because neither traverses junctions the same way. The TIMES are the
comparable part; see the junction-traversal trap above.)
Linux — all measured
- An AUTHORED Landlock grant naming a path that does not exist makes the whole policy
uncompilable — refused, not degraded (
PolicyNotExpressible). Speculative grants skip a missing path; authored ones abort. One bad entry breaks every confined run. - Do not read "Landlock unavailable" as a kernel problem. That error covers both an old kernel and our own policy failing to compile; it claimed a missing feature on a 6.17 kernel with ABI 4.
- A fresh cloud box has no system
node, and the Rust build needscmake. Provision Nodes with the tool under test where possible (dogfoods the real mechanism) — but its layout may be<cache>/nub/node/22.23.1/bin, with novprefix, where nvm usesv22.23.1. Anything parsing a version out of a node path must accept both or it silently returns null. - Absent tooling is environmental, not a defect.
pnpmmissing → a script shelling out to it exits 127; a-muslpackage on a glibc box cannot load. Neither is a bug in the thing under test. - Old Node pins bring old npm, and npm 6 races on its own
_cacacheunder concurrency — surfacing asrimraf: missing path+Callback called more than once. This manufactures FALSE defect verdicts that a double-control cannot catch, because both attempts sit in the same busy window. Re-verify any defect verdict serially once the batch drains.
Remote-shell mechanics that cost a loop every time
- SSH to Windows lands in PowerShell, not bash.
bash.exe -lc "…"must go through a.ps1or PowerShell parses-lcas an expression. Nested quoting throughssh … powershell -Command "…"breaks constantly — prefer a command containing no double quotes at all, orscpa script file (which fails on a full disk, so check that first). - Keep every PowerShell script ASCII. One em-dash anywhere fails with
The string is missing the terminator, pointing at the LAST line of the file rather than the offending one. pkill -f <pattern>matches YOUR OWN command line. Killing-f "search.mjs"from a shell whose command text containssearch.mjskills that shell — the remote command dies mid-way and returns no output, which reads like a hang. Split the kill into its own invocation, or break the literal ("sea""rch.mjs").- Detach long remote runs with
tmux new-session -d. A barenohup … &over SSH hangs the connection and can leave only the first job running. - Prefer native tools for bulk filesystem work.
du -shover Git Bash on a large Windows tree is glacial and will time out; PowerShell equivalents finish.
版本历史
- c10a266 当前 2026-08-04 22:52


