| … | … | @@ -49,6 +49,22 @@ function archLabel (platform, arch) { |
| 49 | 49 | return (osMap[platform] || platform) + '-' + (archMap[arch] || arch) |
| 50 | 50 | } |
| 51 | 51 | |
| 52 | const KNOWN_FLAGS = new Set(['root', 'os', 'arch', 'out', 'stage-out', 'extra-arch']) |
| 53 | for (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 | |
| 52 | 68 | const OS = getFlag('os', os.platform()) |
| 53 | 69 | const ARCH = getFlag('arch', os.arch()) |
| 54 | 70 | const LABEL = archLabel(OS, ARCH) |
| … | … | @@ -73,6 +89,18 @@ console.log(' out: ' + path.join(OUT_DIR, TARBALL_NAME)) |
| 73 | 89 | |
| 74 | 90 | // ── Verify bare binary exists for target arch ── |
| 75 | 91 | |
| 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. |
| 99 | const EXTRA_ARCHES = (getFlag('extra-arch', '') || '') |
| 100 | .split(',').map((a) => a.trim()).filter(Boolean) |
| 101 | const EXTRA_LABELS = EXTRA_ARCHES.map((a) => archLabel(OS, a)).filter((l) => l !== LABEL) |
| 102 | const ALL_LABELS = [LABEL, ...EXTRA_LABELS] |
| 103 | |
| 76 | 104 | const BARE_ARCH_PKG = `bare-runtime-${LABEL}` |
| 77 | 105 | const BARE_BIN_SRC = path.join(ROOT, 'node_modules', BARE_ARCH_PKG, 'bin', 'bare') |
| 78 | 106 | if (!fs.existsSync(BARE_BIN_SRC)) { |
| … | … | @@ -205,14 +233,14 @@ console.log(' smart-nested: removed ' + nestedStats.dupsRemoved + ' hoist-dup |
| 205 | 233 | const afterNested = du(path.join(stageRoot, 'node_modules')) |
| 206 | 234 | console.log(' after smart-nested-dup prune: ' + fmtBytes(afterNested)) |
| 207 | 235 | |
| 208 | | prunePrebuilds(path.join(stageRoot, 'node_modules'), LABEL) |
| 236 | prunePrebuilds(path.join(stageRoot, 'node_modules'), ALL_LABELS) |
| 209 | 237 | const afterPrebuilds = du(path.join(stageRoot, 'node_modules')) |
| 210 | 238 | console.log(' after prebuilds prune: ' + fmtBytes(afterPrebuilds)) |
| 211 | 239 | |
| 212 | 240 | // Prune non-target bare-runtime per-arch packages. We only need the |
| 213 | 241 | // target's binary; the others (bare-runtime-darwin-arm64, etc.) are |
| 214 | 242 | // pulled by npm as optional deps but useless on this bundle. |
| 215 | | pruneOtherBareArches(path.join(stageRoot, 'node_modules'), LABEL) |
| 243 | pruneOtherBareArches(path.join(stageRoot, 'node_modules'), ALL_LABELS) |
| 216 | 244 | const afterArches = du(path.join(stageRoot, 'node_modules')) |
| 217 | 245 | console.log(' after bare-runtime arch prune: ' + fmtBytes(afterArches)) |
| 218 | 246 | |
| … | … | @@ -230,6 +258,27 @@ fs.copyFileSync(BARE_BIN_SRC, bareBinDest) |
| 230 | 258 | fs.chmodSync(bareBinDest, 0o755) |
| 231 | 259 | console.log(' + runtime/bare (' + fmtBytes(fs.statSync(bareBinDest).size) + ')') |
| 232 | 260 | |
| 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. |
| 264 | for (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 | |
| 233 | 282 | // ── Shell launcher at bin/<BIN_NAME> + entry preserved as .bare.js ── |
| 234 | 283 | // |
| 235 | 284 | // install.sh symlinks ~/.local/bin/<BIN_NAME> -> <bootstrap>/bin/<BIN_NAME>, |
| … | … | @@ -269,7 +318,12 @@ const launcherScript = `#!/bin/sh |
| 269 | 318 | # (or wherever install.sh placed it) still finds the bundled runtime. |
| 270 | 319 | HERE="$(cd "$(dirname "$(readlink -f "$0" || echo "$0")")" >/dev/null 2>&1 && pwd)" |
| 271 | 320 | ROOT="$(cd "$HERE/.." >/dev/null 2>&1 && pwd)" |
| 272 | | exec "$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. |
| 323 | HOSTLABEL="$(uname -s | tr 'A-Z' 'a-z')-$(uname -m | sed 's/x86_64/x64/; s/aarch64/arm64/')" |
| 324 | BARE="$ROOT/by-arch/$HOSTLABEL/runtime/bare" |
| 325 | [ -x "$BARE" ] || BARE="$ROOT/runtime/bare" |
| 326 | exec "$BARE" "$ROOT/${entryFromRoot}" "$@" |
| 273 | 327 | ` |
| 274 | 328 | fs.writeFileSync(launcherPath, launcherScript) |
| 275 | 329 | fs.chmodSync(launcherPath, 0o755) |
| … | … | @@ -358,16 +412,49 @@ try { |
| 358 | 412 | // dir, rolling one .bak.prev (mirrors scripts/deploy-bootstrap.sh). Lets |
| 359 | 413 | // `pgh publish` keep the curl-installable tarballs fresh in a single step. |
| 360 | 414 | const 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 }, ... ] } |
| 419 | const EMIT_MANIFEST = getFlag('emit-manifest', null) |
| 420 | const deployedArtifacts = [] |
| 361 | 421 | if (DEPLOY_DIR) { |
| 362 | 422 | const dir = path.resolve(DEPLOY_DIR.replace(/^~(?=$|\/)/, os.homedir())) |
| 363 | 423 | fs.mkdirSync(dir, { recursive: true }) |
| 364 | 424 | 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) { |
| 366 | 438 | if (!fs.existsSync(src)) continue |
| 367 | 439 | if (fs.existsSync(dst)) fs.renameSync(dst, dst + '.bak.prev') |
| 368 | 440 | fs.copyFileSync(src, dst) |
| 441 | deployedArtifacts.push({ name, path: dst }) |
| 369 | 442 | } |
| 370 | 443 | 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 | |
| 449 | if (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) |
| 371 | 458 | } |
| 372 | 459 | |
| 373 | 460 | // ── Helpers ── |
| … | … | @@ -473,7 +560,8 @@ function pruneForceStrip (topNodeModules) { |
| 473 | 560 | // The fix there resolves a nested copy against its NEAREST node_modules |
| 474 | 561 | // ancestor, not just the bundle root — see that module for why. |
| 475 | 562 | |
| 476 | | function prunePrebuilds (rootDir, targetLabel) { |
| 563 | function prunePrebuilds (rootDir, targetLabels) { |
| 564 | const keep = new Set([].concat(targetLabels)) |
| 477 | 565 | function visit (dir) { |
| 478 | 566 | let entries |
| 479 | 567 | try { entries = fs.readdirSync(dir) } catch { return } |
| … | … | @@ -484,7 +572,7 @@ function prunePrebuilds (rootDir, targetLabel) { |
| 484 | 572 | if (!stat.isDirectory() || stat.isSymbolicLink()) continue |
| 485 | 573 | if (name === 'prebuilds') { |
| 486 | 574 | 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 }) |
| 488 | 576 | } |
| 489 | 577 | } else { |
| 490 | 578 | visit(p) |
| … | … | @@ -494,9 +582,10 @@ function prunePrebuilds (rootDir, targetLabel) { |
| 494 | 582 | visit(rootDir) |
| 495 | 583 | } |
| 496 | 584 | |
| 497 | | function pruneOtherBareArches (topNodeModules, keepLabel) { |
| 585 | function pruneOtherBareArches (topNodeModules, keepLabels) { |
| 586 | const keep = new Set([].concat(keepLabels).map((l) => 'bare-runtime-' + l)) |
| 498 | 587 | 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)) { |
| 500 | 589 | fs.rmSync(path.join(topNodeModules, name), { recursive: true, force: true }) |
| 501 | 590 | } |
| 502 | 591 | } |