OverviewFilesCommitsIssuesDocsReleasesPeers

pgh-sidecar commits / f0ae796

0.2.59: sync build-bootstrap-bare.js from pgh + reject unknown flags
0.2.58 published a SINGLE-arch tree despite publish.js reporting "multi-arch: also staging arm64". publish.js prefers the app's OWN scripts/build-bootstrap-bare.js over pgh's shared copy, and this app shipped a Jun 6 verbatim copy with no --extra-arch support. It accepted the flag, ignored it, and reported success. Each bootstrap app carrying its own copy of the builder means a fix to pgh's copy does not propagate. The diff here was exactly the 89 lines added to pgh's copy, nothing else, so the two were hand-synced verbatim. Both copies now reject unknown flags. A stale builder silently dropping a new flag yields a confidently wrong artifact — which is how a --extra-arch build produced a tree that looked multi-arch, passed every check, and could not run on the target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f0ae796107a67d913b0bd131d4984ed8a61f3e17
by plankatron <fleeky@gmail.com> · 2026-08-25T20:35:43.000Z

Changed files (2) +98 -9

package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pgh-sidecar",
3 "version": "0.2.58",
3 "version": "0.2.59",
44 "description": "Small optional user-level daemon for pgh \u2014 shared identity vault, OTA polling, device linking, bootstrap seed, NameRegistry host",
55 "type": "module",
66 "main": "lib/index.js",
scripts/build-bootstrap-bare.js +97 -8
……@@ -49,6 +49,22 @@ function archLabel (platform, arch) {
4949 return (osMap[platform] || platform) + '-' + (archMap[arch] || arch)
5050}
5151
52const KNOWN_FLAGS = new Set(['root', 'os', 'arch', 'out', 'stage-out', 'extra-arch'])
53for (let i = 2; i < process.argv.length; i++) {
54 const a = process.argv[i]
55 if (!a.startsWith('--')) continue
56 const name = a.slice(2).split('=')[0]
57 if (!KNOWN_FLAGS.has(name)) {
58 // Loud on purpose. These builders are COPIED per bootstrap app, so a stale
59 // copy silently ignoring a new flag yields a confidently wrong artifact —
60 // exactly how a --extra-arch build produced a single-arch tree that still
61 // reported success.
62 console.error('error: unknown flag ' + a + ' — this build-bootstrap-bare.js is probably stale.')
63 console.error(' Known flags: --' + [...KNOWN_FLAGS].join(' --'))
64 process.exit(1)
65 }
66}
67
5268const OS = getFlag('os', os.platform())
5369const ARCH = getFlag('arch', os.arch())
5470const LABEL = archLabel(OS, ARCH)
……@@ -73,6 +89,18 @@ console.log(' out: ' + path.join(OUT_DIR, TARBALL_NAME))
7389
7490// ── Verify bare binary exists for target arch ──
7591
92// Additional arches staged under by-arch/<label>/runtime/bare. The install
93// side already prefers by-arch/<host>/runtime/bare (lib/runtime-factory.js) and
94// skips foreign subtrees when materialising (lib/store.js), so a multi-arch
95// tree costs a remote host nothing to install — but WITHOUT it an x64 publisher
96// can only ever produce x64-runnable installs, because both the runtime binary
97// and the native prebuilds are pruned to the build host. That is why an aarch64
98// device could fetch pgh-sidecar and still not run it.
99const EXTRA_ARCHES = (getFlag('extra-arch', '') || '')
100 .split(',').map((a) => a.trim()).filter(Boolean)
101const EXTRA_LABELS = EXTRA_ARCHES.map((a) => archLabel(OS, a)).filter((l) => l !== LABEL)
102const ALL_LABELS = [LABEL, ...EXTRA_LABELS]
103
76104const BARE_ARCH_PKG = `bare-runtime-${LABEL}`
77105const BARE_BIN_SRC = path.join(ROOT, 'node_modules', BARE_ARCH_PKG, 'bin', 'bare')
78106if (!fs.existsSync(BARE_BIN_SRC)) {
……@@ -205,14 +233,14 @@ console.log(' smart-nested: removed ' + nestedStats.dupsRemoved + ' hoist-dup
205233const afterNested = du(path.join(stageRoot, 'node_modules'))
206234console.log(' after smart-nested-dup prune: ' + fmtBytes(afterNested))
207235
208prunePrebuilds(path.join(stageRoot, 'node_modules'), LABEL)
236prunePrebuilds(path.join(stageRoot, 'node_modules'), ALL_LABELS)
209237const afterPrebuilds = du(path.join(stageRoot, 'node_modules'))
210238console.log(' after prebuilds prune: ' + fmtBytes(afterPrebuilds))
211239
212240// Prune non-target bare-runtime per-arch packages. We only need the
213241// target's binary; the others (bare-runtime-darwin-arm64, etc.) are
214242// pulled by npm as optional deps but useless on this bundle.
215pruneOtherBareArches(path.join(stageRoot, 'node_modules'), LABEL)
243pruneOtherBareArches(path.join(stageRoot, 'node_modules'), ALL_LABELS)
216244const afterArches = du(path.join(stageRoot, 'node_modules'))
217245console.log(' after bare-runtime arch prune: ' + fmtBytes(afterArches))
218246
……@@ -230,6 +258,27 @@ fs.copyFileSync(BARE_BIN_SRC, bareBinDest)
230258fs.chmodSync(bareBinDest, 0o755)
231259console.log(' + runtime/bare (' + fmtBytes(fs.statSync(bareBinDest).size) + ')')
232260
261// Extra arches live at by-arch/<label>/runtime/bare. runtime/bare stays the
262// BUILD host's copy so existing launchers, which hardcode that path, keep
263// working against an older tree.
264for (const label of EXTRA_LABELS) {
265 const src = path.join(ROOT, 'node_modules', 'bare-runtime-' + label, 'bin', 'bare')
266 if (!fs.existsSync(src)) {
267 // Hard failure: a silently-missing arch produces a by-arch/ tree that
268 // LOOKS multi-arch and still cannot run on that host.
269 console.error('error: --extra-arch ' + label + ' requested but ' + src + ' not found.')
270 console.error(' Vendor it first: npm pack bare-runtime-' + label + '@<pinned>')
271 console.error(' and extract into node_modules/bare-runtime-' + label + '/')
272 process.exit(1)
273 }
274 const dir = path.join(stageRoot, 'by-arch', label, 'runtime')
275 fs.mkdirSync(dir, { recursive: true })
276 const dest = path.join(dir, 'bare')
277 fs.copyFileSync(src, dest)
278 fs.chmodSync(dest, 0o755)
279 console.log(' + by-arch/' + label + '/runtime/bare (' + fmtBytes(fs.statSync(dest).size) + ')')
280}
281
233282// ── Shell launcher at bin/<BIN_NAME> + entry preserved as .bare.js ──
234283//
235284// install.sh symlinks ~/.local/bin/<BIN_NAME> -> <bootstrap>/bin/<BIN_NAME>,
……@@ -269,7 +318,12 @@ const launcherScript = `#!/bin/sh
269318# (or wherever install.sh placed it) still finds the bundled runtime.
270319HERE="$(cd "$(dirname "$(readlink -f "$0" || echo "$0")")" >/dev/null 2>&1 && pwd)"
271320ROOT="$(cd "$HERE/.." >/dev/null 2>&1 && pwd)"
272exec "$ROOT/runtime/bare" "$ROOT/${entryFromRoot}" "$@"
321# Prefer this host's arch when the tree carries several; fall back to the
322# build host's copy for single-arch trees.
323HOSTLABEL="$(uname -s | tr 'A-Z' 'a-z')-$(uname -m | sed 's/x86_64/x64/; s/aarch64/arm64/')"
324BARE="$ROOT/by-arch/$HOSTLABEL/runtime/bare"
325[ -x "$BARE" ] || BARE="$ROOT/runtime/bare"
326exec "$BARE" "$ROOT/${entryFromRoot}" "$@"
273327`
274328fs.writeFileSync(launcherPath, launcherScript)
275329fs.chmodSync(launcherPath, 0o755)
……@@ -358,16 +412,49 @@ try {
358412// dir, rolling one .bak.prev (mirrors scripts/deploy-bootstrap.sh). Lets
359413// `pgh publish` keep the curl-installable tarballs fresh in a single step.
360414const DEPLOY_DIR = getFlag('deploy-dir', null)
415// --emit-manifest <path>: after deploying, write a machine-readable list of
416// the deployed artifacts so `pgh publish` can register them into the daemon's
417// overlay + signed BootstrapLog (hyper-fleeky-dev #66). Shape:
418// { app, version, files: [ { name, path }, ... ] }
419const EMIT_MANIFEST = getFlag('emit-manifest', null)
420const deployedArtifacts = []
361421if (DEPLOY_DIR) {
362422 const dir = path.resolve(DEPLOY_DIR.replace(/^~(?=$|\/)/, os.homedir()))
363423 fs.mkdirSync(dir, { recursive: true })
364424 const base = path.basename(outFile)
365 for (const [src, dst] of [[outFile, path.join(dir, base)], [outFile + '.sha256', path.join(dir, base + '.sha256')]]) {
425
426 // install.sh travels with every bootstrap deploy so a fresh machine can
427 // `curl <host>/bootstrap/install.sh | sh`. Source = the app's own
428 // scripts/install.sh (already staged via INCLUDE). Without this, install.sh
429 // only ever reached the served dir by a manual copy — the #66 antipattern.
430 const installSrc = path.join(ROOT, 'scripts', 'install.sh')
431
432 const copies = [
433 [outFile, path.join(dir, base), base],
434 [outFile + '.sha256', path.join(dir, base + '.sha256'), base + '.sha256'],
435 [installSrc, path.join(dir, 'install.sh'), 'install.sh']
436 ]
437 for (const [src, dst, name] of copies) {
366438 if (!fs.existsSync(src)) continue
367439 if (fs.existsSync(dst)) fs.renameSync(dst, dst + '.bak.prev')
368440 fs.copyFileSync(src, dst)
441 deployedArtifacts.push({ name, path: dst })
369442 }
370443 console.log(' deployed -> ' + path.join(dir, base))
444 if (deployedArtifacts.find(a => a.name === 'install.sh')) {
445 console.log(' deployed -> ' + path.join(dir, 'install.sh'))
446 }
447}
448
449if (EMIT_MANIFEST) {
450 const dst = path.resolve(EMIT_MANIFEST.replace(/^~(?=$|\/)/, os.homedir()))
451 fs.mkdirSync(path.dirname(dst), { recursive: true })
452 fs.writeFileSync(dst, JSON.stringify({
453 app: PKG.name,
454 version: VERSION,
455 files: deployedArtifacts
456 }, null, 2))
457 console.log(' manifest -> ' + dst)
371458}
372459
373460// ── Helpers ──
……@@ -473,7 +560,8 @@ function pruneForceStrip (topNodeModules) {
473560// The fix there resolves a nested copy against its NEAREST node_modules
474561// ancestor, not just the bundle root — see that module for why.
475562
476function prunePrebuilds (rootDir, targetLabel) {
563function prunePrebuilds (rootDir, targetLabels) {
564 const keep = new Set([].concat(targetLabels))
477565 function visit (dir) {
478566 let entries
479567 try { entries = fs.readdirSync(dir) } catch { return }
……@@ -484,7 +572,7 @@ function prunePrebuilds (rootDir, targetLabel) {
484572 if (!stat.isDirectory() || stat.isSymbolicLink()) continue
485573 if (name === 'prebuilds') {
486574 for (const arch of fs.readdirSync(p)) {
487 if (arch !== targetLabel) fs.rmSync(path.join(p, arch), { recursive: true, force: true })
575 if (!keep.has(arch)) fs.rmSync(path.join(p, arch), { recursive: true, force: true })
488576 }
489577 } else {
490578 visit(p)
……@@ -494,9 +582,10 @@ function prunePrebuilds (rootDir, targetLabel) {
494582 visit(rootDir)
495583}
496584
497function pruneOtherBareArches (topNodeModules, keepLabel) {
585function pruneOtherBareArches (topNodeModules, keepLabels) {
586 const keep = new Set([].concat(keepLabels).map((l) => 'bare-runtime-' + l))
498587 for (const name of fs.readdirSync(topNodeModules)) {
499 if (name.startsWith('bare-runtime-') && name !== 'bare-runtime-' + keepLabel) {
588 if (name.startsWith('bare-runtime-') && !keep.has(name)) {
500589 fs.rmSync(path.join(topNodeModules, name), { recursive: true, force: true })
501590 }
502591 }