diff --git a/.github/workflows/browser-demos-pages.yml b/.github/workflows/browser-demos-pages.yml index 6b24955cfc..68e4a38851 100644 --- a/.github/workflows/browser-demos-pages.yml +++ b/.github/workflows/browser-demos-pages.yml @@ -107,8 +107,9 @@ jobs: # bottle member. Repeating selection or extraction here could make # inspection and the bytes actually served by Vite disagree. bootstrap="$PWD/apps/browser-demos/public/homebrew-bootstrap.zip" + portable_ruby="$PWD/apps/browser-demos/public/homebrew-portable-ruby.zip" report="$RUNNER_TEMP/homebrew-main-shell-public-product.json" - for path in "$image" "$bootstrap"; do + for path in "$image" "$bootstrap" "$portable_ruby"; do test -f "$path" test ! -L "$path" done @@ -125,6 +126,7 @@ jobs: scripts/inspect-homebrew-main-shell-public-product.ts \ --image "$image" \ --homebrew-bootstrap-archive "$bootstrap" \ + --homebrew-portable-ruby-archive "$portable_ruby" \ --homebrew-bootstrap-spec \ homebrew/main-shell-brew-package-tree.json \ --homebrew-runtime-support \ @@ -137,6 +139,8 @@ jobs: (.image.bytes > 0) and (.homebrew_bootstrap.sha256 | test("^[0-9a-f]{64}$")) and (.homebrew_bootstrap.bytes > 0) and + (.homebrew_portable_ruby.sha256 | test("^[0-9a-f]{64}$")) and + (.homebrew_portable_ruby.bytes > 0) and .homebrew_bootstrap.activation_root == "/usr/bin/brew" and (.bottle_mirror.plan_url | startswith("https://github.com/") and @@ -146,6 +150,7 @@ jobs: { echo "image=$image" echo "bootstrap=$bootstrap" + echo "portable_ruby=$portable_ruby" echo "image_sha256=$(jq -er '.image.sha256' "$report")" echo "bootstrap_sha256=$(jq -er \ '.homebrew_bootstrap.sha256' "$report")" @@ -165,6 +170,8 @@ jobs: npm run build cmp "${{ steps.shell_product.outputs.bootstrap }}" \ dist/homebrew-bootstrap.zip + cmp "${{ steps.shell_product.outputs.portable_ruby }}" \ + dist/homebrew-portable-ruby.zip # WHY: Vite imports the resolver-selected shell under a hashed asset # name. Verify that exact emitted asset; an unhashed public copy can # be absent or stale without affecting the image the browser boots. diff --git a/.github/workflows/homebrew-main-shell-ci.yml b/.github/workflows/homebrew-main-shell-ci.yml index 1e3161a3fd..d3c6bb3a46 100644 --- a/.github/workflows/homebrew-main-shell-ci.yml +++ b/.github/workflows/homebrew-main-shell-ci.yml @@ -683,9 +683,11 @@ jobs: --output-directory "$prepared_inputs" bootstrap_root="$prepared_inputs/bootstrap" bootstrap="$bootstrap_root/homebrew-bootstrap.zip" + portable_ruby="$bootstrap_root/homebrew-portable-ruby.zip" bootstrap_env="$bootstrap_root/homebrew-brew.env" bootstrap_bottle_report="$bootstrap_root/report.json" test -f "$bootstrap" + test -f "$portable_ruby" test -f "$bootstrap_env" test -f "$bootstrap_bottle_report" artifact_state=$(jq -er '.state' \ @@ -727,6 +729,7 @@ jobs: echo "image=$candidate_root/main-shell.vfs.zst" echo "report=$candidate_root/main-shell-report.json" echo "bootstrap=$bootstrap" + echo "portable_ruby=$portable_ruby" echo "bootstrap_env=$bootstrap_env" echo "bootstrap_bottle_report=$bootstrap_bottle_report" echo "tap_sha=$tap_sha" @@ -975,6 +978,7 @@ jobs: --image "${{ steps.bottle_candidate.outputs.image }}" \ --homebrew-bootstrap-spec homebrew/main-shell-brew-package-tree.json \ --homebrew-bootstrap-archive "${{ steps.bottle_candidate.outputs.bootstrap }}" \ + --homebrew-portable-ruby-archive "${{ steps.bottle_candidate.outputs.portable_ruby }}" \ --homebrew-bootstrap-env "${{ steps.bottle_candidate.outputs.bootstrap_env }}" \ --bottle-mirror "$RUNNER_TEMP/homebrew-main-shell-bottles" \ --fixed-asset-url-root "$fixed_url_root" \ @@ -999,6 +1003,7 @@ jobs: install_browser_fixture_asset "${{ steps.bottle_candidate.outputs.image }}" install_browser_fixture_asset homebrew/main-shell-brew-package-tree.json install_browser_fixture_asset "${{ steps.bottle_candidate.outputs.bootstrap }}" + install_browser_fixture_asset "${{ steps.bottle_candidate.outputs.portable_ruby }}" install_browser_fixture_asset "${{ steps.bottle_candidate.outputs.bootstrap_env }}" echo "fixture=$fixture" >> "$GITHUB_OUTPUT" @@ -1032,6 +1037,8 @@ jobs: if [ "$SHELL_ACTIVATION_MODE" = bottles ]; then cmp "${{ steps.bottle_candidate.outputs.bootstrap }}" \ dist/homebrew-bootstrap.zip + cmp "${{ steps.bottle_candidate.outputs.portable_ruby }}" \ + dist/homebrew-portable-ruby.zip fi - name: Boot the exact installed bytes in Node @@ -1053,6 +1060,7 @@ jobs: --migration-lock homebrew/main-shell-migration-lock.json --homebrew-bootstrap-spec homebrew/main-shell-brew-package-tree.json --homebrew-bootstrap-archive "${{ steps.bottle_candidate.outputs.bootstrap }}" + --homebrew-portable-ruby-archive "${{ steps.bottle_candidate.outputs.portable_ruby }}" --homebrew-bootstrap-env "${{ steps.bottle_candidate.outputs.bootstrap_env }}" --homebrew-bootstrap-state deferred --homebrew-runtime-support homebrew/main-shell-homebrew-runtime-support.json @@ -1091,6 +1099,7 @@ jobs: --image "${{ steps.bottle_candidate.outputs.image }}" \ --homebrew-bootstrap-spec homebrew/main-shell-brew-package-tree.json \ --homebrew-bootstrap-archive "${{ steps.bottle_candidate.outputs.bootstrap }}" \ + --homebrew-portable-ruby-archive "${{ steps.bottle_candidate.outputs.portable_ruby }}" \ --homebrew-bootstrap-env "${{ steps.bottle_candidate.outputs.bootstrap_env }}" \ --transport-mode closed \ --bottle-mirror-plan "${{ steps.mirror.outputs.plan }}" \ @@ -1220,6 +1229,7 @@ jobs: ${{ steps.source_candidate.outputs.manifest }} ${{ steps.bottle_candidate.outputs.report }} ${{ steps.bottle_candidate.outputs.bootstrap }} + ${{ steps.bottle_candidate.outputs.portable_ruby }} ${{ steps.bottle_candidate.outputs.bootstrap_env }} ${{ steps.bottle_candidate.outputs.bootstrap_bottle_report }} ${{ runner.temp }}/homebrew-main-shell-bottles diff --git a/.github/workflows/reusable-homebrew-main-shell-mirror-publish.yml b/.github/workflows/reusable-homebrew-main-shell-mirror-publish.yml index 24d054b4c1..de6b2319e1 100644 --- a/.github/workflows/reusable-homebrew-main-shell-mirror-publish.yml +++ b/.github/workflows/reusable-homebrew-main-shell-mirror-publish.yml @@ -242,8 +242,9 @@ jobs: --require-sealed \ --output-directory "$bootstrap_root" bootstrap="$bootstrap_root/homebrew-bootstrap.zip" + portable_ruby="$bootstrap_root/homebrew-portable-ruby.zip" bootstrap_env="$bootstrap_root/homebrew-brew.env" - for path in "$image" "$bootstrap" "$bootstrap_env"; do + for path in "$image" "$bootstrap" "$portable_ruby" "$bootstrap_env"; do test -f "$path" test ! -L "$path" done @@ -254,6 +255,7 @@ jobs: { echo "image=$image" echo "bootstrap=$bootstrap" + echo "portable-ruby=$portable_ruby" echo "bootstrap-env=$bootstrap_env" } >>"$GITHUB_OUTPUT" @@ -305,6 +307,7 @@ jobs: env: IMAGE: ${{ steps.shell.outputs.image }} BOOTSTRAP: ${{ steps.shell.outputs.bootstrap }} + PORTABLE_RUBY: ${{ steps.shell.outputs.portable-ruby }} BOOTSTRAP_ENV: ${{ steps.shell.outputs.bootstrap-env }} MIRROR: ${{ steps.mirror.outputs.root }} PUBLISH: ${{ steps.mirror.outputs.publish }} @@ -321,6 +324,7 @@ jobs: mkdir "$handoff" cp "$IMAGE" "$handoff/main-shell.vfs.zst" cp "$BOOTSTRAP" "$handoff/homebrew-bootstrap.zip" + cp "$PORTABLE_RUBY" "$handoff/homebrew-portable-ruby.zip" cp "$BOOTSTRAP_ENV" "$handoff/homebrew-brew.env" cp "$PUBLISH" "$handoff/publish.json" cp -R "$MIRROR" "$handoff/mirror" @@ -345,6 +349,8 @@ jobs: --argjson image_bytes "$(bytes "$handoff/main-shell.vfs.zst")" \ --arg bootstrap_sha "$(digest "$handoff/homebrew-bootstrap.zip")" \ --argjson bootstrap_bytes "$(bytes "$handoff/homebrew-bootstrap.zip")" \ + --arg portable_ruby_sha "$(digest "$handoff/homebrew-portable-ruby.zip")" \ + --argjson portable_ruby_bytes "$(bytes "$handoff/homebrew-portable-ruby.zip")" \ --arg env_sha "$(digest "$handoff/homebrew-brew.env")" \ --argjson env_bytes "$(bytes "$handoff/homebrew-brew.env")" \ --arg publish_sha "$(digest "$handoff/publish.json")" \ @@ -367,6 +373,9 @@ jobs: "homebrew-bootstrap.zip": { sha256: $bootstrap_sha, bytes: $bootstrap_bytes }, + "homebrew-portable-ruby.zip": { + sha256: $portable_ruby_sha, bytes: $portable_ruby_bytes + }, "homebrew-brew.env": { sha256: $env_sha, bytes: $env_bytes }, @@ -408,6 +417,7 @@ jobs: env: IMAGE: ${{ steps.shell.outputs.image }} BOOTSTRAP: ${{ steps.shell.outputs.bootstrap }} + PORTABLE_RUBY: ${{ steps.shell.outputs.portable-ruby }} BOOTSTRAP_ENV: ${{ steps.shell.outputs.bootstrap-env }} MIRROR_PLAN: ${{ steps.mirror.outputs.root }}/kandelo-homebrew-bottle-mirror-plan.json KANDELO_REF: ${{ inputs.kandelo-ref }} @@ -431,6 +441,7 @@ jobs: --homebrew-bootstrap-spec \ homebrew/main-shell-brew-package-tree.json \ --homebrew-bootstrap-archive "$BOOTSTRAP" \ + --homebrew-portable-ruby-archive "$PORTABLE_RUBY" \ --homebrew-bootstrap-env "$BOOTSTRAP_ENV" \ --bottle-mirror-plan "$MIRROR_PLAN" \ --kandelo-ref "$KANDELO_REF" \ @@ -627,7 +638,7 @@ jobs: .repository == "kandelo-dev/homebrew-tap-core" and .target_commitish == $tap and .immutable == true and - (.assets | length) == 4 + (.assets | length) == 5 ' "$lifecycle_receipt" >/dev/null { echo "mirror-receipt=$mirror_receipt" @@ -789,6 +800,7 @@ jobs: main-shell.vfs.zst \ main-shell-brew-package-tree.json \ homebrew-bootstrap.zip \ + homebrew-portable-ruby.zip \ homebrew-brew.env do env -u GH_TOKEN -u GITHUB_TOKEN \ @@ -803,11 +815,14 @@ jobs: "$lifecycle/main-shell-brew-package-tree.json" cmp "$handoff/homebrew-bootstrap.zip" \ "$lifecycle/homebrew-bootstrap.zip" + cmp "$handoff/homebrew-portable-ruby.zip" \ + "$lifecycle/homebrew-portable-ruby.zip" cmp "$handoff/homebrew-brew.env" \ "$lifecycle/homebrew-brew.env" { echo "image=$lifecycle/main-shell.vfs.zst" echo "bootstrap=$lifecycle/homebrew-bootstrap.zip" + echo "portable-ruby=$lifecycle/homebrew-portable-ruby.zip" echo "bootstrap-env=$lifecycle/homebrew-brew.env" } >>"$GITHUB_OUTPUT" @@ -825,6 +840,7 @@ jobs: env: IMAGE: ${{ steps.public.outputs.image }} BOOTSTRAP: ${{ steps.public.outputs.bootstrap }} + PORTABLE_RUBY: ${{ steps.public.outputs.portable-ruby }} BOOTSTRAP_ENV: ${{ steps.public.outputs.bootstrap-env }} TAP_CATALOG_REF: ${{ inputs.tap-catalog-ref }} CANARY_REF: ${{ inputs.canary-ref }} @@ -839,6 +855,7 @@ jobs: env: IMAGE: ${{ steps.public.outputs.image }} BOOTSTRAP: ${{ steps.public.outputs.bootstrap }} + PORTABLE_RUBY: ${{ steps.public.outputs.portable-ruby }} BOOTSTRAP_ENV: ${{ steps.public.outputs.bootstrap-env }} TAP_CATALOG_REF: ${{ inputs.tap-catalog-ref }} CANARY_REF: ${{ inputs.canary-ref }} @@ -980,9 +997,11 @@ jobs: lifecycle="$RUNNER_TEMP/homebrew-guest-lifecycle-inputs-handoff" image="$(bash scripts/resolve-binary.sh programs/shell.vfs.zst)" bootstrap="$PWD/apps/browser-demos/public/homebrew-bootstrap.zip" + portable_ruby="$PWD/apps/browser-demos/public/homebrew-portable-ruby.zip" bootstrap_env="$lifecycle/homebrew-brew.env" cmp "$image" "$handoff/main-shell.vfs.zst" cmp "$bootstrap" "$handoff/homebrew-bootstrap.zip" + cmp "$portable_ruby" "$handoff/homebrew-portable-ruby.zip" cmp "$bootstrap_env" "$handoff/homebrew-brew.env" plan="$handoff/mirror/kandelo-homebrew-bottle-mirror-plan.json" plan_url="$(jq -er '.release_root + "/" + .manifest_asset' "$plan")" @@ -994,6 +1013,7 @@ jobs: main-shell.vfs.zst \ main-shell-brew-package-tree.json \ homebrew-bootstrap.zip \ + homebrew-portable-ruby.zip \ homebrew-brew.env do env -u GH_TOKEN -u GITHUB_TOKEN \ @@ -1006,10 +1026,12 @@ jobs: cmp homebrew/main-shell-brew-package-tree.json \ "$lifecycle/main-shell-brew-package-tree.json" cmp "$bootstrap" "$lifecycle/homebrew-bootstrap.zip" + cmp "$portable_ruby" "$lifecycle/homebrew-portable-ruby.zip" cmp "$bootstrap_env" "$lifecycle/homebrew-brew.env" { echo "image=$image" echo "bootstrap=$bootstrap" + echo "portable-ruby=$portable_ruby" echo "bootstrap-env=$bootstrap_env" echo "plan-url=$plan_url" echo "image-sha=$(sha256sum "$image" | awk '{print $1}')" @@ -1027,12 +1049,17 @@ jobs: run: | set -euo pipefail browser_bootstrap=public/homebrew-bootstrap.zip + browser_portable_ruby=public/homebrew-portable-ruby.zip test -f "$browser_bootstrap" test ! -L "$browser_bootstrap" + test -f "$browser_portable_ruby" + test ! -L "$browser_portable_ruby" # WHY: prepare-browser owns this fixed same-origin asset. The build # must consume those admitted bytes instead of silently replacing # them from a second package source. cmp "${{ steps.public.outputs.bootstrap }}" "$browser_bootstrap" + cmp "${{ steps.public.outputs.portable-ruby }}" \ + "$browser_portable_ruby" # WHY: dev-shell intentionally drops ambient variables. Forward the # selector or Vite builds its default pages and omits the proof page. bash ../../scripts/dev-shell.sh env \ @@ -1043,6 +1070,8 @@ jobs: test -f dist/pages/homebrew-vfs-test/index.html cmp "${{ steps.public.outputs.bootstrap }}" \ dist/homebrew-bootstrap.zip + cmp "${{ steps.public.outputs.portable-ruby }}" \ + dist/homebrew-portable-ruby.zip bash ../../scripts/verify-browser-shell-vfs-asset.sh \ dist "${{ steps.public.outputs.image }}" @@ -1065,6 +1094,8 @@ jobs: "$lifecycle/main-shell-brew-package-tree.json" \ --homebrew-bootstrap-archive \ "$lifecycle/homebrew-bootstrap.zip" \ + --homebrew-portable-ruby-archive \ + "$lifecycle/homebrew-portable-ruby.zip" \ --homebrew-bootstrap-env "$lifecycle/homebrew-brew.env" \ --bottle-mirror \ "$RUNNER_TEMP/main-shell-mirror-handoff/mirror" \ diff --git a/apps/browser-demos/pages/homebrew-benchmark/index.html b/apps/browser-demos/pages/homebrew-benchmark/index.html new file mode 100644 index 0000000000..b79cc00752 --- /dev/null +++ b/apps/browser-demos/pages/homebrew-benchmark/index.html @@ -0,0 +1,12 @@ + + + + + + Kandelo Homebrew query benchmark + + +
Preparing Homebrew query benchmark…
+ + + diff --git a/apps/browser-demos/pages/homebrew-benchmark/main.ts b/apps/browser-demos/pages/homebrew-benchmark/main.ts new file mode 100644 index 0000000000..3bc2cefcdf --- /dev/null +++ b/apps/browser-demos/pages/homebrew-benchmark/main.ts @@ -0,0 +1,350 @@ +import { BrowserKernel } from "@host/browser-kernel-host"; +import type { SyscallTraceEvent } from "@host/kernel-worker"; +import type { LazyDownloadEvent } from "@host/vfs/memory-fs"; +import type { ClosedLazyAsset } from "@host/vfs/closed-lazy-assets"; +import { + assertHomebrewQueryFixture, + commandScript, + emptyProcessCounts, + medianMetrics, + summarizeLazyDownloads, + type HomebrewQueryArtifact, + type HomebrewQueryCommand, + type HomebrewQueryCommandResult, + type HomebrewQueryFixtureManifest, + type HomebrewQueryNetworkAudit, + type HomebrewQueryProcessCounts, + type HomebrewQueryScenarioResult, +} from "../../../../benchmarks/homebrew-query/contracts"; +import { + runHomebrewQueryScenario, + type HomebrewQueryMachine, + type HomebrewQueryMachineFactory, +} from "../../../../benchmarks/homebrew-query/scenario"; + +declare global { + interface Window { + __homebrewQueryBenchmarkReady: boolean; + __runHomebrewQueryBenchmark( + rounds: number, + auditNetwork?: boolean, + ): Promise<{ + fixture: HomebrewQueryFixtureManifest; + rounds: HomebrewQueryScenarioResult[]; + median: Record; + }>; + } +} + +interface ProcessEvent { + kind: "spawn" | "exec" | "exit"; +} + +const COMMAND_TIMEOUT_MS = 10 * 60_000; +const logElement = document.getElementById("log")!; +const decoder = new TextDecoder("utf-8", { fatal: false }); + +function log(message: string): void { + logElement.textContent += `${message}\n`; + console.log(message); +} + +async function prepare(): Promise<{ + fixture: HomebrewQueryFixtureManifest; + factory: BrowserHomebrewQueryMachineFactory; +}> { + const fixtureParameter = new URL(location.href).searchParams.get("fixture"); + if (!fixtureParameter) throw new Error("Homebrew benchmark fixture URL is missing"); + const fixtureUrl = new URL(fixtureParameter, location.href); + const response = await fetch(fixtureUrl); + if (!response.ok) throw new Error(`Fixture returned HTTP ${response.status}`); + const fixture = await response.json(); + assertHomebrewQueryFixture(fixture); + const kernel = await fetchVerifiedArtifact(fixtureUrl, fixture.kernel); + const lazyAssets: ClosedLazyAsset[] = []; + for (const artifact of fixture.lazyAssets) { + lazyAssets.push({ + url: artifact.url, + sha256: artifact.sha256, + size: artifact.bytes, + bytes: await fetchVerifiedArtifact(fixtureUrl, artifact), + }); + } + return { + fixture, + factory: new BrowserHomebrewQueryMachineFactory( + fixtureUrl, + fixture, + kernel, + lazyAssets, + ), + }; +} + +class BrowserHomebrewQueryMachineFactory + implements HomebrewQueryMachineFactory { + constructor( + private readonly fixtureUrl: URL, + private readonly fixture: HomebrewQueryFixtureManifest, + private readonly kernel: Uint8Array, + private readonly lazyAssets: ClosedLazyAsset[], + ) {} + + async create(image: "lazy" | "eager"): Promise { + const artifact = image === "lazy" + ? this.fixture.rootfs + : this.fixture.eagerRootfs; + if (!artifact) throw new Error("Eager Homebrew query image is unavailable"); + return new BrowserHomebrewQueryMachine( + this.fixture, + await fetchVerifiedArtifact(this.fixtureUrl, artifact), + this.kernel, + image === "lazy" ? this.lazyAssets : undefined, + ); + } +} + +class BrowserHomebrewQueryMachine implements HomebrewQueryMachine { + private readonly stdout: Uint8Array[] = []; + private readonly stderr: Uint8Array[] = []; + private readonly processEvents: ProcessEvent[] = []; + private readonly lazyEvents: LazyDownloadEvent[] = []; + private readonly diagnostics: string[] = []; + private readonly kernel: BrowserKernel; + + constructor( + private readonly fixture: HomebrewQueryFixtureManifest, + private readonly image: Uint8Array, + private readonly kernelWasm: Uint8Array, + private readonly lazyAssets?: ClosedLazyAsset[], + ) { + this.kernel = new BrowserKernel({ + maxWorkers: 4, + kernelOwnedFs: true, + onStdout: (value) => this.stdout.push(value.slice()), + onStderr: (value) => this.stderr.push(value.slice()), + onProcessEvent: (event) => this.processEvents.push(event), + onLazyDownload: (event) => this.lazyEvents.push(event), + onHostDiagnostic: (diagnostic) => { + this.diagnostics.push( + `pid=${diagnostic.pid} source=${diagnostic.source}: ` + + diagnostic.message, + ); + }, + }); + } + + async start(): Promise { + const startedAt = performance.now(); + await this.kernel.initFromOwnedImage({ + kernelWasm: wholeArrayBuffer(this.kernelWasm), + vfsImage: wholeArrayBuffer(this.image), + lazyUrlBase: this.fixture.lazyUrlBase, + ...(this.lazyAssets === undefined + ? {} + : { closedLazyAssets: this.lazyAssets }), + }); + return performance.now() - startedAt; + } + + async run(command: HomebrewQueryCommand): Promise { + const stdoutStart = this.stdout.length; + const stderrStart = this.stderr.length; + const processStart = this.processEvents.length; + const lazyStart = this.lazyEvents.length; + const diagnosticStart = this.diagnostics.length; + const startedAt = performance.now(); + const spawned = await this.kernel.spawnFromVfs( + this.fixture.shell.path, + [this.fixture.shell.argv0, "-l", "-c", commandScript(command)], + { + env: this.fixture.homebrew.environment, + cwd: "/home/user", + uid: 1000, + gid: 1000, + stdin: new Uint8Array(), + }, + ); + // BrowserKernel's VFS spawn is a main-thread root event and therefore is + // not echoed by the worker's fork/exec event stream. + this.processEvents.push({ kind: "spawn" }); + const status = await withTimeout( + spawned.exit, + COMMAND_TIMEOUT_MS, + `Homebrew query timed out: ${command.id}`, + ); + const elapsedMs = performance.now() - startedAt; + const stdout = concatenate(this.stdout.slice(stdoutStart)); + const stderr = concatenate(this.stderr.slice(stderrStart)); + const invalidQueryOutput = command.id !== "shell_boot" && ( + stdout.byteLength === 0 || stderr.byteLength !== 0 + ); + if ( + status !== 0 || invalidQueryOutput || + this.diagnostics.length !== diagnosticStart + ) { + throw new Error( + `Homebrew query ${command.id} did not produce clean output ` + + `(status=${status}, stdout=${stdout.byteLength}, ` + + `stderr=${stderr.byteLength}): ` + + decoder.decode(stderr).slice(-4_000) + "\n" + + this.diagnostics.slice(diagnosticStart).join("\n"), + ); + } + return { + id: command.id, + argv: command.argv, + elapsedMs, + status, + stdoutBytes: stdout.byteLength, + stderrBytes: stderr.byteLength, + stdoutSha256: await sha256(stdout), + stderrSha256: await sha256(stderr), + processCounts: countProcessEvents(this.processEvents.slice(processStart)), + lazy: summarizeLazyDownloads(this.lazyEvents.slice(lazyStart)), + }; + } + + async auditNetwork( + commands: readonly HomebrewQueryCommand[], + ): Promise { + const events: SyscallTraceEvent[] = []; + const unsubscribe = this.kernel.subscribeSyscalls((event) => events.push(event)); + try { + const output: HomebrewQueryNetworkAudit["commands"] = []; + for (const command of commands) { + const start = events.length; + const result = await this.run(command); + await delay(350); + output.push({ + id: command.id, + status: result.status, + networkSyscalls: countNetworkSyscalls(events.slice(start)), + }); + } + return { commands: output }; + } finally { + unsubscribe(); + } + } + + async destroy(): Promise { + // WHY: this harness runs Chromium only. Avoid importing the WebKit + // reclamation helper because that module also imports the repository's + // default rootfs, while this benchmark must use only its verified fixture. + await this.kernel.destroy(); + } +} + +function countNetworkSyscalls( + events: readonly SyscallTraceEvent[], +): Record { + const counts: Record = {}; + for (const event of events) { + const name = event.decoded?.match( + /\]\s+(socket|connect|send|sendto|sendmsg|recv|recvfrom|recvmsg)\b/, + )?.[1]; + if (name) counts[name] = (counts[name] ?? 0) + 1; + } + return counts; +} + +function countProcessEvents( + events: readonly ProcessEvent[], +): HomebrewQueryProcessCounts { + const counts = emptyProcessCounts(); + for (const event of events) counts[event.kind] += 1; + return counts; +} + +async function fetchVerifiedArtifact( + fixtureUrl: URL, + artifact: HomebrewQueryArtifact, +): Promise { + const url = new URL(artifact.file, fixtureUrl); + const response = await fetch(url); + if (!response.ok) throw new Error(`${url} returned HTTP ${response.status}`); + const value = new Uint8Array(await response.arrayBuffer()); + if (value.byteLength !== artifact.bytes || await sha256(value) !== artifact.sha256) { + throw new Error(`Homebrew query artifact changed identity: ${url}`); + } + return value; +} + +function concatenate(chunks: readonly Uint8Array[]): Uint8Array { + const output = new Uint8Array( + chunks.reduce((total, chunk) => total + chunk.byteLength, 0), + ); + let offset = 0; + for (const chunk of chunks) { + output.set(chunk, offset); + offset += chunk.byteLength; + } + return output; +} + +function wholeArrayBuffer(value: Uint8Array): ArrayBuffer { + return value.buffer.slice( + value.byteOffset, + value.byteOffset + value.byteLength, + ) as ArrayBuffer; +} + +async function sha256(value: Uint8Array): Promise { + const digest = new Uint8Array(await crypto.subtle.digest( + "SHA-256", + wholeArrayBuffer(value), + )); + return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function delay(ms: number): Promise { + return new Promise((resolveDelay) => setTimeout(resolveDelay, ms)); +} + +async function withTimeout( + promise: Promise, + timeoutMs: number, + message: string, +): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + promise, + new Promise((_resolve, reject) => { + timer = setTimeout(() => reject(new Error(message)), timeoutMs); + }), + ]); + } finally { + if (timer !== undefined) clearTimeout(timer); + } +} + +const prepared = prepare(); +window.__homebrewQueryBenchmarkReady = false; +window.__runHomebrewQueryBenchmark = async (roundCount, auditNetwork = true) => { + if (!Number.isSafeInteger(roundCount) || roundCount <= 0) { + throw new Error("Homebrew query rounds must be a positive integer"); + } + const { fixture, factory } = await prepared; + const rounds: HomebrewQueryScenarioResult[] = []; + for (let index = 0; index < roundCount; index += 1) { + log(`Homebrew query Chromium round ${index + 1}/${roundCount}`); + rounds.push(await runHomebrewQueryScenario({ + fixture, + machines: factory, + auditNetwork: auditNetwork && index === 0, + })); + } + return { fixture, rounds, median: medianMetrics(rounds) }; +}; +void prepared.then( + () => { + window.__homebrewQueryBenchmarkReady = true; + log("Homebrew query benchmark ready"); + }, + (error) => { + window.__homebrewQueryBenchmarkReady = true; + log(`Homebrew query benchmark failed to prepare: ${String(error)}`); + }, +); diff --git a/benchmarks/homebrew-query/README.md b/benchmarks/homebrew-query/README.md new file mode 100644 index 0000000000..f2cee08f06 --- /dev/null +++ b/benchmarks/homebrew-query/README.md @@ -0,0 +1,77 @@ +# In-guest Homebrew query benchmark + +This benchmark measures real Homebrew metadata queries inside Kandelo on Node +and Chromium. It separates a cold machine plus its first Brew invocation, the +first invocation after a shell process has already run, repeated warm +invocations, and an optional fully materialized VFS control. + +The preparation step is the only networked phase. It embeds exact tap +checkouts into a copy of a published shell image, downloads every deferred +artifact, and verifies each artifact's declared byte count and SHA-256 digest. +The runners then bind those bytes as closed lazy assets. Node disables its TCP +backend, and Chromium blocks and reports every request outside the local +fixture origin. The first round also records guest socket syscalls for every +command. + +Prepare one fixture and reuse that exact manifest for both sides of a +comparison: + +```bash +npx tsx benchmarks/homebrew-query/prepare.ts \ + --image /path/to/shell.vfs.zst \ + --kernel /path/to/kandelo-kernel.wasm \ + --bootstrap /path/to/homebrew-bootstrap.zip \ + --output /tmp/kandelo-homebrew-query \ + --source-commit FULL_PRODUCER_COMMIT \ + --tap kandelo-dev/tap-core=/path/to/tap-core \ + --tap owner/canary-tap=/path/to/canary-tap \ + --canary-formula owner/canary-tap/formula \ + --trust-formula kandelo-dev/tap-core/dash \ + --eager +``` + +The shell metadata supplies the Homebrew prefix and shell path; the harness +does not assume either the rev22 compatibility prefix or the canonical prefix. +Tap checkouts should be clean and pinned to the commits under test. +`--trust-formula` records ordinary formula-granular Homebrew trust in the +fixture. The generated environment points Homebrew at that immutable store +under `/etc` through `XDG_CONFIG_HOME`, because Kandelo intentionally replaces +`/home/user` with fresh scratch storage at boot. Use it when a third-party +Formula has a fully qualified dependency; do not disable Homebrew's tap-trust +enforcement for the benchmark. + +Run the focused workload on both hosts: + +```bash +npx tsx benchmarks/homebrew-query/run-node.ts \ + --fixture /tmp/kandelo-homebrew-query/manifest.json \ + --rounds 3 --output /tmp/homebrew-node.json +npx tsx benchmarks/homebrew-query/run-browser.ts \ + --fixture /tmp/kandelo-homebrew-query/manifest.json \ + --rounds 3 --output /tmp/homebrew-chromium.json +``` + +Each Chromium sample uses a newly launched browser. Browser launch is outside +the timed machine and command intervals. This makes rounds independent and +guarantees that a previous round's renderer, Workers, JavaScript Realms, +WebAssembly memories, and shared buffers cannot survive into the next sample. +Used guest Workers are still terminated within each round; no guest Realm or +process state is reused. + +The runners reject a zero exit with empty standard output or non-empty standard +error for a measured Brew command. This catches shell wrappers that accidentally +mask an inner command failure instead of recording a fast false success. + +Results include artifact and tap identities, host versions, machine details, +host-side monotonic timings, process counts, lazy transfer counts and bytes, +output hashes, and the syscall network audit. Compare like-for-like results +with a deliberately broad 25 percent threshold that detects a large regression +without turning ordinary host timing noise into a failure: + +```bash +npx tsx benchmarks/homebrew-query/compare.ts \ + /tmp/homebrew-before.json /tmp/homebrew-after.json +``` + +This focused threshold does not replace the repository's full Node and browser +performance contract in `docs/agent-guidance/performance.md`. diff --git a/benchmarks/homebrew-query/compare.ts b/benchmarks/homebrew-query/compare.ts new file mode 100644 index 0000000000..5993ce7cf1 --- /dev/null +++ b/benchmarks/homebrew-query/compare.ts @@ -0,0 +1,176 @@ +#!/usr/bin/env -S npx tsx + +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +import type { + HomebrewQueryBenchmarkResult, + HomebrewQueryFixtureManifest, +} from "./contracts"; + +interface Options { + beforePath: string; + afterPath: string; + maxRegressionPercent: number; +} + +interface Comparison { + metric: string; + before: number; + after: number; + changePercent: number; + regression: boolean; +} + +function main(): void { + const options = parseOptions(process.argv.slice(2)); + const before = readResult(options.beforePath); + const after = readResult(options.afterPath); + assertComparable(before, after); + const comparisons = compareMedians( + before.median, + after.median, + options.maxRegressionPercent, + ); + + process.stdout.write("| Metric | Before (ms) | After (ms) | Change |\n"); + process.stdout.write("|---|---:|---:|---:|\n"); + for (const comparison of comparisons) { + const change = Number.isFinite(comparison.changePercent) + ? `${comparison.changePercent >= 0 ? "+" : ""}` + + `${comparison.changePercent.toFixed(1)}%` + : "+infinity"; + process.stdout.write( + `| ${comparison.metric} | ${comparison.before.toFixed(2)} | ` + + `${comparison.after.toFixed(2)} | ` + + `${comparison.regression ? `**${change}**` : change} |\n`, + ); + } + + const regressions = comparisons.filter((comparison) => comparison.regression); + if (regressions.length > 0) { + process.stderr.write( + `${regressions.length} Homebrew query metric(s) regressed by more than ` + + `${options.maxRegressionPercent}%\n`, + ); + process.exitCode = 1; + } +} + +export function compareMedians( + before: Readonly>, + after: Readonly>, + maxRegressionPercent: number, +): Comparison[] { + if (!Number.isFinite(maxRegressionPercent) || maxRegressionPercent <= 0) { + throw new Error("Homebrew regression threshold must be positive"); + } + const beforeKeys = Object.keys(before).sort(); + const afterKeys = Object.keys(after).sort(); + if (JSON.stringify(beforeKeys) !== JSON.stringify(afterKeys)) { + throw new Error("Homebrew benchmark results contain different metrics"); + } + return beforeKeys.map((metric) => { + const baseline = before[metric]!; + const candidate = after[metric]!; + if ( + !Number.isFinite(baseline) || baseline < 0 || + !Number.isFinite(candidate) || candidate < 0 + ) { + throw new Error(`Homebrew benchmark metric ${metric} is invalid`); + } + const changePercent = baseline === 0 + ? (candidate === 0 ? 0 : Number.POSITIVE_INFINITY) + : ((candidate - baseline) / baseline) * 100; + return { + metric, + before: baseline, + after: candidate, + changePercent, + regression: changePercent > maxRegressionPercent, + }; + }); +} + +function readResult(path: string): HomebrewQueryBenchmarkResult { + const value = JSON.parse(readFileSync(path, "utf-8")) as + Partial; + if ( + value.schema !== 1 || + value.kind !== "kandelo-homebrew-query-benchmark-result" || + (value.host !== "node" && value.host !== "chromium") || + typeof value.median !== "object" || value.median === null || + typeof value.fixture !== "object" || value.fixture === null + ) { + throw new Error(`Invalid Homebrew query benchmark result: ${path}`); + } + return value as HomebrewQueryBenchmarkResult; +} + +function assertComparable( + before: HomebrewQueryBenchmarkResult, + after: HomebrewQueryBenchmarkResult, +): void { + if (before.host !== after.host) { + throw new Error(`Cannot compare ${before.host} with ${after.host}`); + } + const beforeIdentity = fixtureIdentity(before.fixture); + const afterIdentity = fixtureIdentity(after.fixture); + if (beforeIdentity !== afterIdentity) { + throw new Error("Homebrew benchmark results used different input artifacts"); + } +} + +function fixtureIdentity(fixture: HomebrewQueryFixtureManifest): string { + return JSON.stringify({ + sourceCommit: fixture.sourceCommit, + rootfs: fixture.rootfs, + eagerRootfs: fixture.eagerRootfs, + kernel: fixture.kernel, + lazyUrlBase: fixture.lazyUrlBase, + lazyAssets: fixture.lazyAssets, + shell: fixture.shell, + homebrew: fixture.homebrew, + taps: fixture.taps, + commands: fixture.commands, + focusCommandId: fixture.focusCommandId, + }); +} + +function parseOptions(argv: string[]): Options { + const positional: string[] = []; + let maxRegressionPercent = 25; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]!; + if (argument.startsWith("--max-regression-percent=")) { + maxRegressionPercent = Number(argument.slice(argument.indexOf("=") + 1)); + } else if (argument === "--max-regression-percent") { + maxRegressionPercent = Number(argv[++index]); + } else if (argument.startsWith("--")) { + usage(`Unknown argument: ${argument}`); + } else { + positional.push(argument); + } + } + if (positional.length !== 2) usage("Before and after result paths are required"); + if (!Number.isFinite(maxRegressionPercent) || maxRegressionPercent <= 0) { + usage("--max-regression-percent must be positive"); + } + return { + beforePath: positional[0]!, + afterPath: positional[1]!, + maxRegressionPercent, + }; +} + +function usage(message: string): never { + process.stderr.write(`${message}\n`); + process.stderr.write( + "Usage: npx tsx benchmarks/homebrew-query/compare.ts " + + "BEFORE.json AFTER.json [--max-regression-percent 25]\n", + ); + process.exit(2); +} + +if (import.meta.url === pathToFileURL(resolve(process.argv[1]!)).href) main(); diff --git a/benchmarks/homebrew-query/contracts.ts b/benchmarks/homebrew-query/contracts.ts new file mode 100644 index 0000000000..18ce6d17b0 --- /dev/null +++ b/benchmarks/homebrew-query/contracts.ts @@ -0,0 +1,307 @@ +import type { LazyDownloadEvent } from "../../host/src/vfs/memory-fs"; + +export interface HomebrewQueryArtifact { + file: string; + bytes: number; + sha256: string; +} + +export interface HomebrewQueryLazyArtifact extends HomebrewQueryArtifact { + url: string; +} + +export interface HomebrewQueryTap { + name: string; + repository: string; + commit: string; +} + +export interface HomebrewQueryCommand { + id: string; + argv: string[]; +} + +export interface HomebrewQueryFixtureManifest { + schema: 1; + kind: "kandelo-homebrew-query-benchmark"; + createdAt: string; + sourceCommit: string; + rootfs: HomebrewQueryArtifact; + eagerRootfs?: HomebrewQueryArtifact; + kernel: HomebrewQueryArtifact; + lazyUrlBase: string; + lazyAssets: HomebrewQueryLazyArtifact[]; + shell: { + path: string; + argv0: string; + }; + homebrew: { + prefix: string; + environment: string[]; + trustedFormulae: string[]; + }; + taps: HomebrewQueryTap[]; + commands: HomebrewQueryCommand[]; + focusCommandId: string; +} + +export interface HomebrewQueryProcessCounts { + spawn: number; + exec: number; + exit: number; +} + +export interface HomebrewQueryLazySummary { + fetches: number; + completed: number; + /** The current VFS reports transfers, not individual cached path accesses. */ + cacheHits: null; + bytes: number; + urls: string[]; +} + +export interface HomebrewQueryCommandResult { + id: string; + argv: string[]; + elapsedMs: number; + status: number; + stdoutBytes: number; + stderrBytes: number; + stdoutSha256: string; + stderrSha256: string; + processCounts: HomebrewQueryProcessCounts; + lazy: HomebrewQueryLazySummary; +} + +export interface HomebrewQueryNetworkAudit { + commands: Array<{ + id: string; + status: number; + networkSyscalls: Record; + }>; +} + +export interface HomebrewQueryScenarioResult { + cold: { + machineBootMs: number; + machineBootAndFirstBrewMs: number; + first: HomebrewQueryCommandResult; + warm: HomebrewQueryCommandResult; + }; + booted: { + machineBootMs: number; + shellBootMs: number; + first: HomebrewQueryCommandResult[]; + warm: HomebrewQueryCommandResult[]; + }; + eager?: { + machineBootMs: number; + shellBootMs: number; + first: HomebrewQueryCommandResult; + warm: HomebrewQueryCommandResult; + }; + networkAudit?: HomebrewQueryNetworkAudit; +} + +export interface HomebrewQueryBenchmarkResult { + schema: 1; + kind: "kandelo-homebrew-query-benchmark-result"; + host: "node" | "chromium"; + hostVersion: string; + kandeloCommit: string; + recordedAt: string; + machine: { + platform: string; + architecture: string; + cpu: string; + }; + fixture: HomebrewQueryFixtureManifest; + rounds: HomebrewQueryScenarioResult[]; + median: Record; +} + +const SHA256_RE = /^[0-9a-f]{64}$/; +const SAFE_RELATIVE_PATH_RE = /^[^\\\0/]+(?:\/[^\\\0/]+)*$/; + +export function assertHomebrewQueryFixture( + value: unknown, +): asserts value is HomebrewQueryFixtureManifest { + if (typeof value !== "object" || value === null) { + throw new Error("Homebrew query fixture must be an object"); + } + const fixture = value as Partial; + if ( + fixture.schema !== 1 || + fixture.kind !== "kandelo-homebrew-query-benchmark" || + typeof fixture.createdAt !== "string" || + !Number.isFinite(Date.parse(fixture.createdAt)) || + typeof fixture.sourceCommit !== "string" || + !/^[0-9a-f]{40}$/.test(fixture.sourceCommit) || + typeof fixture.lazyUrlBase !== "string" || + !Array.isArray(fixture.lazyAssets) || + !Array.isArray(fixture.commands) || + !Array.isArray(fixture.taps) || + typeof fixture.focusCommandId !== "string" || + typeof fixture.shell?.path !== "string" || + !fixture.shell.path.startsWith("/") || + typeof fixture.shell.argv0 !== "string" || + typeof fixture.homebrew?.prefix !== "string" || + !fixture.homebrew.prefix.startsWith("/") || + !Array.isArray(fixture.homebrew.environment) || + !Array.isArray(fixture.homebrew.trustedFormulae) + ) { + throw new Error("Homebrew query fixture has invalid top-level fields"); + } + new URL(fixture.lazyUrlBase); + assertArtifact(fixture.rootfs, "rootfs"); + assertArtifact(fixture.kernel, "kernel"); + if (fixture.eagerRootfs !== undefined) { + assertArtifact(fixture.eagerRootfs, "eagerRootfs"); + } + const commandIds = new Set(); + for (const command of fixture.commands) { + if ( + typeof command?.id !== "string" || command.id === "" || + !Array.isArray(command.argv) || command.argv.length === 0 || + command.argv.some((argument) => typeof argument !== "string") || + commandIds.has(command.id) + ) { + throw new Error("Homebrew query fixture has an invalid command"); + } + commandIds.add(command.id); + } + if (!commandIds.has(fixture.focusCommandId)) { + throw new Error("Homebrew query fixture focus command is missing"); + } + const tapNames = new Set(); + for (const tap of fixture.taps) { + if ( + typeof tap?.name !== "string" || tap.name === "" || + typeof tap.repository !== "string" || tap.repository === "" || + typeof tap.commit !== "string" || !/^[0-9a-f]{40}$/.test(tap.commit) || + tapNames.has(tap.name) + ) { + throw new Error("Homebrew query fixture has an invalid tap"); + } + tapNames.add(tap.name); + } + const lazyUrls = new Set(); + for (const asset of fixture.lazyAssets) { + assertArtifact(asset, "lazy asset"); + if (typeof asset.url !== "string" || lazyUrls.has(asset.url)) { + throw new Error("Homebrew query fixture has an invalid lazy URL"); + } + new URL(asset.url); + lazyUrls.add(asset.url); + } + for (const value of fixture.homebrew.environment) { + if (typeof value !== "string" || !value.includes("=")) { + throw new Error("Homebrew query fixture has an invalid environment"); + } + } + for (const value of fixture.homebrew.trustedFormulae) { + if (typeof value !== "string" || value.split("/").length !== 3) { + throw new Error("Homebrew query fixture has invalid Formula trust state"); + } + } +} + +function assertArtifact( + artifact: HomebrewQueryArtifact | undefined, + label: string, +): void { + if ( + typeof artifact?.file !== "string" || artifact.file === "" || + !SAFE_RELATIVE_PATH_RE.test(artifact.file) || + artifact.file.split("/").some((part) => part === "." || part === "..") || + !Number.isSafeInteger(artifact.bytes) || artifact.bytes <= 0 || + typeof artifact.sha256 !== "string" || !SHA256_RE.test(artifact.sha256) + ) { + throw new Error(`Homebrew query fixture ${label} is invalid`); + } +} + +export function shellQuote(argument: string): string { + return `'${argument.replaceAll("'", `'\\''`)}'`; +} + +export function commandScript(command: HomebrewQueryCommand): string { + return command.argv.map(shellQuote).join(" "); +} + +export function emptyProcessCounts(): HomebrewQueryProcessCounts { + return { spawn: 0, exec: 0, exit: 0 }; +} + +export function summarizeLazyDownloads( + events: readonly LazyDownloadEvent[], +): HomebrewQueryLazySummary { + const byId = new Map(); + for (const event of events) { + const list = byId.get(event.id) ?? []; + list.push(event); + byId.set(event.id, list); + } + let completed = 0; + let bytes = 0; + const urls = new Set(); + for (const list of byId.values()) { + const final = list.at(-1)!; + urls.add(final.url); + if (final.status === "complete") completed += 1; + bytes += final.loadedBytes; + } + return { + fetches: byId.size, + completed, + cacheHits: null, + bytes, + urls: [...urls].sort(), + }; +} + +export function medianMetrics( + rounds: readonly HomebrewQueryScenarioResult[], +): Record { + const values = new Map(); + const add = (key: string, value: number): void => { + const list = values.get(key) ?? []; + list.push(value); + values.set(key, list); + }; + for (const round of rounds) { + add("cold.machine_boot_ms", round.cold.machineBootMs); + add( + "cold.machine_boot_and_first_brew_ms", + round.cold.machineBootAndFirstBrewMs, + ); + add("cold.focus_first_ms", round.cold.first.elapsedMs); + add("cold.focus_warm_ms", round.cold.warm.elapsedMs); + add("booted.machine_boot_ms", round.booted.machineBootMs); + add("booted.shell_boot_ms", round.booted.shellBootMs); + for (const result of round.booted.first) { + add(`booted.first.${result.id}_ms`, result.elapsedMs); + } + for (const result of round.booted.warm) { + add(`booted.warm.${result.id}_ms`, result.elapsedMs); + } + if (round.eager) { + add("eager.machine_boot_ms", round.eager.machineBootMs); + add("eager.shell_boot_ms", round.eager.shellBootMs); + add("eager.focus_first_ms", round.eager.first.elapsedMs); + add("eager.focus_warm_ms", round.eager.warm.elapsedMs); + } + } + return Object.fromEntries( + [...values].map(([key, samples]) => [key, median(samples)]), + ); +} + +function median(samples: readonly number[]): number { + const sorted = [...samples].sort((left, right) => left - right); + const middle = Math.floor(sorted.length / 2); + const value = sorted.length % 2 === 0 + ? (sorted[middle - 1]! + sorted[middle]!) / 2 + : sorted[middle]!; + return Math.round(value * 100) / 100; +} diff --git a/benchmarks/homebrew-query/prepare.ts b/benchmarks/homebrew-query/prepare.ts new file mode 100644 index 0000000000..82663d88ea --- /dev/null +++ b/benchmarks/homebrew-query/prepare.ts @@ -0,0 +1,548 @@ +#!/usr/bin/env -S npx tsx + +import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { + copyFileSync, + existsSync, + lstatSync, + mkdirSync, + readFileSync, + readdirSync, + readlinkSync, + writeFileSync, +} from "node:fs"; +import { join, relative, resolve } from "node:path"; +import { constants as zlibConstants, zstdCompressSync } from "node:zlib"; + +import { + MemoryFileSystem, + type SerializedLazyTree, + type VfsImageMetadata, +} from "../../host/src/vfs/memory-fs"; +import { restoreVerifiedVfsImage } from "../../host/src/vfs/load-image"; +import { ensureDirRecursive } from "../../host/src/vfs/image-helpers"; +import { ENOENT } from "../../host/src/vfs/sharedfs-vendor"; +import type { + HomebrewQueryArtifact, + HomebrewQueryFixtureManifest, + HomebrewQueryLazyArtifact, + HomebrewQueryTap, +} from "./contracts"; + +interface Options { + imagePath: string; + kernelPath: string; + bootstrapPath: string; + outputDirectory: string; + sourceCommit: string; + lazyUrlBase: string; + taps: Array<{ name: string; path: string }>; + coreFormula: string; + canaryFormula?: string; + trustedFormulae: string[]; + eager: boolean; +} + +const decoder = new TextDecoder("utf-8", { fatal: true }); +const SHA256_RE = /^[0-9a-f]{64}$/; +const S_IFMT = 0xf000; +const S_IFREG = 0x8000; +const S_IFDIR = 0x4000; +const S_IFLNK = 0xa000; + +async function main(): Promise { + const options = parseOptions(process.argv.slice(2)); + mkdirSync(options.outputDirectory, { recursive: true }); + const lazyDirectory = join(options.outputDirectory, "lazy"); + mkdirSync(lazyDirectory, { recursive: true }); + + const sourceImage = bytes(options.imagePath); + const fs = await restoreVerifiedVfsImage(sourceImage); + const metadata = fs.getImageMetadata() as (VfsImageMetadata & { + homebrewBootstrap?: { ownership?: { prefix?: unknown } }; + homebrew?: { defaultShell?: { path?: unknown; argv?: unknown } }; + }) | null; + const prefix = metadata?.homebrewBootstrap?.ownership?.prefix; + const shellPath = metadata?.homebrew?.defaultShell?.path; + const shellArgv = metadata?.homebrew?.defaultShell?.argv; + if ( + typeof prefix !== "string" || !prefix.startsWith("/") || + typeof shellPath !== "string" || !shellPath.startsWith("/") || + !Array.isArray(shellArgv) || typeof shellArgv[0] !== "string" + ) { + throw new Error("Homebrew image does not declare its prefix and shell"); + } + + const taps: HomebrewQueryTap[] = []; + const tapRepositories = new Set(); + for (const tap of options.taps) { + const repository = tapRepository(tap.name); + if (tapRepositories.has(repository)) { + throw new Error(`Duplicate tap repository: ${repository}`); + } + tapRepositories.add(repository); + const dirty = git(tap.path, ["status", "--porcelain=v1"]); + if (dirty !== "") { + throw new Error(`Tap checkout is not clean: ${tap.path}`); + } + const commit = git(tap.path, ["rev-parse", "HEAD"]); + if (!/^[0-9a-f]{40}$/.test(commit)) { + throw new Error(`Tap ${tap.name} did not resolve to a commit`); + } + const target = `${prefix}/Library/Taps/${repository}`; + removeVfsTree(fs, target); + copyHostTree(fs, tap.path, target, 1000, 1000); + taps.push({ name: tap.name, repository, commit }); + } + + // Homebrew treats Caskroom as part of an initialized prefix even when no + // casks are installed. Published shells mount an empty writable directory + // here; put the same state in the cross-host fixture so `brew list` measures + // discovery rather than an incomplete harness mount. + const caskroom = `${prefix}/Caskroom`; + ensureDirRecursive(fs, caskroom, 0o755); + fs.chown(caskroom, 1000, 1000); + fs.chmod(caskroom, 0o755); + + const environment = [ + "HOME=/home/user", + "USER=user", + "LOGNAME=user", + `SHELL=${shellPath}`, + `PATH=${prefix}/bin:${prefix}/sbin:/usr/local/bin:/usr/bin:/bin`, + ...readVfsText(fs, "/etc/homebrew/brew.env") + .split("\n") + .map((line) => line.trim()) + .filter((line) => line !== "" && !line.startsWith("#")), + ]; + for (const required of [ + "HOMEBREW_NO_AUTO_UPDATE=1", + "HOMEBREW_NO_INSTALL_FROM_API=1", + "HOMEBREW_NO_ANALYTICS=1", + ]) { + if (!environment.includes(required)) environment.push(required); + } + const trustedFormulae = [...new Set(options.trustedFormulae)].sort(); + if (trustedFormulae.length > 0) { + // WHY: Kandelo mounts /home/user and /var as fresh per-boot scratch on + // both hosts, so user state embedded there is intentionally hidden at + // runtime. Keep this immutable benchmark input in the rootfs-backed /etc + // tree and select it through the standard XDG configuration variable. + const configRoot = "/etc/kandelo/homebrew-query-config"; + const configHome = `${configRoot}/homebrew`; + for (let index = environment.length - 1; index >= 0; index -= 1) { + if ( + environment[index]!.startsWith("XDG_CONFIG_HOME=") || + environment[index]!.startsWith("HOMEBREW_USER_CONFIG_HOME=") + ) { + environment.splice(index, 1); + } + } + environment.push(`XDG_CONFIG_HOME=${configRoot}`); + ensureDirRecursive(fs, configHome, 0o700); + fs.chown(configRoot, 1000, 1000); + fs.chmod(configRoot, 0o700); + fs.chown(configHome, 1000, 1000); + fs.chmod(configHome, 0o700); + fs.createFileWithOwner( + `${configHome}/trust.json`, + 0o600, + 1000, + 1000, + new TextEncoder().encode( + `${JSON.stringify({ trustedformulae: trustedFormulae }, null, 2)}\n`, + ), + ); + } + + const inheritedMetadata: VfsImageMetadata = metadata ?? { version: 1 }; + fs.setImageMetadata({ + ...inheritedMetadata, + homebrewQueryBenchmark: { + schema: 1, + taps, + sourceImageSha256: sha256(sourceImage), + }, + }); + const rootfsPath = join(options.outputDirectory, "rootfs.vfs.zst"); + await saveCompressedImage(fs, rootfsPath); + + const bootstrap = bytes(options.bootstrapPath); + const lazyAssets = await materializeLazyArtifacts({ + groups: fs.exportLazyArchiveEntries(), + bootstrap, + lazyUrlBase: options.lazyUrlBase, + outputDirectory: options.outputDirectory, + }); + + let eagerRootfs: HomebrewQueryArtifact | undefined; + if (options.eager) { + const eagerFs = await restoreVerifiedVfsImage(bytes(rootfsPath)); + const assetsByUrl = new Map( + lazyAssets.map((asset) => [ + asset.url, + bytes(join(options.outputDirectory, asset.file)), + ]), + ); + eagerFs.setLazyFetcher(async (input) => { + const url = new URL(input, options.lazyUrlBase).href; + const asset = assetsByUrl.get(url); + if (!asset) throw new Error(`Unbound eager lazy URL: ${url}`); + return new Response(wholeArrayBuffer(asset)); + }); + const eagerPath = join(options.outputDirectory, "rootfs-eager.vfs.zst"); + await saveCompressedImage(eagerFs, eagerPath, true); + eagerRootfs = fingerprint(eagerPath, options.outputDirectory); + } + + const kernelOutput = join(options.outputDirectory, "kernel.wasm"); + copyFileSync(options.kernelPath, kernelOutput); + const commands = [ + { id: "version", argv: [`${prefix}/bin/brew`, "--version"] }, + { id: "config", argv: [`${prefix}/bin/brew`, "config"] }, + { + id: "info", + argv: [`${prefix}/bin/brew`, "info", options.coreFormula], + }, + { + id: "info_json_v2", + argv: [ + `${prefix}/bin/brew`, + "info", + "--json=v2", + "--formula", + options.coreFormula, + ], + }, + { + id: "list_versions", + argv: [`${prefix}/bin/brew`, "list", "--versions"], + }, + ...(options.canaryFormula === undefined + ? [] + : [{ + id: "canary_info", + argv: [`${prefix}/bin/brew`, "info", options.canaryFormula], + }]), + ]; + const manifest: HomebrewQueryFixtureManifest = { + schema: 1, + kind: "kandelo-homebrew-query-benchmark", + createdAt: new Date().toISOString(), + sourceCommit: options.sourceCommit, + rootfs: fingerprint(rootfsPath, options.outputDirectory), + ...(eagerRootfs === undefined ? {} : { eagerRootfs }), + kernel: fingerprint(kernelOutput, options.outputDirectory), + lazyUrlBase: options.lazyUrlBase, + lazyAssets, + shell: { path: shellPath, argv0: shellArgv[0] }, + homebrew: { prefix, environment, trustedFormulae }, + taps, + commands, + focusCommandId: "info", + }; + const manifestPath = join(options.outputDirectory, "manifest.json"); + writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + process.stdout.write( + `Prepared ${manifestPath} with ${lazyAssets.length} closed lazy bindings\n`, + ); +} + +async function materializeLazyArtifacts(options: { + groups: SerializedLazyTree[]; + bootstrap: Uint8Array; + lazyUrlBase: string; + outputDirectory: string; +}): Promise { + const bootstrapSha256 = sha256(options.bootstrap); + const contentFiles = new Map(); + const output: HomebrewQueryLazyArtifact[] = []; + const seenUrls = new Set(); + for (const group of options.groups) { + const content = group.content; + const integrity = group.integrity; + const expectedSha256 = content?.sha256 ?? integrity?.sha256; + const expectedBytes = content?.bytes ?? integrity?.bytes; + const transports = content?.transports ?? [group.url]; + if ( + typeof expectedSha256 !== "string" || !SHA256_RE.test(expectedSha256) || + typeof expectedBytes !== "number" || + !Number.isSafeInteger(expectedBytes) || expectedBytes <= 0 || + transports.length === 0 + ) { + throw new Error(`Deferred tree ${group.mountPrefix} lacks content identity`); + } + let relativeFile = contentFiles.get(expectedSha256); + if (relativeFile === undefined) { + relativeFile = `lazy/${expectedSha256}.bin`; + const outputPath = join(options.outputDirectory, relativeFile); + if (expectedSha256 === bootstrapSha256) { + writeFileSync(outputPath, options.bootstrap); + } else if (!existsSync(outputPath)) { + const sourceUrl = new URL(transports[0]!, options.lazyUrlBase).href; + process.stdout.write(`Fetching ${sourceUrl}\n`); + const response = await fetch(sourceUrl, { + credentials: "omit", + redirect: "follow", + }); + if (!response.ok) { + throw new Error(`Lazy asset returned HTTP ${response.status}: ${sourceUrl}`); + } + writeFileSync(outputPath, new Uint8Array(await response.arrayBuffer())); + } + const artifact = fingerprint(outputPath, options.outputDirectory); + if ( + artifact.sha256 !== expectedSha256 || artifact.bytes !== expectedBytes + ) { + throw new Error(`Lazy asset changed identity: ${outputPath}`); + } + contentFiles.set(expectedSha256, relativeFile); + } + for (const transport of transports) { + const url = new URL(transport, options.lazyUrlBase).href; + if (seenUrls.has(url)) continue; + seenUrls.add(url); + output.push({ + url, + file: relativeFile, + bytes: expectedBytes, + sha256: expectedSha256, + }); + } + } + return output.sort((left, right) => left.url.localeCompare(right.url)); +} + +async function saveCompressedImage( + fs: MemoryFileSystem, + outputPath: string, + materializeAll = false, +): Promise { + const image = await fs.saveImage({ + materializeAll, + normalizeTimestampsMs: 0, + }); + const compressed = zstdCompressSync(image, { + params: { [zlibConstants.ZSTD_c_compressionLevel]: 19 }, + }); + writeFileSync(outputPath, compressed); +} + +function copyHostTree( + fs: MemoryFileSystem, + source: string, + target: string, + uid: number, + gid: number, +): void { + const stat = lstatSync(source); + const mode = stat.mode & 0o7777; + if (stat.isDirectory()) { + ensureDirRecursive(fs, target, mode || 0o755); + fs.chown(target, uid, gid); + fs.chmod(target, mode || 0o755); + for (const name of readdirSync(source).sort()) { + copyHostTree(fs, join(source, name), `${target}/${name}`, uid, gid); + } + } else if (stat.isFile()) { + ensureDirRecursive(fs, target.slice(0, target.lastIndexOf("/"))); + fs.createFileWithOwner(target, mode || 0o644, uid, gid, bytes(source)); + } else if (stat.isSymbolicLink()) { + ensureDirRecursive(fs, target.slice(0, target.lastIndexOf("/"))); + fs.symlinkWithOwner(readlinkSync(source), target, uid, gid); + } else { + throw new Error(`Tap contains unsupported host entry: ${source}`); + } +} + +function removeVfsTree(fs: MemoryFileSystem, path: string): void { + let mode: number; + try { + mode = fs.lstat(path).mode & S_IFMT; + } catch (error) { + if (vfsErrorCode(error) === ENOENT) return; + throw error; + } + if (mode === S_IFDIR) { + const handle = fs.opendir(path); + const children: string[] = []; + try { + for (;;) { + const entry = fs.readdir(handle); + if (!entry) break; + if (entry.name !== "." && entry.name !== "..") { + children.push(entry.name); + } + } + } finally { + fs.closedir(handle); + } + for (const child of children) removeVfsTree(fs, `${path}/${child}`); + fs.rmdir(path); + } else if (mode === S_IFREG || mode === S_IFLNK) { + fs.unlink(path); + } else { + throw new Error(`VFS tap path has unsupported type: ${path}`); + } +} + +function readVfsText(fs: MemoryFileSystem, path: string): string { + const stat = fs.stat(path); + const output = new Uint8Array(stat.size); + const fd = fs.open(path, 0, 0); + try { + let offset = 0; + while (offset < output.byteLength) { + const count = fs.read( + fd, + output.subarray(offset), + null, + output.byteLength - offset, + ); + if (count <= 0) throw new Error(`Short VFS read: ${path}`); + offset += count; + } + } finally { + fs.close(fd); + } + return decoder.decode(output); +} + +function fingerprint( + path: string, + root: string, +): HomebrewQueryArtifact { + const value = bytes(path); + return { + file: relative(root, path).split("\\").join("/"), + bytes: value.byteLength, + sha256: sha256(value), + }; +} + +function sha256(value: Uint8Array): string { + return createHash("sha256").update(value).digest("hex"); +} + +function bytes(path: string): Uint8Array { + const value = readFileSync(path); + return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); +} + +function wholeArrayBuffer(value: Uint8Array): ArrayBuffer { + return value.buffer.slice( + value.byteOffset, + value.byteOffset + value.byteLength, + ) as ArrayBuffer; +} + +function tapRepository(name: string): string { + const match = /^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/.exec(name); + if (!match) throw new Error(`Invalid tap name: ${name}`); + return `${match[1]}/homebrew-${match[2]}`; +} + +function git(directory: string, args: string[]): string { + return execFileSync("git", args, { + cwd: directory, + encoding: "utf-8", + stdio: ["ignore", "pipe", "inherit"], + }).trim(); +} + +function vfsErrorCode(error: unknown): number | undefined { + return typeof error === "object" && error !== null && "code" in error + ? (error as { code?: number }).code + : undefined; +} + +function parseOptions(argv: string[]): Options { + const allowed = new Set([ + "image", + "kernel", + "bootstrap", + "output", + "source-commit", + "lazy-url-base", + "tap", + "core-formula", + "canary-formula", + "trust-formula", + ]); + const values = new Map(); + let eager = false; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]!; + if (argument === "--eager") { + eager = true; + continue; + } + if (!argument.startsWith("--")) usage(`Unexpected argument: ${argument}`); + const equals = argument.indexOf("="); + const name = equals < 0 ? argument.slice(2) : argument.slice(2, equals); + if (!allowed.has(name)) usage(`Unknown option: --${name}`); + const value = equals < 0 ? argv[++index] : argument.slice(equals + 1); + if (value === undefined || value.startsWith("--")) { + usage(`Missing value for --${name}`); + } + values.set(name, [...(values.get(name) ?? []), value]); + } + const required = (name: string): string => { + const value = values.get(name)?.at(-1); + if (!value) usage(`--${name} is required`); + return resolve(value!); + }; + const sourceCommit = values.get("source-commit")?.at(-1); + if (!sourceCommit || !/^[0-9a-f]{40}$/.test(sourceCommit)) { + usage("--source-commit must be a full Git commit"); + } + const taps = (values.get("tap") ?? []).map((value) => { + const separator = value.indexOf("="); + if (separator <= 0) usage("--tap must be TAP_NAME=CHECKOUT_PATH"); + return { + name: value.slice(0, separator), + path: resolve(value.slice(separator + 1)), + }; + }); + if (taps.length === 0) usage("At least one --tap is required"); + const lazyUrlBase = values.get("lazy-url-base")?.at(-1) ?? + "https://homebrew-query-benchmark.invalid/"; + if (!lazyUrlBase.endsWith("/")) { + usage("--lazy-url-base must end with /"); + } + new URL(lazyUrlBase); + return { + imagePath: required("image"), + kernelPath: required("kernel"), + bootstrapPath: required("bootstrap"), + outputDirectory: required("output"), + sourceCommit, + lazyUrlBase, + taps, + coreFormula: values.get("core-formula")?.at(-1) ?? + "kandelo-dev/tap-core/dash", + canaryFormula: values.get("canary-formula")?.at(-1), + trustedFormulae: values.get("trust-formula") ?? [], + eager, + }; +} + +function usage(message: string): never { + process.stderr.write(`${message}\n\n`); + process.stderr.write( + "Usage: npx tsx benchmarks/homebrew-query/prepare.ts " + + "--image PATH --kernel PATH --bootstrap PATH --output DIR " + + "--source-commit COMMIT --tap TAP=PATH [--tap TAP=PATH] " + + "[--core-formula NAME] [--canary-formula NAME] " + + "[--trust-formula NAME] [--eager]\n", + ); + process.exit(2); +} + +void main().catch((error) => { + process.stderr.write( + `homebrew-query prepare failed: ${error instanceof Error ? error.stack : error}\n`, + ); + process.exitCode = 1; +}); diff --git a/benchmarks/homebrew-query/run-browser.ts b/benchmarks/homebrew-query/run-browser.ts new file mode 100644 index 0000000000..7bbfdc899b --- /dev/null +++ b/benchmarks/homebrew-query/run-browser.ts @@ -0,0 +1,281 @@ +#!/usr/bin/env -S npx tsx + +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { arch, cpus, platform, release } from "node:os"; +import { dirname, extname, relative, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { chromium } from "playwright"; +import { createServer, type Plugin, type ViteDevServer } from "vite"; + +import { withRejectingTimeout } from "../timeout"; +import { medianMetrics } from "./contracts"; +import type { + HomebrewQueryBenchmarkResult, + HomebrewQueryFixtureManifest, + HomebrewQueryScenarioResult, +} from "./contracts"; + +interface Options { + fixturePath: string; + rounds: number; + outputPath?: string; +} + +interface PageResult { + fixture: HomebrewQueryFixtureManifest; + rounds: HomebrewQueryScenarioResult[]; + median: Record; +} + +declare global { + interface Window { + __homebrewQueryBenchmarkReady: boolean; + __runHomebrewQueryBenchmark( + rounds: number, + auditNetwork?: boolean, + ): Promise; + } +} + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const browserRoot = resolve(repoRoot, "apps/browser-demos"); +const BENCHMARK_TIMEOUT_MS = 90 * 60_000; + +async function main(): Promise { + const options = parseOptions(process.argv.slice(2)); + const fixtureRoot = dirname(options.fixturePath); + const fixtureRoute = "/__homebrew_query_fixture/manifest.json"; + let server: ViteDevServer | undefined; + let browser: Awaited> | undefined; + try { + server = await createServer({ + root: browserRoot, + configFile: resolve(browserRoot, "vite.config.ts"), + logLevel: "warn", + server: { + port: 0, + headers: { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", + }, + }, + optimizeDeps: { + entries: [ + resolve(browserRoot, "pages/homebrew-benchmark/index.html"), + ], + }, + plugins: [fixturePlugin(fixtureRoot, options.fixturePath)], + }); + await server.listen(); + const address = server.httpServer?.address(); + if (typeof address !== "object" || address === null) { + throw new Error("Vite did not expose its benchmark port"); + } + const origin = `http://127.0.0.1:${address.port}`; + let browserVersion: string | undefined; + let fixture: HomebrewQueryFixtureManifest | undefined; + const rounds: HomebrewQueryScenarioResult[] = []; + const externalRequests: string[] = []; + for (let index = 0; index < options.rounds; index += 1) { + process.stderr.write( + `Homebrew query independent Chromium round ${index + 1}/${options.rounds}\n`, + ); + browser = await chromium.launch(); + browserVersion ??= browser.version(); + const context = await browser.newContext(); + try { + await context.route("**/*", async (route) => { + const url = route.request().url(); + if ( + url.startsWith(`${origin}/`) || + url.startsWith(`http://localhost:${address.port}/`) + ) { + await route.continue(); + } else { + externalRequests.push(url); + await route.abort("blockedbyclient"); + } + }); + const page = await context.newPage(); + let rejectPageCrash!: (error: Error) => void; + const pageCrash = new Promise((_resolve, reject) => { + rejectPageCrash = reject; + }); + page.on("crash", () => { + rejectPageCrash(new Error( + "Chromium renderer crashed during the Homebrew query benchmark", + )); + }); + page.on("console", (message) => { + if (["log", "info", "warning", "error"].includes(message.type())) { + process.stderr.write(`[Chromium] ${message.text()}\n`); + } + }); + page.on("pageerror", (error) => { + process.stderr.write(`[Chromium page error] ${error.stack ?? error.message}\n`); + }); + await page.goto( + `${origin}/pages/homebrew-benchmark/?fixture=${encodeURIComponent(fixtureRoute)}`, + { waitUntil: "domcontentloaded", timeout: 30_000 }, + ); + await page.waitForFunction( + () => window.__homebrewQueryBenchmarkReady === true, + undefined, + { timeout: 180_000 }, + ); + const pageResult = await withRejectingTimeout( + Promise.race([ + page.evaluate( + ({ auditNetwork }) => window.__runHomebrewQueryBenchmark( + 1, + auditNetwork, + ), + { auditNetwork: index === 0 }, + ), + pageCrash, + ]), + BENCHMARK_TIMEOUT_MS, + `Chromium Homebrew query benchmark timed out after ${BENCHMARK_TIMEOUT_MS}ms`, + ); + if (pageResult.rounds.length !== 1) { + throw new Error("Chromium Homebrew query round returned invalid results"); + } + fixture ??= pageResult.fixture; + rounds.push(pageResult.rounds[0]!); + } finally { + try { + await context.close(); + } finally { + // WHY: each round must release its renderer, Realms, workers and + // SABs. Keeping Chromium alive made independent rounds retain enough + // opaque engine state to crash before a three-round run ended. + await browser.close(); + browser = undefined; + } + } + } + if (externalRequests.length > 0) { + throw new Error( + "Chromium Homebrew query benchmark attempted external requests:\n" + + [...new Set(externalRequests)].join("\n"), + ); + } + if (browserVersion === undefined || fixture === undefined) { + throw new Error("Chromium Homebrew query benchmark produced no rounds"); + } + const result: HomebrewQueryBenchmarkResult = { + schema: 1, + kind: "kandelo-homebrew-query-benchmark-result", + host: "chromium", + hostVersion: browserVersion, + kandeloCommit: gitHead(), + recordedAt: new Date().toISOString(), + machine: { + platform: `${platform()} ${release()}`, + architecture: arch(), + cpu: cpus()[0]?.model ?? "unknown", + }, + fixture, + rounds, + median: medianMetrics(rounds), + }; + const encoded = `${JSON.stringify(result, null, 2)}\n`; + if (options.outputPath) { + mkdirSync(dirname(options.outputPath), { recursive: true }); + writeFileSync(options.outputPath, encoded); + process.stderr.write(`Wrote ${options.outputPath}\n`); + } + process.stdout.write(encoded); + } finally { + await browser?.close(); + await server?.close(); + } +} + +function fixturePlugin(fixtureRoot: string, manifestPath: string): Plugin { + const root = resolve(fixtureRoot); + return { + name: "homebrew-query-fixture", + configureServer(server) { + server.middlewares.use("/__homebrew_query_fixture/", (request, response) => { + const requestPath = decodeURIComponent( + (request.url ?? "").split("?", 1)[0]!.replace(/^\/+/, ""), + ); + const path = requestPath === "manifest.json" + ? manifestPath + : resolve(root, requestPath); + const relativePath = relative(root, path); + if ( + relativePath === ".." || relativePath.startsWith(`..${sep}`) || + !existsSync(path) || !statSync(path).isFile() + ) { + response.statusCode = 404; + response.end(); + return; + } + response.statusCode = 200; + response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); + response.setHeader("Content-Type", contentType(path)); + response.end(readFileSync(path)); + }); + }, + }; +} + +function contentType(path: string): string { + switch (extname(path)) { + case ".json": return "application/json"; + case ".wasm": return "application/wasm"; + default: return "application/octet-stream"; + } +} + +function gitHead(): string { + return execFileSync("git", ["rev-parse", "HEAD"], { + cwd: repoRoot, + encoding: "utf-8", + stdio: ["ignore", "pipe", "inherit"], + }).trim(); +} + +function parseOptions(argv: string[]): Options { + let fixturePath: string | undefined; + let outputPath: string | undefined; + let rounds = 3; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]!; + const [name, inline] = argument.split("=", 2); + const value = inline ?? argv[++index]; + if (name === "--fixture") fixturePath = value; + else if (name === "--output") outputPath = value; + else if (name === "--rounds") rounds = Number(value); + else usage(`Unknown argument: ${argument}`); + } + if (!fixturePath) usage("--fixture is required"); + if (!Number.isSafeInteger(rounds) || rounds <= 0) { + usage("--rounds must be a positive integer"); + } + return { + fixturePath: resolve(fixturePath), + rounds, + ...(outputPath === undefined ? {} : { outputPath: resolve(outputPath) }), + }; +} + +function usage(message: string): never { + process.stderr.write(`${message}\n`); + process.stderr.write( + "Usage: npx tsx benchmarks/homebrew-query/run-browser.ts " + + "--fixture PATH [--rounds 3] [--output PATH]\n", + ); + process.exit(2); +} + +void main().catch((error) => { + process.stderr.write( + `homebrew-query Chromium failed: ${error instanceof Error ? error.stack : error}\n`, + ); + process.exitCode = 1; +}); diff --git a/benchmarks/homebrew-query/run-node.ts b/benchmarks/homebrew-query/run-node.ts new file mode 100644 index 0000000000..0223eeca7d --- /dev/null +++ b/benchmarks/homebrew-query/run-node.ts @@ -0,0 +1,357 @@ +#!/usr/bin/env -S npx tsx + +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { arch, cpus, platform, release } from "node:os"; +import { dirname, resolve } from "node:path"; + +import { NodeKernelHost } from "../../host/src/node-kernel-host"; +import type { SyscallTraceEvent } from "../../host/src/kernel-worker"; +import type { LazyDownloadEvent } from "../../host/src/vfs/memory-fs"; +import type { ClosedLazyAsset } from "../../host/src/vfs/closed-lazy-assets"; +import { + assertHomebrewQueryFixture, + commandScript, + emptyProcessCounts, + medianMetrics, + summarizeLazyDownloads, + type HomebrewQueryArtifact, + type HomebrewQueryBenchmarkResult, + type HomebrewQueryCommand, + type HomebrewQueryCommandResult, + type HomebrewQueryFixtureManifest, + type HomebrewQueryNetworkAudit, + type HomebrewQueryProcessCounts, +} from "./contracts"; +import { + runHomebrewQueryScenario, + type HomebrewQueryMachine, + type HomebrewQueryMachineFactory, +} from "./scenario"; + +interface Options { + fixturePath: string; + rounds: number; + outputPath?: string; +} + +interface ProcessEvent { + kind: "spawn" | "exec" | "exit"; +} + +const COMMAND_TIMEOUT_MS = 10 * 60_000; +const decoder = new TextDecoder("utf-8", { fatal: false }); + +async function main(): Promise { + const options = parseOptions(process.argv.slice(2)); + const fixtureRoot = dirname(options.fixturePath); + const fixture = JSON.parse(readFileSync(options.fixturePath, "utf-8")); + assertHomebrewQueryFixture(fixture); + const kernel = readVerifiedArtifact(fixtureRoot, fixture.kernel); + const lazyAssets = fixture.lazyAssets.map((artifact) => ({ + url: artifact.url, + sha256: artifact.sha256, + size: artifact.bytes, + bytes: readVerifiedArtifact(fixtureRoot, artifact), + } satisfies ClosedLazyAsset)); + const factory = new NodeHomebrewQueryMachineFactory( + fixtureRoot, + fixture, + kernel, + lazyAssets, + ); + const rounds = []; + for (let index = 0; index < options.rounds; index += 1) { + process.stderr.write(`Homebrew query Node round ${index + 1}/${options.rounds}\n`); + rounds.push(await runHomebrewQueryScenario({ + fixture, + machines: factory, + auditNetwork: index === 0, + })); + } + const result: HomebrewQueryBenchmarkResult = { + schema: 1, + kind: "kandelo-homebrew-query-benchmark-result", + host: "node", + hostVersion: process.version, + kandeloCommit: gitHead(), + recordedAt: new Date().toISOString(), + machine: { + platform: `${platform()} ${release()}`, + architecture: arch(), + cpu: cpus()[0]?.model ?? "unknown", + }, + fixture, + rounds, + median: medianMetrics(rounds), + }; + const encoded = `${JSON.stringify(result, null, 2)}\n`; + if (options.outputPath) { + mkdirSync(dirname(options.outputPath), { recursive: true }); + writeFileSync(options.outputPath, encoded); + process.stderr.write(`Wrote ${options.outputPath}\n`); + } + process.stdout.write(encoded); +} + +class NodeHomebrewQueryMachineFactory implements HomebrewQueryMachineFactory { + constructor( + private readonly fixtureRoot: string, + private readonly fixture: HomebrewQueryFixtureManifest, + private readonly kernel: Uint8Array, + private readonly lazyAssets: ClosedLazyAsset[], + ) {} + + async create(image: "lazy" | "eager"): Promise { + const artifact = image === "lazy" + ? this.fixture.rootfs + : this.fixture.eagerRootfs; + if (!artifact) throw new Error("Eager Homebrew query image is unavailable"); + return new NodeHomebrewQueryMachine( + this.fixture, + readVerifiedArtifact(this.fixtureRoot, artifact), + this.kernel, + image === "lazy" ? this.lazyAssets : undefined, + ); + } +} + +class NodeHomebrewQueryMachine implements HomebrewQueryMachine { + private readonly stdout: Uint8Array[] = []; + private readonly stderr: Uint8Array[] = []; + private readonly processEvents: ProcessEvent[] = []; + private readonly lazyEvents: LazyDownloadEvent[] = []; + private readonly host: NodeKernelHost; + + constructor( + private readonly fixture: HomebrewQueryFixtureManifest, + image: Uint8Array, + private readonly kernel: Uint8Array, + lazyAssets?: ClosedLazyAsset[], + ) { + this.host = new NodeKernelHost({ + maxWorkers: 4, + rootfsImage: image, + rootfsLazyUrlBase: fixture.lazyUrlBase, + ...(lazyAssets === undefined ? {} : { rootfsLazyAssets: lazyAssets }), + // A successful audited query with this backend disabled cannot have + // depended on an external request. + enableTcpNetwork: false, + onStdout: (_pid, value) => this.stdout.push(value.slice()), + onStderr: (_pid, value) => this.stderr.push(value.slice()), + onProcessEvent: (event) => this.processEvents.push(event), + onLazyDownload: (event) => this.lazyEvents.push(event), + }); + } + + async start(): Promise { + const startedAt = performance.now(); + await this.host.init(wholeArrayBuffer(this.kernel)); + return performance.now() - startedAt; + } + + async run(command: HomebrewQueryCommand): Promise { + const stdoutStart = this.stdout.length; + const stderrStart = this.stderr.length; + const processStart = this.processEvents.length; + const lazyStart = this.lazyEvents.length; + const startedAt = performance.now(); + const spawned = await this.host.spawnFromVfs( + this.fixture.shell.path, + [this.fixture.shell.argv0, "-l", "-c", commandScript(command)], + { + env: this.fixture.homebrew.environment, + cwd: "/home/user", + uid: 1000, + gid: 1000, + stdin: new Uint8Array(), + }, + ); + const status = await withTimeout( + spawned.exit, + COMMAND_TIMEOUT_MS, + `Homebrew query timed out: ${command.id}`, + ); + const elapsedMs = performance.now() - startedAt; + const stdout = concatenate(this.stdout.slice(stdoutStart)); + const stderr = concatenate(this.stderr.slice(stderrStart)); + const invalidQueryOutput = command.id !== "shell_boot" && ( + stdout.byteLength === 0 || stderr.byteLength !== 0 + ); + if (status !== 0 || invalidQueryOutput) { + throw new Error( + `Homebrew query ${command.id} did not produce clean output ` + + `(status=${status}, stdout=${stdout.byteLength}, ` + + `stderr=${stderr.byteLength}): ` + + decoder.decode(stderr).slice(-4_000), + ); + } + return { + id: command.id, + argv: command.argv, + elapsedMs, + status, + stdoutBytes: stdout.byteLength, + stderrBytes: stderr.byteLength, + stdoutSha256: sha256(stdout), + stderrSha256: sha256(stderr), + processCounts: countProcessEvents(this.processEvents.slice(processStart)), + lazy: summarizeLazyDownloads(this.lazyEvents.slice(lazyStart)), + }; + } + + async auditNetwork( + commands: readonly HomebrewQueryCommand[], + ): Promise { + const events: SyscallTraceEvent[] = []; + const unsubscribe = this.host.subscribeSyscalls((event) => events.push(event)); + try { + const output: HomebrewQueryNetworkAudit["commands"] = []; + for (const command of commands) { + const start = events.length; + const result = await this.run(command); + // Trace delivery is deliberately batched outside the syscall hot path. + await delay(350); + output.push({ + id: command.id, + status: result.status, + networkSyscalls: countNetworkSyscalls(events.slice(start)), + }); + } + return { commands: output }; + } finally { + unsubscribe(); + } + } + + destroy(): Promise { + return this.host.destroy(); + } +} + +function countNetworkSyscalls( + events: readonly SyscallTraceEvent[], +): Record { + const counts: Record = {}; + for (const event of events) { + const name = event.decoded?.match( + /\]\s+(socket|connect|send|sendto|sendmsg|recv|recvfrom|recvmsg)\b/, + )?.[1]; + if (name) counts[name] = (counts[name] ?? 0) + 1; + } + return counts; +} + +function countProcessEvents( + events: readonly ProcessEvent[], +): HomebrewQueryProcessCounts { + const counts = emptyProcessCounts(); + for (const event of events) counts[event.kind] += 1; + return counts; +} + +function readVerifiedArtifact( + fixtureRoot: string, + artifact: HomebrewQueryArtifact, +): Uint8Array { + const path = resolve(fixtureRoot, artifact.file); + const value = readFileSync(path); + const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + if (bytes.byteLength !== artifact.bytes || sha256(bytes) !== artifact.sha256) { + throw new Error(`Homebrew query artifact changed identity: ${path}`); + } + return bytes; +} + +function concatenate(chunks: readonly Uint8Array[]): Uint8Array { + const output = new Uint8Array( + chunks.reduce((total, chunk) => total + chunk.byteLength, 0), + ); + let offset = 0; + for (const chunk of chunks) { + output.set(chunk, offset); + offset += chunk.byteLength; + } + return output; +} + +function wholeArrayBuffer(value: Uint8Array): ArrayBuffer { + return value.buffer.slice( + value.byteOffset, + value.byteOffset + value.byteLength, + ) as ArrayBuffer; +} + +function sha256(value: Uint8Array): string { + return createHash("sha256").update(value).digest("hex"); +} + +function gitHead(): string { + return execFileSync("git", ["rev-parse", "HEAD"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "inherit"], + }).trim(); +} + +function delay(ms: number): Promise { + return new Promise((resolveDelay) => setTimeout(resolveDelay, ms)); +} + +async function withTimeout( + promise: Promise, + timeoutMs: number, + message: string, +): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + promise, + new Promise((_resolve, reject) => { + timer = setTimeout(() => reject(new Error(message)), timeoutMs); + }), + ]); + } finally { + if (timer !== undefined) clearTimeout(timer); + } +} + +function parseOptions(argv: string[]): Options { + let fixturePath: string | undefined; + let outputPath: string | undefined; + let rounds = 3; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]!; + const [name, inline] = argument.split("=", 2); + const value = inline ?? argv[++index]; + if (name === "--fixture") fixturePath = value; + else if (name === "--output") outputPath = value; + else if (name === "--rounds") rounds = Number(value); + else usage(`Unknown argument: ${argument}`); + } + if (!fixturePath) usage("--fixture is required"); + if (!Number.isSafeInteger(rounds) || rounds <= 0) { + usage("--rounds must be a positive integer"); + } + return { + fixturePath: resolve(fixturePath), + rounds, + ...(outputPath === undefined ? {} : { outputPath: resolve(outputPath) }), + }; +} + +function usage(message: string): never { + process.stderr.write(`${message}\n`); + process.stderr.write( + "Usage: npx tsx benchmarks/homebrew-query/run-node.ts " + + "--fixture PATH [--rounds 3] [--output PATH]\n", + ); + process.exit(2); +} + +void main().catch((error) => { + process.stderr.write( + `homebrew-query Node failed: ${error instanceof Error ? error.stack : error}\n`, + ); + process.exitCode = 1; +}); diff --git a/benchmarks/homebrew-query/scenario.ts b/benchmarks/homebrew-query/scenario.ts new file mode 100644 index 0000000000..e8b5bfcdd8 --- /dev/null +++ b/benchmarks/homebrew-query/scenario.ts @@ -0,0 +1,101 @@ +import type { + HomebrewQueryCommand, + HomebrewQueryCommandResult, + HomebrewQueryFixtureManifest, + HomebrewQueryNetworkAudit, + HomebrewQueryScenarioResult, +} from "./contracts"; + +export interface HomebrewQueryMachine { + start(): Promise; + run(command: HomebrewQueryCommand): Promise; + auditNetwork( + commands: readonly HomebrewQueryCommand[], + ): Promise; + destroy(): Promise; +} + +export interface HomebrewQueryMachineFactory { + create(image: "lazy" | "eager"): Promise; +} + +const SHELL_BOOT_COMMAND: HomebrewQueryCommand = { + id: "shell_boot", + argv: [":"], +}; + +export async function runHomebrewQueryScenario(options: { + fixture: HomebrewQueryFixtureManifest; + machines: HomebrewQueryMachineFactory; + auditNetwork: boolean; +}): Promise { + const focus = options.fixture.commands.find( + (command) => command.id === options.fixture.focusCommandId, + )!; + + const coldStartedAt = performance.now(); + const coldMachine = await options.machines.create("lazy"); + let cold: HomebrewQueryScenarioResult["cold"]; + try { + const machineBootMs = await coldMachine.start(); + const first = await coldMachine.run(focus); + const machineBootAndFirstBrewMs = performance.now() - coldStartedAt; + const warm = await coldMachine.run(focus); + cold = { + machineBootMs, + machineBootAndFirstBrewMs, + first, + warm, + }; + } finally { + await coldMachine.destroy(); + } + + const bootedMachine = await options.machines.create("lazy"); + try { + const bootedMachineBootMs = await bootedMachine.start(); + const shellBoot = await bootedMachine.run(SHELL_BOOT_COMMAND); + const first: HomebrewQueryCommandResult[] = []; + const repeated: HomebrewQueryCommandResult[] = []; + for (const command of options.fixture.commands) { + first.push(await bootedMachine.run(command)); + } + for (const command of options.fixture.commands) { + repeated.push(await bootedMachine.run(command)); + } + const networkAudit = options.auditNetwork + ? await bootedMachine.auditNetwork(options.fixture.commands) + : undefined; + + let eager: HomebrewQueryScenarioResult["eager"]; + if (options.fixture.eagerRootfs !== undefined) { + const eagerMachine = await options.machines.create("eager"); + try { + const machineBootMs = await eagerMachine.start(); + const eagerShellBoot = await eagerMachine.run(SHELL_BOOT_COMMAND); + eager = { + machineBootMs, + shellBootMs: eagerShellBoot.elapsedMs, + first: await eagerMachine.run(focus), + warm: await eagerMachine.run(focus), + }; + } finally { + await eagerMachine.destroy(); + } + } + + return { + cold, + booted: { + machineBootMs: bootedMachineBootMs, + shellBootMs: shellBoot.elapsedMs, + first, + warm: repeated, + }, + ...(eager === undefined ? {} : { eager }), + ...(networkAudit === undefined ? {} : { networkAudit }), + }; + } finally { + await bootedMachine.destroy(); + } +} diff --git a/crates/wasm-local-root-spill/Cargo.toml b/crates/wasm-local-root-spill/Cargo.toml index f08b281954..f7bbc82752 100644 --- a/crates/wasm-local-root-spill/Cargo.toml +++ b/crates/wasm-local-root-spill/Cargo.toml @@ -18,8 +18,8 @@ path = "src/lib.rs" anyhow = "1" clap = { version = "4", features = ["derive"] } walrus = "0.26" +wasmparser = "0.247" [dev-dependencies] -wasmparser = "0.247" wasmprinter = "0.247" wat = "1" diff --git a/crates/wasm-local-root-spill/README.md b/crates/wasm-local-root-spill/README.md index 63f54819f7..c37ffa3545 100644 --- a/crates/wasm-local-root-spill/README.md +++ b/crates/wasm-local-root-spill/README.md @@ -107,7 +107,9 @@ and browser hosts diverge. These are deliberate limits of the first Ruby-focused implementation: - only the `ruby` profile is accepted; -- only wasm32 modules with a mutable `i32` `__stack_pointer` are supported; +- wasm32 main modules must expose a mutable `i32` `__stack_pointer`; a + `dylink.0` side module whose linker omitted an otherwise-unused stack pointer + imports the process-wide global before spill frames are added; - only 32-bit `VALUE`-like roots are spilled; - `i64`, `f32`, `f64`, `v128`, and reference-typed values are not root slots; - memory64 and runtimes that depend on Wasm GC refs are unsupported; diff --git a/crates/wasm-local-root-spill/src/lib.rs b/crates/wasm-local-root-spill/src/lib.rs index f144810662..13bf55533e 100644 --- a/crates/wasm-local-root-spill/src/lib.rs +++ b/crates/wasm-local-root-spill/src/lib.rs @@ -30,6 +30,7 @@ use walrus::{ TryTable, TryTableCatch, UnaryOp, Value, }, }; +use wasmparser::{Parser, Payload}; #[derive(Debug, Clone)] pub struct Options { @@ -84,7 +85,8 @@ pub fn spill(input: &[u8], opts: &Options) -> Result> { let mut module = Module::from_buffer(input).context("failed to parse input wasm module for root spill")?; - let stack_pointer = find_stack_pointer(&module)?; + let stack_pointer = + find_or_import_stack_pointer(&mut module, input_starts_with_dylink(input)?)?; let memory = find_wasm32_memory(&module)?; let targets: Vec = module @@ -101,7 +103,79 @@ pub fn spill(input: &[u8], opts: &Options) -> Result> { .with_context(|| format!("spilling {}", function_name(&module, func_id)))?; } - Ok(module.emit_wasm()) + let output = module.emit_wasm(); + restore_leading_dylink_section(input, output) +} + +/// Walrus emits raw custom sections after the standard sections. Ruby native +/// extensions are Wasm side modules, whose `dylink.0` section must remain the +/// first section for the dynamic loader to recognize them. Preserve that +/// input ordering after adding root spill frames; otherwise protecting a +/// native extension from GC would make the same extension unloadable. +fn restore_leading_dylink_section(input: &[u8], output: Vec) -> Result> { + if !input_starts_with_dylink(input)? { + return Ok(output); + } + + let mut dylink_range = None; + for payload in Parser::new(0).parse_all(&output) { + if let Payload::CustomSection(section) = payload? { + if section.name() != "dylink.0" { + continue; + } + ensure!( + dylink_range.is_none(), + "root-spilled module contains more than one dylink.0 section" + ); + dylink_range = Some(section.range()); + } + } + + let payload_range = dylink_range.context( + "input shared module started with dylink.0, but the root-spilled output lost it", + )?; + let payload_len = u32::try_from(payload_range.len()) + .context("dylink.0 section is too large for a wasm section")?; + let section_header_len = 1 + u32_leb_len(payload_len); + let section_start = payload_range + .start + .checked_sub(section_header_len) + .context("dylink.0 section range does not include a valid header")?; + ensure!( + section_start >= 8 && output.get(section_start) == Some(&0), + "dylink.0 section does not have a valid custom-section header" + ); + if section_start == 8 { + return Ok(output); + } + + let mut reordered = Vec::with_capacity(output.len()); + reordered.extend_from_slice(&output[..8]); + reordered.extend_from_slice(&output[section_start..payload_range.end]); + reordered.extend_from_slice(&output[8..section_start]); + reordered.extend_from_slice(&output[payload_range.end..]); + Ok(reordered) +} + +fn input_starts_with_dylink(input: &[u8]) -> Result { + let mut payloads = Parser::new(0).parse_all(input); + let _version = payloads + .next() + .transpose() + .context("parsing input wasm header")?; + Ok(matches!( + payloads.next().transpose()?, + Some(Payload::CustomSection(section)) if section.name() == "dylink.0" + )) +} + +fn u32_leb_len(mut value: u32) -> usize { + let mut len = 1; + while value >= 0x80 { + value >>= 7; + len += 1; + } + len } fn spill_function( @@ -1199,7 +1273,7 @@ fn emit_store_local( push(out, store_i32(memory, offset), loc()); } -fn find_stack_pointer(module: &Module) -> Result { +fn find_or_import_stack_pointer(module: &mut Module, is_side_module: bool) -> Result { let from_export = module.exports.iter().find_map(|export| match export.item { ExportItem::Global(id) if export.name == "__stack_pointer" => Some(id), _ => None, @@ -1211,7 +1285,22 @@ fn find_stack_pointer(module: &Module) -> Result { .find(|global| global.name.as_deref() == Some("__stack_pointer")) .map(|global| global.id()) }); - let id = id.context("mutable i32 `__stack_pointer` global not found")?; + let id = match id { + Some(id) => id, + None if is_side_module => { + // WHY: wasm-ld omits __stack_pointer from a side module when the + // original object code needs no linear stack. Root spilling can + // still need a frame for live GC roots, as in a call-bearing Ruby + // extension initializer. Import the process's existing stack + // pointer exactly as wasm-ld does for side modules that happened + // to use stack storage; this adds no private stack or host ABI. + let (id, _) = + module.add_import_global("env", "__stack_pointer", ValType::I32, true, false); + module.exports.add("__stack_pointer", id); + id + } + None => bail!("mutable i32 `__stack_pointer` global not found"), + }; let global = module.globals.get(id); ensure!( global.ty == ValType::I32 && global.mutable, diff --git a/crates/wasm-local-root-spill/tests/root_spill.rs b/crates/wasm-local-root-spill/tests/root_spill.rs index d287394ad4..df2b1e5e21 100644 --- a/crates/wasm-local-root-spill/tests/root_spill.rs +++ b/crates/wasm-local-root-spill/tests/root_spill.rs @@ -1,5 +1,5 @@ use walrus::{ - ExportItem, FunctionId, FunctionKind, GlobalId, LocalFunction, Module, + ExportItem, FunctionId, FunctionKind, GlobalId, ImportKind, LocalFunction, Module, ir::{self, Instr, InstrSeqId}, }; use wasm_local_root_spill::{Options, SpillSet, spill}; @@ -225,6 +225,61 @@ fn rejects_missing_stack_pointer() { assert!(format!("{err:#}").contains("__stack_pointer")); } +#[test] +fn imports_process_stack_pointer_for_stackless_side_module() { + let wat = r#" + (module + (@custom "dylink.0" (before first) "test metadata") + (import "env" "memory" (memory 1)) + (import "env" "alloc" (func $alloc)) + (func $main (export "main") (param $root i32) (result i32) + call $alloc + local.get $root)) + "#; + + let bytes = spill_wat(wat); + validate(&bytes); + let module = module(&bytes); + let stack_pointer = stack_pointer(&module); + + assert!(module.imports.iter().any(|import| { + import.module == "env" + && import.name == "__stack_pointer" + && matches!(import.kind, ImportKind::Global(id) if id == stack_pointer) + })); + assert_eq!(count_stack_pointer_sets(&module, "main"), 2); +} + +#[test] +fn keeps_side_module_dylink_section_first() { + let wat = r#" + (module + (@custom "dylink.0" (before first) "test metadata") + (memory (export "memory") 1) + (global $__stack_pointer + (export "__stack_pointer") (mut i32) (i32.const 65536)) + (func $alloc) + (func $main (export "main") (param $root i32) (result i32) + call $alloc + local.get $root)) + "#; + + let bytes = spill_wat(wat); + validate(&bytes); + let mut payloads = wasmparser::Parser::new(0).parse_all(&bytes); + assert!(matches!( + payloads.next().unwrap().unwrap(), + wasmparser::Payload::Version { .. } + )); + match payloads.next().unwrap().unwrap() { + wasmparser::Payload::CustomSection(section) => { + assert_eq!(section.name(), "dylink.0"); + assert_eq!(section.data(), b"test metadata"); + } + other => panic!("dylink.0 must remain the first section, got {other:?}"), + } +} + #[test] fn spills_operand_stack_i32_carryover_across_call() { let wat = fixture( diff --git a/docs/homebrew-packaging-system.md b/docs/homebrew-packaging-system.md index 3eb96b5780..a32db1cb74 100644 --- a/docs/homebrew-packaging-system.md +++ b/docs/homebrew-packaging-system.md @@ -488,9 +488,17 @@ not turn an arbitrary URL in VFS metadata into network authority. Guest Homebrew has two separate inputs: 1. `homebrew-bootstrap` contains one reviewed upstream Homebrew source - tree plus Kandelo's guest-platform patch and environment policy. -2. A bottle-backed runtime-support closure supplies Ruby and the - ordinary tools that Homebrew needs. + tree plus Kandelo's guest-platform patch, environment policy, and the + source-selected portable Ruby as separate authenticated outputs. +2. A bottle-backed runtime-support closure supplies the ordinary tools that + Homebrew needs. + +Portable Ruby uses Homebrew's standard +`Library/Homebrew/vendor/portable-ruby/` layout and `current` +symlink. The version is read from the exact Homebrew source tree, and the +guest prefix is derived from the authenticated deferred-tree descriptor. +Neither the compatibility prefix nor the future canonical prefix is encoded +in the portable-Ruby composition logic. The base shell can expose `/usr/bin/brew` as a lazy activation reference. A user who never runs `brew` does not pay the bootstrap and diff --git a/docs/homebrew-publishing.md b/docs/homebrew-publishing.md index 5ebba2ae7c..3968ad15d1 100644 --- a/docs/homebrew-publishing.md +++ b/docs/homebrew-publishing.md @@ -147,20 +147,33 @@ for dispatch, recovery, seal-last publication, and mutation handling. ## Guest Homebrew Bootstrap Bottle The patched Homebrew Ruby tree used by a guest is the support-data Formula -`homebrew-bootstrap`. Its bottle contains two declared `libexec` members: +`homebrew-bootstrap`. Its bottle contains three declared `libexec` members: `homebrew-bootstrap.zip`, built from one sealed Homebrew checkout and the reviewed guest-platform patch, and `homebrew-brew.env`, which owns the matching -architecture and system-environment policy. The tap-native recipe lock at -`Kandelo/recipes/homebrew-bootstrap/source-lock.json` binds both outputs by -path, SHA-256, and byte count. +architecture and system-environment policy, plus +`homebrew-portable-ruby.zip`. The latter packages the source-selected Ruby +dependency in Homebrew's standard versioned vendor layout, including the +`current` symlink and the real Bootsnap and msgpack gems. Bootsnap's only +guest-platform patch extends its upstream supported-platform test to Ruby's +Wasm target; the cache and loader remain Bootsnap's implementation. The +tap-native recipe lock at +`Kandelo/recipes/homebrew-bootstrap/source-lock.json` binds all three outputs +by path, SHA-256, and byte count. The product shell fetches that public bottle anonymously. The shared typed extractor binds the exact tap checkout, aggregate catalog and Formula sidecar, recipe lock, link manifest, bottle digest and size, keg identity, immutable -build receipt, and both output members. The shell composer reruns the same +build receipt, and all three output members. The shell composer reruns the same typed verifier against the detached files before embedding its canonical report. It does not consult the Kandelo package registry for these bytes. +The composer reads +`Library/Homebrew/vendor/portable-ruby-version` from the authenticated source +ZIP and requires the portable-Ruby output to expose that exact version. It +derives the vendor mount and executable from the bootstrap descriptor's guest +prefix. This preserves Homebrew's normal portable-Ruby detection and startup +without encoding either the compatibility or canonical Kandelo prefix. + Browser builds preserve that ownership. Their publication workflows extract `homebrew-bootstrap.zip` from the verified support-data bottle and stage it at the same-origin `homebrew-bootstrap.zip` URL used by the @@ -3776,6 +3789,15 @@ hardlink aliases for `@@HOMEBREW_PREFIX@@`, `@@HOMEBREW_CELLAR@@`, receipt-selected `@@HOMEBREW_JAVA@@` replacement. The eager composer uses the same browser-safe relocation implementation. +Read-only package discovery must not activate an otherwise unused bottle just +to read its standard metadata. For every deferred Formula, the shell composer +therefore copies the exact authenticated `.brew/.rb` and +`INSTALL_RECEIPT.json` bytes from the verified pour into the image. It removes +only those guest entries from the deferred projection; the complete bottle +source inventory, payload digest, and byte count remain authoritative. The +materialization evidence hashes the eager metadata independently. This is an +exact projection of upstream bottle members, not reconstructed Homebrew state. + First use still fetches and verifies the complete unmodified `.tar.gz`; the content digest and byte count never describe relocated or recompressed bytes. After decoding and complete source-inventory validation, the runtime requires @@ -4032,13 +4054,13 @@ or stale compatibility link fail before browser acceptance. The shell closure does not by itself claim that `/usr/bin/brew` can run. `homebrew/main-shell-homebrew-runtime-support.json` declares a second, first-use atomic layer. It binds the lazy `homebrew-bootstrap` bottle's -extracted source and launcher outputs to seven reviewed runtime roots—Ruby, -Git, curl, Findutils, Gawk, Tar, and `posix-utils-lite`—and to their exact -dependency-first closure derived from the selected tap metadata. Dependencies -already present in the complete shell remain shared. Any additional dependency -is another atomic lazy bottle tree; for example, a Ruby bottle that declares -`libyaml` adds both `libyaml` and Ruby beyond the base closure. The finalizer -derives this difference instead of preserving a stale hand-counted inventory. +extracted source, launcher, and portable-Ruby outputs to six reviewed Formula +roots—Git, curl, Findutils, Gawk, Tar, and `posix-utils-lite`—and to their +exact dependency-first closure derived from the selected tap metadata. +Dependencies already present in the complete shell remain shared. Any +additional Formula dependency is another atomic lazy bottle tree. The +finalizer derives this difference instead of preserving a stale hand-counted +inventory. The availability audit includes every Formula considered during the runtime rollout plus any newly selected runtime dependency. Every member must have an diff --git a/docs/measurements/2026-08-04-homebrew-query-performance.md b/docs/measurements/2026-08-04-homebrew-query-performance.md new file mode 100644 index 0000000000..63de972689 --- /dev/null +++ b/docs/measurements/2026-08-04-homebrew-query-performance.md @@ -0,0 +1,399 @@ +# In-guest Homebrew query performance — 2026-08-04 + +## Status and scope + +This record contains the final three-round focused benchmark and the required +repository-wide before/after benchmark. The implementation commit is +`58e0c41e63d46f403d211c0aabfe7bb51baa9987`, based on protected `main` +commit `880e5be7d2dbd3772602ff7eaed952c0b8c6608f`. The focused fixture records +producer commit `5a025d98a5f6f1856618f0309c4ad7144dc78e52`. The deployed rev22 +compatibility-shell baseline records commit +`00e30e1aeb9a1b691b2fd8ffcf0cf7ef561aab3c`. + +The rev22 and candidate shell images necessarily contain different runtime +artifacts and tap-core revisions, so their focused numbers are a deployed +product comparison, not an exact-artifact A/B. Each individual Node/browser +pair used the same immutable image, kernel, taps, lazy bytes, command inputs, +and environment. The repository-wide comparison used freshly built hosts at +protected `main` and the candidate with identical benchmark workload +artifacts, whose digests are recorded by the benchmark runner. + +The retained implementation does not reuse JavaScript Realms. A process may +consume a pristine prestarted Worker, but that Worker is one-shot and is +terminated after the process retires. Unused reserve Workers contain no guest +memory. This preserves process isolation and lets Kandelo release every +reference it owns to the process `SharedArrayBuffer` (SAB) and +`WebAssembly.Memory`. + +## Exact diagnostic inputs + +The final focused Node and Chromium results were recorded on 2026-08-04 at +`2026-08-04T20:37:42.645Z` and `2026-08-04T20:43:45.208Z` with: + +- Apple M5 Max, arm64, Darwin 25.6.0; +- Node.js `v24.15.0`; +- Chrome for Testing `149.0.7827.55` for the matched browser attempt; +- kernel SHA-256 + `bf11b2a61959cacfb05fb3f21cdbba8b8109ea56231379c07adf1f4f4365ecfd`; +- lazy rootfs SHA-256 + `b270afb52a0c36e6c23c95290c1c24fed295d9998e2e70fa60921bf821f9f571`; +- eager rootfs SHA-256 + `b5c819b196a1d8953f059f4b95135116581cb6bb271f6baa56e81e4bd6e865dc`; +- `kandelo-dev/tap-core` commit + `28ddbc5352749f217ad89bf010e0f16b7e4b3d81`; and +- `brandonpayton/kandelo-canary` commit + `b86d1810c68e3ab17bdab218856da3a7516ec95c`. + +The fixture used the deployed compatibility prefix +`/home/linuxbrew/.linuxbrew`. The implementation derives that prefix from +authenticated descriptors; the benchmark and runtime do not assume it. + +Every command received: + +```text +HOMEBREW_NO_AUTO_UPDATE=1 +HOMEBREW_NO_INSTALL_FROM_API=1 +HOMEBREW_NO_ANALYTICS=1 +``` + +The workload covered `brew --version`, `brew config`, both text and JSON v2 +`brew info` for `kandelo-dev/tap-core/dash`, `brew list --versions`, and +`brew info brandonpayton/kandelo-canary/m4-canary`. + +Node ran with its TCP backend disabled. The first round also traced guest +socket syscalls. Every measured command reported an empty network-syscall +map. Chromium served only the closed local fixture and rejected other +requests. No measured query depended on the public network. + +## Final focused result + +These are host-side monotonic medians from three independent rounds. Each +Chromium round launched a fresh browser so a prior renderer, Worker, Realm, +memory, or shared buffer could not survive into the next sample: + +| Phase | Node | Chromium | +| --- | ---: | ---: | +| Machine boot | 867 ms | 1,109 ms | +| Cold first invocation | 9,586 ms | 9,106 ms | +| Cold boot plus first invocation | 10,455 ms | 10,232 ms | +| Immediate repeat on that machine | 4,920 ms | 5,335 ms | +| First invocation after a shell was booted | 5,616 ms | 5,385 ms | +| Repeated warm invocation | 4,616 ms | 5,005 ms | +| Eager-image first invocation | 7,110 ms | 6,415 ms | +| Eager-image warm invocation | 4,594 ms | 5,059 ms | + +The cold query fetched seven closed lazy assets totaling 28,040,511 bytes. +Those assets were the Homebrew source tree, portable Ruby, and the ordinary +tools actually executed by the runtime. The warm query fetched zero bytes. +Thus a second `brew info` is not waiting on the already materialized lazy +references; it repeats process startup, Ruby loading, discovery, and parsing. + +Warm values for the complete command set were: + +| Command | Node | Chromium | +| --- | ---: | ---: | +| `brew --version` | 484 ms | 914 ms | +| `brew config` | 5,570 ms | 15,532 ms | +| text `brew info` | 4,616 ms | 5,005 ms | +| JSON v2 `brew info` | 4,908 ms | 5,544 ms | +| `brew list --versions` | 3,729 ms | 3,789 ms | +| canary `brew info` | 5,399 ms | 6,181 ms | + +Every command completed successfully with nonempty standard output and empty +standard error. Node and Chromium produced the same output digest for each +command. The Chromium `brew config` result remained unusually noisy and slow; +the table reports its median rather than treating the earlier low sample as +representative. + +The deployed rev22 Node baseline and candidate compare as follows. This is a +product-level comparison with the exact revisions disclosed above, not an +exact-artifact micro A/B: + +| Metric | Rev22 | Candidate | Reduction | +| --- | ---: | ---: | ---: | +| Cold boot plus first `brew info` | 27,052 ms | 10,455 ms | 61.4% (2.59x) | +| Immediate warm `brew info` | 26,081 ms | 4,920 ms | 81.1% (5.30x) | +| Booted warm `brew info` | 25,560 ms | 4,616 ms | 81.9% (5.54x) | +| Warm JSON v2 `brew info` | 28,633 ms | 4,908 ms | 82.9% (5.83x) | +| Warm `brew config` | 30,814 ms | 5,570 ms | 81.9% (5.53x) | +| Warm `brew list --versions` | 20,390 ms | 3,729 ms | 81.7% (5.47x) | +| Warm canary `brew info` | 31,745 ms | 5,399 ms | 83.0% (5.88x) | + +Chrome 149 consistently lost the renderer on rev22 before the harness could +produce one valid complete round. Required-unused rootfs bytes and disabling +Wasm tier-up did not make rev22 complete, so there is no honest Chromium +"before" duration. The candidate completed all three isolated Chromium rounds; +its values are reported above without fabricating a speedup ratio. + +## Attribution + +### Warm Ruby and syscall timeline + +A sampled 6,674.7 ms Ruby worker profile attributed self time as follows: + +| Ruby worker frame | Sampled self time | +| --- | ---: | +| libc syscall path | 3,728.3 ms | +| Ruby parser | 591.6 ms | +| Ruby VM core | 179.3 ms | +| Ruby lexer | 92.7 ms | +| compiler helper | 63.5 ms | + +The syscall figure includes time waiting for the kernel Worker. A separate +host-side timing of the synchronous kernel work for a 5,960.6 ms warm query +accounted for 1,367.3 ms: + +| Operation | Calls | Kernel time | +| --- | ---: | ---: | +| `open` | 4,694 | 1,064.6 ms | +| `stat` | 2,068 | 169.7 ms | +| `realpath` | 1,236 | 71.8 ms | +| `read` | 4,055 | 8.9 ms | +| `readlink` | 170 | 6.6 ms | +| `close` | 3,120 | 5.8 ms | + +The difference between Ruby's syscall-wait samples and synchronous kernel +work is channel transfer, scheduling, wakeup, and time spent behind other +processes. The file bytes are already memory-resident on a warm run, but each +POSIX call still crosses from Wasm libc through the shared channel to the +dedicated kernel Worker and back. Thousands of individually cheap lookups +therefore remain expensive. + +### Why so many absent paths are opened + +This is mostly normal CRuby and Homebrew behavior, amplified by Kandelo's +per-syscall cost: + +- Homebrew's generated Bundler setup adds 85 vendor load paths that are absent + in this production runtime. There are 107 Ruby load-path entries in total; + 22 exist. +- For a feature backed by a native extension, CRuby searches `.rb` candidates + across load paths before searching `.so` candidates. Bootsnap itself is a + concrete example: the genuine extension exists, but ordinary lookup probes + earlier Ruby-source candidates first. +- RubyGems, Git, curl, Bash login startup, and Homebrew command discovery also + test optional configuration and platform paths whose absence is meaningful. + +Classifying the 4,694 open paths against the resulting VFS found 3,115 calls +to paths absent after the run, including 2,135 under Homebrew's vendor bundle. +A separate full result trace observed about 2,500 actual `ENOENT` opens per +warm invocation. These failed opens are not VFS corruption: returning +`ENOENT` is the required observable result. Removing candidates in Kandelo or +lying about their existence would change Ruby/Homebrew behavior. + +Bootsnap is now the real upstream gem in the real portable-Ruby layout. It +reduces repeated load-path scans where Homebrew enables it, but it cannot make +every optional-path probe disappear. Preloading a generated Bootsnap cache +added about 2 MiB to the image and did not improve the controlled workload. + +### Process cost and Ruby's `posix_spawn` patch + +The text `brew info` command created 159 processes and performed 55 execs in +the focused result. A detailed trace counted 158 forks. Of those, 90 occurred +inside Bash-based Git shims, 32 inside the Bash curl shim, and 29 in the outer +`brew.sh` path. Only four were issued by Ruby; three belonged to the outer +`bin/brew` path. Ruby also created seven threads. + +Therefore the Ruby patch that selects `posix_spawn()` instead of `vfork()` is +not the primary cause of this query's fork count. Bash uses ordinary `fork()` +for these scripts and has no relevant build-time switch that converts them to +`posix_spawn()`. An ABI-43 `vfork()` experiment reduced the focused query by +only about 2.3–2.4% and regressed broader startup controls by 37–50%. That +candidate is not part of this ABI-42 work. + +### Lazy metadata and image cost + +`INSTALL_RECEIPT.json` and `.brew/.rb` are upstream bottle members. +Previously, reading either from a deferred Formula activated the complete +bottle transport. The candidate embeds those exact authenticated member bytes +while retaining the complete source inventory and bottle digest for later +whole-archive validation. + +The measured compressed shell image grew from 5,754,898 to 5,782,818 bytes: +27,920 bytes. In a closed zlib control this avoided a 107,543-byte bottle +fetch. Local wall time was neutral because the closed asset had no network +latency and Ruby startup dominated, but the ownership and transfer behavior +are now correct for metadata-only access. + +The immutable benchmark fixture's portable-Ruby artifact costs: + +| Artifact | Size | +| --- | ---: | +| Lazy ZIP download | 12,184,183 bytes (11.62 MiB) | +| Expanded portable tree | 41,331,702 bytes (39.42 MiB) | +| Main `ruby.wasm` within the tree | 21,365,256 bytes (20.38 MiB) | +| Files in the expanded tree | 2,493 | + +This is paid only on first Homebrew activation in the lazy shell. It supplies +Homebrew's standard versioned Ruby tree plus the real Bootsnap and msgpack +gems. A narrow Bootsnap patch adds Ruby's Wasm platform to its upstream +supported-platform predicate; it does not replace the load-path cache. There +is no separate system-Ruby compatibility path. + +The final clean package rebuild produced a 12,184,488-byte (11.62 MiB) +portable-Ruby ZIP, a 41,331,878-byte (39.42 MiB) expanded tree with 2,493 +members, and a 21,365,814-byte (20.38 MiB) main `ruby.wasm`. The fixture is +305 bytes smaller because it predates the final build-tool-only correction; +the runtime composition and measured behavior are the same. + +### Upstream patch accounting + +This work does not patch `brew info`, formula discovery, Ruby `require`, +Bundler's generated paths, or Bootsnap's cache algorithm. It uses upstream +Homebrew, CRuby, msgpack, and Bootsnap through their normal runtime paths. + +Kandelo already carried three reviewed compatibility changes before this +investigation: + +- Homebrew's source is patched to recognize Kandelo's Wasm bottle tags and + prefix relocation contract; +- CRuby selects Kandelo's non-forking `posix_spawn()` path for commands whose + requested process attributes can be represented faithfully; and +- CRuby keeps command-line `-r` library roots visible across Kandelo's + fork-instrumented startup path. + +The only new patch to upstream source is +`packages/registry/ruby/patches/bootsnap-wasm-platform.patch`. It adds `wasm` +to three existing supported-platform regular expressions. Bootsnap 1.24.5 +otherwise rejects the target before running its genuine loader and compile +caches. The patch changes no cache, path, or fallback semantics. + +The remaining Ruby work is build configuration and Kandelo platform support: +the package follows Homebrew's portable-Ruby directory layout, stages the +exact upstream msgpack and Bootsnap gems by digest, builds their ordinary +native extensions as Wasm side modules, and loads those modules through +Kandelo's general `dlopen()` implementation. That support is not conditional +on Homebrew or on a Brew subcommand. + +## Retained improvements + +The old system-Ruby diagnostic took 23,601 ms and emitted 92,678 traced +syscalls, including 54,206 opens and 9,070 readlinks. A comparable instrumented +portable-Ruby diagnostic took 7,050 ms with 29,221 traced syscalls, 4,694 +opens, and 170 readlinks. The final uninstrumented three-round warm medians are +4,616 ms on Node and 5,005 ms on Chromium. The traced pair identifies the +dominant improvement: use Homebrew's real portable Ruby and upstream load-path +cache path. + +Two platform-owned changes have separate controlled evidence: + +- resolving an existing HostFS prefix without redundant component-by-component + host calls reduced a cold query from about 12.13 s to 8.97 s, roughly 3.2 s; + the fast path falls back whenever a symlink or missing component requires + full POSIX resolution; and +- pristine one-shot prestarted process Workers saved about 1.5–1.8 s without + reusing a Realm or retaining a used Worker's process memory. + +Directory indexes, cached channel views, syscall classification, polling, +main-thread kernel execution, and Realm reuse are not part of the retained +implementation. Experiments with broader SharedFS indexing and cache +preloading were wall-time neutral or worse and were reverted. + +## Chromium boundary + +Chrome for Testing 149 originally lost the renderer after repeatedly compiling +the same 20.38 MiB Ruby module in fresh process Workers. Stock Ruby reproduced +the failure on launch 13; a diagnostic Ruby with a smaller linear memory +reproduced it on launch 4. Disabling V8's Wasm tier-up made 20 launches +complete, which isolated the failure from guest linear-memory retention. + +The retained candidate caches the immutable compiled `WebAssembly.Module` in +the machine's centralized kernel Worker. It does not reuse process Workers, +Realms, memories, or syscall channels. A rapid stock-Ruby test with ordinary +V8 tiering completed 20 of 20 launches. It created 65 Workers, peaked at seven +live Workers, and ended with only the centralized kernel Worker alive. + +Cache hits compare the current VFS executable byte-for-byte against a private +copy before reusing compiled code. Exact content reached through several paths +shares one entry; path and `argv[0]` behavior do not change. This matters for +multicall programs: seven coreutils paths in the trace were the same 4.75 MiB +executable, and both Bash paths were the same 3.35 MiB executable. A path-only +cache exceeded its 64 MiB budget, evicted Ruby, and reproduced the crash on the +second query. Content deduplication held the complete query working set as +eight unique modules and 16 aliases in 44,845,570 comparison bytes with zero +evictions. + +The stock Ruby comparison took about 3.4 ms per lookup in a Node +micro-measurement. Ruby accounts for 21,365,256 of the retained comparison +bytes, plus V8-owned compiled code whose size is not exposed. The cache is +bounded to 16 content entries, 256 path aliases, and 64 MiB of comparison bytes +per machine. A clean Node A/B/A used five invocations per machine. The four +warm values were 4,738–4,916 ms with the cache, 6,701–8,354 ms with capacity +set to zero, and 4,729–4,930 ms after restoring it. Warm medians were 4,804, +7,460, and 4,831 ms respectively: about a 35% reduction in this focused +control. Cold values were 10,088, 10,398, and 9,834 ms. This is evidence for +the mechanism, not the required three-round performance result. + +With exact-content deduplication, the three independent complete Chromium +rounds covered cold and warm machines, every command, the eager control, and +the network audit without a renderer loss. Their medians were 9,106 ms cold, +5,335 ms immediate warm, 5,385 ms first on the booted command-sequence +machine, 5,005 ms repeated warm, 6,415 ms eager first, and 5,059 ms eager +warm. All six audited commands reported zero network syscalls. + +Stock Ruby still grows one process memory to 303,300,608 bytes (289.25 MiB). +Exactly 256 MiB comes from CRuby 4.0.5's optional red-black object-shape cache: +native Ruby reserves it through sparse `mmap`, while Wasm linear memory grows +contiguously. A no-cache diagnostic reduced the final memory to 33.25 MiB, but +no Ruby source special case is retained. That upstream/configuration boundary +is separate from the compiled-module lifecycle fix. + +The remaining 256 MiB Ruby object-shape reservation is not retained by a +terminated process Worker, but it makes every live Ruby process expensive. +Changing that upstream/configuration choice remains follow-up work rather than +part of this performance claim. + +## Repository-wide performance contract + +Freshly built protected `main` and candidate hosts ran these required commands +with three rounds and matching workload artifacts: + +```text +npx tsx benchmarks/run.ts --rounds=3 +npx tsx benchmarks/run.ts --host=browser --rounds=3 +npx tsx benchmarks/compare.ts BEFORE.json AFTER.json +``` + +The final Node comparison improved process `clone`, `fork`, and `spawn` by +91.0%, 85.8%, and 88.8%; `exec` improved 10.5%. WordPress improved 0.4% for +its CLI path and 0.5% for first HTTP response. File reads improved 6.0%, while +writes fell 9.4%. Median syscall latency increased by 1.08 us, or 7.8%. + +The full candidate-first/main-second Node pair showed several 5.0–16.1% +MariaDB query differences, especially in `mariadb-innodb-64`. An immediately +following three-round control of that exact suite and those exact artifact +digests put candidate queries between 1.6% faster and 3.0% slower, with +bootstrap 3.0% faster. An earlier reverse-order control was similarly within +1.9%. The full-suite values are therefore sensitive to surrounding run state; +they remain disclosed rather than supporting a universal regression-free +claim. + +The final Chromium comparison improved process `clone`, `fork`, and `spawn` +by 63.6%, 57.1%, and 38.5%. WordPress boot changed by +0.9%, while first HTTP +response improved 49.6%. MariaDB bootstrap improved 7.0–8.4% across variants. +Median syscall latency increased from 14 to 18 us, file writes fell 11.1%, +and pipe throughput improved 111.1%. Some sub-14 ms SQL operations produced +large percentages because of timer quantization: the largest candidate +increase was 4.9 ms, while most select and join medians improved. These +results support the measured Homebrew and process-start claims, not a claim +that every microbenchmark improved. + +## Remaining work, ranked by measured impact + +1. Reduce the remaining 159 process launches, 55 execs, and roughly 29,000 + syscalls in a warm text query through general process and channel work. + Preserve the dedicated kernel Worker, wakeup draining, POSIX semantics, and + host parity. ABI batching work is the likely owner if this requires an ABI + change. +2. Investigate Chromium's remaining 0.39 s warm text-query gap and 9.96 s + `brew config` gap relative to Node. The same output and process counts point + to host scheduling or compilation behavior, not a different Homebrew path. +3. Bound V8-owned compiled-code memory, whose size V8 does not expose. The + complete Chromium query suite and focused 20-launch control are stable + without Realm reuse, but only comparison bytes have an explicit 64 MiB cap. +4. Continue general VFS lookup optimization only where exact symlink, + mutation, and `ENOENT` behavior can be proved. The accepted HostFS fast + path is one such case; speculative negative caches are not. +5. Revisit Ruby parsing after the syscall/process costs. The measured parser + opportunity is about 0.6 s; the Prism control currently traps on a Psych + path and is not a valid product candidate. diff --git a/docs/package-management.md b/docs/package-management.md index f1ccfed401..668c2f12cb 100644 --- a/docs/package-management.md +++ b/docs/package-management.md @@ -686,14 +686,16 @@ remain unfetched until a path owned by that dependency is used. The guest `brew` implementation is distributed as the `homebrew-bootstrap` support-data Formula bottle. Its tap-native recipe -declares two `libexec` outputs: `homebrew-bootstrap.zip`, a deterministic +declares three `libexec` outputs: `homebrew-bootstrap.zip`, a deterministic archive of one exact upstream Homebrew commit plus Kandelo's reviewed -guest-platform patch, and `homebrew-brew.env`, the architecture tag and system -environment policy consumed with that exact tree. Consumers extract both from -the same immutable public bottle; they must not reconstruct the environment -file or combine it with a ZIP from another build. Neither output is Wasm, so -the Formula uses the support-data bottle test contract rather than claiming -Node or browser execution evidence. +guest-platform patch; `homebrew-brew.env`, the architecture tag and system +environment policy consumed with that exact tree; and +`homebrew-portable-ruby.zip`, the source-selected Ruby runtime in Homebrew's +ordinary versioned vendor layout. Consumers extract all three from the same +immutable public bottle; they must not reconstruct the environment file or +combine outputs from different builds. These support-data outputs use the +support-data bottle test contract rather than making an execution claim about +the ZIP or environment members themselves. The generic support-data extractor verifies the exact checkout, catalog and Formula sidecar, tap recipe lock, link manifest, keg, bottle digest and size, @@ -704,23 +706,30 @@ checkout commit, aggregate metadata publication commit, and bottle Formula SHA-256 and Homebrew's `.brew` receipt SHA-256 are separate too, because the receipt canonically omits the finalized bottle block. -The bootstrap archive is not a bundled Homebrew runtime. It contains neither -Ruby nor Git, curl, extraction tools, or their data/dependencies. The base -shell therefore registers `/usr/bin/brew` as a lazy activation reference -without claiming the bootstrap source can execute by itself. +The bootstrap source archive is not a bundled Homebrew runtime. It contains +neither Ruby nor Git, curl, extraction tools, or their data/dependencies. +Portable Ruby remains a separate, digest-bound output. Its exact version comes +from the source archive's +`Library/Homebrew/vendor/portable-ruby-version`; the image derives both its +mount and executable paths from the authenticated bootstrap descriptor rather +than assuming a guest prefix. The output exposes the standard +`Library/Homebrew/vendor/portable-ruby/` tree and `current` symlink, +so Homebrew follows its normal portable-Ruby and Bootsnap startup path. + +The base shell therefore registers `/usr/bin/brew` as a lazy activation +reference without claiming the bootstrap source can execute by itself. `homebrew/main-shell-homebrew-runtime-support.json` owns the separate atomic -runtime-support closure. Its seven reviewed roots resolve through the exact -closed-selection metadata that the shell consumes. Dependencies already in the -complete shell remain shared; additional dependencies become part of the same -atomic lazy group. This matters when a Formula changes its real dependency -graph—for example, Ruby now requires `libyaml`. The selection-aware release -finalizer derives the new order and base-relative difference, requires an -admitted public ABI-42 bottle for every member, and seals the selection and -artifact locks together. The base keeps the active layer deferred; an opt-in -demo may materialize the same layer. A guest lifecycle is valid only when -every declared tree has an exact admitted ABI/digest/size identity, all -support bytes come from that declaration, and the independent-tap Formula is -installed live rather than smuggled into the image. +runtime-support closure. Portable Ruby and six reviewed Formula roots resolve +through the exact closed-selection metadata that the shell consumes. +Dependencies already in the complete shell remain shared; additional Formula +dependencies become part of the same atomic lazy group. The selection-aware +release finalizer derives the new order and base-relative difference, requires +an admitted public ABI-42 bottle for every Formula member, and seals the +selection and artifact locks together. The base keeps the active layer +deferred; an opt-in demo may materialize the same layer. A guest lifecycle is +valid only when every declared tree has an exact admitted ABI/digest/size +identity, all support bytes come from that declaration, and the +independent-tap Formula is installed live rather than smuggled into the image. `Kandelo/recipes/homebrew-bootstrap/source-lock.json` in the tap is the Formula's reviewed source/output identity. It binds the prepared source inputs diff --git a/homebrew/main-shell-homebrew-runtime-support.json b/homebrew/main-shell-homebrew-runtime-support.json index 97f46dc85a..d1b8f0a907 100644 --- a/homebrew/main-shell-homebrew-runtime-support.json +++ b/homebrew/main-shell-homebrew-runtime-support.json @@ -57,7 +57,8 @@ "name": "homebrew-bootstrap", "outputs": [ "homebrew-bootstrap.zip", - "homebrew-brew.env" + "homebrew-brew.env", + "homebrew-portable-ruby.zip" ], "required_kernel_abi": 42 }, @@ -65,10 +66,6 @@ "demo_variant": "may-materialize" }, "formula_roots": [ - { - "package": "kandelo-dev/tap-core/ruby", - "reason": "Homebrew executes its Ruby command implementation after the Bash launcher." - }, { "package": "kandelo-dev/tap-core/git", "reason": "Stock brew tap and exact-revision lifecycle operations use Git." @@ -95,8 +92,6 @@ } ], "formula_order": [ - "kandelo-dev/tap-core/zlib", - "kandelo-dev/tap-core/ruby", "kandelo-dev/tap-core/coreutils", "kandelo-dev/tap-core/dash", "kandelo-dev/tap-core/ed", @@ -106,6 +101,7 @@ "kandelo-dev/tap-core/ncurses", "kandelo-dev/tap-core/less", "kandelo-dev/tap-core/openssl", + "kandelo-dev/tap-core/zlib", "kandelo-dev/tap-core/libcurl", "kandelo-dev/tap-core/sed", "kandelo-dev/tap-core/vim", @@ -117,15 +113,8 @@ "kandelo-dev/tap-core/tar", "kandelo-dev/tap-core/posix-utils-lite" ], - "additional_formula_order": [ - "kandelo-dev/tap-core/ruby" - ], + "additional_formula_order": [], "required_commands": [ - { - "path": "/usr/bin/ruby", - "package": "kandelo-dev/tap-core/ruby", - "reason": "Homebrew's Bash launcher transfers control to Ruby." - }, { "path": "/usr/bin/git", "package": "kandelo-dev/tap-core/git", @@ -163,14 +152,13 @@ "metadata_sha256": "34f1cb2454ff6d9c134195302a5c5fcac85b9502f2522d3e584bf55d539c9a7d", "metadata_tap_commit": "b5ffda55d9b0e27efdfdec30ebb38d48a21518c4", "kandelo_commit": "c647adda31d0918de944135543fb94039135cef1", - "runtime_bottle_provenance_sha256": "b9070afcb5ddcdc112c1b994c16f8f46251d78dff3877e01adf30b226e4c31d2", + "runtime_bottle_provenance_sha256": "a1269194b223c47c97ccc0504369b889357f77dc0675bb7245d14c587de082dd", "kandelo_abi": 42, "release_tag": "bottles-abi-v42", "required_arch": "wasm32" }, "reusable_public_abi42": [ "kandelo-dev/tap-core/zlib", - "kandelo-dev/tap-core/ruby", "kandelo-dev/tap-core/coreutils", "kandelo-dev/tap-core/dash", "kandelo-dev/tap-core/ed", diff --git a/homebrew/main-shell-lazy-artifact-lock.json b/homebrew/main-shell-lazy-artifact-lock.json index 0933dd6671..e221beda81 100644 --- a/homebrew/main-shell-lazy-artifact-lock.json +++ b/homebrew/main-shell-lazy-artifact-lock.json @@ -9,8 +9,8 @@ "demo_config_sha256": "6a09a53f0b169400948aa4f5c9c7a4b904ed7007ad0912d9cd73a66f7b57e455", "materialization_policy_sha256": "fb170c25f71e6e7fdd3470901b3ab3a42e7765648d55f62fad38230e30461fe0", "migration_lock_sha256": "92b6c3946e40e9384de0ed98c13b6f9ec595e487a5112cc8a450bc2ac3ea524e", - "runtime_support_sha256": "4ee373429d0f26e459cd9fe5ea16901d1769f9947f89cffeaee262b94e403616", - "selection_lock_sha256": "7811caefed4ca503b49fb2bd7e338d6e7c21e3dd3ecdfc4783ea9d24514f5b73", + "runtime_support_sha256": "9d4f185b58f0c8e99f58c3785be095921fb75ee5016c733f9be6a5b98e756c78", + "selection_lock_sha256": "13bb84866474be01dcf2cb942060e16df5651fdb0db9cdc05414c4e16bb60e84", "shell_config_sha256": "bc3c4027b5f83fd74178364bf0b073b546eca5a098bf8bf47bc38a555b4cfe0c" }, "image": null diff --git a/homebrew/main-shell-selection-lock.json b/homebrew/main-shell-selection-lock.json index 6152bc5361..c850d65781 100644 --- a/homebrew/main-shell-selection-lock.json +++ b/homebrew/main-shell-selection-lock.json @@ -15,7 +15,7 @@ }, "runtime_support": { "path": "homebrew/main-shell-homebrew-runtime-support.json", - "sha256": "4ee373429d0f26e459cd9fe5ea16901d1769f9947f89cffeaee262b94e403616" + "sha256": "9d4f185b58f0c8e99f58c3785be095921fb75ee5016c733f9be6a5b98e756c78" } }, "kind": "kandelo-homebrew-main-shell-closed-selection-lock", diff --git a/homebrew/test/homebrew_guest_lifecycle_browser.ts b/homebrew/test/homebrew_guest_lifecycle_browser.ts index aa4ffe1f68..272e350759 100644 --- a/homebrew/test/homebrew_guest_lifecycle_browser.ts +++ b/homebrew/test/homebrew_guest_lifecycle_browser.ts @@ -105,6 +105,8 @@ export async function runHomebrewGuestLifecycleInBrowser(options: { bootstrapSpecBytes: loaded.bootstrapSpecBytes, bootstrapArchiveBytes: loaded.bootstrapArchiveBytes, bootstrapArchiveSha256: fixture.bootstrap.archive.sha256, + portableRubyArchiveBytes: loaded.portableRubyArchiveBytes, + portableRubyArchiveSha256: fixture.bootstrap.portableRuby.sha256, bootstrapEnvironmentBytes: loaded.bootstrapEnvironmentBytes, coreRevision: fixture.revisions.coreRevision, transportMode: fixture.transportMode, @@ -115,6 +117,8 @@ export async function runHomebrewGuestLifecycleInBrowser(options: { ...(publicTransport ? { expectedBootstrapTransportUrl: fixture.bootstrap.archive.url, + expectedPortableRubyTransportUrl: + fixture.bootstrap.portableRuby.url, } : { closedBottleAssets: loaded.closedBottleAssets!, diff --git a/homebrew/test/homebrew_guest_lifecycle_browser_fixture.test.ts b/homebrew/test/homebrew_guest_lifecycle_browser_fixture.test.ts index 4a098d192b..3bde167c96 100644 --- a/homebrew/test/homebrew_guest_lifecycle_browser_fixture.test.ts +++ b/homebrew/test/homebrew_guest_lifecycle_browser_fixture.test.ts @@ -124,6 +124,10 @@ test("loads every exact fixture byte and binds payloads to the mirror plan", asy assert.deepEqual(loaded.imageBytes, fixture.imageBytes); assert.deepEqual(loaded.bootstrapSpecBytes, fixture.specBytes); assert.deepEqual(loaded.bootstrapArchiveBytes, fixture.archiveBytes); + assert.deepEqual( + loaded.portableRubyArchiveBytes, + fixture.portableRubyBytes, + ); assert.deepEqual(loaded.bootstrapEnvironmentBytes, fixture.environmentBytes); assert.deepEqual(loaded.bottleMirrorPlanBytes, fixture.planBytes); assert.deepEqual(loaded.closedBottleAssets, [{ @@ -379,7 +383,7 @@ test("preserves caller cancellation during public plan identity validation", asy data: BufferSource, ): Promise => { digestCalls += 1; - if (digestCalls === 6) { + if (digestCalls === 7) { markIdentityStarted(); await identityGate; } @@ -463,6 +467,7 @@ function createFixture() { const imageBytes = new Uint8Array([1, 2]); const specBytes = new Uint8Array([3]); const archiveBytes = new Uint8Array([4]); + const portableRubyBytes = new Uint8Array([9, 10]); const environmentBytes = new Uint8Array([5]); const payloadBytes = new Uint8Array([6, 7, 8]); const repository = "example/project"; @@ -497,6 +502,7 @@ function createFixture() { image: "https://example.test/main-shell.vfs.zst", spec: "https://example.test/main-shell-brew-package-tree.json", archive: "https://example.test/homebrew-bootstrap.zip", + portableRuby: "https://example.test/homebrew-portable-ruby.zip", environment: "https://example.test/homebrew-brew.env", plan: `${releaseRoot}/${HOMEBREW_BOTTLE_MIRROR_PLAN_ASSET}`, }; @@ -508,6 +514,7 @@ function createFixture() { bootstrap: { spec: exact(urls.spec, specBytes), archive: exact(urls.archive, archiveBytes), + portableRuby: exact(urls.portableRuby, portableRubyBytes), environment: exact(urls.environment, environmentBytes), }, bottleMirror: { @@ -527,6 +534,7 @@ function createFixture() { [urls.image, imageBytes], [urls.spec, specBytes], [urls.archive, archiveBytes], + [urls.portableRuby, portableRubyBytes], [urls.environment, environmentBytes], [urls.plan, planBytes], [plan.assets[0]!.url, payloadBytes], @@ -537,6 +545,7 @@ function createFixture() { imageBytes, specBytes, archiveBytes, + portableRubyBytes, environmentBytes, payloadBytes, plan, diff --git a/homebrew/test/homebrew_guest_lifecycle_browser_fixture.ts b/homebrew/test/homebrew_guest_lifecycle_browser_fixture.ts index 70a6954605..9b645c006e 100644 --- a/homebrew/test/homebrew_guest_lifecycle_browser_fixture.ts +++ b/homebrew/test/homebrew_guest_lifecycle_browser_fixture.ts @@ -38,6 +38,7 @@ export interface HomebrewGuestLifecycleBrowserFixture { bootstrap: { spec: HomebrewGuestLifecycleExactAsset; archive: HomebrewGuestLifecycleExactAsset; + portableRuby: HomebrewGuestLifecycleExactAsset; environment: HomebrewGuestLifecycleExactAsset; }; bottleMirror: { @@ -53,6 +54,7 @@ export interface LoadedHomebrewGuestLifecycleBrowserFixture { imageBytes: Uint8Array; bootstrapSpecBytes: Uint8Array; bootstrapArchiveBytes: Uint8Array; + portableRubyArchiveBytes: Uint8Array; bootstrapEnvironmentBytes: Uint8Array; bottleMirrorPlanBytes: Uint8Array; closedBottleAssets?: readonly ClosedLazyAsset[]; @@ -127,13 +129,18 @@ const TOP_LEVEL_KEYS = [ "revisions", "timeoutMs", ] as const; -const BOOTSTRAP_KEYS = ["spec", "archive", "environment"] as const; +const BOOTSTRAP_KEYS = [ + "spec", + "archive", + "portableRuby", + "environment", +] as const; const MIRROR_KEYS = ["plan", "payloads"] as const; const REVISION_KEYS = ["coreRevision", "canaryRevision"] as const; const ASSET_KEYS = ["url", "sha256", "bytes"] as const; const PAYLOAD_KEYS = ["asset", "url", "sha256", "bytes"] as const; const SHA256_RE = /^[0-9a-f]{64}$/; -const FIXED_EXACT_ASSET_COUNT = 5; +const FIXED_EXACT_ASSET_COUNT = 6; /** * Reject ambient or partially specified live inputs before any browser fetch. @@ -220,6 +227,10 @@ export function projectHomebrewGuestLifecycleBrowserFixture( value.bootstrap.archive, "bootstrap archive", ), + portableRuby: projectExactAsset( + value.bootstrap.portableRuby, + "portable Ruby archive", + ), environment: projectExactAsset( value.bootstrap.environment, "bootstrap environment", @@ -258,6 +269,7 @@ async function loadHomebrewGuestLifecycleBrowserFixtureImpl( fixture.image, fixture.bootstrap.spec, fixture.bootstrap.archive, + fixture.bootstrap.portableRuby, fixture.bootstrap.environment, fixture.bottleMirror.plan, ...payloads, @@ -287,12 +299,14 @@ async function loadHomebrewGuestLifecycleBrowserFixtureImpl( image, bootstrapSpec, bootstrapArchive, + portableRubyArchive, bootstrapEnvironment, bottleMirrorPlan, ] = loadedFixedAssets; const imageBytes = image!.bytes; const bootstrapSpecBytes = bootstrapSpec!.bytes; const bootstrapArchiveBytes = bootstrapArchive!.bytes; + const portableRubyArchiveBytes = portableRubyArchive!.bytes; const bootstrapEnvironmentBytes = bootstrapEnvironment!.bytes; const bottleMirrorPlanBytes = bottleMirrorPlan!.bytes; const plan = decodeHomebrewBottleMirrorPlan( @@ -314,6 +328,7 @@ async function loadHomebrewGuestLifecycleBrowserFixtureImpl( imageBytes, bootstrapSpecBytes, bootstrapArchiveBytes, + portableRubyArchiveBytes, bootstrapEnvironmentBytes, bottleMirrorPlanBytes, }; @@ -381,6 +396,7 @@ async function loadHomebrewGuestLifecycleBrowserFixtureImpl( imageBytes, bootstrapSpecBytes, bootstrapArchiveBytes, + portableRubyArchiveBytes, bootstrapEnvironmentBytes, bottleMirrorPlanBytes, closedBottleAssets, diff --git a/homebrew/test/homebrew_guest_lifecycle_node.ts b/homebrew/test/homebrew_guest_lifecycle_node.ts index 68abff29c5..b356a30979 100644 --- a/homebrew/test/homebrew_guest_lifecycle_node.ts +++ b/homebrew/test/homebrew_guest_lifecycle_node.ts @@ -8,6 +8,8 @@ import { pathToFileURL } from "node:url"; import { NodeKernelHost } from "../../host/src/node-kernel-host"; import type { LazyDownloadEvent } from "../../host/src/vfs/memory-fs"; import { assertHomebrewBottleMirrorPlan } from "../../host/src/homebrew-vfs-composer"; +import { deriveHomebrewPortableRubyTree } from + "../../host/src/homebrew-portable-ruby"; import { assertPackageDeferredZipTreeState, derivePackageDeferredZipTree, @@ -51,6 +53,7 @@ interface Options extends HomebrewGuestLifecycleRevisions { imagePath: string; bootstrapSpecPath: string; bootstrapArchivePath: string; + portableRubyArchivePath: string; bootstrapEnvironmentPath: string; transportMode: "closed" | "public"; bottleMirrorPlanPath?: string; @@ -166,6 +169,10 @@ async function loadRootfsRuntimeInputs( options.bootstrapArchivePath, "Homebrew bootstrap archive", ); + const portableRubyArchiveBytes = readRegularFile( + options.portableRubyArchivePath, + "Homebrew portable Ruby archive", + ); const bootstrapEnvironmentBytes = readRegularFile( options.bootstrapEnvironmentPath, "Homebrew bootstrap environment", @@ -181,23 +188,39 @@ async function loadRootfsRuntimeInputs( parseJson(bootstrapSpecBytes, options.bootstrapSpecPath), bootstrapArchiveBytes, ); + const portableRubyTree = deriveHomebrewPortableRubyTree( + bootstrapTree, + bootstrapArchiveBytes, + portableRubyArchiveBytes, + ); const bootstrapArchiveSha256 = createHash("sha256") .update(bootstrapArchiveBytes) .digest("hex"); + const portableRubyArchiveSha256 = createHash("sha256") + .update(portableRubyArchiveBytes) + .digest("hex"); const lazyUrlBase = options.transportMode === "closed" ? "https://closed.kandelo.invalid/homebrew-guest-lifecycle/" : pathToFileURL(`${dirname(options.bootstrapArchivePath)}/`).toString(); return await deriveHomebrewGuestLifecycleRuntimeInputs({ imageBytes, - validateImageFileSystem: (imageFileSystem) => + validateImageFileSystem: (imageFileSystem) => { assertPackageDeferredZipTreeState( imageFileSystem, bootstrapTree, "deferred", - ), + ); + assertPackageDeferredZipTreeState( + imageFileSystem, + portableRubyTree, + "deferred", + ); + }, bootstrapSpecBytes, bootstrapArchiveBytes, bootstrapArchiveSha256, + portableRubyArchiveBytes, + portableRubyArchiveSha256, bootstrapEnvironmentBytes, coreRevision: options.coreRevision, transportMode: options.transportMode, @@ -208,6 +231,9 @@ async function loadRootfsRuntimeInputs( expectedBootstrapTransportUrl: pathToFileURL( options.bootstrapArchivePath, ).toString(), + expectedPortableRubyTransportUrl: pathToFileURL( + options.portableRubyArchivePath, + ).toString(), } : { loadClosedBottleAssets: (embeddedPlanBytes, pendingBottleTrees) => @@ -539,6 +565,7 @@ function parseOptions(args: string[]): Options { "--image", "--homebrew-bootstrap-spec", "--homebrew-bootstrap-archive", + "--homebrew-portable-ruby-archive", "--homebrew-bootstrap-env", "--transport-mode", "--bottle-mirror-plan", @@ -564,6 +591,9 @@ function parseOptions(args: string[]): Options { const image = values.get("--image"); const bootstrapSpec = values.get("--homebrew-bootstrap-spec"); const bootstrapArchive = values.get("--homebrew-bootstrap-archive"); + const portableRubyArchive = values.get( + "--homebrew-portable-ruby-archive", + ); const bootstrapEnvironment = values.get("--homebrew-bootstrap-env"); const transportMode = values.get("--transport-mode"); const bottleMirrorPlan = values.get("--bottle-mirror-plan"); @@ -578,6 +608,7 @@ function parseOptions(args: string[]): Options { !image || !bootstrapSpec || !bootstrapArchive || + !portableRubyArchive || !bootstrapEnvironment || !coreRevision || !canaryRevision || @@ -604,6 +635,7 @@ function parseOptions(args: string[]): Options { imagePath: resolve(image), bootstrapSpecPath: resolve(bootstrapSpec), bootstrapArchivePath: resolve(bootstrapArchive), + portableRubyArchivePath: resolve(portableRubyArchive), bootstrapEnvironmentPath: resolve(bootstrapEnvironment), transportMode, proofMode, @@ -625,6 +657,7 @@ function usage(): never { "--image " + "--homebrew-bootstrap-spec " + "--homebrew-bootstrap-archive " + + "--homebrew-portable-ruby-archive " + "--homebrew-bootstrap-env " + "--transport-mode " + "[--bottle-mirror-plan ] " + diff --git a/homebrew/test/homebrew_guest_lifecycle_runtime_inputs.test.ts b/homebrew/test/homebrew_guest_lifecycle_runtime_inputs.test.ts index 2ba8e559df..dc297cf0c7 100644 --- a/homebrew/test/homebrew_guest_lifecycle_runtime_inputs.test.ts +++ b/homebrew/test/homebrew_guest_lifecycle_runtime_inputs.test.ts @@ -13,6 +13,8 @@ import { } from "../../host/src/homebrew-bottle-mirror-plan"; import { homebrewRuntimeLayerPayloadAsset } from "../../host/src/homebrew-runtime-layer-limits"; +import { deriveHomebrewPortableRubyTree } from + "../../host/src/homebrew-portable-ruby"; import { MemoryFileSystem } from "../../host/src/vfs/memory-fs"; import { derivePackageDeferredZipTree, @@ -32,14 +34,21 @@ test("binds verified bootstrap bytes and bottle payloads to one exact image", as const bootstrapArchive = zipSync({ "bin/": zipEntry(new Uint8Array(), 0o040755), "bin/brew": zipEntry(encoder.encode("#!/bin/sh\n"), 0o100755), + "Library/": zipEntry(new Uint8Array(), 0o040755), + "Library/Homebrew/": zipEntry(new Uint8Array(), 0o040755), + "Library/Homebrew/vendor/": zipEntry(new Uint8Array(), 0o040755), + "Library/Homebrew/vendor/portable-ruby-version": zipEntry( + encoder.encode("4.0.5\n"), + 0o100644, + ), }, { level: 9 }); const bootstrapSpec = { schema: 1, kind: "kandelo-package-deferred-zip-tree", - id: "shell/homebrew-bootstrap", + id: "homebrew-bootstrap/source-tree", content_role: "source-tree", package: { - name: "shell", + name: "homebrew-bootstrap", output: "homebrew-bootstrap.zip", }, archive: { @@ -59,6 +68,17 @@ test("binds verified bootstrap bytes and bottle payloads to one exact image", as bootstrapSpec, bootstrapArchive, ); + const portableRubyArchive = zipSync({ + "4.0.5/": zipEntry(new Uint8Array(), 0o040755), + "4.0.5/bin/": zipEntry(new Uint8Array(), 0o040755), + "4.0.5/bin/ruby": zipEntry(encoder.encode("ruby-wasm\n"), 0o100755), + current: zipEntry(encoder.encode("4.0.5"), 0o120777), + }, { level: 9 }); + const portableRubyTree = deriveHomebrewPortableRubyTree( + bootstrapTree, + bootstrapArchive, + portableRubyArchive, + ); const bottleBytes = new Uint8Array([42]); const mirror = createMirrorPlan(bottleBytes); const mirrorBytes = encodeHomebrewBottleMirrorPlan(mirror); @@ -117,6 +137,7 @@ test("binds verified bootstrap bytes and bottle payloads to one exact image", as ); writeFile(fs, HOMEBREW_BOTTLE_MIRROR_PLAN_VFS_PATH, mirrorBytes); registerPackageDeferredZipTree(fs, bootstrapTree); + registerPackageDeferredZipTree(fs, portableRubyTree); fs.registerLazyTree( { decoder: "zip-v1", @@ -151,6 +172,7 @@ test("binds verified bootstrap bytes and bottle payloads to one exact image", as await fs.sealLazyAtomicGroup("homebrew-runtime-support", [ bootstrapTree.descriptor.id, "bottle-test", + portableRubyTree.descriptor.id, ]); const imageBytes = await fs.saveImage(); @@ -158,6 +180,8 @@ test("binds verified bootstrap bytes and bottle payloads to one exact image", as bootstrapSpecBytes: encoder.encode(JSON.stringify(bootstrapSpec)), bootstrapArchiveBytes: bootstrapArchive, bootstrapArchiveSha256: sha256(bootstrapArchive), + portableRubyArchiveBytes: portableRubyArchive, + portableRubyArchiveSha256: sha256(portableRubyArchive), bootstrapEnvironmentBytes: environmentBytes, coreRevision, transportMode: "closed" as const, @@ -186,15 +210,27 @@ test("binds verified bootstrap bytes and bottle payloads to one exact image", as "https://closed.kandelo.invalid/lifecycle/homebrew-bootstrap.zip", ); assert.equal(runtime.bootstrapBytes, bootstrapArchive.byteLength); - assert.deepEqual(runtime.runtimeSupportTrees, [{ - url: mirror.assets[0]!.url, - bytes: bottleBytes.byteLength, - }]); - assert.equal(runtime.lazyAssets?.length, 2); + assert.deepEqual(runtime.runtimeSupportTrees, [ + { + url: mirror.assets[0]!.url, + bytes: bottleBytes.byteLength, + }, + { + url: + "https://closed.kandelo.invalid/lifecycle/" + + "homebrew-portable-ruby.zip", + bytes: portableRubyArchive.byteLength, + }, + ]); + assert.equal(runtime.lazyAssets?.length, 3); assert.equal( runtime.lazyAssets?.[1]?.sha256, bootstrapTree.content.sha256, ); + assert.equal( + runtime.lazyAssets?.[2]?.sha256, + portableRubyTree.content.sha256, + ); assert.deepEqual(validatedMirror, mirror); for (const [forgery, expected] of [ diff --git a/homebrew/test/homebrew_guest_lifecycle_runtime_inputs.ts b/homebrew/test/homebrew_guest_lifecycle_runtime_inputs.ts index e868cee17b..bd00631b09 100644 --- a/homebrew/test/homebrew_guest_lifecycle_runtime_inputs.ts +++ b/homebrew/test/homebrew_guest_lifecycle_runtime_inputs.ts @@ -20,6 +20,13 @@ import { assertHomebrewGuestLifecycleCatalog, resolveHomebrewGuestLifecycleShell, } from "./homebrew_guest_lifecycle_runtime_contract"; +import { + HOMEBREW_PORTABLE_RUBY_OUTPUT, + HOMEBREW_PORTABLE_RUBY_TREE_ID, + homebrewPortableRubyExecutable, + homebrewPortableRubyMountPrefix, + readHomebrewPortableRubyVersion, +} from "../../host/src/homebrew-portable-ruby-contract"; export type HomebrewGuestLifecycleTransportMode = "closed" | "public"; @@ -36,7 +43,7 @@ export interface HomebrewGuestLifecycleRuntimeInputs { lazyAssets?: readonly ClosedLazyAsset[]; bootstrapTransportUrl: string; bootstrapBytes: number; - /** Bottle trees committed by the same first-use transaction as bootstrap. */ + /** Trees committed by the same first-use transaction as Brew's source. */ runtimeSupportTrees?: readonly { url: string; bytes: number }[]; } @@ -52,11 +59,14 @@ export interface DeriveHomebrewGuestLifecycleRuntimeInputs { bootstrapSpecBytes: Uint8Array; bootstrapArchiveBytes: Uint8Array; bootstrapArchiveSha256: string; + portableRubyArchiveBytes: Uint8Array; + portableRubyArchiveSha256: string; bootstrapEnvironmentBytes: Uint8Array; coreRevision: string; transportMode: HomebrewGuestLifecycleTransportMode; lazyUrlBase: string; expectedBootstrapTransportUrl?: string; + expectedPortableRubyTransportUrl?: string; expectedEmbeddedBottlePlanBytes?: Uint8Array; validateEmbeddedBottlePlan?: (plan: HomebrewBottleMirrorPlan) => void; closedBottleAssets?: readonly ClosedLazyAsset[]; @@ -89,9 +99,11 @@ export async function deriveHomebrewGuestLifecycleRuntimeInputs( ); if ( !SHA256_RE.test(input.bootstrapArchiveSha256) || - input.bootstrapArchiveBytes.byteLength === 0 + input.bootstrapArchiveBytes.byteLength === 0 || + !SHA256_RE.test(input.portableRubyArchiveSha256) || + input.portableRubyArchiveBytes.byteLength === 0 ) { - throw new Error("Homebrew bootstrap archive identity is invalid"); + throw new Error("Homebrew runtime archive identity is invalid"); } const fs = MemoryFileSystem.fromImage(input.imageBytes); // WHY: callbacks and closed-asset binding act on exported lazy metadata; @@ -112,12 +124,14 @@ export async function deriveHomebrewGuestLifecycleRuntimeInputs( const pendingTrees = classifyPendingTrees(fs.exportLazyArchiveEntries()); if ( pendingTrees.bootstrap.length !== 1 || + pendingTrees.portableRuby.length !== 1 || pendingTrees.unclassified.length !== 0 ) { throw new Error( `lifecycle image has ${pendingTrees.bootstrap.length} pending Homebrew ` + - `source trees and ${pendingTrees.unclassified.length} unclassified ` + - `package trees`, + `source trees, ${pendingTrees.portableRuby.length} portable Ruby ` + + `trees, and ${pendingTrees.unclassified.length} unclassified package ` + + `trees`, ); } assertBootstrapTreeBinding( @@ -127,6 +141,17 @@ export async function deriveHomebrewGuestLifecycleRuntimeInputs( input.bootstrapArchiveSha256, input.bootstrapArchiveBytes.byteLength, ); + const portableRubyVersion = readHomebrewPortableRubyVersion( + input.bootstrapArchiveBytes, + ); + assertPortableRubyTreeBinding( + fs, + pendingTrees.portableRuby[0]!, + bootstrapSpec, + portableRubyVersion, + input.portableRubyArchiveSha256, + input.portableRubyArchiveBytes.byteLength, + ); const embeddedPlanBytes = readVfsFile( fs, @@ -157,19 +182,23 @@ export async function deriveHomebrewGuestLifecycleRuntimeInputs( ); const bootstrapAtomicGroup = pendingTrees.bootstrap[0]!.activation?.atomicGroup; - const runtimeSupportTrees = bootstrapAtomicGroup === undefined + const runtimeSupportBottleTrees = bootstrapAtomicGroup === undefined ? [] : pendingTrees.bottles.filter( (tree) => tree.activation?.atomicGroup?.id === bootstrapAtomicGroup.id, ); + const portableRubyTree = pendingTrees.portableRuby[0]!; + const runtimeSupportTrees = [ + ...runtimeSupportBottleTrees, + portableRubyTree, + ]; if ( - bootstrapAtomicGroup !== undefined && + bootstrapAtomicGroup === undefined || ( bootstrapAtomicGroup.descriptorSha256 === undefined || bootstrapAtomicGroup.expectedCount !== runtimeSupportTrees.length + 1 || bootstrapAtomicGroup.cohortSha256 === undefined || - runtimeSupportTrees.length === 0 || runtimeSupportTrees.some( (tree) => tree.activation?.mode !== "first-use" || @@ -191,6 +220,10 @@ export async function deriveHomebrewGuestLifecycleRuntimeInputs( bootstrapSpec.archive.url, input.lazyUrlBase, ).toString(); + const portableRubyTransportUrl = new URL( + HOMEBREW_PORTABLE_RUBY_OUTPUT, + input.lazyUrlBase, + ).toString(); if ( input.expectedBootstrapTransportUrl !== undefined && bootstrapTransportUrl !== input.expectedBootstrapTransportUrl @@ -200,6 +233,16 @@ export async function deriveHomebrewGuestLifecycleRuntimeInputs( `expected ${input.expectedBootstrapTransportUrl}`, ); } + if ( + input.expectedPortableRubyTransportUrl !== undefined && + portableRubyTransportUrl !== input.expectedPortableRubyTransportUrl + ) { + throw new Error( + `Homebrew portable Ruby transport resolves to ` + + `${portableRubyTransportUrl}, expected ` + + input.expectedPortableRubyTransportUrl, + ); + } const lazyAssets = bindClosedLifecycleAssets( input, @@ -209,6 +252,9 @@ export async function deriveHomebrewGuestLifecycleRuntimeInputs( bootstrapTransportUrl, input.bootstrapArchiveSha256, input.bootstrapArchiveBytes.byteLength, + portableRubyTransportUrl, + input.portableRubyArchiveSha256, + input.portableRubyArchiveBytes.byteLength, ); input.validateImageFileSystem?.(fs); @@ -223,10 +269,16 @@ export async function deriveHomebrewGuestLifecycleRuntimeInputs( ...(lazyAssets === undefined ? {} : { lazyAssets }), bootstrapTransportUrl, bootstrapBytes: input.bootstrapArchiveBytes.byteLength, - runtimeSupportTrees: runtimeSupportTrees.map((tree) => ({ - url: tree.content!.transports[0]!, - bytes: tree.content!.bytes, - })), + runtimeSupportTrees: [ + ...runtimeSupportBottleTrees.map((tree) => ({ + url: tree.content!.transports[0]!, + bytes: tree.content!.bytes, + })), + { + url: portableRubyTransportUrl, + bytes: portableRubyTree.content!.bytes, + }, + ], }; } @@ -337,6 +389,120 @@ function assertBootstrapTreeBinding( } } +function assertPortableRubyTreeBinding( + fs: MemoryFileSystem, + tree: SerializedLazyArchiveEntry, + sourceSpec: PackageDeferredZipTreeSpec, + version: string, + archiveSha256: string, + archiveBytes: number, +): void { + const content = tree.content; + const inventory = tree.inventory; + const inventoryBytes = inventory?.reduce( + (total, entry) => total + entry.size, + 0, + ); + const mountPrefix = homebrewPortableRubyMountPrefix( + sourceSpec.mount_prefix, + ); + const rubyPath = homebrewPortableRubyExecutable( + sourceSpec.mount_prefix, + version, + ); + const currentPath = `${mountPrefix}/current`; + const atomicGroup = tree.activation?.atomicGroup; + const sourceAtomicGroup = sourceSpec.activation.atomicGroup; + const current = inventory?.find((entry) => entry.vfsPath === currentPath); + const ruby = inventory?.find((entry) => entry.vfsPath === rubyPath); + const allowedTopLevel = new Set([version, "current"]); + if ( + sourceSpec.package.name !== "homebrew-bootstrap" || + sourceSpec.package.output !== "homebrew-bootstrap.zip" || + sourceAtomicGroup === undefined || + tree.kind !== "kandelo-deferred-tree-v3" || + tree.materialized || + tree.mountPrefix !== mountPrefix || + tree.url !== HOMEBREW_PORTABLE_RUBY_OUTPUT || + content === undefined || + content.decoder !== "zip-v1" || + content.mediaType !== "application/zip" || + content.sha256 !== archiveSha256 || + content.bytes !== archiveBytes || + content.expandedBytes !== inventoryBytes || + content.sourceEntryCount !== inventory?.length || + content.transports.length !== 1 || + content.transports[0] !== HOMEBREW_PORTABLE_RUBY_OUTPUT || + content.modePolicy !== "portable-posix-v1" || + content.source !== undefined || + inventory === undefined || + inventory.length === 0 || + tree.activation?.mode !== "first-use" || + JSON.stringify(tree.activation.capabilities) !== + JSON.stringify(["homebrew:runtime"]) || + JSON.stringify(tree.activation.roots) !== JSON.stringify([rubyPath]) || + atomicGroup?.id !== sourceAtomicGroup.id || + atomicGroup.member !== HOMEBREW_PORTABLE_RUBY_TREE_ID || + atomicGroup.descriptorSha256 === undefined || + atomicGroup.expectedCount === undefined || + atomicGroup.cohortSha256 === undefined || + current?.type !== "symlink" || + current.target !== version || + ruby?.type !== "file" || + ruby.size <= 0 || + (ruby.mode & 0o111) === 0 || + inventory.some((entry) => { + if (!entry.vfsPath.startsWith(`${mountPrefix}/`)) return true; + const relative = entry.vfsPath.slice(mountPrefix.length + 1); + return !allowedTopLevel.has(relative.split("/", 1)[0]!); + }) + ) { + throw new Error( + "Homebrew portable Ruby deferred tree changed descriptor: " + + JSON.stringify({ + kind: tree.kind, + materialized: tree.materialized, + mountPrefix: tree.mountPrefix, + url: tree.url, + content, + inventoryBytes, + inventoryLength: inventory?.length, + activation: tree.activation, + }), + ); + } + + for (const entry of inventory) { + if (entry.type === "hardlink") { + throw new Error( + "Homebrew portable Ruby ZIP tree contains a hardlink inventory entry", + ); + } + const stat = fs.lstat(entry.vfsPath); + const expectedType = entry.type === "directory" + ? S_IFDIR + : entry.type === "symlink" + ? S_IFLNK + : S_IFREG; + if ( + (stat.mode & S_IFMT) !== expectedType || + (stat.mode & 0o7777) !== entry.mode || + stat.uid !== sourceSpec.owner.uid || + stat.gid !== sourceSpec.owner.gid || + (entry.type !== "directory" && stat.size !== entry.size) || + (entry.type === "file" && !fs.isPathDeferred(entry.vfsPath)) || + ( + entry.type === "symlink" && + fs.readlink(entry.vfsPath) !== entry.target + ) + ) { + throw new Error( + `Homebrew portable Ruby deferred tree changed ${entry.vfsPath}`, + ); + } + } +} + function bindClosedLifecycleAssets( input: DeriveHomebrewGuestLifecycleRuntimeInputs, plan: HomebrewBottleMirrorPlan, @@ -345,6 +511,9 @@ function bindClosedLifecycleAssets( bootstrapTransportUrl: string, bootstrapSha256: string, bootstrapBytes: number, + portableRubyTransportUrl: string, + portableRubySha256: string, + portableRubyBytes: number, ): readonly ClosedLazyAsset[] | undefined { if (input.transportMode === "public") { if ( @@ -377,6 +546,12 @@ function bindClosedLifecycleAssets( size: bootstrapBytes, bytes: input.bootstrapArchiveBytes, }, + { + url: portableRubyTransportUrl, + sha256: portableRubySha256, + size: portableRubyBytes, + bytes: input.portableRubyArchiveBytes, + }, ]; } @@ -412,6 +587,7 @@ function assertClosedBottleAssets( function classifyPendingTrees(entries: readonly SerializedLazyArchiveEntry[]): { bottles: SerializedLazyArchiveEntry[]; bootstrap: SerializedLazyArchiveEntry[]; + portableRuby: SerializedLazyArchiveEntry[]; unclassified: SerializedLazyArchiveEntry[]; } { const pending = entries.filter((tree) => tree.content !== undefined); @@ -420,12 +596,15 @@ function classifyPendingTrees(entries: readonly SerializedLazyArchiveEntry[]): { const bottleCapabilities = capabilities.filter((capability) => capability.startsWith("homebrew-bottle:") ); + const ownershipCapabilities = [ + bottleCapabilities.length > 0, + capabilities.includes("homebrew:bootstrap"), + capabilities.includes("homebrew:runtime") && + !capabilities.includes("homebrew:bootstrap"), + ].filter(Boolean).length; if ( bottleCapabilities.length > 1 || - ( - bottleCapabilities.length === 1 && - capabilities.includes("homebrew:bootstrap") - ) + ownershipCapabilities > 1 ) { throw new Error( `pending tree ${tree.mountPrefix} has ambiguous Homebrew ownership`, @@ -440,10 +619,20 @@ function classifyPendingTrees(entries: readonly SerializedLazyArchiveEntry[]): { const bootstrap = pending.filter((tree) => tree.activation?.capabilities.includes("homebrew:bootstrap") ); + const portableRuby = pending.filter((tree) => + tree.activation?.capabilities.includes("homebrew:runtime") && + !tree.activation.capabilities.includes("homebrew:bootstrap") && + !tree.activation.capabilities.some((capability) => + capability.startsWith("homebrew-bottle:") + ) + ); const unclassified = pending.filter( - (tree) => !bottles.includes(tree) && !bootstrap.includes(tree), + (tree) => + !bottles.includes(tree) && + !bootstrap.includes(tree) && + !portableRuby.includes(tree), ); - return { bottles, bootstrap, unclassified }; + return { bottles, bootstrap, portableRuby, unclassified }; } function readVfsFile(fs: MemoryFileSystem, path: string): Uint8Array { diff --git a/homebrew/test/run_homebrew_guest_shipping_scope.sh b/homebrew/test/run_homebrew_guest_shipping_scope.sh index bdc45ddd59..53c0ae7d4a 100755 --- a/homebrew/test/run_homebrew_guest_shipping_scope.sh +++ b/homebrew/test/run_homebrew_guest_shipping_scope.sh @@ -17,6 +17,7 @@ esac : "${IMAGE:?missing public Homebrew image}" : "${BOOTSTRAP:?missing public Homebrew bootstrap archive}" +: "${PORTABLE_RUBY:?missing public Homebrew portable Ruby archive}" : "${BOOTSTRAP_ENV:?missing public Homebrew bootstrap environment}" : "${TAP_CATALOG_REF:?missing sealed tap catalog revision}" : "${CANARY_REF:?missing canary revision}" @@ -94,6 +95,7 @@ bash scripts/dev-shell.sh npx tsx \ --homebrew-bootstrap-spec \ homebrew/main-shell-brew-package-tree.json \ --homebrew-bootstrap-archive "$BOOTSTRAP" \ + --homebrew-portable-ruby-archive "$PORTABLE_RUBY" \ --homebrew-bootstrap-env "$BOOTSTRAP_ENV" \ --transport-mode public \ --proof-mode "$scope" \ diff --git a/host/src/browser-kernel-worker-entry.ts b/host/src/browser-kernel-worker-entry.ts index ea6be65aed..beb4bca8ac 100644 --- a/host/src/browser-kernel-worker-entry.ts +++ b/host/src/browser-kernel-worker-entry.ts @@ -24,6 +24,7 @@ import type { } from "./kernel-worker"; import type { KernelPointer } from "./kernel"; import { BrowserWorkerAdapter } from "./worker-adapter-browser"; +import { prestartedWorkerPoolSize } from "./prestarted-worker-pool"; import { DeferredWorkerHandle } from "./deferred-worker-handle"; import { readPreparedPlatformFile, @@ -38,7 +39,10 @@ import { BrowserTimeProvider } from "./vfs/time"; import { restoreBrowserKernelInitMounts } from "./browser-kernel-vfs-init"; import type { MountConfig } from "./vfs/types"; import { TlsNetworkBackend } from "./networking/tls-network-backend"; -import { patchWasmForThread } from "./worker-main"; +import { + detectChannelBaseTlsOffset, + patchWasmForThread, +} from "./worker-main"; import { detectPtrWidth, extractAbiVersion, extractHeapBase, isWasmModuleBytes } from "./constants"; import { ThreadExitCoordinator } from "./thread-exit-coordinator"; import { @@ -56,6 +60,7 @@ import { createWorkerQuiescence, type WorkerQuiescence, waitForExecRetirement as waitForExecRetirementFence, + waitForThreadExecRetirement, waitForWorkerQuiescence as waitForWorkerQuiescenceFence, } from "./worker-quiescence"; import { RootfsSnapshotGate } from "./rootfs-snapshot-gate"; @@ -90,6 +95,12 @@ import type { KernelToMainMessage, } from "./browser-kernel-protocol"; import { kernelRealmDestroyResult } from "./kernel-realm-destroy"; +import { + DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_ALIASES, + DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_ENTRIES, + DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_RETAINED_BYTES, + ExecutableModuleCache, +} from "./executable-module-cache"; const PAGE_SIZE = 65536; // State @@ -142,7 +153,9 @@ interface ProcessInfo extends ProcessGenerationOwnership { /** Exact browser-main alias teardown, shared by competing failure paths. */ framebufferRelease?: Promise; programBytes: ArrayBuffer; - programModule?: WebAssembly.Module; + programModule: WebAssembly.Module; + programAbiVersion: number | null; + channelBaseTlsOffset: number; worker: ReturnType; argv: string[]; channelOffset: number; @@ -189,6 +202,18 @@ const DESTROY_KILL_DRAIN_POLL_MS = 15; const intentionallyTerminated = new WeakSet(); const MAX_SHEBANG_DEPTH = 4; +interface ExecutableModuleMetadata { + programAbiVersion: number | null; + channelBaseTlsOffset: number; +} +// The cache is machine-scoped and bounded independently of the VFS. It never +// extends a process worker or Realm lifetime; only immutable compiled code and +// exact comparison bytes survive between execs. +const executableModuleCache = new ExecutableModuleCache({ + maxEntries: DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_ENTRIES, + maxAliases: DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_ALIASES, + maxRetainedBytes: DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_RETAINED_BYTES, +}); function parseShebang(bytes: ArrayBuffer): { interpreter: string; arg?: string } | null { const view = new Uint8Array(bytes); @@ -214,6 +239,16 @@ async function resolveExecutableForLaunch( const shebang = parseShebang(bytes); if (!shebang) { if (!isWasmModuleBytes(bytes)) return { errno: ENOEXEC }; + const cached = executableModuleCache.get(path, bytes); + if (cached) { + return { + programBytes: bytes, + programModule: cached.module, + programAbiVersion: cached.metadata.programAbiVersion, + channelBaseTlsOffset: cached.metadata.channelBaseTlsOffset, + argv, + }; + } let programModule: WebAssembly.Module; try { programModule = await WebAssembly.compile(bytes); @@ -225,7 +260,18 @@ async function resolveExecutableForLaunch( if (declaredAbi !== null && declaredAbi !== kernelWorker.getKernelAbiVersion()) { return { errno: ENOEXEC }; } - return { programBytes: bytes, programModule, argv }; + const channelBaseTlsOffset = detectChannelBaseTlsOffset(bytes); + executableModuleCache.set(path, bytes, programModule, { + programAbiVersion: declaredAbi, + channelBaseTlsOffset, + }); + return { + programBytes: bytes, + programModule, + programAbiVersion: declaredAbi, + channelBaseTlsOffset, + argv, + }; } const scriptArgv = [ @@ -248,6 +294,7 @@ interface ThreadWorkerInfo { quiescent: boolean; workerQuiescence: WorkerQuiescence; execRetirement: WorkerQuiescence; + threadExit: WorkerQuiescence; termination?: Promise; } const threadWorkers = new Map(); @@ -374,9 +421,11 @@ async function terminateThreadWorkers( const quiescence = await Promise.all( threads.map((thread) => requireExecRetirement - ? waitForExecRetirement( + ? waitForThreadExecRetirement( thread.execRetirement, + thread.threadExit, thread.workerQuiescence, + EXEC_WORKER_RETIREMENT_WAIT_MS, ) : waitForWorkerQuiescence(thread.workerQuiescence)), ); @@ -847,6 +896,9 @@ async function createFreshProcessMemory( async function handleInit(msg: Extract) { initReady = false; + // A kernel Worker normally owns one machine, but keep the cache lifetime + // explicit if an embedder initializes the realm again after teardown. + executableModuleCache.clear(); initFailure = null; maxPages = msg.config.maxMemoryPages; defaultThreadSlots = msg.config.defaultThreadSlots ?? DEFAULT_PROCESS_THREAD_SLOTS; @@ -940,7 +992,12 @@ async function handleInit(msg: Extract) { } // Create worker adapter for spawning sub-workers - workerAdapter = new BrowserWorkerAdapter(msg.workerEntryUrl); + // Keep Node/browser parity with a bounded reserve of pristine runners. Used + // realms retain the existing one-process lifetime and physical termination. + workerAdapter = new BrowserWorkerAdapter( + msg.workerEntryUrl, + prestartedWorkerPoolSize(msg.config.maxWorkers), + ); // Create CentralizedKernelWorker kernelWorker = new CentralizedKernelWorker( @@ -1157,6 +1214,34 @@ async function handleSpawn(msg: Extract) respondError(msg.requestId, "ENOEXEC: program is not a WebAssembly module"); return; } + const cached = msg.programPath + ? executableModuleCache.get(msg.programPath, programBytes) + : undefined; + let programModule = cached?.module; + let programAbiVersion = cached?.metadata.programAbiVersion; + let channelBaseTlsOffset = cached?.metadata.channelBaseTlsOffset; + if (!programModule) { + try { + programModule = await WebAssembly.compile(programBytes); + } catch (error) { + if (error instanceof WebAssembly.CompileError) { + respondError(msg.requestId, "ENOEXEC: program is not a valid WebAssembly module"); + return; + } + throw error; + } + programAbiVersion = extractAbiVersion(programBytes); + channelBaseTlsOffset = detectChannelBaseTlsOffset(programBytes); + if (msg.programPath) { + executableModuleCache.set(msg.programPath, programBytes, programModule, { + programAbiVersion, + channelBaseTlsOffset, + }); + } + } + if (programAbiVersion === undefined || channelBaseTlsOffset === undefined) { + throw new Error("compiled executable metadata is unavailable"); + } const pid = kernelWorker.createProcess( msg.pty ? TERMINAL_STDIO : CAPTURED_STDIO, @@ -1214,7 +1299,9 @@ async function handleSpawn(msg: Extract) const initData: CentralizedWorkerInitMessage = { type: "centralized_init", pid, - programBytes, + programModule, + programAbiVersion, + channelBaseTlsOffset, memory, channelOffset, env: launchEnv, @@ -1236,6 +1323,9 @@ async function handleSpawn(msg: Extract) memoryRetirementSafe: true, framebufferExposed: false, programBytes, + programModule, + programAbiVersion, + channelBaseTlsOffset, worker, argv: msg.argv, channelOffset, @@ -1505,8 +1595,9 @@ async function handleFork( const childInitData: CentralizedWorkerInitMessage = { type: "centralized_init", pid: childPid, - programBytes: parentInfo.programBytes, programModule: parentInfo.programModule, + programAbiVersion: parentInfo.programAbiVersion, + channelBaseTlsOffset: parentInfo.channelBaseTlsOffset, memory: childMemory, channelOffset: childChannelOffset, isForkChild: true, @@ -1531,6 +1622,8 @@ async function handleFork( framebufferExposed: false, programBytes: parentInfo.programBytes, programModule: parentInfo.programModule, + programAbiVersion: parentInfo.programAbiVersion, + channelBaseTlsOffset: parentInfo.channelBaseTlsOffset, worker, argv: parentInfo.argv, channelOffset: childChannelOffset, @@ -1618,6 +1711,11 @@ async function handleExec( if (!resolved) return -2; // ENOENT if ("errno" in resolved) return -resolved.errno; const { programBytes: bytes, programModule, argv: launchArgv } = resolved; + const programAbiVersion = resolved.programAbiVersion !== undefined + ? resolved.programAbiVersion + : extractAbiVersion(bytes); + const channelBaseTlsOffset = resolved.channelBaseTlsOffset ?? + detectChannelBaseTlsOffset(bytes); // Preallocate the replacement address space before the irreversible commit. const ptrWidth = detectPtrWidth(bytes); const metadataResult = kernelWorker.validateExecMetadata( @@ -1772,8 +1870,9 @@ async function handleExec( const execInitData: CentralizedWorkerInitMessage = { type: "centralized_init", pid, - programBytes: bytes, programModule, + programAbiVersion, + channelBaseTlsOffset, memory: newMemory, channelOffset: newChannelOffset, argv: launchArgv, @@ -1811,6 +1910,8 @@ async function handleExec( framebufferExposed: false, programBytes: bytes, programModule, + programAbiVersion, + channelBaseTlsOffset, worker: replacementWorker, argv: launchArgv, channelOffset: newChannelOffset, @@ -1979,6 +2080,11 @@ async function handlePosixSpawn( post({ type: "proc_event", kind: "spawn", pid: childPid, ppid: parentPid }); const { programBytes, programModule, argv } = program; + const programAbiVersion = program.programAbiVersion !== undefined + ? program.programAbiVersion + : extractAbiVersion(programBytes); + const channelBaseTlsOffset = program.channelBaseTlsOffset ?? + detectChannelBaseTlsOffset(programBytes); const ptrWidth = detectPtrWidth(programBytes); let fresh: Awaited>; try { @@ -2032,8 +2138,9 @@ async function handlePosixSpawn( const initData: CentralizedWorkerInitMessage = { type: "centralized_init", pid: childPid, - programBytes, programModule, + programAbiVersion, + channelBaseTlsOffset, memory: newMemory, channelOffset: newChannelOffset, argv, @@ -2056,6 +2163,8 @@ async function handlePosixSpawn( framebufferExposed: false, programBytes, programModule, + programAbiVersion, + channelBaseTlsOffset, worker, argv, channelOffset: newChannelOffset, @@ -2187,8 +2296,9 @@ async function handleClone( type: "centralized_thread_init", pid, tid, - programBytes: processInfo.programBytes, programModule: threadModule, + programAbiVersion: processInfo.programAbiVersion, + channelBaseTlsOffset: processInfo.channelBaseTlsOffset, memory, processChannelOffset: processInfo.channelOffset, channelOffset: alloc.channelOffset, @@ -2215,6 +2325,7 @@ async function handleClone( quiescent: false, workerQuiescence: createWorkerQuiescence(), execRetirement: createWorkerQuiescence(), + threadExit: createWorkerQuiescence(), }; threadWorkers.get(pid)!.push(threadEntry); @@ -2288,7 +2399,8 @@ async function handleClone( const m = msg as WorkerToHostMessage; if (m.type === "exec_retired" && m.tid === tid) { threadEntry.execRetirement.settle(); - } else if (m.type === "thread_exit") { + } else if (m.type === "thread_exit" && m.tid === tid) { + threadEntry.threadExit.settle(); if (!isCurrentThreadGeneration()) { void terminateThreadEntry(); return; @@ -2876,11 +2988,17 @@ async function performDestroy() { ); } } + // Unleased reserve workers are live browser realms even though they own no + // guest memory. Explicitly terminate them at the machine boundary. + await workerAdapter.destroy(); vmInterruptTimers.clearAll(); // The enclosing creator gate is closed and drained, so this exact map/ledger // state cannot be invalidated by a later spawn/exec/fork/clone continuation. let gracefulDetachComplete = processGenerationDetaches.pendingCount === 0 && processes.size === 0; + // Release retained comparison bytes and compiled code before the enclosing + // kernel Worker realm receives its terminal acknowledgement. + executableModuleCache.clear(); threadModuleCache.clear(); threadWorkers.clear(); threadedProcessPids.clear(); diff --git a/host/src/executable-module-cache.ts b/host/src/executable-module-cache.ts new file mode 100644 index 0000000000..d4e8f15fba --- /dev/null +++ b/host/src/executable-module-cache.ts @@ -0,0 +1,285 @@ +export interface ExecutableModuleCacheOptions { + maxEntries: number; + maxAliases: number; + maxRetainedBytes: number; +} + +// WHY: input bytes are retained to prove exact VFS identity on every hit, and +// V8's compiled-code size is not observable here. Bound every retained index: +// 16 entries cover a small interpreter/tool working set, 256 aliases cover a +// normal Unix multicall layout, and 64 MiB admits large executables without +// turning the cache into an unbounded image mirror. +export const DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_ENTRIES = 16; +export const DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_ALIASES = 256; +export const DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_RETAINED_BYTES = + 64 * 1024 * 1024; + +export interface ExecutableModuleCacheEntry { + module: WebAssembly.Module; + metadata: Metadata; +} + +export interface ExecutableModuleCacheStats { + hits: number; + contentAliasHits: number; + misses: number; + contentMismatches: number; + evictions: number; + aliasEvictions: number; + retainedEntries: number; + retainedAliases: number; + retainedBytes: number; +} + +interface RetainedExecutableModule + extends ExecutableModuleCacheEntry { + bytes: ArrayBuffer; + keys: Set; +} + +function requireNonNegativeSafeInteger(value: number, label: string): number { + if (!Number.isSafeInteger(value) || value < 0) { + throw new RangeError(`${label} must be a non-negative safe integer`); + } + return value; +} + +/** Exact comparison keeps cache validity tied to authoritative file bytes. */ +function equalBytes(left: ArrayBuffer, right: ArrayBuffer): boolean { + if (left.byteLength !== right.byteLength) return false; + + const wordCount = Math.floor(left.byteLength / Uint32Array.BYTES_PER_ELEMENT); + const leftWords = new Uint32Array(left, 0, wordCount); + const rightWords = new Uint32Array(right, 0, wordCount); + for (let index = 0; index < wordCount; index++) { + if (leftWords[index] !== rightWords[index]) return false; + } + + const byteOffset = wordCount * Uint32Array.BYTES_PER_ELEMENT; + const leftTail = new Uint8Array(left, byteOffset); + const rightTail = new Uint8Array(right, byteOffset); + for (let index = 0; index < leftTail.byteLength; index++) { + if (leftTail[index] !== rightTail[index]) return false; + } + return true; +} + +/** + * Bounded cache for immutable compiled WebAssembly executables. + * + * A hit reuses only a `WebAssembly.Module`; process workers, Realms, linear + * memory, channels, and guest state remain one-shot. The cache owns an exact + * byte copy so a path is never allowed to return code compiled from stale VFS + * contents, even when an executable is replaced without changing its size or + * timestamps. Identical bytes reached through different paths share one entry; + * metadata stored here must therefore be derived only from executable content. + */ +export class ExecutableModuleCache { + private readonly maxEntries: number; + private readonly maxAliases: number; + private readonly maxRetainedBytes: number; + private readonly entriesByKey = new Map< + string, + RetainedExecutableModule + >(); + private readonly entriesBySize = new Map< + number, + Set> + >(); + /** Set insertion order is the least-recently-used eviction order. */ + private readonly lru = new Set>(); + private retainedBytes = 0; + private hits = 0; + private contentAliasHits = 0; + private misses = 0; + private contentMismatches = 0; + private evictions = 0; + private aliasEvictions = 0; + + constructor(options: ExecutableModuleCacheOptions) { + this.maxEntries = requireNonNegativeSafeInteger( + options.maxEntries, + "Executable module cache maxEntries", + ); + this.maxAliases = requireNonNegativeSafeInteger( + options.maxAliases, + "Executable module cache maxAliases", + ); + this.maxRetainedBytes = requireNonNegativeSafeInteger( + options.maxRetainedBytes, + "Executable module cache maxRetainedBytes", + ); + } + + get(key: string, bytes: ArrayBuffer): ExecutableModuleCacheEntry | undefined { + const keyed = this.entriesByKey.get(key); + if (keyed && equalBytes(keyed.bytes, bytes)) { + this.refreshKey(key, keyed); + this.refresh(keyed); + this.hits++; + return { module: keyed.module, metadata: keyed.metadata }; + } + + if (keyed) { + this.detachKey(key, keyed); + this.contentMismatches++; + } + + // WHY: Homebrew and ordinary Unix images expose multicall executables and + // hardlinks through many pathnames. Size narrows the candidates cheaply; + // the exact comparison prevents an alias or hash collision from selecting + // code compiled from different authoritative VFS bytes. + const shared = this.findExactContent(bytes); + if (shared) { + this.attachKey(key, shared); + this.refresh(shared); + this.hits++; + this.contentAliasHits++; + return { module: shared.module, metadata: shared.metadata }; + } + + this.misses++; + return undefined; + } + + set( + key: string, + bytes: ArrayBuffer, + module: WebAssembly.Module, + metadata: Metadata, + ): void { + const prior = this.entriesByKey.get(key); + if (prior) { + this.detachKey(key, prior); + } + + const shared = this.findExactContent(bytes); + if (shared) { + this.attachKey(key, shared); + this.refresh(shared); + return; + } + + if ( + this.maxEntries === 0 || + bytes.byteLength > this.maxRetainedBytes + ) { + return; + } + + // WHY: callers retain and sometimes transfer executable buffers. The + // cache needs immutable comparison bytes whose ownership never leaves it. + const ownedBytes = bytes.slice(0); + while ( + this.lru.size >= this.maxEntries || + this.retainedBytes + ownedBytes.byteLength > this.maxRetainedBytes + ) { + const oldest = this.lru.values().next().value as + | RetainedExecutableModule + | undefined; + if (!oldest) break; + this.removeEntry(oldest, true); + } + + const retained = { + bytes: ownedBytes, + keys: new Set(), + module, + metadata, + }; + let sameSize = this.entriesBySize.get(ownedBytes.byteLength); + if (!sameSize) { + sameSize = new Set(); + this.entriesBySize.set(ownedBytes.byteLength, sameSize); + } + sameSize.add(retained); + this.lru.add(retained); + this.retainedBytes += ownedBytes.byteLength; + this.attachKey(key, retained); + } + + clear(): void { + this.entriesByKey.clear(); + this.entriesBySize.clear(); + this.lru.clear(); + this.retainedBytes = 0; + } + + stats(): ExecutableModuleCacheStats { + return { + hits: this.hits, + contentAliasHits: this.contentAliasHits, + misses: this.misses, + contentMismatches: this.contentMismatches, + evictions: this.evictions, + aliasEvictions: this.aliasEvictions, + retainedEntries: this.lru.size, + retainedAliases: this.entriesByKey.size, + retainedBytes: this.retainedBytes, + }; + } + + private findExactContent( + bytes: ArrayBuffer, + ): RetainedExecutableModule | undefined { + for (const retained of this.entriesBySize.get(bytes.byteLength) ?? []) { + if (equalBytes(retained.bytes, bytes)) return retained; + } + return undefined; + } + + private refresh(retained: RetainedExecutableModule): void { + this.lru.delete(retained); + this.lru.add(retained); + } + + private refreshKey( + key: string, + retained: RetainedExecutableModule, + ): void { + this.entriesByKey.delete(key); + this.entriesByKey.set(key, retained); + } + + private attachKey( + key: string, + retained: RetainedExecutableModule, + ): void { + retained.keys.add(key); + this.entriesByKey.set(key, retained); + while (this.entriesByKey.size > this.maxAliases) { + const oldestKey = this.entriesByKey.keys().next().value as + | string + | undefined; + if (oldestKey === undefined) break; + const oldest = this.entriesByKey.get(oldestKey)!; + this.detachKey(oldestKey, oldest); + this.aliasEvictions++; + } + } + + private detachKey( + key: string, + retained: RetainedExecutableModule, + ): void { + this.entriesByKey.delete(key); + retained.keys.delete(key); + if (retained.keys.size === 0) this.removeEntry(retained, false); + } + + private removeEntry( + retained: RetainedExecutableModule, + eviction: boolean, + ): void { + for (const key of retained.keys) this.entriesByKey.delete(key); + retained.keys.clear(); + const sameSize = this.entriesBySize.get(retained.bytes.byteLength); + sameSize?.delete(retained); + if (sameSize?.size === 0) { + this.entriesBySize.delete(retained.bytes.byteLength); + } + this.lru.delete(retained); + this.retainedBytes -= retained.bytes.byteLength; + if (eviction) this.evictions++; + } +} diff --git a/host/src/homebrew-bottle-relocation.ts b/host/src/homebrew-bottle-relocation.ts index 130577e655..6b60509d42 100644 --- a/host/src/homebrew-bottle-relocation.ts +++ b/host/src/homebrew-bottle-relocation.ts @@ -6,23 +6,20 @@ * archive has been verified and decoded. */ -import { KANDELO_HOMEBREW_GUEST_LAYOUT } from "./homebrew-guest-layout"; - const MAX_BOTTLE_CHANGED_FILES = 100_000; const MAX_BOTTLE_PATH_BYTES = 4096; -const HOMEBREW_PREFIX = KANDELO_HOMEBREW_GUEST_LAYOUT.prefix; -const HOMEBREW_REPLACEMENTS = [ - ["@@HOMEBREW_PREFIX@@", HOMEBREW_PREFIX], - ["@@HOMEBREW_CELLAR@@", `${HOMEBREW_PREFIX}/Cellar`], - ["@@HOMEBREW_REPOSITORY@@", HOMEBREW_PREFIX], - ["@@HOMEBREW_LIBRARY@@", `${HOMEBREW_PREFIX}/Library`], - ["@@HOMEBREW_PERL@@", `${HOMEBREW_PREFIX}/opt/perl/bin/perl`], +const HOMEBREW_PLACEHOLDERS = [ + "@@HOMEBREW_PREFIX@@", + "@@HOMEBREW_CELLAR@@", + "@@HOMEBREW_REPOSITORY@@", + "@@HOMEBREW_LIBRARY@@", + "@@HOMEBREW_PERL@@", ] as const; const HOMEBREW_JAVA_PLACEHOLDER = "@@HOMEBREW_JAVA@@"; const HOMEBREW_OPENJDK_NAME_RE = /^openjdk(?:@\d+(?:\.\d+)*)?/; const TEXT_ENCODER = new TextEncoder(); const PLACEHOLDER_BYTES = [ - ...HOMEBREW_REPLACEMENTS.map(([placeholder]) => placeholder), + ...HOMEBREW_PLACEHOLDERS, HOMEBREW_JAVA_PLACEHOLDER, ].map((placeholder) => ({ placeholder, @@ -89,9 +86,21 @@ export function relocateHomebrewBottleFile( bytes: Uint8Array, receipt: HomebrewInstallReceiptRelocation, path: string, + prefix: string, ): Uint8Array { + validateHomebrewPrefix(prefix); + // WHY: the prefix belongs to the authenticated package/tree projection. + // Deriving replacements here keeps already-published compatibility images + // usable without weakening the canonical prefix selected by new products. + const replacements = [ + ["@@HOMEBREW_PREFIX@@", prefix], + ["@@HOMEBREW_CELLAR@@", `${prefix}/Cellar`], + ["@@HOMEBREW_REPOSITORY@@", prefix], + ["@@HOMEBREW_LIBRARY@@", `${prefix}/Library`], + ["@@HOMEBREW_PERL@@", `${prefix}/opt/perl/bin/perl`], + ] as const; let relocated = bytes; - for (const [placeholder, replacement] of HOMEBREW_REPLACEMENTS) { + for (const [placeholder, replacement] of replacements) { relocated = replaceBytes( relocated, TEXT_ENCODER.encode(placeholder), @@ -100,7 +109,7 @@ export function relocateHomebrewBottleFile( } const javaPlaceholder = TEXT_ENCODER.encode(HOMEBREW_JAVA_PLACEHOLDER); if (containsBytes(relocated, javaPlaceholder)) { - const javaHome = homebrewJavaHome(receipt.runtimeDependencies); + const javaHome = homebrewJavaHome(receipt.runtimeDependencies, prefix); if (javaHome === undefined) { throw new Error( `Homebrew changed file ${path} uses ${HOMEBREW_JAVA_PLACEHOLDER} ` + @@ -118,7 +127,7 @@ export function relocateHomebrewBottleFile( return relocated; } -function homebrewJavaHome(value: unknown): string | undefined { +function homebrewJavaHome(value: unknown, prefix: string): string | undefined { if (!Array.isArray(value)) return undefined; const names: string[] = []; for (const dependency of value) { @@ -140,10 +149,24 @@ function homebrewJavaHome(value: unknown): string | undefined { } const unique = [...new Set(names)]; return unique.length === 1 - ? `${HOMEBREW_PREFIX}/opt/${unique[0]}/libexec` + ? `${prefix}/opt/${unique[0]}/libexec` : undefined; } +function validateHomebrewPrefix(prefix: string): void { + if ( + typeof prefix !== "string" || prefix === "/" || !prefix.startsWith("/") || + prefix.endsWith("/") || prefix.includes("\\") || prefix.includes("\0") || + hasLoneUnicodeSurrogate(prefix) || + TEXT_ENCODER.encode(prefix).byteLength > MAX_BOTTLE_PATH_BYTES || + prefix.slice(1).split("/").some((part) => + part === "" || part === "." || part === ".." + ) + ) { + throw new Error(`Homebrew relocation prefix is not canonical: ${prefix}`); + } +} + function validateSafeRelativePath(value: string, label: string): void { if ( value.length === 0 || value.startsWith("/") || value.includes("\\") || diff --git a/host/src/homebrew-lazy-layer.ts b/host/src/homebrew-lazy-layer.ts index 615f60a049..327fd924e8 100644 --- a/host/src/homebrew-lazy-layer.ts +++ b/host/src/homebrew-lazy-layer.ts @@ -41,7 +41,6 @@ import { parseHomebrewInstallReceiptRelocation, relocateHomebrewBottleFile, } from "./homebrew-bottle-relocation"; -import { KANDELO_HOMEBREW_GUEST_LAYOUT } from "./homebrew-guest-layout"; export type { HomebrewDeferredTreeDescriptor, HomebrewDeferredTreeDraftDescriptor, @@ -78,7 +77,6 @@ const HOMEBREW_VFS_REPORT_ASSET = "kandelo-homebrew-vfs-report.json"; const HOMEBREW_NODE_EVIDENCE_ASSET = "kandelo-homebrew-node-evidence.json"; const HOMEBREW_BROWSER_EVIDENCE_ASSET = "kandelo-homebrew-browser-evidence.json"; const HOMEBREW_COMPOSITION_PATH = "/etc/kandelo/homebrew-vfs.json"; -const HOME_BREW_PREFIX = KANDELO_HOMEBREW_GUEST_LAYOUT.prefix; const ZIP_EPOCH = new Date(1980, 0, 1, 0, 0, 0); const S_IFMT = 0xf000; const S_IFREG = 0x8000; @@ -198,6 +196,7 @@ export async function buildHomebrewOriginalBottleCollection( } await authenticateHomebrewCompositionBase(options.baseFs); commonArch(plan); + const guestPrefix = commonPrefix(plan); const tapLock = planTapLock(plan); validatePackageTapOwnership(plan.packages, tapLock); if (plan.packages.length === 0) { @@ -284,7 +283,11 @@ export async function buildHomebrewOriginalBottleCollection( compatibilityPolicy: options.compatibilityPolicy, consumerState: "defer", }); - const finalEntries = collectLayerEntries(options.fs, options.baseFs); + const finalEntries = collectLayerEntries( + options.fs, + options.baseFs, + guestPrefix, + ); const trees = createOriginalBottleTrees( bottles, finalEntries, @@ -1013,7 +1016,12 @@ function prepareOriginalBottleRelocation( sourcePaths.add(source.path); const canonical = resolveTarRegularSource(source, sourceByPath, pkg); try { - const relocated = relocateHomebrewBottleFile(canonical.data, receipt, guestPath); + const relocated = relocateHomebrewBottleFile( + canonical.data, + receipt, + guestPath, + pkg.prefix, + ); const prior = bytesByCanonicalSource.get(canonical.path); if (prior !== undefined && !bytesEqual(prior, relocated)) { throw new Error("hard-link aliases produce different relocated bytes"); @@ -1577,6 +1585,17 @@ function commonArch(plan: HomebrewVfsPlan): "wasm32" | "wasm64" { return arch; } +function commonPrefix(plan: HomebrewVfsPlan): string { + const prefix = plan.packages[0]?.prefix; + if ( + prefix === undefined || + plan.packages.some((pkg) => pkg.prefix !== prefix) + ) { + throw new Error("Homebrew lazy layer plan must have one non-empty prefix"); + } + return prefix; +} + function planTapLock(plan: HomebrewVfsPlan): HomebrewVfsTapIdentity[] { const federated = plan as Partial; const candidates = Array.isArray(federated.taps) @@ -1868,12 +1887,17 @@ function packageArtifactIdentity(value: unknown): Record { function collectLayerEntries( layerFs: MemoryFileSystem, baseFs: MemoryFileSystem, + guestPrefix: string, ): HomebrewLazyLayerEntry[] { - if (!pathExists(layerFs, HOME_BREW_PREFIX)) { + // WHY: the bottle plan is the authenticated owner of its poured prefix. + // Compatibility products and the canonical product use different prefixes, + // so consulting a process-global layout here could omit the complete layer + // after a valid pour or accidentally collect an unrelated namespace. + if (!pathExists(layerFs, guestPrefix)) { throw new Error("Homebrew lazy layer is missing its poured prefix"); } const entries: HomebrewLazyLayerEntry[] = []; - collectPath(layerFs, HOME_BREW_PREFIX, entries, new Map()); + collectPath(layerFs, guestPrefix, entries, new Map()); entries.sort((left, right) => compareHomebrewCanonicalText(left.path, right.path)); for (const entry of entries) { const basePath = `/${entry.path}`; diff --git a/host/src/homebrew-portable-ruby-contract.ts b/host/src/homebrew-portable-ruby-contract.ts new file mode 100644 index 0000000000..1b1bbd369b --- /dev/null +++ b/host/src/homebrew-portable-ruby-contract.ts @@ -0,0 +1,77 @@ +import { + extractZipEntryBounded, + parseZipCentralDirectory, +} from "./vfs/zip"; + +export const HOMEBREW_PORTABLE_RUBY_VERSION_MEMBER = + "Library/Homebrew/vendor/portable-ruby-version"; +export const HOMEBREW_PORTABLE_RUBY_RELATIVE_PREFIX = + "Library/Homebrew/vendor/portable-ruby"; +export const HOMEBREW_PORTABLE_RUBY_OUTPUT = + "homebrew-portable-ruby.zip"; +export const HOMEBREW_PORTABLE_RUBY_TREE_ID = + "homebrew-bootstrap/portable-ruby"; + +const MAX_PORTABLE_RUBY_VERSION_BYTES = 64; +const PORTABLE_RUBY_VERSION_RE = /^\d+\.\d+\.\d+(?:_\d+)?$/; + +/** Read the portable-Ruby version selected by Homebrew's exact source tree. */ +export function readHomebrewPortableRubyVersion( + sourceArchiveBytes: Uint8Array, +): string { + const matches = parseZipCentralDirectory(sourceArchiveBytes).filter( + (entry) => entry.fileName === HOMEBREW_PORTABLE_RUBY_VERSION_MEMBER, + ); + if ( + matches.length !== 1 || + matches[0]!.isDirectory || + matches[0]!.isSymlink || + matches[0]!.uncompressedSize > MAX_PORTABLE_RUBY_VERSION_BYTES + ) { + throw new Error( + "Homebrew bootstrap source has no unique portable Ruby version file", + ); + } + const bytes = extractZipEntryBounded( + sourceArchiveBytes, + matches[0]!, + matches[0]!.uncompressedSize, + ); + let text: string; + try { + text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } catch (error) { + throw new Error("Homebrew portable Ruby version is not UTF-8", { + cause: error, + }); + } + if (!text.endsWith("\n") || text.indexOf("\n") !== text.length - 1) { + throw new Error("Homebrew portable Ruby version is not one line"); + } + const version = text.slice(0, -1); + if (!PORTABLE_RUBY_VERSION_RE.test(version)) { + throw new Error("Homebrew portable Ruby version is invalid"); + } + return version; +} + +export function homebrewPortableRubyMountPrefix(sourcePrefix: string): string { + return appendGuestPath( + sourcePrefix, + HOMEBREW_PORTABLE_RUBY_RELATIVE_PREFIX, + ); +} + +export function homebrewPortableRubyExecutable( + sourcePrefix: string, + version: string, +): string { + return appendGuestPath( + homebrewPortableRubyMountPrefix(sourcePrefix), + `${version}/bin/ruby`, + ); +} + +function appendGuestPath(prefix: string, relative: string): string { + return prefix === "/" ? `/${relative}` : `${prefix}/${relative}`; +} diff --git a/host/src/homebrew-portable-ruby.ts b/host/src/homebrew-portable-ruby.ts new file mode 100644 index 0000000000..55aca80c69 --- /dev/null +++ b/host/src/homebrew-portable-ruby.ts @@ -0,0 +1,120 @@ +import { createHash } from "node:crypto"; + +import { + derivePackageDeferredZipTree, + type DerivedPackageDeferredZipTree, +} from "./vfs/package-deferred-tree"; +import { + HOMEBREW_PORTABLE_RUBY_OUTPUT, + HOMEBREW_PORTABLE_RUBY_TREE_ID, + homebrewPortableRubyExecutable, + homebrewPortableRubyMountPrefix, + readHomebrewPortableRubyVersion, +} from "./homebrew-portable-ruby-contract"; + +/** + * Derive Homebrew's ordinary vendor/portable-ruby tree from the exact source + * tree that names its required version. + * + * The source descriptor owns the guest prefix. Deriving every portable-Ruby + * path from that descriptor keeps this valid for both compatibility images and + * the canonical product without teaching the runtime either concrete prefix. + */ +export function deriveHomebrewPortableRubyTree( + sourceTree: DerivedPackageDeferredZipTree, + sourceArchiveBytes: Uint8Array, + portableRubyArchiveBytes: Uint8Array, +): DerivedPackageDeferredZipTree { + const source = sourceTree.descriptor; + const atomicGroup = source.activation.atomicGroup; + if ( + source.content_role !== "source-tree" || + source.package.name !== "homebrew-bootstrap" || + source.package.output !== "homebrew-bootstrap.zip" || + source.activation.mode !== "first-use" || + atomicGroup === undefined + ) { + throw new Error( + "Homebrew portable Ruby requires an atomic deferred bootstrap source tree", + ); + } + if ( + sourceArchiveBytes.byteLength !== sourceTree.content.bytes || + !bytesEqualSha256(sourceArchiveBytes, sourceTree.content.sha256) + ) { + throw new Error("Homebrew bootstrap source bytes changed identity"); + } + + const version = readHomebrewPortableRubyVersion(sourceArchiveBytes); + const mountPrefix = homebrewPortableRubyMountPrefix(source.mount_prefix); + const rubyPath = homebrewPortableRubyExecutable( + source.mount_prefix, + version, + ); + const derived = derivePackageDeferredZipTree( + { + schema: 1, + kind: "kandelo-package-deferred-zip-tree", + id: HOMEBREW_PORTABLE_RUBY_TREE_ID, + content_role: "runtime-tree", + package: { + name: source.package.name, + output: HOMEBREW_PORTABLE_RUBY_OUTPUT, + }, + archive: { + url: HOMEBREW_PORTABLE_RUBY_OUTPUT, + mode_policy: "portable-posix-v1", + }, + mount_prefix: mountPrefix, + owner: { ...source.owner }, + activation: { + mode: "first-use", + capabilities: ["homebrew:runtime"], + // The ZIP owns the real versioned executable. `current` is its normal + // upstream symlink and resolves to this same deferred inode on access. + roots: [rubyPath], + atomic_group: atomicGroup.id, + }, + }, + portableRubyArchiveBytes, + ); + assertPortableRubyInventory(derived, version, mountPrefix, rubyPath); + return derived; +} + +function assertPortableRubyInventory( + tree: DerivedPackageDeferredZipTree, + version: string, + mountPrefix: string, + rubyPath: string, +): void { + const currentPath = `${mountPrefix}/current`; + const current = tree.entries.find((entry) => entry.vfsPath === currentPath); + const ruby = tree.entries.find((entry) => entry.vfsPath === rubyPath); + if ( + current?.type !== "symlink" || + current.target !== version || + ruby?.type !== "file" || + (ruby.mode & 0o111) === 0 || + ruby.size <= 0 + ) { + throw new Error( + "Homebrew portable Ruby archive does not expose its source-matched executable", + ); + } + const allowedTopLevel = new Set([version, "current"]); + if ( + tree.entries.some((entry) => { + const relative = entry.vfsPath.slice(mountPrefix.length + 1); + return !allowedTopLevel.has(relative.split("/", 1)[0]!); + }) + ) { + throw new Error("Homebrew portable Ruby archive has an unexpected root"); + } +} + +function bytesEqualSha256(bytes: Uint8Array, expected: string): boolean { + // Avoid a second archive parser/descriptor implementation: the exact bytes + // must bind to the already-derived source tree before cross-output checks. + return createHash("sha256").update(bytes).digest("hex") === expected; +} diff --git a/host/src/homebrew-runtime-support.ts b/host/src/homebrew-runtime-support.ts index 0a60c756c7..e3550fa03f 100644 --- a/host/src/homebrew-runtime-support.ts +++ b/host/src/homebrew-runtime-support.ts @@ -79,6 +79,7 @@ export function parseHomebrewRuntimeSupportContract( const additionalFormulaOrder = formulaArray( root.additional_formula_order, "Homebrew runtime-support additional Formula order", + true, ); if ( JSON.stringify(additionalFormulaOrder) !== @@ -269,9 +270,10 @@ export function assertHomebrewRuntimeSupportPlan( /** * Produce only the support trees absent from the base shell contract. * - * The current reviewed contract adds 18 trees, but deriving the boundary from - * the contract keeps a future catalog update from silently disagreeing with - * the runtime implementation. + * The current reviewed contract obtains Ruby from Homebrew's portable runtime + * output and therefore adds no Formula trees. Deriving the boundary from the + * contract keeps a future catalog update from silently disagreeing with the + * runtime implementation. */ export function projectHomebrewRuntimeSupportDelta( contract: HomebrewRuntimeSupportContract, @@ -298,9 +300,15 @@ function record(value: unknown, label: string): Record { return value as Record; } -function formulaArray(value: unknown, label: string): string[] { - if (!Array.isArray(value) || value.length === 0) { - throw new Error(`${label} must be a nonempty array`); +function formulaArray( + value: unknown, + label: string, + allowEmpty = false, +): string[] { + if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) { + throw new Error( + `${label} must be ${allowEmpty ? "an array" : "a nonempty array"}`, + ); } const result = value.map((entry, index) => formula(entry, `${label} ${index}`), diff --git a/host/src/homebrew-vfs-builder.ts b/host/src/homebrew-vfs-builder.ts index 80a73cb95b..4e88c24171 100644 --- a/host/src/homebrew-vfs-builder.ts +++ b/host/src/homebrew-vfs-builder.ts @@ -812,7 +812,12 @@ function relocateBottlePlaceholders( } let bytes: Uint8Array; try { - bytes = relocateHomebrewBottleFile(readVfsFile(fs, path), relocation, path); + bytes = relocateHomebrewBottleFile( + readVfsFile(fs, path), + relocation, + path, + pkg.prefix, + ); } catch (error) { fail(pkg, error instanceof Error ? error.message : String(error)); } diff --git a/host/src/homebrew-vfs-composer.ts b/host/src/homebrew-vfs-composer.ts index 53f88adf53..33a0052150 100644 --- a/host/src/homebrew-vfs-composer.ts +++ b/host/src/homebrew-vfs-composer.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { applyHomebrewVfsConsumerState, + homebrewManifestSourcePath, writeHomebrewVfsComposition, type HomebrewVfsBuildReport, type HomebrewVfsCatalogCheckout, @@ -110,6 +111,16 @@ interface MaterializedEntryEvidence { target?: string; } +interface EmbeddedDeferredMetadataEntry { + package: string; + treeId: string; + path: string; + sourcePath: string; + mode: number; + size: number; + bytes: Uint8Array; +} + export interface HomebrewVfsMaterializationEvidence { embedded: Array<{ package: string; @@ -200,13 +211,21 @@ export async function buildHomebrewMaterializedVfs( embeddedSet.has(binding.package), provisionalMirrorByPackage.get(binding.package), )); + const deferredMetadata = embedDeferredPackageMetadata( + options.collectionFs, + plan, + deferredBindings, + closedTrees, + ); + const deferredMetadataEntries = [...deferredMetadata.entries]; createHomebrewPrefixAncestors(options.fs, plan); const registered = registerHomebrewDeferredTreeCollection({ fs: options.fs, id: "main-shell", schema: 5, - trees: closedTrees, + trees: deferredMetadata.trees, }); + writeEmbeddedDeferredMetadata(options.fs, deferredMetadata.entries); const registeredByPackage = bindRegisteredTrees(registered, bindings); for (const pkg of selection.embeddedPackages) { @@ -244,25 +263,32 @@ export async function buildHomebrewMaterializedVfs( runtimeSupport.plan, ); runtimeSupportPlan = deltaPlan; - const supportCollection = await buildHomebrewOriginalBottleCollection( - deltaPlan, - { - fs: options.runtimeSupportCollectionFs, - baseFs: options.fs, - loadBottleBytes: options.loadBottleBytes, - compatibilityPolicy: options.compatibilityPolicy, - catalogCheckout: options.catalogCheckout, - createdBy: options.createdBy ?? "host/src/homebrew-vfs-composer.ts", - }, - ); + // Portable Ruby can satisfy the complete runtime delta outside the bottle + // catalog. An empty delta is authoritative and must not be converted into + // a fake bottle collection merely to keep this composition path nonempty. + const supportCollection = deltaPlan.packages.length === 0 + ? undefined + : await buildHomebrewOriginalBottleCollection( + deltaPlan, + { + fs: options.runtimeSupportCollectionFs, + baseFs: options.fs, + loadBottleBytes: options.loadBottleBytes, + compatibilityPolicy: options.compatibilityPolicy, + catalogCheckout: options.catalogCheckout, + createdBy: options.createdBy ?? "host/src/homebrew-vfs-composer.ts", + }, + ); runtimeSupportCollectionConflicts = - supportCollection.report.link_conflicts ?? []; - runtimeSupportPackageReports = supportCollection.report.packages; - const boundSupport = bindCollection( - deltaPlan, - supportCollection.deferredTrees, - supportCollection.payloads, - ); + supportCollection?.report.link_conflicts ?? []; + runtimeSupportPackageReports = supportCollection?.report.packages ?? []; + const boundSupport = supportCollection === undefined + ? [] + : bindCollection( + deltaPlan, + supportCollection.deferredTrees, + supportCollection.payloads, + ); const supportByPackage = new Map( boundSupport.map((binding) => [binding.package, binding]), ); @@ -331,14 +357,24 @@ export async function buildHomebrewMaterializedVfs( mirrorByPackage.get(binding.package), ) ); - registerHomebrewDeferredTreeCollection({ - fs: options.fs, - id: runtimeSupport.contract.id, - schema: 5, - trees: supportTrees, - atomicActivationGroup: - runtimeSupport.contract.activation.atomicGroup, - }); + if (supportTrees.length > 0) { + const supportMetadata = embedDeferredPackageMetadata( + options.runtimeSupportCollectionFs!, + runtimeSupportPlan!, + runtimeSupportBindings, + supportTrees, + ); + registerHomebrewDeferredTreeCollection({ + fs: options.fs, + id: runtimeSupport.contract.id, + schema: 5, + trees: supportMetadata.trees, + atomicActivationGroup: + runtimeSupport.contract.activation.atomicGroup, + }); + writeEmbeddedDeferredMetadata(options.fs, supportMetadata.entries); + deferredMetadataEntries.push(...supportMetadata.entries); + } } writeHomebrewBottleMirrorPlan(options.fs, mirrorPlanAsset); @@ -440,6 +476,7 @@ export async function buildHomebrewMaterializedVfs( mirrorPlan, allDeferredBindings, runtimeSupport?.contract, + deferredMetadataEntries, ); // Runtime-support trees are intentionally incomplete until the image // builder adds the separately packaged Homebrew bootstrap tree and seals @@ -837,6 +874,140 @@ function closeCollectionTree( }; } +function embedDeferredPackageMetadata( + collectionFs: MemoryFileSystem, + plan: HomebrewVfsPlan, + deferredBindings: readonly BoundCollectionTree[], + trees: readonly HomebrewDeferredTreeDescriptor[], +): { + trees: HomebrewDeferredTreeDescriptor[]; + entries: EmbeddedDeferredMetadataEntry[]; +} { + const deferredPackages = new Set( + deferredBindings.map((binding) => binding.package), + ); + const packages = new Map( + plan.packages.map((pkg) => [pkg.fullName, pkg]), + ); + const embedded: EmbeddedDeferredMetadataEntry[] = []; + const projected = trees.map((tree): HomebrewDeferredTreeDescriptor => { + const packageName = tree.package; + if (packageName === undefined || !deferredPackages.has(packageName)) { + return tree; + } + const pkg = packages.get(packageName); + if (pkg === undefined) { + throw new Error(`Homebrew deferred metadata has no package ${packageName}`); + } + const receiptPaths = new Set( + pkg.linkManifest.receipts.map((receipt) => + homebrewManifestSourcePath(pkg, receipt).slice(1) + ), + ); + const metadataEntries = tree.inventory.entries.filter((entry) => + receiptPaths.has(entry.path) + ); + if (metadataEntries.length !== receiptPaths.size) { + throw new Error( + `Homebrew deferred tree ${tree.id} omits declared package metadata`, + ); + } + for (const entry of metadataEntries) { + const aliases = tree.inventory.entries.filter((candidate) => + candidate.inode_group === entry.inode_group + ); + if ( + entry.type !== "file" || + entry.ownership !== "layer" || + entry.inode_group === undefined || + aliases.length !== 1 + ) { + throw new Error( + `Homebrew deferred tree ${tree.id} metadata /${entry.path} ` + + "must be an independent regular file", + ); + } + const bytes = readVfsFile(collectionFs, `/${entry.path}`); + const stat = collectionFs.stat(`/${entry.path}`); + if ( + bytes.byteLength !== entry.size || + (stat.mode & S_IFMT) !== S_IFREG || + (stat.mode & 0o7777) !== entry.mode + ) { + throw new Error( + `Homebrew deferred tree ${tree.id} metadata /${entry.path} ` + + "differs from its verified pour", + ); + } + embedded.push({ + package: packageName, + treeId: tree.id, + path: entry.path, + sourcePath: entry.source_path, + mode: entry.mode, + size: entry.size, + bytes, + }); + } + + const entries = tree.inventory.entries.filter((entry) => + !receiptPaths.has(entry.path) + ); + if ( + entries.some((entry) => + entry.type === "hardlink" && receiptPaths.has(entry.target!) + ) || + !entries.some((entry) => + entry.ownership === "layer" && entry.type !== "directory" + ) + ) { + throw new Error( + `Homebrew deferred tree ${tree.id} cannot separate its package metadata`, + ); + } + const inodeGroups = new Set( + entries.flatMap((entry) => + entry.inode_group === undefined ? [] : [entry.inode_group] + ), + ); + return { + ...tree, + inventory: { + ...tree.inventory, + entry_count: entries.length, + regular_inode_count: inodeGroups.size, + layer_entry_count: entries.filter((entry) => + entry.ownership === "layer" + ).length, + mergeable_directory_count: entries.filter((entry) => + entry.ownership === "mergeable-directory" + ).length, + payload_bytes: entries + .filter((entry) => entry.type === "file") + .reduce((total, entry) => total + entry.size, 0), + entries, + }, + }; + }); + embedded.sort((left, right) => + compareText(left.path, right.path) || compareText(left.package, right.package) + ); + return { trees: projected, entries: embedded }; +} + +function writeEmbeddedDeferredMetadata( + fs: MemoryFileSystem, + entries: readonly EmbeddedDeferredMetadataEntry[], +): void { + for (const entry of entries) { + // WHY: these bytes came from the same digest-verified bottle pour whose + // complete source inventory remains in the deferred descriptor. Keeping + // Homebrew's ordinary receipt files eager lets metadata-only queries read + // authoritative package state without fetching unrelated executables. + writeVfsBinary(fs, `/${entry.path}`, entry.bytes, entry.mode); + } +} + function bindRegisteredTrees( registered: readonly RegisteredHomebrewDeferredTree[], bindings: readonly BoundCollectionTree[], @@ -880,7 +1051,8 @@ function createMaterializationEvidence( mirrorPlanAsset: HomebrewBottleMirrorPlanAsset, mirrorPlan: HomebrewBottleMirrorPlan, deferredBindings: readonly BoundCollectionTree[], - runtimeSupport?: HomebrewRuntimeSupportContract, + runtimeSupport: HomebrewRuntimeSupportContract | undefined, + deferredMetadataEntries: readonly EmbeddedDeferredMetadataEntry[], ): HomebrewVfsMaterializationEvidence { const mirrorByPackage = new Map( mirrorPlan.assets.map((asset) => [asset.package, asset]), @@ -910,7 +1082,15 @@ function createMaterializationEvidence( ...(entry.type === "file" || entry.type === "hardlink" ? { sha256: digest(readVfsFile(collectionFs, `/${entry.path}`)) } : {}), - })); + })) + .concat(deferredMetadataEntries.map((entry) => ({ + path: entry.path, + type: "file" as const, + mode: entry.mode, + size: entry.size, + sha256: digest(entry.bytes), + }))) + .sort((left, right) => compareText(left.path, right.path)); return { embedded: selection.embeddedPackages.map(toIdentity), deferred: deferredBindings.map((binding) => { diff --git a/host/src/kernel-worker.ts b/host/src/kernel-worker.ts index 4f96e2fede..8ea5553d49 100644 --- a/host/src/kernel-worker.ts +++ b/host/src/kernel-worker.ts @@ -880,6 +880,9 @@ export interface ForkLaunchRequest { export interface ResolvedSpawnProgram { programBytes: ArrayBuffer; programModule: WebAssembly.Module; + /** Pre-inspected worker launch metadata; hosts derive it if omitted. */ + programAbiVersion?: number | null; + channelBaseTlsOffset?: number; argv: string[]; } diff --git a/host/src/node-kernel-worker-entry.ts b/host/src/node-kernel-worker-entry.ts index a8275d278e..4d0037f0d7 100644 --- a/host/src/node-kernel-worker-entry.ts +++ b/host/src/node-kernel-worker-entry.ts @@ -48,9 +48,13 @@ import { resolveLazyUrl } from "./vfs/lazy-url"; import { TcpNetworkBackend } from "./networking/tcp-backend"; import { findRepoRoot } from "./binary-resolver"; import { NodeWorkerAdapter } from "./worker-adapter"; +import { prestartedWorkerPoolSize } from "./prestarted-worker-pool"; import { DeferredWorkerHandle } from "./deferred-worker-handle"; import { ThreadPageAllocator } from "./thread-allocator"; -import { patchWasmForThread } from "./worker-main"; +import { + detectChannelBaseTlsOffset, + patchWasmForThread, +} from "./worker-main"; import { ThreadExitCoordinator } from "./thread-exit-coordinator"; import { detectPtrWidth, extractAbiVersion, extractHeapBase, isWasmModuleBytes } from "./constants"; import { CH_TOTAL_SIZE, DEFAULT_MAX_PAGES, PAGES_PER_THREAD, WASM_PAGE_SIZE } from "./constants"; @@ -69,6 +73,7 @@ import { createWorkerQuiescence, type WorkerQuiescence, waitForExecRetirement, + waitForThreadExecRetirement, waitForWorkerQuiescence, } from "./worker-quiescence"; import { RootfsSnapshotGate } from "./rootfs-snapshot-gate"; @@ -90,6 +95,12 @@ import { type ExactProcessGenerationDetachResult, } from "./process-generation-detach"; import { ProcessMemoryCreatorGate } from "./process-memory-creator-gate"; +import { + DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_ALIASES, + DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_ENTRIES, + DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_RETAINED_BYTES, + ExecutableModuleCache, +} from "./executable-module-cache"; import type { PlatformIO } from "./types"; import type { CentralizedWorkerInitMessage, @@ -177,7 +188,9 @@ interface ProcessInfo extends ProcessGenerationOwnership { workerQuiescence: WorkerQuiescence; execRetirement: WorkerQuiescence; programBytes: ArrayBuffer; - programModule?: WebAssembly.Module; + programModule: WebAssembly.Module; + programAbiVersion: number | null; + channelBaseTlsOffset: number; worker: ReturnType; channelOffset: number; ptrWidth: 4 | 8; @@ -290,6 +303,7 @@ interface ThreadWorkerInfo { basePage: number; workerQuiescence: WorkerQuiescence; execRetirement: WorkerQuiescence; + threadExit: WorkerQuiescence; termination?: Promise; } const threadWorkers = new Map(); @@ -312,8 +326,9 @@ async function terminateThreadWorkers( const quiescence = await Promise.all( threads.map((thread) => requireExecRetirement - ? waitForExecRetirement( + ? waitForThreadExecRetirement( thread.execRetirement, + thread.threadExit, thread.workerQuiescence, EXEC_WORKER_RETIREMENT_WAIT_MS, ) @@ -693,6 +708,17 @@ async function resolveExec(path: string): Promise { } const MAX_SHEBANG_DEPTH = 4; +interface ExecutableModuleMetadata { + programAbiVersion: number | null; + channelBaseTlsOffset: number; +} +// Keep Node and browser launch semantics identical: workers and process state +// remain one-shot while immutable compiled code is reused within one machine. +const executableModuleCache = new ExecutableModuleCache({ + maxEntries: DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_ENTRIES, + maxAliases: DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_ALIASES, + maxRetainedBytes: DEFAULT_EXECUTABLE_MODULE_CACHE_MAX_RETAINED_BYTES, +}); function parseShebang(bytes: ArrayBuffer): { interpreter: string; arg?: string } | null { const view = new Uint8Array(bytes); @@ -718,6 +744,16 @@ async function resolveExecutableForLaunch( const shebang = parseShebang(bytes); if (!shebang) { if (!isWasmModuleBytes(bytes)) return { errno: ENOEXEC }; + const cached = executableModuleCache.get(path, bytes); + if (cached) { + return { + programBytes: bytes, + programModule: cached.module, + programAbiVersion: cached.metadata.programAbiVersion, + channelBaseTlsOffset: cached.metadata.channelBaseTlsOffset, + argv, + }; + } let programModule: WebAssembly.Module; try { programModule = await WebAssembly.compile(bytes); @@ -729,7 +765,18 @@ async function resolveExecutableForLaunch( if (declaredAbi !== null && declaredAbi !== kernelWorker.getKernelAbiVersion()) { return { errno: ENOEXEC }; } - return { programBytes: bytes, programModule, argv }; + const channelBaseTlsOffset = detectChannelBaseTlsOffset(bytes); + executableModuleCache.set(path, bytes, programModule, { + programAbiVersion: declaredAbi, + channelBaseTlsOffset, + }); + return { + programBytes: bytes, + programModule, + programAbiVersion: declaredAbi, + channelBaseTlsOffset, + argv, + }; } const scriptArgv = [ @@ -840,6 +887,9 @@ function cleanupSessionDir(): void { async function handleInit(msg: InitMessage) { initReady = false; + // A kernel Worker normally owns one machine, but keep the cache lifetime + // explicit if an embedder initializes the realm again after teardown. + executableModuleCache.clear(); maxPages = msg.config.maxPages ?? DEFAULT_MAX_PAGES; defaultThreadSlots = msg.config.defaultThreadSlots ?? DEFAULT_PROCESS_THREAD_SLOTS; processMemoryAllocator = new ProcessMemoryAllocator({ @@ -855,7 +905,13 @@ async function handleInit(msg: InitMessage) { retirementPressureHook: processMemoryRetirementPressureHook, }); execPrograms = msg.execPrograms ?? {}; - workerAdapter = new NodeWorkerAdapter(); + // Prestart only pristine runners. Once a process receives its Memory, its + // Worker is one-shot and the existing quiescence/termination path reclaims + // the complete realm instead of returning it to this reserve. + workerAdapter = new NodeWorkerAdapter( + undefined, + prestartedWorkerPoolSize(msg.config.maxWorkers), + ); const io: PlatformIO = msg.rootfsImage ? await buildVirtualPlatformIO( @@ -969,7 +1025,6 @@ async function handleSpawn(msg: SpawnMessage) { } const programBytes = msg.programBytes ?? await readExecFromVfs(msg.programPath!); - const programModule = hasProgramBytes ? msg.programModule : undefined; if (programBytes === null) { respondError(msg.requestId, `ENOENT: ${msg.programPath}`); return; @@ -978,6 +1033,34 @@ async function handleSpawn(msg: SpawnMessage) { respondError(msg.requestId, "ENOEXEC: program is not a WebAssembly module"); return; } + const cached = hasProgramPath + ? executableModuleCache.get(msg.programPath!, programBytes) + : undefined; + let programModule = hasProgramBytes ? msg.programModule : cached?.module; + let programAbiVersion = cached?.metadata.programAbiVersion; + let channelBaseTlsOffset = cached?.metadata.channelBaseTlsOffset; + if (!programModule) { + try { + programModule = await WebAssembly.compile(programBytes); + } catch (error) { + if (error instanceof WebAssembly.CompileError) { + respondError(msg.requestId, "ENOEXEC: program is not a valid WebAssembly module"); + return; + } + throw error; + } + programAbiVersion = extractAbiVersion(programBytes); + channelBaseTlsOffset = detectChannelBaseTlsOffset(programBytes); + if (hasProgramPath) { + executableModuleCache.set(msg.programPath!, programBytes, programModule, { + programAbiVersion, + channelBaseTlsOffset, + }); + } + } + // A caller-supplied Module still needs byte-derived launch metadata. + programAbiVersion ??= extractAbiVersion(programBytes); + channelBaseTlsOffset ??= detectChannelBaseTlsOffset(programBytes); const pid = kernelWorker.createProcess( msg.pty ? TERMINAL_STDIO : CAPTURED_STDIO, @@ -1035,8 +1118,9 @@ async function handleSpawn(msg: SpawnMessage) { const initData: CentralizedWorkerInitMessage = { type: "centralized_init", pid, - programBytes, programModule, + programAbiVersion, + channelBaseTlsOffset, memory, channelOffset, env: msg.env, @@ -1057,6 +1141,8 @@ async function handleSpawn(msg: SpawnMessage) { execRetirement: createWorkerQuiescence(), programBytes, programModule, + programAbiVersion, + channelBaseTlsOffset, worker, channelOffset, ptrWidth, @@ -1189,8 +1275,9 @@ async function handleFork( const childInitData: CentralizedWorkerInitMessage = { type: "centralized_init", pid: childPid, - programBytes: parentProgram, programModule: parentInfo.programModule, + programAbiVersion: parentInfo.programAbiVersion, + channelBaseTlsOffset: parentInfo.channelBaseTlsOffset, memory: childMemory, channelOffset: childChannelOffset, isForkChild: true, @@ -1212,6 +1299,8 @@ async function handleFork( execRetirement: createWorkerQuiescence(), programBytes: parentProgram, programModule: parentInfo.programModule, + programAbiVersion: parentInfo.programAbiVersion, + channelBaseTlsOffset: parentInfo.channelBaseTlsOffset, worker, channelOffset: childChannelOffset, ptrWidth, @@ -1294,6 +1383,11 @@ async function handleExec( if (!resolved) return -2; // ENOENT if ("errno" in resolved) return -resolved.errno; const { programBytes, programModule, argv: launchArgv } = resolved; + const programAbiVersion = resolved.programAbiVersion !== undefined + ? resolved.programAbiVersion + : extractAbiVersion(programBytes); + const channelBaseTlsOffset = resolved.channelBaseTlsOffset ?? + detectChannelBaseTlsOffset(programBytes); const newPtrWidth = detectPtrWidth(programBytes); const metadataResult = kernelWorker.validateExecMetadata( launchArgv, @@ -1424,8 +1518,9 @@ async function handleExec( const initData: CentralizedWorkerInitMessage = { type: "centralized_init", pid, - programBytes, programModule, + programAbiVersion, + channelBaseTlsOffset, memory: newMemory, channelOffset: newChannelOffset, argv: launchArgv, @@ -1461,6 +1556,8 @@ async function handleExec( execRetirement: createWorkerQuiescence(), programBytes, programModule, + programAbiVersion, + channelBaseTlsOffset, worker: replacementWorker, channelOffset: newChannelOffset, ptrWidth: newPtrWidth, @@ -1625,6 +1722,11 @@ async function handlePosixSpawn( post({ type: "proc_event", kind: "spawn", pid: childPid, ppid: parentPid }); const { programBytes, programModule, argv } = program; + const programAbiVersion = program.programAbiVersion !== undefined + ? program.programAbiVersion + : extractAbiVersion(programBytes); + const channelBaseTlsOffset = program.channelBaseTlsOffset ?? + detectChannelBaseTlsOffset(programBytes); const ptrWidth = detectPtrWidth(programBytes); let fresh: Awaited>; try { @@ -1666,8 +1768,9 @@ async function handlePosixSpawn( const initData: CentralizedWorkerInitMessage = { type: "centralized_init", pid: childPid, - programBytes, programModule, + programAbiVersion, + channelBaseTlsOffset, memory, channelOffset, argv, @@ -1687,6 +1790,8 @@ async function handlePosixSpawn( execRetirement: createWorkerQuiescence(), programBytes, programModule, + programAbiVersion, + channelBaseTlsOffset, worker, channelOffset, ptrWidth, @@ -1811,8 +1916,9 @@ async function handleClone( type: "centralized_thread_init", pid, tid, - programBytes: processInfo.programBytes, programModule: threadModule, + programAbiVersion: processInfo.programAbiVersion, + channelBaseTlsOffset: processInfo.channelBaseTlsOffset, memory, processChannelOffset: processInfo.channelOffset, channelOffset: alloc.channelOffset, @@ -1838,6 +1944,7 @@ async function handleClone( basePage: alloc.slotStartPage, workerQuiescence: createWorkerQuiescence(), execRetirement: createWorkerQuiescence(), + threadExit: createWorkerQuiescence(), }; threadWorkers.get(pid)!.push(threadEntry); @@ -1904,7 +2011,8 @@ async function handleClone( const m = msg as WorkerToHostMessage; if (m.type === "exec_retired" && m.tid === tid) { threadEntry.execRetirement.settle(); - } else if (m.type === "thread_exit") { + } else if (m.type === "thread_exit" && m.tid === tid) { + threadEntry.threadExit.settle(); if (!isCurrentThreadGeneration()) { void terminateThreadEntry(); return; @@ -2224,6 +2332,10 @@ async function performDestroy() { t.worker.terminate().catch(() => {}); } } + // Unleased reserve workers own no process Memory, but their JS realms remain + // explicit host resources. Destroy the entire adapter reserve before + // acknowledging teardown so sessions cannot accumulate idle Workers. + await workerAdapter.destroy(); // Only exact-generation transactions may remove map entries. The enclosing // creator gate proves no later spawn/exec/fork/clone continuation can install // another Worker alias after this check. @@ -2232,6 +2344,9 @@ async function performDestroy() { vmInterruptTimers.clearAll(); processTeardowns.clear(); reportedExits.clear(); + // Release retained comparison bytes and compiled code before the enclosing + // kernel Worker realm receives its terminal acknowledgement. + executableModuleCache.clear(); threadModuleCache.clear(); threadWorkers.clear(); ptyByPid.clear(); diff --git a/host/src/prestarted-worker-pool.ts b/host/src/prestarted-worker-pool.ts new file mode 100644 index 0000000000..3b1d3ef0c5 --- /dev/null +++ b/host/src/prestarted-worker-pool.ts @@ -0,0 +1,150 @@ +import type { WorkerHandle } from "./worker-adapter"; + +/** + * Keep a small number of pristine process-worker realms ahead of demand. + * + * A physical worker leaves this reserve forever as soon as it receives process + * initialization. The ordinary process lifecycle still terminates that worker + * after its exact memory-quiescence fence, so no realm or guest Memory is ever + * shared with a later process. + */ +export const DEFAULT_PRESTARTED_WORKER_POOL_SIZE = 4; + +/** Bound pristine runners by both the machine admission limit and a small cap. */ +export function prestartedWorkerPoolSize(maxWorkers: number): number { + return Math.min(maxWorkers, DEFAULT_PRESTARTED_WORKER_POOL_SIZE); +} + +type ManagedWorkerState = "idle" | "leased" | "retiring" | "exited"; + +interface ManagedWorker { + readonly handle: WorkerHandle; + state: ManagedWorkerState; + termination: Promise | null; + readonly onError: (...args: unknown[]) => void; + readonly onExit: (...args: unknown[]) => void; +} + +/** + * A bounded reserve of one-shot physical Workers. + * + * Construction starts the reserve before any guest process asks for it. A + * lease is irreversible: used workers remain in `leased` state until the + * kernel-owned process lifecycle physically terminates them, while a new, + * untouched worker is started for the reserve. `destroy()` also terminates + * both pristine and currently leased workers as a final ownership backstop. + */ +export class PrestartedWorkerPool { + private readonly workers = new Set(); + private readonly idleWorkers: ManagedWorker[] = []; + private destroyed = false; + + constructor( + private readonly createPhysicalWorker: () => WorkerHandle, + private readonly targetSize = DEFAULT_PRESTARTED_WORKER_POOL_SIZE, + ) { + if (!Number.isSafeInteger(targetSize) || targetSize < 0) { + throw new Error(`invalid prestarted worker pool size: ${targetSize}`); + } + + try { + this.fillReserve(); + } catch (error) { + // A constructor cannot expose `destroy()`. Terminate any workers that + // were already started before surfacing the resource-creation failure. + for (const worker of this.workers) void this.retire(worker); + throw error; + } + } + + createWorker(initData: unknown): WorkerHandle { + if (this.destroyed) { + throw new Error("prestarted worker pool is destroyed"); + } + + const worker = this.idleWorkers.shift() ?? this.createManagedWorker("leased"); + worker.state = "leased"; + try { + worker.handle.postMessage(initData); + } catch (error) { + // Structured-clone rejection must not leave a worker waiting forever for + // an initialization that it can never receive. + void this.retire(worker); + throw error; + } + + // WHY: createWorker() callers attach failure listeners immediately after + // it returns. Replenish in the next microtask so starting the replacement + // cannot delay that attachment or the leased process's first instruction. + queueMicrotask(() => this.fillReserveBestEffort()); + return worker.handle; + } + + async destroy(): Promise { + if (this.destroyed) return; + this.destroyed = true; + this.idleWorkers.splice(0); + await Promise.allSettled([...this.workers].map((worker) => this.retire(worker))); + } + + private fillReserve(): void { + while (!this.destroyed && this.idleWorkers.length < this.targetSize) { + this.idleWorkers.push(this.createManagedWorker("idle")); + } + } + + private fillReserveBestEffort(): void { + try { + this.fillReserve(); + } catch { + // A reserve is a latency optimization, not authoritative capacity. If + // speculative creation fails, the next real lease retries synchronously + // and exposes a persistent Worker-creation failure through the normal + // process path. + } + } + + private createManagedWorker(state: ManagedWorkerState): ManagedWorker { + const handle = this.createPhysicalWorker(); + let worker!: ManagedWorker; + worker = { + handle, + state, + termination: null, + onError: () => { + // No process owns an idle worker, so there is nobody else to retire a + // realm whose module failed to initialize. Leased failures remain on + // the normal process error/teardown path. + if (worker.state === "idle") void this.retire(worker); + }, + onExit: () => this.remove(worker), + }; + handle.on("error", worker.onError); + handle.on("exit", worker.onExit); + this.workers.add(worker); + return worker; + } + + private remove(worker: ManagedWorker): void { + if (worker.state === "exited") return; + worker.state = "exited"; + this.workers.delete(worker); + const idleIndex = this.idleWorkers.indexOf(worker); + if (idleIndex >= 0) this.idleWorkers.splice(idleIndex, 1); + worker.handle.off("error", worker.onError); + worker.handle.off("exit", worker.onExit); + } + + private retire(worker: ManagedWorker): Promise { + if (worker.termination) return worker.termination; + if (worker.state === "exited") return Promise.resolve(0); + worker.state = "retiring"; + const idleIndex = this.idleWorkers.indexOf(worker); + if (idleIndex >= 0) this.idleWorkers.splice(idleIndex, 1); + worker.termination = Promise.resolve() + .then(() => worker.handle.terminate()) + .catch(() => 1) + .finally(() => this.remove(worker)); + return worker.termination; + } +} diff --git a/host/src/vfs/host-fs.ts b/host/src/vfs/host-fs.ts index 450a4d14d3..280610a8f6 100644 --- a/host/src/vfs/host-fs.ts +++ b/host/src/vfs/host-fs.ts @@ -152,6 +152,11 @@ export class HostFileSystem implements FileSystemBackend { */ private safePath(relative: string, followFinal = true): string { const hadTrailingSlash = relative.length > 1 && /\/+$/.test(relative); + const fastPath = this.tryUnchangedNativePath(relative, followFinal); + if (fastPath !== null) { + return this.withTrailingSlash(fastPath, hadTrailingSlash); + } + let current = this.rootPath; let pending = this.pathParts(relative); let symlinkDepth = 0; @@ -211,19 +216,78 @@ export class HostFileSystem implements FileSystemBackend { } } + current = this.withTrailingSlash(current, hadTrailingSlash); + this.assertWithinRoot(current); + return current; + } + + /** + * Resolve the common symlink-free case with one native canonicalization. + * + * Accepting any path merely because realpath leaves it inside rootPath is + * not sufficient: an absolute symlink contains a guest path, while the host + * realpath implementation interprets it in the host namespace. Requiring + * the canonical path to equal the lexical path proves that no symlink + * changed the lookup. Ambiguous paths fall back to the component resolver, + * which remains the source of truth for guest symlinks and POSIX `..` + * ordering. Nothing is cached, so externally replaced directories are + * revalidated on every operation just as they are on the slow path. + */ + private tryUnchangedNativePath( + relative: string, + followFinal: boolean, + ): string | null { + const parts = this.pathParts(relative); + if (parts.includes("..")) return null; + + const candidate = nodePath.join(this.rootPath, ...parts); + this.assertWithinRoot(candidate); + if (followFinal) { + try { + return fs.realpathSync(candidate) === candidate ? candidate : null; + } catch (error: any) { + if (error?.code !== "ENOENT" || parts.length === 0) return null; + + // realpath cannot distinguish a missing final name from a dangling + // final symlink. Validate the parent first, then require lstat to prove + // that the final directory entry itself is absent. + const parent = nodePath.dirname(candidate); + try { + if (fs.realpathSync(parent) !== parent) return null; + } catch { + return null; + } + try { + fs.lstatSync(candidate); + return null; + } catch (missingError: any) { + return missingError?.code === "ENOENT" ? candidate : null; + } + } + } + + if (parts.length === 0) return this.rootPath; + const parent = nodePath.dirname(candidate); + try { + return fs.realpathSync(parent) === parent ? candidate : null; + } catch { + return null; + } + } + + private withTrailingSlash(path: string, hadTrailingSlash: boolean): string { if ( hadTrailingSlash && - current !== this.rootPath && - !current.endsWith(nodePath.sep) + path !== this.rootPath && + !path.endsWith(nodePath.sep) ) { // Keep a final separator for native fs calls. POSIX requires a // trailing slash to resolve the preceding component as a directory; the // native call then returns ENOTDIR for regular files while still // permitting operations such as mkdir("new-dir/"). - current += nodePath.sep; + return `${path}${nodePath.sep}`; } - this.assertWithinRoot(current); - return current; + return path; } private normalizeGuestMountPoint(mountPoint: string): string { diff --git a/host/src/vfs/memory-fs.ts b/host/src/vfs/memory-fs.ts index 3fb01f468d..baaa0ddeb9 100644 --- a/host/src/vfs/memory-fs.ts +++ b/host/src/vfs/memory-fs.ts @@ -1319,6 +1319,39 @@ function resolveLazyTreeSourceHardlinks( return canonicalByPath; } +function homebrewPrefixFromLazyTree( + inventory: readonly LazyTreeRegistrationEntry[], + activation: LazyTreeActivation | undefined, + receiptSourcePath: string, + receiptSourceRoot: string, +): string { + const candidates = new Set(); + const receiptSuffix = `/Cellar/${receiptSourcePath}`; + for (const entry of inventory) { + if ( + entry.sourcePath === receiptSourcePath && + entry.vfsPath.endsWith(receiptSuffix) + ) { + candidates.add(entry.vfsPath.slice(0, -receiptSuffix.length)); + } + } + if (receiptSourceRoot !== "") { + const kegSuffix = `/Cellar/${receiptSourceRoot}`; + for (const root of activation?.roots ?? []) { + if (root.endsWith(kegSuffix)) { + candidates.add(root.slice(0, -kegSuffix.length)); + } + } + } + if (candidates.size !== 1) { + throw new Error( + "Lazy Homebrew bottle cannot derive one relocation prefix from its " + + "authenticated guest projection", + ); + } + return candidates.values().next().value!; +} + function requireCanonicalTreePath( path: unknown, absolute: boolean, @@ -4175,10 +4208,15 @@ export class MemoryFileSystem implements FileSystemBackend { } if (owner !== undefined) { - // WHY: ownership is part of the package namespace contract. Apply it - // before publishing any lazy-inode metadata or returning a direct - // materialization handle, so callers cannot observe a registered tree - // whose stubs still carry SharedFS's default owner. + // WHY: a ZIP inventory starts below its mount and therefore does not + // contain an entry for the mount directory itself. That directory is + // still part of the package namespace: leaving a newly created mount as + // root while all descendants use the declared owner produces a prefix + // the package cannot mutate. Apply the owner before publishing any lazy + // inode metadata or returning a direct-materialization handle. + if (validatedMountPrefix !== "/") { + this.lchown(validatedMountPrefix, owner.uid, owner.gid); + } for (const entry of entries) { this.lchown(entry.vfsPath, owner.uid, owner.gid); } @@ -5341,12 +5379,30 @@ export class MemoryFileSystem implements FileSystemBackend { const sourceRoot = separator < 0 ? "" : receiptSource.sourcePath.slice(0, separator); + const prefix = homebrewPrefixFromLazyTree( + inventory, + atomicSnapshot?.activation ?? group.activation, + receiptSource.sourcePath, + sourceRoot, + ); const receiptChangedSources = new Set(receipt.changedFiles.map((path) => sourceRoot.length === 0 ? path : `${sourceRoot}/${path}` )); + // The authenticated source inventory may intentionally be broader + // than this tree's guest projection. Only projected changed members + // need lazy relocation markers; an omitted source member produces no + // lazy write (for example, an exact receipt embedded in the image). + const projectedChangedSources = new Set( + inventory.flatMap((entry) => + entry.materialization !== "descriptor" && + receiptChangedSources.has(entry.sourcePath) + ? [entry.sourcePath] + : [] + ), + ); if ( - relocationSources.size !== receiptChangedSources.size || - [...relocationSources].some((path) => !receiptChangedSources.has(path)) + relocationSources.size !== projectedChangedSources.size || + [...relocationSources].some((path) => !projectedChangedSources.has(path)) ) { throw new Error( "Lazy Homebrew bottle relocation markers differ from INSTALL_RECEIPT.json", @@ -5370,7 +5426,12 @@ export class MemoryFileSystem implements FileSystemBackend { ); } if (relocatedCanonicalSources.has(canonical.sourcePath)) continue; - actual.data = relocateHomebrewBottleFile(actual.data, receipt, sourcePath); + actual.data = relocateHomebrewBottleFile( + actual.data, + receipt, + sourcePath, + prefix, + ); relocatedCanonicalSources.add(canonical.sourcePath); } } diff --git a/host/src/vfs/package-deferred-tree.ts b/host/src/vfs/package-deferred-tree.ts index 04081be4e0..3fc0623228 100644 --- a/host/src/vfs/package-deferred-tree.ts +++ b/host/src/vfs/package-deferred-tree.ts @@ -435,7 +435,16 @@ function preflightNamespace( } const entry = entryByPath.get(path); if (entry === undefined) { - if ((existing.mode & S_IFMT) !== S_IFDIR) { + if ( + (existing.mode & S_IFMT) !== S_IFDIR || + ( + path === descriptor.mount_prefix && + ( + existing.uid !== descriptor.owner.uid || + existing.gid !== descriptor.owner.gid + ) + ) + ) { throw new Error( `package deferred ZIP tree ancestor collides at ${path}`, ); diff --git a/host/src/worker-adapter-browser.ts b/host/src/worker-adapter-browser.ts index c4c2917b9e..e5326dd0ea 100644 --- a/host/src/worker-adapter-browser.ts +++ b/host/src/worker-adapter-browser.ts @@ -1,19 +1,40 @@ import type { WorkerAdapter, WorkerHandle } from "./worker-adapter"; +import { PrestartedWorkerPool } from "./prestarted-worker-pool"; export class BrowserWorkerAdapter implements WorkerAdapter { private entryUrl: string | URL; + private readonly workerPool: PrestartedWorkerPool | null; - constructor(entryUrl: string | URL) { + constructor(entryUrl: string | URL, prestartedWorkers?: number) { this.entryUrl = entryUrl; + // Only the machine-owned adapter opts into retention and has a matching + // destroy boundary. Other public callers keep one Worker per handle. + this.workerPool = prestartedWorkers === undefined + ? null + : new PrestartedWorkerPool( + () => this.createPhysicalWorker(), + prestartedWorkers, + ); } createWorker(workerData: unknown): WorkerHandle { - const worker = new Worker(this.entryUrl, { type: "module" }); - // Web Workers don't have workerData — send init data via postMessage - const handle = new BrowserWorkerHandle(worker); - worker.postMessage(workerData); + // Web Workers don't have workerData. A reserved realm receives exactly one + // initialization message and is never returned to the reserve afterward. + if (this.workerPool) return this.workerPool.createWorker(workerData); + const handle = this.createPhysicalWorker(); + handle.postMessage(workerData); return handle; } + + async destroy(): Promise { + await this.workerPool?.destroy(); + } + + private createPhysicalWorker(): WorkerHandle { + return new BrowserWorkerHandle( + new Worker(this.entryUrl, { type: "module" }), + ); + } } class BrowserWorkerHandle implements WorkerHandle { @@ -35,6 +56,7 @@ class BrowserWorkerHandle implements WorkerHandle { this.terminated = true; for (const h of this.handlers.get("exit") ?? []) h(1); } + this.releaseCallbacks(); }; } @@ -83,6 +105,17 @@ class BrowserWorkerHandle implements WorkerHandle { this.terminated = true; for (const h of this.handlers.get("exit") ?? []) h(0); } + this.releaseCallbacks(); return 0; } + + private releaseCallbacks(): void { + // WHY: the native Worker owns these closures, and each closure owns this + // handle's process listeners. Leaving them attached after termination can + // retain the just-finished process's Module and SharedArrayBuffer graph + // until cycle collection, even though that worker realm is never reused. + this.worker.onmessage = null; + this.worker.onerror = null; + this.handlers.clear(); + } } diff --git a/host/src/worker-adapter.ts b/host/src/worker-adapter.ts index 4964e8d6a6..69b4f8098a 100644 --- a/host/src/worker-adapter.ts +++ b/host/src/worker-adapter.ts @@ -9,6 +9,7 @@ export interface WorkerHandle { export interface WorkerAdapter { createWorker(workerData: unknown): WorkerHandle; + destroy?(): Promise; } // --- Mock implementation for testing --- @@ -93,6 +94,8 @@ export class MockWorkerAdapter implements WorkerAdapter { this.allWorkers.push(handle); return handle; } + + async destroy(): Promise {} } // --- Node.js implementation --- @@ -102,6 +105,7 @@ import { pathToFileURL } from "node:url"; import { createRequire } from "node:module"; import { existsSync } from "node:fs"; import { NODE_WORKER_INIT_BY_MESSAGE } from "./node-worker-initialization"; +import { PrestartedWorkerPool } from "./prestarted-worker-pool"; // Wasm guest stacks consume the embedding worker's native stack when engines // recurse through Wasm frames. Keep the default high enough for stack-heavy @@ -164,8 +168,8 @@ export function nodeWorkerInitialization( // WHY: built-in process init carries Shared WebAssembly.Memory. Node exposes // the structured clone as the worker module's workerData value. The matched // lifecycle reached lower peak RSS when Kandelo avoided that startup route. - // A one-shot message keeps the same structured-clone semantics while letting - // its listener and message be released after delivery. + // A message keeps the same structured-clone semantics while letting its + // listener and activation-owned value be released after delivery. return { transport: "message", workerDataValue: NODE_WORKER_INIT_BY_MESSAGE, @@ -177,8 +181,12 @@ export class NodeWorkerAdapter implements WorkerAdapter { private entryUrl: URL; private _compiledEntry: URL | false | undefined; private readonly initializeByMessage: boolean; + private readonly workerPool: PrestartedWorkerPool | null; - constructor(entryUrl?: URL) { + constructor( + entryUrl?: URL, + prestartedWorkers?: number, + ) { // WHY: arbitrary custom entries may read workerData directly and do not // understand Kandelo's marker/message protocol. Only the built-in entry can // safely select the new transport without changing the public custom-entry @@ -186,6 +194,15 @@ export class NodeWorkerAdapter implements WorkerAdapter { this.initializeByMessage = entryUrl === undefined; this.entryUrl = entryUrl ?? new URL("./worker-entry.ts", currentModuleUrl()); + // The machine-owned adapter starts a bounded reserve of pristine runners. + // Direct/public adapter callers retain their historical one-shot Worker + // lifetime unless they opt in explicitly. + this.workerPool = this.initializeByMessage && prestartedWorkers !== undefined + ? new PrestartedWorkerPool(() => + new NodeWorkerHandle( + this.createWorkerThread(NODE_WORKER_INIT_BY_MESSAGE), + ), prestartedWorkers) + : null; } /** @@ -237,20 +254,12 @@ export class NodeWorkerAdapter implements WorkerAdapter { return new NodeWorkerHandle(worker); } - createWorker(workerData: unknown): WorkerHandle { - const initialization = nodeWorkerInitialization( - workerData, - this.initializeByMessage, - ); + private createWorkerThread(workerData: unknown): Worker { // Try the compiled JS entry first (much faster startup — avoids tsx // bootstrap which takes >500ms with 10+ concurrent workers). const compiledEntry = this.resolveCompiledEntry(); if (compiledEntry) { - const worker = new Worker( - compiledEntry, - nodeWorkerOptions(initialization.workerDataValue), - ); - return this.initializeWorker(worker, initialization); + return new Worker(compiledEntry, nodeWorkerOptions(workerData)); } // Fallback: tsx eval bootstrap for running from TypeScript source. @@ -265,15 +274,33 @@ export class NodeWorkerAdapter implements WorkerAdapter { `await import('${entryUrl}');`, ].join("\n"); - const worker = new Worker( + return new Worker( bootstrap, - nodeWorkerOptions(initialization.workerDataValue, { eval: true }), + nodeWorkerOptions(workerData, { eval: true }), ); - return this.initializeWorker(worker, initialization); + } + + createWorker(workerData: unknown): WorkerHandle { + if (this.workerPool) return this.workerPool.createWorker(workerData); + + const initialization = nodeWorkerInitialization( + workerData, + this.initializeByMessage, + ); + return this.initializeWorker( + this.createWorkerThread(initialization.workerDataValue), + initialization, + ); + } + + async destroy(): Promise { + await this.workerPool?.destroy(); } } class NodeWorkerHandle implements WorkerHandle { + private terminationPromise: Promise | null = null; + constructor(private worker: Worker) {} postMessage(message: unknown, transfer?: Transferable[]): void { @@ -301,6 +328,9 @@ class NodeWorkerHandle implements WorkerHandle { } async terminate(): Promise { - return this.worker.terminate(); + if (this.terminationPromise) return this.terminationPromise; + const termination = this.worker.terminate(); + this.terminationPromise = termination; + return termination; } } diff --git a/host/src/worker-main.ts b/host/src/worker-main.ts index 8320770511..68ea1d1340 100644 --- a/host/src/worker-main.ts +++ b/host/src/worker-main.ts @@ -1528,7 +1528,7 @@ function hasCompleteForkInstrumentation( * before `_start`, which breaks runtimes such as SpiderMonkey. */ function verifyProgramAbi( - programBytes: ArrayBuffer, + actual: number | null, expected: number | undefined, pid: number, ): void { @@ -1537,7 +1537,6 @@ function verifyProgramAbi( // Will be removed once all callers are updated. return; } - const actual = extractAbiVersion(programBytes); if (actual === null) { if (!abiMissingWarned) { abiMissingWarned = true; @@ -1573,10 +1572,28 @@ export async function centralizedWorkerMain( try { const { memory, programBytes, channelOffset, pid } = initData; const ptrWidth = initData.ptrWidth ?? 4; + if (!initData.programModule && !programBytes) { + throw new Error(`pid=${pid}: process worker has neither program bytes nor a module`); + } + if ( + !programBytes && + (initData.programAbiVersion === undefined || + initData.channelBaseTlsOffset === undefined) + ) { + throw new Error( + `pid=${pid}: byte-free process worker is missing inspected launch metadata`, + ); + } // Use pre-compiled module if provided (avoids recompilation in workers) const module = initData.programModule ? initData.programModule - : await WebAssembly.compile(programBytes); + : await WebAssembly.compile(programBytes!); + const programAbiVersion = initData.programAbiVersion !== undefined + ? initData.programAbiVersion + : extractAbiVersion(programBytes!); + const channelBaseTlsOffset = initData.channelBaseTlsOffset !== undefined + ? initData.channelBaseTlsOffset + : detectChannelBaseTlsOffset(programBytes!); // --- WASI module detection and handling --- if (isWasiModule(module)) { if (wasiModuleDefinesMemory(module)) { @@ -1778,7 +1795,7 @@ export async function centralizedWorkerMain( if (initData.isForkChild) { dlopenSupport.resetForkChildLock(); } - verifyProgramAbi(programBytes, initData.kernelAbiVersion, pid); + verifyProgramAbi(programAbiVersion, initData.kernelAbiVersion, pid); // For the fork-parent case (initial launch, not a fork child), install // __channel_base now — the parent's __tls_base is already correctly @@ -1789,7 +1806,14 @@ export async function centralizedWorkerMain( // the child's __tls_base from the fork save buffer; setupChannelBase // would otherwise see a zeroed __tls_base. if (!initData.isForkChild) { - setupChannelBase(instance, module, memory, channelOffset, programBytes as ArrayBuffer, ptrWidth); + setupChannelBase( + instance, + module, + memory, + channelOffset, + channelBaseTlsOffset, + ptrWidth, + ); } // Signal ready @@ -1866,7 +1890,14 @@ export async function centralizedWorkerMain( ); // Now that rewind_begin has restored __tls_base, install // __channel_base for this (child) instance. - setupChannelBase(instance, module, memory, channelOffset, programBytes as ArrayBuffer, ptrWidth); + setupChannelBase( + instance, + module, + memory, + channelOffset, + channelBaseTlsOffset, + ptrWidth, + ); if (initData.isForkChild && !replayedForkChildDlopens) { try { dlopenSupport.replayDlopens(); @@ -1966,9 +1997,16 @@ export async function centralizedWorkerMain( }); const instance = await WebAssembly.instantiate(module, importObject); processInstance = instance; - verifyProgramAbi(programBytes, initData.kernelAbiVersion, pid); + verifyProgramAbi(programAbiVersion, initData.kernelAbiVersion, pid); - setupChannelBase(instance, module, memory, channelOffset, programBytes as ArrayBuffer, ptrWidth); + setupChannelBase( + instance, + module, + memory, + channelOffset, + channelBaseTlsOffset, + ptrWidth, + ); port.postMessage({ type: "ready", pid } satisfies WorkerToHostMessage); @@ -2040,7 +2078,7 @@ export async function centralizedWorkerMain( * We find this function by looking at the export wrapper's call target. * Returns the i32.const value, or -1 if detection fails. */ -function detectChannelBaseTlsOffset(programBytes: ArrayBuffer): number { +export function detectChannelBaseTlsOffset(programBytes: ArrayBuffer): number { const src = new Uint8Array(programBytes); if (src.length < 8) return -1; @@ -2187,7 +2225,7 @@ function setupChannelBase( module: WebAssembly.Module, memory: WebAssembly.Memory, channelOffset: number, - programBytes?: ArrayBuffer, + channelBaseTlsOffset = -1, ptrWidth: 4 | 8 = 4, ): void { // If the module imports env.__channel_base as a global, the channel offset was @@ -2203,11 +2241,9 @@ function setupChannelBase( const tlsAddr = tlsBase ? Number(tlsBase.value) : 0; if (tlsAddr > 0) { - let detectedOffset = -1; - if (programBytes) { - detectedOffset = detectChannelBaseTlsOffset(programBytes); - } - const addr = tlsAddr + (detectedOffset >= 0 ? detectedOffset : 0); + const addr = tlsAddr + ( + channelBaseTlsOffset >= 0 ? channelBaseTlsOffset : 0 + ); if (ptrWidth === 8) { view.setBigUint64(addr, BigInt(channelOffset), true); } else { @@ -2370,6 +2406,13 @@ export function patchWasmForThread(bytes: ArrayBuffer): ArrayBuffer { } } + // WHY: --export-all executables expose the authoritative constructor + // function directly. Prefer that identity over call-frequency inference: + // large programs can have another helper (notably __errno_location) called + // by several exported wrappers, and replacing a result-returning helper with + // a void no-op makes the thread module invalid. + ctorFuncIndex = exportFuncIndicesByName.get("__wasm_call_ctors") ?? -1; + function skipLEB(pos: number): number { const [, n] = readLEB128(src, pos); return pos + n; @@ -2469,8 +2512,9 @@ export function patchWasmForThread(bytes: ArrayBuffer): ArrayBuffer { return calls; } - // Find the Code section and identify a call target shared by LLVM helper exports. - for (const sec of sections) { + // Older executables do not export __wasm_call_ctors. For those, find the + // Code section and identify the call target shared by LLVM helper exports. + for (const sec of ctorFuncIndex < 0 ? sections : []) { if (sec.id === 10 && exportedFuncIndices.length > 0) { const helperNames = [ "__wasm_init_tls", @@ -2678,11 +2722,26 @@ export async function centralizedThreadWorkerMain( // thread; re-running constructors would clobber global state. let programBytes: ArrayBuffer | null = null; if (!initData.programModule) { + if (!initData.programBytes) { + throw new Error( + `pid=${pid} tid=${tid}: thread worker has neither program bytes nor a module`, + ); + } programBytes = patchWasmForThread(initData.programBytes); } const module = initData.programModule ? initData.programModule : new WebAssembly.Module(programBytes!); + if ( + !initData.programBytes && initData.channelBaseTlsOffset === undefined + ) { + throw new Error( + `pid=${pid} tid=${tid}: byte-free thread worker is missing channel metadata`, + ); + } + const channelBaseTlsOffset = initData.channelBaseTlsOffset !== undefined + ? initData.channelBaseTlsOffset + : detectChannelBaseTlsOffset(initData.programBytes!); const moduleExports = WebAssembly.Module.exports(module); const hasForkInstrumentation = hasCompleteForkInstrumentation(moduleExports, pid); @@ -2872,7 +2931,14 @@ export async function centralizedThreadWorkerMain( // Set __channel_base without calling the exported helper. lld can prefix // exported functions with __wasm_call_ctors, and thread workers must not // re-run constructors in shared process memory. - setupChannelBase(instance, module, memory, channelOffset, initData.programBytes, ptrWidth); + setupChannelBase( + instance, + module, + memory, + channelOffset, + channelBaseTlsOffset, + ptrWidth, + ); // Call the thread function via indirect function table const table = instance.exports.__indirect_function_table as WebAssembly.Table | undefined; diff --git a/host/src/worker-protocol.ts b/host/src/worker-protocol.ts index d9c331dfd7..a5d4714dec 100644 --- a/host/src/worker-protocol.ts +++ b/host/src/worker-protocol.ts @@ -24,10 +24,22 @@ export type HostToWorkerMessage = export interface CentralizedWorkerInitMessage { type: "centralized_init"; pid: number; - /** User program bytes (compiled with channel_syscall.c — no kernel imports) */ - programBytes: ArrayBuffer; + /** + * User program bytes (compiled with channel_syscall.c — no kernel imports). + * + * Direct worker callers may provide bytes and let the worker compile and + * inspect them. Kernel-owned launches instead provide `programModule` plus + * the two inspected fields below, keeping the authoritative bytes in the + * kernel realm instead of structured-cloning a large executable into every + * one-shot process realm. + */ + programBytes?: ArrayBuffer; /** Pre-compiled WebAssembly module (avoids recompilation in web workers) */ programModule?: WebAssembly.Module; + /** ABI marker inspected from the authoritative program bytes; null if absent. */ + programAbiVersion?: number | null; + /** TLS offset used by legacy channel-base binaries; -1 when unavailable. */ + channelBaseTlsOffset?: number; /** Shared Memory for this process (also shared with CentralizedKernelWorker) */ memory: WebAssembly.Memory; /** Channel offset within the shared Memory for this thread's syscall channel */ @@ -70,8 +82,10 @@ export interface CentralizedThreadInitMessage { type: "centralized_thread_init"; pid: number; tid: number; - programBytes: ArrayBuffer; + programBytes?: ArrayBuffer; programModule?: WebAssembly.Module; + programAbiVersion?: number | null; + channelBaseTlsOffset?: number; memory: WebAssembly.Memory; /** Main process channel offset. The thread reads the process-wide dlopen * archive head relative to this live shared-memory anchor before fork. */ diff --git a/host/src/worker-quiescence.ts b/host/src/worker-quiescence.ts index c35aca9d76..90243abef5 100644 --- a/host/src/worker-quiescence.ts +++ b/host/src/worker-quiescence.ts @@ -107,3 +107,34 @@ export async function waitForExecRetirement( // memory_quiescent proves worker-main returned and no longer owns Memory. return retirement.settled && quiescence.settled; } + +/** + * Wait for a sibling thread to leave an address space discarded by exec. + * + * A thread can race exec by completing its ordinary thread-exit path before + * the host queues the private exec-retirement marker. Those dispositions are + * mutually exclusive, but both are authoritative only after the wrapper's + * exact `memory_quiescent` fence proves that the Worker realm has returned. + */ +export async function waitForThreadExecRetirement( + retirement: WorkerQuiescence, + threadExit: WorkerQuiescence, + quiescence: WorkerQuiescence, + timeoutMs: number, +): Promise { + if ((retirement.settled || threadExit.settled) && quiescence.settled) { + return true; + } + let timeout: ReturnType | undefined; + await Promise.race([ + Promise.all([ + Promise.race([retirement.promise, threadExit.promise]), + quiescence.promise, + ]), + new Promise((resolve) => { + timeout = setTimeout(resolve, timeoutMs); + }), + ]); + if (timeout !== undefined) clearTimeout(timeout); + return (retirement.settled || threadExit.settled) && quiescence.settled; +} diff --git a/host/test/browser-worker-adapter.test.ts b/host/test/browser-worker-adapter.test.ts index 82274c1575..f053acfa5e 100644 --- a/host/test/browser-worker-adapter.test.ts +++ b/host/test/browser-worker-adapter.test.ts @@ -40,6 +40,7 @@ class MockBrowserWorker { // Keep a reference to the last constructed MockBrowserWorker so tests // can interact with it after BrowserWorkerAdapter creates one internally. let lastMockWorker: MockBrowserWorker | null = null; +let mockWorkers: MockBrowserWorker[] = []; const OriginalMockBrowserWorker = MockBrowserWorker; function TrackingMockBrowserWorker( @@ -49,6 +50,7 @@ function TrackingMockBrowserWorker( ) { const instance = new OriginalMockBrowserWorker(url, options); lastMockWorker = instance; + mockWorkers.push(instance); return instance; } @@ -59,6 +61,7 @@ function TrackingMockBrowserWorker( describe("BrowserWorkerAdapter", () => { beforeEach(() => { lastMockWorker = null; + mockWorkers = []; vi.stubGlobal("Worker", TrackingMockBrowserWorker); }); @@ -105,6 +108,33 @@ describe("BrowserWorkerAdapter", () => { expect(typeof handle.off).toBe("function"); expect(typeof handle.terminate).toBe("function"); }); + + it("leases each prestarted Worker once and replaces it", async () => { + const adapter = new BrowserWorkerAdapter("worker.js", 1); + const firstPhysical = lastMockWorker!; + const first = adapter.createWorker({ type: "centralized_init", pid: 71 }); + await Promise.resolve(); + const secondPhysical = lastMockWorker!; + + firstPhysical.simulateMessage({ type: "memory_quiescent", pid: 71 }); + await first.terminate(); + const second = adapter.createWorker({ + type: "centralized_init", + pid: 72, + }); + + expect(secondPhysical).not.toBe(firstPhysical); + expect(firstPhysical.terminated).toBe(true); + expect(firstPhysical.sentMessages).toEqual([ + { type: "centralized_init", pid: 71 }, + ]); + expect(secondPhysical.sentMessages).toEqual([ + { type: "centralized_init", pid: 72 }, + ]); + + await second.terminate(); + await adapter.destroy(); + }); }); // ---- BrowserWorkerHandle message routing -------------------------------- @@ -235,6 +265,8 @@ describe("BrowserWorkerAdapter", () => { await handle.terminate(); expect(lastMockWorker!.terminated).toBe(true); + expect(lastMockWorker!.onmessage).toBeNull(); + expect(lastMockWorker!.onerror).toBeNull(); }); it("should fire exit handlers with code 0", async () => { @@ -246,6 +278,10 @@ describe("BrowserWorkerAdapter", () => { await handle.terminate(); expect(exitCodes).toEqual([0]); + + lastMockWorker!.simulateMessage("after termination"); + lastMockWorker!.simulateError("after termination"); + expect(exitCodes).toEqual([0]); }); it("should return 0", async () => { diff --git a/host/test/dlopen-e2e.test.ts b/host/test/dlopen-e2e.test.ts index 2438d8b843..1ebf52fd0f 100644 --- a/host/test/dlopen-e2e.test.ts +++ b/host/test/dlopen-e2e.test.ts @@ -157,6 +157,92 @@ describe.skipIf(!hasSysroot || !hasKernel || !hasCompiler())("dlopen end-to-end" expect(result.stdout).toContain("done"); }); + it("exports libc functions referenced only by a side module", { timeout: 30_000 }, async () => { + const soPath = buildSharedLib( + ` + #include + unsigned short network_value(void) { return ntohs(0x3412); } + `, + "lib-process-libc", + ); + const wasmPath = buildMainProgram( + ` + #include + #include + + int main(int argc, char *argv[]) { + void *lib = dlopen(argv[1], RTLD_NOW); + if (!lib) { + printf("dlopen failed: %s\\n", dlerror()); + return 1; + } + unsigned short (*network_value)(void) = + (unsigned short (*)(void))dlsym(lib, "network_value"); + if (!network_value) { + printf("dlsym failed: %s\\n", dlerror()); + return 2; + } + printf("network-value=%u\\n", network_value()); + return dlclose(lib); + } + `, + "test-dlopen-process-libc", + ); + + const result = await runCentralizedProgram({ + programPath: wasmPath, + argv: ["test-dlopen-process-libc", soPath], + timeout: 10_000, + io: io(), + }); + + expect(result.exitCode, `stdout=${result.stdout}\nstderr=${result.stderr}`).toBe(0); + expect(result.stdout).toBe("network-value=4660\n"); + expect(result.stderr).toBe(""); + }); + + it("prefers a strong executable libc interposition over the dynamic fallback", async () => { + const interposeSource = join(BUILD_DIR, "putenv-interpose.c"); + const interposeObject = join(BUILD_DIR, "putenv-interpose.o"); + const interposeArchive = join(BUILD_DIR, "libputenv-interpose.a"); + writeFileSync(interposeSource, ` + #include + int putenv(char *value) { return value[0] == 'K' ? 73 : 74; } + `); + execFileSync("wasm32posix-cc", [ + "-O2", "-c", interposeSource, "-o", interposeObject, + ], { stdio: "pipe" }); + execFileSync("wasm32posix-ar", [ + "rcs", interposeArchive, interposeObject, + ], { stdio: "pipe" }); + + const wasmPath = buildMainProgram( + ` + #include + #include + + int main(void) { + char value[] = "KANDELO_INTERPOSE=1"; + printf("interposed=%d\\n", putenv(value)); + return 0; + } + `, + "test-dlopen-libc-interposition", + [interposeArchive], + ); + + const result = await runCentralizedProgram({ + programPath: wasmPath, + argv: ["test-dlopen-libc-interposition"], + timeout: 10_000, + useDefaultRootfs: false, + }); + + expect(result.exitCode, `stdout=${result.stdout}\nstderr=${result.stderr}`).toBe(0); + expect(result.stdout).toBe("interposed=73\n"); + expect(result.stderr).toBe(""); + }); + it.skipIf(!hasSysroot64 || !hasCompiler("wasm64posix-cc"))( "loads and resolves a memory64 shared library through dlopen/dlsym", { timeout: 30_000 }, diff --git a/host/test/executable-module-cache.test.ts b/host/test/executable-module-cache.test.ts new file mode 100644 index 0000000000..4b6a6c996d --- /dev/null +++ b/host/test/executable-module-cache.test.ts @@ -0,0 +1,181 @@ +import { describe, expect, it } from "vitest"; + +import { ExecutableModuleCache } from "../src/executable-module-cache"; + +const EMPTY_WASM = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]); + +function bytes(...suffix: number[]): ArrayBuffer { + const value = new Uint8Array(EMPTY_WASM.byteLength + suffix.length); + value.set(EMPTY_WASM); + value.set(suffix, EMPTY_WASM.byteLength); + return value.buffer; +} + +describe("ExecutableModuleCache", () => { + it("returns a module only for byte-identical executable contents", () => { + const cache = new ExecutableModuleCache({ + maxEntries: 2, + maxAliases: 32, + maxRetainedBytes: 1024, + }); + const original = bytes(1, 2, 3, 4, 5); + const module = new WebAssembly.Module(EMPTY_WASM); + cache.set("/bin/tool", original, module, "metadata"); + + expect(cache.get("/bin/tool", original.slice(0))).toEqual({ + module, + metadata: "metadata", + }); + expect(cache.get("/bin/tool", bytes(1, 2, 3, 4, 6))).toBeUndefined(); + expect(cache.stats()).toMatchObject({ + hits: 1, + misses: 1, + contentMismatches: 1, + retainedEntries: 0, + retainedAliases: 0, + retainedBytes: 0, + }); + }); + + it("shares exact executable content reached through different paths", () => { + const cache = new ExecutableModuleCache({ + maxEntries: 1, + maxAliases: 32, + maxRetainedBytes: 1024, + }); + const original = bytes(1, 2, 3); + const module = new WebAssembly.Module(EMPTY_WASM); + cache.set("/bin/tool", original, module, "content metadata"); + + expect(cache.get("/usr/bin/tool", original.slice(0))).toEqual({ + module, + metadata: "content metadata", + }); + expect(cache.stats()).toMatchObject({ + hits: 1, + contentAliasHits: 1, + retainedEntries: 1, + retainedAliases: 2, + retainedBytes: original.byteLength, + }); + }); + + it("does not confuse equal-size executables with different content", () => { + const cache = new ExecutableModuleCache({ + maxEntries: 2, + maxAliases: 32, + maxRetainedBytes: 1024, + }); + cache.set( + "/bin/first", + bytes(1), + new WebAssembly.Module(EMPTY_WASM), + null, + ); + + expect(cache.get("/bin/second", bytes(2))).toBeUndefined(); + expect(cache.stats()).toMatchObject({ + contentAliasHits: 0, + misses: 1, + retainedEntries: 1, + retainedAliases: 1, + }); + }); + + it("keeps shared content when one alias changes", () => { + const cache = new ExecutableModuleCache({ + maxEntries: 2, + maxAliases: 32, + maxRetainedBytes: 1024, + }); + const original = bytes(1); + const module = new WebAssembly.Module(EMPTY_WASM); + cache.set("/bin/tool", original, module, null); + expect(cache.get("/usr/bin/tool", original)).toBeDefined(); + + expect(cache.get("/bin/tool", bytes(2))).toBeUndefined(); + expect(cache.get("/usr/bin/tool", original)).toEqual({ module, metadata: null }); + expect(cache.stats()).toMatchObject({ + contentMismatches: 1, + retainedEntries: 1, + retainedAliases: 1, + }); + }); + + it("bounds retained pathname aliases independently of content", () => { + const cache = new ExecutableModuleCache({ + maxEntries: 2, + maxAliases: 2, + maxRetainedBytes: 1024, + }); + const original = bytes(1); + const module = new WebAssembly.Module(EMPTY_WASM); + cache.set("/bin/tool", original, module, null); + expect(cache.get("/usr/bin/tool", original)).toBeDefined(); + cache.set("/bin/other", bytes(2), module, null); + + expect(cache.stats()).toMatchObject({ + aliasEvictions: 1, + retainedEntries: 2, + retainedAliases: 2, + }); + expect(cache.get("/bin/tool", original)).toEqual({ module, metadata: null }); + expect(cache.stats()).toMatchObject({ + aliasEvictions: 2, + retainedEntries: 2, + retainedAliases: 2, + }); + }); + + it("owns comparison bytes instead of trusting a caller-owned buffer", () => { + const cache = new ExecutableModuleCache({ + maxEntries: 1, + maxAliases: 32, + maxRetainedBytes: 1024, + }); + const original = bytes(7); + const module = new WebAssembly.Module(EMPTY_WASM); + cache.set("/bin/tool", original, module, null); + new Uint8Array(original)[EMPTY_WASM.byteLength] = 8; + + expect(cache.get("/bin/tool", bytes(7))).toEqual({ module, metadata: null }); + }); + + it("bounds retained byte copies with least-recently-used eviction", () => { + const cache = new ExecutableModuleCache({ + maxEntries: 2, + maxAliases: 32, + maxRetainedBytes: 18, + }); + const module = new WebAssembly.Module(EMPTY_WASM); + const first = bytes(1); + const second = bytes(2); + const third = bytes(3); + cache.set("first", first, module, 1); + cache.set("second", second, module, 2); + expect(cache.get("first", first)).toBeDefined(); + cache.set("third", third, module, 3); + + expect(cache.get("second", second)).toBeUndefined(); + expect(cache.get("first", first)).toBeDefined(); + expect(cache.get("third", third)).toBeDefined(); + expect(cache.stats()).toMatchObject({ + evictions: 1, + retainedEntries: 2, + retainedBytes: 18, + }); + }); + + it("does not retain a single executable larger than its byte budget", () => { + const cache = new ExecutableModuleCache({ + maxEntries: 1, + maxAliases: 32, + maxRetainedBytes: 8, + }); + const oversized = bytes(1); + cache.set("large", oversized, new WebAssembly.Module(EMPTY_WASM), null); + + expect(cache.get("large", oversized)).toBeUndefined(); + expect(cache.stats().retainedBytes).toBe(0); + }); +}); diff --git a/host/test/homebrew-portable-ruby.test.ts b/host/test/homebrew-portable-ruby.test.ts new file mode 100644 index 0000000000..2637c9eb81 --- /dev/null +++ b/host/test/homebrew-portable-ruby.test.ts @@ -0,0 +1,203 @@ +import { zipSync, type Zippable } from "fflate"; +import { describe, expect, it, vi } from "vitest"; + +import { deriveHomebrewPortableRubyTree } from + "../src/homebrew-portable-ruby"; +import { + assertPackageDeferredZipTreeState, + derivePackageDeferredZipTree, + registerPackageDeferredZipTree, +} from "../src/vfs/package-deferred-tree"; +import { MemoryFileSystem } from "../src/vfs/memory-fs"; + +const encoder = new TextEncoder(); +const COMPAT_PREFIX = "/home/linuxbrew/.linuxbrew"; + +describe("Homebrew portable Ruby lazy tree", () => { + it("derives the upstream layout from the source-owned prefix and version", () => { + const sourceArchive = bootstrapArchive("4.0.5_1"); + const sourceTree = derivePackageDeferredZipTree( + bootstrapSpec(COMPAT_PREFIX), + sourceArchive, + ); + const portableArchive = portableRubyArchive("4.0.5_1"); + + const portable = deriveHomebrewPortableRubyTree( + sourceTree, + sourceArchive, + portableArchive, + ); + + expect(portable.descriptor).toMatchObject({ + id: "homebrew-bootstrap/portable-ruby", + content_role: "runtime-tree", + package: { + name: "homebrew-bootstrap", + output: "homebrew-portable-ruby.zip", + }, + mount_prefix: + `${COMPAT_PREFIX}/Library/Homebrew/vendor/portable-ruby`, + owner: { uid: 1000, gid: 1000 }, + activation: { + mode: "first-use", + capabilities: ["homebrew:runtime"], + roots: [ + `${COMPAT_PREFIX}/Library/Homebrew/vendor/portable-ruby/` + + "4.0.5_1/bin/ruby", + ], + atomicGroup: { + id: "homebrew-runtime-support", + member: "homebrew-bootstrap/portable-ruby", + }, + }, + }); + expect( + portable.entries.find((entry) => entry.sourcePath === "current"), + ).toMatchObject({ type: "symlink", target: "4.0.5_1" }); + }); + + it("materializes source and portable Ruby atomically on Brew first use", async () => { + const sourceArchive = bootstrapArchive("4.0.5_1"); + const sourceTree = derivePackageDeferredZipTree( + bootstrapSpec(COMPAT_PREFIX), + sourceArchive, + ); + const portableArchive = portableRubyArchive("4.0.5_1"); + const portableTree = deriveHomebrewPortableRubyTree( + sourceTree, + sourceArchive, + portableArchive, + ); + const fs = portableRubyFs(); + registerPackageDeferredZipTree(fs, sourceTree); + registerPackageDeferredZipTree(fs, portableTree); + expect(fs.lstat(portableTree.descriptor.mount_prefix)).toMatchObject({ + uid: 1000, + gid: 1000, + }); + await fs.sealLazyAtomicGroup("homebrew-runtime-support", [ + sourceTree.descriptor.id, + portableTree.descriptor.id, + ]); + const fetcher = vi.fn(async (url: string) => { + const bytes = url === "homebrew-bootstrap.zip" + ? sourceArchive + : url === "homebrew-portable-ruby.zip" + ? portableArchive + : undefined; + if (bytes === undefined) throw new Error(`unexpected URL: ${url}`); + return new Response(bytes, { + headers: { "content-length": String(bytes.byteLength) }, + }); + }); + fs.setLazyFetcher(fetcher); + + await expect(fs.preparePath(`${COMPAT_PREFIX}/bin/brew`)).resolves.toBe( + true, + ); + expect(fetcher).toHaveBeenCalledTimes(2); + assertPackageDeferredZipTreeState(fs, sourceTree, "materialized"); + assertPackageDeferredZipTreeState(fs, portableTree, "materialized"); + expect(readFile( + fs, + `${COMPAT_PREFIX}/Library/Homebrew/vendor/portable-ruby/current/bin/ruby`, + )).toBe("ruby-wasm\n"); + }); + + it("rejects a portable archive that disagrees with Homebrew source", () => { + const sourceArchive = bootstrapArchive("4.0.5_1"); + const sourceTree = derivePackageDeferredZipTree( + bootstrapSpec(COMPAT_PREFIX), + sourceArchive, + ); + + expect(() => + deriveHomebrewPortableRubyTree( + sourceTree, + sourceArchive, + portableRubyArchive("4.0.4_1"), + ) + ).toThrow(/source-matched executable/); + }); +}); + +function bootstrapSpec(prefix: string) { + return { + schema: 1, + kind: "kandelo-package-deferred-zip-tree", + id: "homebrew-bootstrap/source-tree", + content_role: "source-tree", + package: { + name: "homebrew-bootstrap", + output: "homebrew-bootstrap.zip", + }, + archive: { + url: "homebrew-bootstrap.zip", + mode_policy: "portable-posix-v1", + }, + mount_prefix: prefix, + owner: { uid: 1000, gid: 1000 }, + activation: { + mode: "first-use", + capabilities: ["homebrew:bootstrap", "homebrew:runtime"], + roots: [`${prefix}/bin/brew`], + atomic_group: "homebrew-runtime-support", + }, + } as const; +} + +function bootstrapArchive(version: string): Uint8Array { + return zipSync({ + "bin/": zipEntry(new Uint8Array(), 0o040755), + "bin/brew": zipEntry(encoder.encode("#!/bin/bash\n"), 0o100755), + "Library/": zipEntry(new Uint8Array(), 0o040755), + "Library/Homebrew/": zipEntry(new Uint8Array(), 0o040755), + "Library/Homebrew/vendor/": zipEntry(new Uint8Array(), 0o040755), + "Library/Homebrew/vendor/portable-ruby-version": zipEntry( + encoder.encode(`${version}\n`), + 0o100644, + ), + }, { level: 9 }); +} + +function portableRubyArchive(version: string): Uint8Array { + return zipSync({ + [`${version}/`]: zipEntry(new Uint8Array(), 0o040755), + [`${version}/bin/`]: zipEntry(new Uint8Array(), 0o040755), + [`${version}/bin/ruby`]: zipEntry( + encoder.encode("ruby-wasm\n"), + 0o100755, + ), + [`${version}/lib/`]: zipEntry(new Uint8Array(), 0o040755), + [`${version}/lib/ruby.rb`]: zipEntry( + encoder.encode("RUBY = true\n"), + 0o100644, + ), + current: zipEntry(encoder.encode(version), 0o120777), + }, { level: 9 }); +} + +function zipEntry(bytes: Uint8Array, mode: number): Zippable[string] { + return [bytes, { os: 3, attrs: (mode << 16) >>> 0 }]; +} + +function portableRubyFs(): MemoryFileSystem { + const fs = MemoryFileSystem.create(new SharedArrayBuffer(16 * 1024 * 1024)); + for (const path of ["/home", "/home/linuxbrew", COMPAT_PREFIX]) { + fs.mkdir(path, 0o755); + } + fs.chown(COMPAT_PREFIX, 1000, 1000); + return fs; +} + +function readFile(fs: MemoryFileSystem, path: string): string { + const stat = fs.stat(path); + const fd = fs.open(path, 0, 0); + try { + const bytes = new Uint8Array(stat.size); + expect(fs.read(fd, bytes, null, bytes.byteLength)).toBe(bytes.byteLength); + return new TextDecoder().decode(bytes); + } finally { + fs.close(fd); + } +} diff --git a/host/test/homebrew-query-benchmark.test.ts b/host/test/homebrew-query-benchmark.test.ts new file mode 100644 index 0000000000..1011685b33 --- /dev/null +++ b/host/test/homebrew-query-benchmark.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; + +import { compareMedians } from "../../benchmarks/homebrew-query/compare"; + +describe("Homebrew query benchmark comparison", () => { + it("flags only latency increases beyond the large-regression threshold", () => { + expect(compareMedians( + { warm_info_ms: 100, first_info_ms: 100 }, + { warm_info_ms: 126, first_info_ms: 60 }, + 25, + )).toEqual([ + { + metric: "first_info_ms", + before: 100, + after: 60, + changePercent: -40, + regression: false, + }, + { + metric: "warm_info_ms", + before: 100, + after: 126, + changePercent: 26, + regression: true, + }, + ]); + }); + + it("rejects comparisons with different metric sets", () => { + expect(() => compareMedians({ one: 1 }, { two: 1 }, 25)).toThrow( + "different metrics", + ); + }); +}); diff --git a/host/test/homebrew-runtime-support.test.ts b/host/test/homebrew-runtime-support.test.ts index e78c753612..680a3dedd5 100644 --- a/host/test/homebrew-runtime-support.test.ts +++ b/host/test/homebrew-runtime-support.test.ts @@ -19,7 +19,7 @@ const source = JSON.parse( ); describe("Homebrew shell runtime-support contract", () => { - it("binds the declared runtime delta and admits file/libmagic through the base", () => { + it("uses portable Ruby and admits file/libmagic through the base", () => { const contract = parseHomebrewRuntimeSupportContract(source); expect(contract.activation).toEqual({ capability: "homebrew:runtime", @@ -31,7 +31,8 @@ describe("Homebrew shell runtime-support contract", () => { (name) => !contract.baseFormulaOrder.includes(name), ), ); - expect(contract.additionalFormulaOrder).toContain( + expect(contract.additionalFormulaOrder).toEqual([]); + expect(contract.formulaRoots).not.toContain( "kandelo-dev/tap-core/ruby", ); expect(contract.deferredRelocationFormulae).toEqual([]); @@ -57,12 +58,12 @@ describe("Homebrew shell runtime-support contract", () => { it("rejects declaring one Formula as both admitted and deferred", () => { const changed = structuredClone(source); changed.deferred_formulae.push({ - package: "kandelo-dev/tap-core/ruby", + package: "kandelo-dev/tap-core/git", current_state: "public-abi41-only", reason: "fixture", reentry_gate: "fixture", }); - changed.availability.can_be_deferred.push("kandelo-dev/tap-core/ruby"); + changed.availability.can_be_deferred.push("kandelo-dev/tap-core/git"); expect(() => parseHomebrewRuntimeSupportContract(changed)).toThrow( /cannot both admit and defer/, ); diff --git a/host/test/homebrew-vfs-builder.test.ts b/host/test/homebrew-vfs-builder.test.ts index 18b626de87..93f2a303bc 100644 --- a/host/test/homebrew-vfs-builder.test.ts +++ b/host/test/homebrew-vfs-builder.test.ts @@ -63,7 +63,10 @@ import { } from "../src/homebrew-vfs-planner"; import type { HomebrewRuntimeSupportContract } from "../src/homebrew-runtime-support"; -import { MemoryFileSystem } from "../src/vfs/memory-fs"; +import { + MemoryFileSystem, + type LazyTreeRegistrationEntry, +} from "../src/vfs/memory-fs"; import { derivePackageDeferredZipTree, registerPackageDeferredZipTree, @@ -552,6 +555,27 @@ function readVfsFile(fs: MemoryFileSystem, path: string): string { } } +function replaceStringValuesInPlace( + value: unknown, + from: string, + to: string, +): void { + if (Array.isArray(value)) { + for (let index = 0; index < value.length; index += 1) { + const item = value[index]; + if (typeof item === "string") value[index] = item.replaceAll(from, to); + else replaceStringValuesInPlace(item, from, to); + } + return; + } + if (typeof value !== "object" || value === null) return; + const record = value as Record; + for (const [key, item] of Object.entries(record)) { + if (typeof item === "string") record[key] = item.replaceAll(from, to); + else replaceStringValuesInPlace(item, from, to); + } +} + async function lazyLayerFixture(options: { mutateBase?: (fs: MemoryFileSystem) => void; mutatePlan?: (plan: HomebrewVfsPlan) => void; @@ -1544,6 +1568,100 @@ describe("Homebrew runtime layer consumer", () => { .toBe(0o640); }); + it("relocates a published lazy layer with its authenticated legacy prefix", async () => { + const legacyPrefix = "/home/linuxbrew/.linuxbrew"; + const fixture = await runtimeLayerConsumerFixture({ + runtimeReceipt: JSON.stringify({ + changed_files: ["INSTALL_RECEIPT.json", "lib/runtime.conf"], + source: { path: "@@HOMEBREW_LIBRARY@@/Formula/runtime.rb" }, + }) + "\n", + runtimeExtraEntries: [{ + path: "runtime/3.0/lib/runtime.conf", + data: "prefix=@@HOMEBREW_PREFIX@@\ncellar=@@HOMEBREW_CELLAR@@\n", + }], + }); + + // Runtime descriptors are signed around their prefix. Exercise the VFS + // registration boundary directly so this fixture models an authenticated + // artifact published before the canonical-prefix migration, while current + // producers continue to emit only the new prefix. + const tree = structuredClone(descriptorTree(fixture.descriptor)); + replaceStringValuesInPlace(tree, PREFIX, legacyPrefix); + replaceStringValuesInPlace(tree, PREFIX.slice(1), legacyPrefix.slice(1)); + for (const entry of tree.inventory.entries) { + if (entry.type === "symlink") { + entry.size = utf8(entry.target!).byteLength; + } else if ( + entry.materialization === "archive-homebrew-relocate" && + entry.source_path.endsWith("/INSTALL_RECEIPT.json") + ) { + entry.size = utf8(JSON.stringify({ + changed_files: ["INSTALL_RECEIPT.json", "lib/runtime.conf"], + source: { path: `${legacyPrefix}/Library/Formula/runtime.rb` }, + }) + "\n").byteLength; + } else if ( + entry.materialization === "archive-homebrew-relocate" && + entry.source_path.endsWith("/lib/runtime.conf") + ) { + entry.size = utf8( + `prefix=${legacyPrefix}\ncellar=${legacyPrefix}/Cellar\n`, + ).byteLength; + } + } + const fs = MemoryFileSystem.create(new SharedArrayBuffer(16 * 1024 * 1024)); + const entries = tree.inventory.entries.map((entry) => ({ + vfsPath: `/${entry.path}`, + sourcePath: entry.source_path, + ...(entry.materialization === undefined + ? {} + : { materialization: entry.materialization }), + type: entry.type, + mode: entry.mode, + size: entry.size, + ...(entry.target === undefined + ? {} + : { target: entry.type === "hardlink" ? `/${entry.target}` : entry.target }), + ...(entry.inode_group === undefined ? {} : { inodeGroup: entry.inode_group }), + })) satisfies LazyTreeRegistrationEntry[]; + const handle = fs.registerLazyTreeWithMaterializationHandle({ + decoder: tree.content.decoder, + mediaType: tree.content.media_type, + sha256: tree.content.sha256, + bytes: tree.content.bytes, + expandedBytes: tree.inventory.expanded_bytes, + sourceEntryCount: tree.inventory.source_entry_count, + transports: tree.transports.map((transport) => transport.url), + source: { + schema: 1, + kind: "homebrew-bottle-tar-gzip-v1", + entries: tree.inventory.source!.entries.map((entry) => ({ + sourcePath: entry.path, + type: entry.type, + mode: entry.mode, + size: entry.size, + ...(entry.target === undefined ? {} : { target: entry.target }), + })), + }, + }, entries, tree.mount_prefix, { + mode: tree.activation.mode, + capabilities: [...tree.activation.capabilities], + roots: [...tree.activation.roots], + }); + await expect( + fs.materializeRegisteredDeferredTree(handle, fixture.runtimeBytes), + ).resolves.toBe(true); + + const legacyRuntimeKeg = fixture.runtimeKeg.replace(PREFIX, legacyPrefix); + expect(readVfsFile(fs, `${legacyRuntimeKeg}/lib/runtime.conf`)).toBe( + `prefix=${legacyPrefix}\ncellar=${legacyPrefix}/Cellar\n`, + ); + expect(JSON.parse( + readVfsFile(fs, `${legacyRuntimeKeg}/INSTALL_RECEIPT.json`), + )).toMatchObject({ + source: { path: `${legacyPrefix}/Library/Formula/runtime.rb` }, + }); + }); + it("accepts upstream null changed_files as an empty relocation set", async () => { const fixture = await runtimeLayerConsumerFixture({ runtimeReceipt: JSON.stringify({ changed_files: null }) + "\n", @@ -3482,6 +3600,37 @@ describe("Homebrew VFS builder", () => { ); }); + it("collects original bottles from the prefix owned by their plan", async () => { + const compatibilityPrefix = "/home/linuxbrew/.linuxbrew"; + const fixture = await lazyLayerFixture({ + includeLayerDependency: true, + mutatePlan(plan) { + replaceStringValuesInPlace(plan, PREFIX, compatibilityPrefix); + }, + }); + const selectedPlan: HomebrewVfsPlan = { + ...fixture.plan, + packages: fixture.plan.packages.filter((pkg) => pkg.name !== "hello"), + }; + + const collection = await buildHomebrewOriginalBottleCollection(selectedPlan, { + fs: MemoryFileSystem.create(new SharedArrayBuffer(16 * 1024 * 1024)), + baseFs: fixture.baseFs, + loadBottleBytes: (pkg) => + pkg.name === "runtime-dep" ? fixture.dependencyBytes : fixture.runtimeBytes, + }); + + expect(collection.deferredTrees).toHaveLength(2); + const compatibilityRoot = compatibilityPrefix.slice(1); + expect( + collection.deferredTrees.flatMap((tree) => tree.inventory.entries) + .every((entry) => + entry.path === compatibilityRoot || + entry.path.startsWith(`${compatibilityRoot}/`) + ), + ).toBe(true); + }); + it.each(["member", "cohort"] as const)( "rejects a forged imported %s seal before generic collection bottle loading or writes", async (forgery) => { @@ -3588,6 +3737,9 @@ describe("Homebrew VFS builder", () => { const fixture = await lazyLayerFixture({ includeLayerDependency: true }); const deferredVersion = "4.0"; const deferredKeg = `${CELLAR}/deferred/${deferredVersion}`; + const deferredReceipt = JSON.stringify({ + changed_files: [".brew/deferred.rb", "INSTALL_RECEIPT.json"], + }) + "\n"; const deferredBytes = bottleTar([ { path: `deferred/${deferredVersion}/bin/deferred`, @@ -3605,7 +3757,7 @@ describe("Homebrew VFS builder", () => { }, { path: `deferred/${deferredVersion}/INSTALL_RECEIPT.json`, - data: "{}\n", + data: deferredReceipt, }, ]); const runtime = fixture.plan.packages.find((pkg) => pkg.name === "runtime")!; @@ -3875,7 +4027,19 @@ describe("Homebrew VFS builder", () => { bytes: 512 * 1024 * 1024 + 1, }], })).toThrow(/asset ownership is not canonical/); - expect(fs.exportLazyArchiveEntries()).toHaveLength(1); + const pendingTrees = fs.exportLazyArchiveEntries(); + expect(pendingTrees).toHaveLength(1); + expect(pendingTrees[0]!.inventory?.some((entry) => + entry.vfsPath === `${deferredKeg}/INSTALL_RECEIPT.json` + )).toBe(false); + expect(pendingTrees[0]!.content?.source?.entries.some((entry) => + entry.sourcePath.endsWith("/INSTALL_RECEIPT.json") + )).toBe(true); + expect(fs.isPathDeferred(`${deferredKeg}/INSTALL_RECEIPT.json`)).toBe(false); + expect(readVfsFile(fs, `${deferredKeg}/INSTALL_RECEIPT.json`)).toBe( + deferredReceipt, + ); + expect(fs.isPathDeferred(`${deferredKeg}/.brew/deferred.rb`)).toBe(false); expect(fs.readlink(`${PREFIX}/bin/runtime`)).toBe(`${runtime.keg}/bin/runtime`); expect(fs.readlink("/bin/deferred")).toBe(`${deferredKeg}/bin/deferred`); expect(fs.lstat("/var/lib/deferred").mode & 0o7777).toBe(0o755); @@ -3888,6 +4052,7 @@ describe("Homebrew VFS builder", () => { const restored = MemoryFileSystem.fromImage(savedImage); assertHomebrewVfsMaterialization(restored, result.evidence); expect(restored.exportLazyArchiveEntries()).toHaveLength(1); + expect(restored.isPathDeferred(`${deferredKeg}/INSTALL_RECEIPT.json`)).toBe(false); const offlineFetch = vi.fn(async () => { throw new Error("network disabled during embedded command proof"); }); @@ -3909,8 +4074,22 @@ describe("Homebrew VFS builder", () => { expect(readVfsFile(restored, `${deferredKeg}/share/deferred.txt`)).toContain( "deferred sibling", ); + expect(readVfsFile(restored, `${deferredKeg}/INSTALL_RECEIPT.json`)).toBe( + deferredReceipt, + ); expect(restored.exportLazyArchiveEntries()).toEqual([]); + const tamperedReceiptFs = MemoryFileSystem.fromImage(savedImage); + writeVfsFile( + tamperedReceiptFs, + `${deferredKeg}/INSTALL_RECEIPT.json`, + deferredReceipt.replace("INSTALL", "iNSTALL"), + ); + expect(() => assertHomebrewVfsMaterialization( + tamperedReceiptFs, + result.evidence, + )).toThrow(/embedded regular bytes changed/); + const tamperedPlanFs = MemoryFileSystem.fromImage(savedImage); writeVfsFile( tamperedPlanFs, @@ -3977,6 +4156,8 @@ describe("Homebrew VFS builder", () => { )!; const supportVersion = "4.0"; const supportKeg = `${CELLAR}/brew-support/${supportVersion}`; + const supportFormula = "class BrewSupport < Formula\nend\n"; + const supportReceipt = '{"changed_files":[]}\n'; const supportBytes = bottleTar([ { path: `brew-support/${supportVersion}/bin/brew-support`, @@ -3985,11 +4166,11 @@ describe("Homebrew VFS builder", () => { }, { path: `brew-support/${supportVersion}/.brew/brew-support.rb`, - data: "class BrewSupport < Formula\nend\n", + data: supportFormula, }, { path: `brew-support/${supportVersion}/INSTALL_RECEIPT.json`, - data: "{}\n", + data: supportReceipt, }, ]); const supportPackage = { @@ -4031,6 +4212,8 @@ describe("Homebrew VFS builder", () => { }; const fallbackVersion = "1.0"; const fallbackKeg = `${CELLAR}/brew-support-fallback/${fallbackVersion}`; + const fallbackFormula = "class BrewSupportFallback < Formula\nend\n"; + const fallbackReceipt = '{"changed_files":[]}\n'; const fallbackBytes = bottleTar([ { path: @@ -4041,12 +4224,12 @@ describe("Homebrew VFS builder", () => { { path: `brew-support-fallback/${fallbackVersion}/.brew/brew-support-fallback.rb`, - data: "class BrewSupportFallback < Formula\nend\n", + data: fallbackFormula, }, { path: `brew-support-fallback/${fallbackVersion}/INSTALL_RECEIPT.json`, - data: "{}\n", + data: fallbackReceipt, }, ]); const fallbackPackage = { @@ -4213,6 +4396,34 @@ describe("Homebrew VFS builder", () => { bootstrap.descriptor.id, ]); assertHomebrewVfsMaterialization(fs, result.evidence); + const sealedImage = await fs.saveImage(); + + const pendingSupportTrees = fs.exportLazyArchiveEntries().filter((tree) => + tree.content?.source?.entries.some((entry) => + entry.sourcePath === + `brew-support/${supportVersion}/INSTALL_RECEIPT.json` + ) + ); + expect(pendingSupportTrees).toHaveLength(1); + expect(pendingSupportTrees[0]!.inventory?.some((entry) => + entry.vfsPath === `${supportKeg}/INSTALL_RECEIPT.json` + )).toBe(false); + expect(fs.isPathDeferred(`${supportKeg}/INSTALL_RECEIPT.json`)).toBe(false); + expect(fs.isPathDeferred(`${supportKeg}/.brew/brew-support.rb`)).toBe(false); + expect(readVfsFile(fs, `${supportKeg}/INSTALL_RECEIPT.json`)).toBe( + supportReceipt, + ); + expect(readVfsFile(fs, `${supportKeg}/.brew/brew-support.rb`)).toBe( + supportFormula, + ); + expect(readVfsFile( + fs, + `${fallbackKeg}/INSTALL_RECEIPT.json`, + )).toBe(fallbackReceipt); + expect(readVfsFile( + fs, + `${fallbackKeg}/.brew/brew-support-fallback.rb`, + )).toBe(fallbackFormula); const supportAsset = result.mirrorPlan.assets.find( (asset) => asset.package === supportPackage.fullName, @@ -4235,6 +4446,7 @@ describe("Homebrew VFS builder", () => { ); }); fs.setLazyFetcher(fetcher); + expect(fetcher).not.toHaveBeenCalled(); await expect( fs.preparePath("/usr/bin/brew-support-command"), @@ -4250,6 +4462,13 @@ describe("Homebrew VFS builder", () => { fs, `${fallbackKeg}/bin/brew-support-fallback`, )).toContain("fallback"); + expect(readVfsFile(fs, `${supportKeg}/INSTALL_RECEIPT.json`)).toBe( + supportReceipt, + ); + expect(readVfsFile( + fs, + `${fallbackKeg}/INSTALL_RECEIPT.json`, + )).toBe(fallbackReceipt); expect(fs.readlink(`${PREFIX}/bin/brew-support`)).toBe( `${supportKeg}/bin/brew-support`, ); @@ -4324,6 +4543,18 @@ describe("Homebrew VFS builder", () => { expect(composition.materialization.runtime_support.package_order).toEqual( contract.additionalFormulaOrder, ); + + const tamperedSupportMetadata = MemoryFileSystem.fromImage(sealedImage); + await tamperedSupportMetadata.verifyImportedLazyAtomicGroupSeals(); + writeVfsFile( + tamperedSupportMetadata, + `${supportKeg}/INSTALL_RECEIPT.json`, + supportReceipt.replace("changed", "Changed"), + ); + expect(() => assertHomebrewVfsMaterialization( + tamperedSupportMetadata, + result.evidence, + )).toThrow(/embedded regular bytes changed/); }); it("applies Bash- and Ruby-owned consumer state once across a Ruby-only delta", async () => { diff --git a/host/test/homebrew-vfs-image-save.test.ts b/host/test/homebrew-vfs-image-save.test.ts index 2c49e2a0ed..cd819a15cb 100644 --- a/host/test/homebrew-vfs-image-save.test.ts +++ b/host/test/homebrew-vfs-image-save.test.ts @@ -58,7 +58,7 @@ describe("Homebrew VFS image publication boundary", () => { const fs = bootstrapConsumerFs(); expect(() => registerPackageDeferredZipTree(fs, derived)).toThrow( - "collides with the base", + /collides/, ); prepareHomebrewBootstrapConsumerNamespace(fs, derived); const registered = registerPackageDeferredZipTree(fs, derived); @@ -143,7 +143,7 @@ describe("Homebrew VFS image publication boundary", () => { valid, bootstrapEnvironment, ), - ).toThrow("canonical deferred source tree"); + ).toThrow("descriptor-owned deferred source tree"); const directory = mkdtempSync(join(tmpdir(), "homebrew-bootstrap-env-")); try { @@ -172,6 +172,37 @@ describe("Homebrew VFS image publication boundary", () => { } }); + it("derives compatibility-image ownership from the authenticated source prefix", () => { + const compatibilityPrefix = "/home/linuxbrew/.linuxbrew"; + const spec = { + ...bootstrapSpec, + mount_prefix: compatibilityPrefix, + activation: { + ...bootstrapSpec.activation, + roots: [`${compatibilityPrefix}/bin/brew`], + }, + } satisfies PackageDeferredZipTreeSpec; + const tree = derivePackageDeferredZipTree(spec, bootstrapArchive(true)); + const fs = bootstrapConsumerFs(compatibilityPrefix); + + prepareHomebrewBootstrapConsumerNamespace(fs, tree); + registerPackageDeferredZipTree(fs, tree); + const consumer = installHomebrewBootstrapConsumerState( + fs, + tree, + bootstrapEnvironment, + ); + + expect(consumer.ownership.prefix).toBe(compatibilityPrefix); + expect(consumer.ownership.mutable_paths).toContain( + `${compatibilityPrefix}/Cellar`, + ); + expect(fs.readlink("/usr/bin/brew")).toBe( + `${compatibilityPrefix}/bin/brew`, + ); + assertHomebrewBootstrapConsumerState(fs, consumer); + }); + it.each(["environment", "entrypoint"] as const)( "does not replace pre-existing Homebrew %s state", (kind) => { @@ -333,33 +364,42 @@ function zipEntry(bytes: Uint8Array, mode: number): Zippable[string] { return [bytes, { os: 3, attrs: (mode << 16) >>> 0 }]; } -function bootstrapConsumerFs(): MemoryFileSystem { +function bootstrapConsumerFs( + prefix = "/opt/kandelo/homebrew", +): MemoryFileSystem { const fs = MemoryFileSystem.create( new SharedArrayBuffer(8 * MiB, { maxByteLength: 32 * MiB }), 32 * MiB, ); - for (const path of [ - "/opt", - "/opt/kandelo", - "/opt/kandelo/homebrew", - "/opt/kandelo/homebrew/bin", - "/opt/kandelo/homebrew/Cellar", - "/opt/kandelo/homebrew/Cellar/existing", - "/opt/kandelo/homebrew/Cellar/existing/1", - "/opt/kandelo/homebrew/Cellar/existing/1/bin", + for (const path of new Set([ + ...prefixAncestors(prefix), + `${prefix}/bin`, + `${prefix}/Cellar`, + `${prefix}/Cellar/existing`, + `${prefix}/Cellar/existing/1`, + `${prefix}/Cellar/existing/1/bin`, "/home", "/home/user", "/usr", "/usr/bin", "/etc", - ]) { + ])) { fs.mkdir(path, 0o755); } writeVfsBinary( fs, - "/opt/kandelo/homebrew/Cellar/existing/1/bin/tool", + `${prefix}/Cellar/existing/1/bin/tool`, encoder.encode("tool\n"), 0o755, ); return fs; } + +function prefixAncestors(prefix: string): string[] { + const result: string[] = []; + const parts = prefix.slice(1).split("/"); + for (let index = 1; index <= parts.length; index += 1) { + result.push(`/${parts.slice(0, index).join("/")}`); + } + return result; +} diff --git a/host/test/package-deferred-tree.test.ts b/host/test/package-deferred-tree.test.ts index 192911f8fe..71a264b1b9 100644 --- a/host/test/package-deferred-tree.test.ts +++ b/host/test/package-deferred-tree.test.ts @@ -169,6 +169,23 @@ describe("package deferred ZIP trees", () => { expect(fetcher).toHaveBeenCalledTimes(1); }); + it("gives a newly created mount directory to the declared package owner", () => { + const archive = packageArchive(); + const derived = derivePackageDeferredZipTree(SPEC, archive); + const fs = MemoryFileSystem.create( + new SharedArrayBuffer(32 * 1024 * 1024), + ); + + registerPackageDeferredZipTree(fs, derived); + + expect(fs.lstat(SPEC.mount_prefix)).toMatchObject({ + mode: expect.any(Number), + uid: SPEC.owner.uid, + gid: SPEC.owner.gid, + }); + assertPackageDeferredZipTreeState(fs, derived, "deferred"); + }); + it("preserves guest-external symlink text without following it", () => { const archive = packageArchive("../../../../usr/bin/env"); const derived = derivePackageDeferredZipTree(SPEC, archive); @@ -285,6 +302,12 @@ describe("package deferred ZIP trees", () => { /collides with the base/, ); + const wrongOwnerFs = packageFs(); + wrongOwnerFs.chown(SPEC.mount_prefix, 0, 0); + expect(() => registerPackageDeferredZipTree(wrongOwnerFs, derived)).toThrow( + /ancestor collides/, + ); + const blockedFs = MemoryFileSystem.create( new SharedArrayBuffer(32 * 1024 * 1024), ); diff --git a/host/test/prestarted-worker-pool.test.ts b/host/test/prestarted-worker-pool.test.ts new file mode 100644 index 0000000000..d2f8ac5dbc --- /dev/null +++ b/host/test/prestarted-worker-pool.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from "vitest"; +import { MockWorkerHandle } from "../src/worker-adapter"; +import { + PrestartedWorkerPool, + prestartedWorkerPoolSize, +} from "../src/prestarted-worker-pool"; + +class TestPhysicalWorker extends MockWorkerHandle { + terminationCount = 0; + + override async terminate(): Promise { + this.terminationCount++; + this.simulateExit(0); + return 0; + } +} + +function testPool(targetSize = 4): { + pool: PrestartedWorkerPool; + physicalWorkers: TestPhysicalWorker[]; +} { + const physicalWorkers: TestPhysicalWorker[] = []; + return { + pool: new PrestartedWorkerPool(() => { + const worker = new TestPhysicalWorker(); + physicalWorkers.push(worker); + return worker; + }, targetSize), + physicalWorkers, + }; +} + +describe("PrestartedWorkerPool", () => { + it("caps pristine runners below larger process-admission limits", () => { + expect(prestartedWorkerPoolSize(2)).toBe(2); + expect(prestartedWorkerPoolSize(24)).toBe(4); + }); + + it("starts its bounded reserve before the first lease", async () => { + const { pool, physicalWorkers } = testPool(2); + + expect(physicalWorkers).toHaveLength(2); + expect(physicalWorkers.map((worker) => worker.sentMessages)).toEqual([ + [], + [], + ]); + await pool.destroy(); + }); + + it("leases every realm once and physically terminates it", async () => { + const { pool, physicalWorkers } = testPool(1); + const firstPhysical = physicalWorkers[0]; + const first = pool.createWorker({ type: "centralized_init", pid: 11 }); + await Promise.resolve(); + const secondPhysical = physicalWorkers[1]; + + // Even a valid terminal fence cannot put a used realm back in the reserve. + firstPhysical.simulateMessage({ type: "memory_quiescent", pid: 11 }); + await first.terminate(); + const second = pool.createWorker({ type: "centralized_init", pid: 12 }); + + expect(secondPhysical).not.toBe(firstPhysical); + expect(firstPhysical.sentMessages).toEqual([ + { type: "centralized_init", pid: 11 }, + ]); + expect(secondPhysical.sentMessages).toEqual([ + { type: "centralized_init", pid: 12 }, + ]); + expect(firstPhysical.terminationCount).toBe(1); + + await second.terminate(); + await pool.destroy(); + }); + + it("retires a failed pristine worker instead of leasing it", async () => { + const { pool, physicalWorkers } = testPool(1); + physicalWorkers[0].simulateError(new Error("module failed")); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const lease = pool.createWorker({ type: "centralized_init", pid: 21 }); + + expect(physicalWorkers).toHaveLength(2); + expect(physicalWorkers[0].terminationCount).toBe(1); + expect(physicalWorkers[1].sentMessages).toEqual([ + { type: "centralized_init", pid: 21 }, + ]); + await lease.terminate(); + await pool.destroy(); + }); + + it("terminates pristine and leased workers at machine teardown", async () => { + const { pool, physicalWorkers } = testPool(2); + pool.createWorker({ type: "centralized_init", pid: 31 }); + await Promise.resolve(); + + expect(physicalWorkers).toHaveLength(3); + await pool.destroy(); + expect(physicalWorkers.map((worker) => worker.terminationCount)).toEqual([ + 1, + 1, + 1, + ]); + expect(() => pool.createWorker({ pid: 32 })).toThrow(/destroyed/); + }); + + it("rejects invalid reserve sizes without starting a worker", () => { + let creations = 0; + expect(() => new PrestartedWorkerPool(() => { + creations++; + return new TestPhysicalWorker(); + }, -1)).toThrow(/invalid prestarted worker pool size/); + expect(creations).toBe(0); + }); +}); diff --git a/host/test/shell-lazy-archive-inputs.test.ts b/host/test/shell-lazy-archive-inputs.test.ts index 974719b9c5..fdadb5c18a 100644 --- a/host/test/shell-lazy-archive-inputs.test.ts +++ b/host/test/shell-lazy-archive-inputs.test.ts @@ -669,9 +669,15 @@ describe("declared shell lazy-archive inputs", () => { (name) => !runtimeSupport.base_formula_order.includes(name), ), ); - expect(runtimeSupport.additional_formula_order).toContain( - "kandelo-dev/tap-core/ruby", - ); + expect(runtimeSupport.additional_formula_order).toEqual([]); + expect(runtimeSupport.formula_roots).not.toContainEqual( + expect.objectContaining({ package: "kandelo-dev/tap-core/ruby" }), + ); + expect(runtimeSupport.activation.bootstrap_package.outputs).toEqual([ + "homebrew-bootstrap.zip", + "homebrew-brew.env", + "homebrew-portable-ruby.zip", + ]); const auditedFormulae = [ ...runtimeSupport.availability.reusable_public_abi42, ...runtimeSupport.availability.requires_rebuild, diff --git a/host/test/vfs/host-fs-path-resolution.test.ts b/host/test/vfs/host-fs-path-resolution.test.ts index 2da92fc605..a7f45d341f 100644 --- a/host/test/vfs/host-fs-path-resolution.test.ts +++ b/host/test/vfs/host-fs-path-resolution.test.ts @@ -61,6 +61,23 @@ describe("HostFileSystem component-wise path resolution", () => { expect(existsSync(join(root, "target.txt"))).toBe(true); }); + it("interprets absolute symlinks in the guest namespace even when the host target exists", () => { + writeFileSync(join(root, "guest-target"), "g"); + mkdirSync(join(root, "host-collision")); + writeFileSync(join(root, "host-collision", "guest-target"), "native"); + + const collidingMountPoint = join(root, "host-collision"); + symlinkSync( + join(collidingMountPoint, "guest-target"), + join(root, "absolute-collision"), + ); + const collidingHostFs = new HostFileSystem(root, collidingMountPoint); + + // Native realpath resolves the link to root/host-collision/guest-target, + // but the guest-absolute target names /guest-target within this mount. + expect(collidingHostFs.stat("/absolute-collision").size).toBe(1); + }); + it("does not follow a final symlink for an exclusive create", () => { symlinkSync("created-through-link", join(root, "exclusive-link")); diff --git a/host/test/worker-main-thread-patch.test.ts b/host/test/worker-main-thread-patch.test.ts new file mode 100644 index 0000000000..2af0a88967 --- /dev/null +++ b/host/test/worker-main-thread-patch.test.ts @@ -0,0 +1,62 @@ +import { execFileSync } from "node:child_process"; +import { + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { patchWasmForThread } from "../src/worker-main"; + +describe("thread Wasm patching", () => { + const fixtureDirectory = mkdtempSync(join(tmpdir(), "kandelo-thread-patch-")); + let programBytes: ArrayBuffer; + + beforeAll(() => { + const watPath = join(fixtureDirectory, "exported-ctors.wat"); + const wasmPath = join(fixtureDirectory, "exported-ctors.wasm"); + writeFileSync( + watPath, + `(module + (func $__errno_location (result i32) + i32.const 7) + (func $__wasm_call_ctors) + (func $__wasm_init_memory) + (func $__abi_version (result i32) + call $__errno_location) + (func $__get_channel_base_addr (result i32) + call $__errno_location) + (func $_start + call $__errno_location + drop) + (start $__wasm_init_memory) + (export "__errno_location" (func $__errno_location)) + (export "__wasm_call_ctors" (func $__wasm_call_ctors)) + (export "__abi_version" (func $__abi_version)) + (export "__get_channel_base_addr" (func $__get_channel_base_addr)) + (export "_start" (func $_start)))\n`, + ); + execFileSync("wat2wasm", [watPath, "-o", wasmPath]); + const bytes = readFileSync(wasmPath); + programBytes = bytes.buffer.slice( + bytes.byteOffset, + bytes.byteOffset + bytes.byteLength, + ) as ArrayBuffer; + }); + + afterAll(() => { + rmSync(fixtureDirectory, { recursive: true, force: true }); + }); + + it("prefers an exported constructor over a frequently called helper", async () => { + const patched = patchWasmForThread(programBytes); + const module = await WebAssembly.compile(patched); + const instance = await WebAssembly.instantiate(module); + const errnoLocation = instance.exports.__errno_location as () => number; + + expect(errnoLocation()).toBe(7); + }); +}); diff --git a/host/test/worker-quiescence.test.ts b/host/test/worker-quiescence.test.ts index 8928ac68bb..1a5c476ce0 100644 --- a/host/test/worker-quiescence.test.ts +++ b/host/test/worker-quiescence.test.ts @@ -4,6 +4,7 @@ import { createWorkerQuiescence, runWithProcessWorkerQuiescence, waitForExecRetirement, + waitForThreadExecRetirement, waitForWorkerQuiescence, } from "../src/worker-quiescence"; @@ -100,4 +101,42 @@ describe("exact process Worker quiescence fences", () => { quiescence.settle(); await expect(complete).resolves.toBe(true); }); + + it("accepts a raced ordinary thread exit only with final quiescence", async () => { + vi.useFakeTimers(); + const retirement = createWorkerQuiescence(); + const threadExit = createWorkerQuiescence(); + const quiescence = createWorkerQuiescence(); + const waiting = waitForThreadExecRetirement( + retirement, + threadExit, + quiescence, + 25, + ); + + threadExit.settle(); + await vi.advanceTimersByTimeAsync(24); + expect(quiescence.settled).toBe(false); + quiescence.settle(); + + await expect(waiting).resolves.toBe(true); + expect(retirement.settled).toBe(false); + }); + + it("does not mistake quiescence without a terminal disposition for safety", async () => { + vi.useFakeTimers(); + const retirement = createWorkerQuiescence(); + const threadExit = createWorkerQuiescence(); + const quiescence = createWorkerQuiescence(); + const waiting = waitForThreadExecRetirement( + retirement, + threadExit, + quiescence, + 25, + ); + quiescence.settle(); + + await vi.advanceTimersByTimeAsync(25); + await expect(waiting).resolves.toBe(false); + }); }); diff --git a/images/vfs/scripts/build-homebrew-vfs-image.ts b/images/vfs/scripts/build-homebrew-vfs-image.ts index a376957258..8a2474c567 100644 --- a/images/vfs/scripts/build-homebrew-vfs-image.ts +++ b/images/vfs/scripts/build-homebrew-vfs-image.ts @@ -37,7 +37,6 @@ import { homebrewRuntimeLayerDescriptorAsset, parseHomebrewLazyLayerBasePackageSource, } from "../../../host/src/homebrew-lazy-layer"; -import { KANDELO_HOMEBREW_GUEST_LAYOUT } from "../../../host/src/homebrew-guest-layout"; import { fetchHomebrewBottleBytes } from "../../../host/src/homebrew-vfs-fetch"; import { planFederatedHomebrewVfs, @@ -52,6 +51,8 @@ import { parseHomebrewRuntimeSupportContract, type HomebrewRuntimeSupportContract, } from "../../../host/src/homebrew-runtime-support"; +import { deriveHomebrewPortableRubyTree } from + "../../../host/src/homebrew-portable-ruby"; import { MemoryFileSystem, type VfsImageMetadata, @@ -116,6 +117,7 @@ interface CliOptions { bottleMirrorOut?: string; packageTreeSpec?: string; packageTreeArchive?: string; + homebrewPortableRubyArchive?: string; homebrewBootstrapEnv?: string; homebrewRuntimeSupport?: string; materializePackageTree: boolean; @@ -173,15 +175,6 @@ const SHARED_FS_BLOCK_BYTES = 4096; const HOMEBREW_COMPOSITION_PATH = "/etc/kandelo/homebrew-vfs.json"; const HOMEBREW_BOOTSTRAP_ENV_PATH = "/etc/homebrew/brew.env"; const HOMEBREW_BOOTSTRAP_ENTRYPOINT = "/usr/bin/brew"; -const HOMEBREW_BOOTSTRAP_PREFIX = KANDELO_HOMEBREW_GUEST_LAYOUT.prefix; -const HOMEBREW_BOOTSTRAP_TARGET = `${HOMEBREW_BOOTSTRAP_PREFIX}/bin/brew`; -const HOMEBREW_BOOTSTRAP_MUTABLE_PATHS = [ - KANDELO_HOMEBREW_GUEST_LAYOUT.cellar, - `${HOMEBREW_BOOTSTRAP_PREFIX}/Library/Taps`, - `${HOMEBREW_BOOTSTRAP_PREFIX}/var/homebrew/linked`, - `${HOMEBREW_BOOTSTRAP_PREFIX}/var/homebrew/locks`, - "/home/user/.cache/Homebrew", -] as const; const MAX_HOMEBREW_BOOTSTRAP_ENV_BYTES = 1024; const MAX_SIDECAR_JSON_BYTES = 16_777_216; const MAX_BREWFILE_BYTES = 65_536; @@ -246,10 +239,10 @@ export interface HomebrewBootstrapConsumerState { }; entrypoint: { path: typeof HOMEBREW_BOOTSTRAP_ENTRYPOINT; - target: typeof HOMEBREW_BOOTSTRAP_TARGET; + target: string; }; ownership: { - prefix: typeof HOMEBREW_BOOTSTRAP_PREFIX; + prefix: string; uid: 1000; gid: 1000; mutable_paths: string[]; @@ -452,6 +445,12 @@ export async function runHomebrewVfsImageBuilder( state: "deferred" | "materialized"; } | undefined; + let portableRubyTree: + | { + derived: DerivedPackageDeferredZipTree; + state: "deferred" | "materialized"; + } + | undefined; let homebrewBootstrapConsumerState: HomebrewBootstrapConsumerState | undefined; if (options.packageTreeSpec !== undefined) { @@ -472,13 +471,63 @@ export async function runHomebrewVfsImageBuilder( prepareHomebrewBootstrapConsumerNamespace(fs, derived); } const registered = registerPackageDeferredZipTree(fs, derived); + let portableRubyRegistered: + | ReturnType + | undefined; + let portableRubyArchiveBytes: Uint8Array | undefined; + if (options.homebrewPortableRubyArchive !== undefined) { + portableRubyArchiveBytes = readPackageTreeArchive( + options.homebrewPortableRubyArchive, + ); + const portableRuby = deriveHomebrewPortableRubyTree( + derived, + archiveBytes, + portableRubyArchiveBytes, + ); + if ( + basename(options.homebrewPortableRubyArchive) !== + portableRuby.descriptor.package.output + ) { + throw new Error( + `Homebrew portable Ruby archive must be named ` + + portableRuby.descriptor.package.output, + ); + } + portableRubyRegistered = registerPackageDeferredZipTree( + fs, + portableRuby, + ); + portableRubyTree = { + derived: portableRuby, + state: options.materializePackageTree + ? "materialized" + : "deferred", + }; + } if (options.materializePackageTree) { await materializePackageDeferredZipTree(fs, registered, archiveBytes); + if ( + portableRubyRegistered !== undefined && + portableRubyArchiveBytes !== undefined + ) { + await materializePackageDeferredZipTree( + fs, + portableRubyRegistered, + portableRubyArchiveBytes, + ); + } } const state = options.materializePackageTree ? "materialized" : "deferred"; packageTree = { derived, state }; if (runtimeSupport === undefined) { assertPackageDeferredZipTreeState(fs, derived, state); + if (portableRubyTree !== undefined) { + assertPackageDeferredZipTreeState( + fs, + portableRubyTree.derived, + portableRubyTree.state, + ); + } } if (homebrewBootstrapEnv !== undefined) { homebrewBootstrapConsumerState = installHomebrewBootstrapConsumerState( @@ -502,6 +551,9 @@ export async function runHomebrewVfsImageBuilder( [ ...materializedBuild.runtimeSupportAtomicMembers, packageTree.derived.descriptor.id, + ...(portableRubyTree === undefined + ? [] + : [portableRubyTree.derived.descriptor.id]), ], ); // WHY: package-tree state validation serializes the lazy registry. A @@ -512,6 +564,13 @@ export async function runHomebrewVfsImageBuilder( packageTree.derived, packageTree.state, ); + if (portableRubyTree !== undefined) { + assertPackageDeferredZipTreeState( + fs, + portableRubyTree.derived, + portableRubyTree.state, + ); + } } materializedBuild?.assert(fs); if (shellConfig) { @@ -559,7 +618,12 @@ export async function runHomebrewVfsImageBuilder( ...(packageTree === undefined ? {} : { - packageDeferredTrees: [packageTreeBinding(packageTree)], + packageDeferredTrees: [ + packageTreeBinding(packageTree), + ...(portableRubyTree === undefined + ? [] + : [packageTreeBinding(portableRubyTree)]), + ], }), ...(homebrewBootstrapConsumerState === undefined ? {} @@ -693,6 +757,13 @@ export async function runHomebrewVfsImageBuilder( packageTree.state, ); } + if (portableRubyTree !== undefined) { + assertPackageDeferredZipTreeState( + restored, + portableRubyTree.derived, + portableRubyTree.state, + ); + } if (homebrewBootstrapConsumerState !== undefined) { assertHomebrewBootstrapConsumerState( restored, @@ -812,7 +883,12 @@ export async function runHomebrewVfsImageBuilder( ...(packageTree === undefined ? {} : { - package_deferred_trees: [packageTreeBinding(packageTree)], + package_deferred_trees: [ + packageTreeBinding(packageTree), + ...(portableRubyTree === undefined + ? [] + : [packageTreeBinding(portableRubyTree)]), + ], }), ...(homebrewBootstrapConsumerState === undefined ? {} @@ -1000,6 +1076,12 @@ function parseArgs(args: string[]): CliOptions { } options.packageTreeArchive = requireValue(args, ++i, arg); break; + case "--homebrew-portable-ruby-archive": + if (options.homebrewPortableRubyArchive !== undefined) { + usage("--homebrew-portable-ruby-archive may be provided only once"); + } + options.homebrewPortableRubyArchive = requireValue(args, ++i, arg); + break; case "--homebrew-bootstrap-env": if (options.homebrewBootstrapEnv !== undefined) { usage("--homebrew-bootstrap-env may be provided only once"); @@ -1094,6 +1176,7 @@ function parseArgs(args: string[]): CliOptions { options.materializationPolicy === undefined || options.catalogCommit === undefined || options.packageTreeSpec === undefined || + options.homebrewPortableRubyArchive === undefined || options.homebrewBootstrapEnv === undefined || options.materializePackageTree || !existsSync(options.homebrewRuntimeSupport) @@ -1101,7 +1184,8 @@ function parseArgs(args: string[]): CliOptions { ) { usage( "--homebrew-runtime-support requires an existing contract, " + - "--catalog-commit, a deferred package bootstrap with its environment, " + + "--catalog-commit, a deferred package bootstrap with portable Ruby " + + "and its environment, " + "and materialized shell composition", ); } @@ -1123,6 +1207,12 @@ function parseArgs(args: string[]): CliOptions { if (options.materializePackageTree && options.packageTreeSpec === undefined) { usage("--materialize-package-tree requires a package tree"); } + if ( + options.homebrewPortableRubyArchive !== undefined && + options.packageTreeSpec === undefined + ) { + usage("--homebrew-portable-ruby-archive requires a package tree"); + } if ( options.homebrewBootstrapEnv !== undefined && options.packageTreeSpec === undefined @@ -1141,6 +1231,15 @@ function parseArgs(args: string[]): CliOptions { ) { usage(`package tree archive does not exist: ${options.packageTreeArchive}`); } + if ( + options.homebrewPortableRubyArchive !== undefined && + !existsSync(options.homebrewPortableRubyArchive) + ) { + usage( + `Homebrew portable Ruby archive does not exist: ` + + options.homebrewPortableRubyArchive, + ); + } if ( options.homebrewBootstrapEnv !== undefined && !existsSync(options.homebrewBootstrapEnv) @@ -1329,26 +1428,29 @@ export function installHomebrewBootstrapConsumerState( environment: Uint8Array, ): HomebrewBootstrapConsumerState { const descriptor = tree.descriptor; + const prefix = descriptor.mount_prefix; + const target = `${prefix}/bin/brew`; if ( descriptor.content_role !== "source-tree" || descriptor.package.name !== "homebrew-bootstrap" || - descriptor.mount_prefix !== HOMEBREW_BOOTSTRAP_PREFIX || - !descriptor.activation.roots.includes(HOMEBREW_BOOTSTRAP_TARGET) || + descriptor.owner.uid !== 1000 || + descriptor.owner.gid !== 1000 || + !descriptor.activation.roots.includes(target) || !descriptor.inventory.some( (entry) => - entry.vfs_path === HOMEBREW_BOOTSTRAP_TARGET && + entry.vfs_path === target && entry.type === "file" && (entry.mode & 0o111) !== 0, ) ) { throw new Error( - "Homebrew bootstrap environment requires the canonical deferred source tree", + "Homebrew bootstrap environment requires its descriptor-owned deferred source tree", ); } // WHY: descriptor ownership alone does not prove the registered VFS still // contains its activation root. Check before writing consumer state so a // deleted tree member cannot leave a new dangling /usr/bin/brew alias. - assertHomebrewBootstrapTarget(fs, HOMEBREW_BOOTSTRAP_TARGET); + assertHomebrewBootstrapTarget(fs, target); for (const path of [ HOMEBREW_BOOTSTRAP_ENV_PATH, HOMEBREW_BOOTSTRAP_ENTRYPOINT, @@ -1361,10 +1463,10 @@ export function installHomebrewBootstrapConsumerState( } ensureDirRecursive(fs, dirname(HOMEBREW_BOOTSTRAP_ENV_PATH)); writeVfsBinary(fs, HOMEBREW_BOOTSTRAP_ENV_PATH, environment, 0o644); - // WHY: Homebrew derives its canonical Kandelo prefix from this public alias. - // Pointing PATH straight at bin/brew appears to work but bypasses that - // launcher contract and can select the wrong prefix or bottle tag. - fs.symlink(HOMEBREW_BOOTSTRAP_TARGET, HOMEBREW_BOOTSTRAP_ENTRYPOINT); + // WHY: Homebrew derives the prefix owned by this authenticated source tree + // from the stable public alias. Pointing PATH straight at bin/brew bypasses + // that launcher contract and can select the wrong prefix or bottle tag. + fs.symlink(target, HOMEBREW_BOOTSTRAP_ENTRYPOINT); const state: HomebrewBootstrapConsumerState = { environment: { path: HOMEBREW_BOOTSTRAP_ENV_PATH, @@ -1373,13 +1475,13 @@ export function installHomebrewBootstrapConsumerState( }, entrypoint: { path: HOMEBREW_BOOTSTRAP_ENTRYPOINT, - target: HOMEBREW_BOOTSTRAP_TARGET, + target, }, ownership: { - prefix: HOMEBREW_BOOTSTRAP_PREFIX, + prefix, uid: 1000, gid: 1000, - mutable_paths: [...HOMEBREW_BOOTSTRAP_MUTABLE_PATHS], + mutable_paths: homebrewBootstrapMutablePaths(prefix), }, }; assertHomebrewBootstrapConsumerState(fs, state); @@ -1443,7 +1545,7 @@ function assertHomebrewBootstrapTarget( } } catch (error) { throw new Error( - "Homebrew bootstrap environment requires the canonical deferred source tree", + "Homebrew bootstrap environment requires its descriptor-owned deferred source tree", { cause: error }, ); } @@ -1454,24 +1556,37 @@ export function prepareHomebrewBootstrapConsumerNamespace( tree: DerivedPackageDeferredZipTree, ): void { if ( + tree.descriptor.content_role !== "source-tree" || tree.descriptor.package.name !== "homebrew-bootstrap" || - tree.descriptor.mount_prefix !== HOMEBREW_BOOTSTRAP_PREFIX + tree.descriptor.owner.uid !== 1000 || + tree.descriptor.owner.gid !== 1000 ) { throw new Error( - "Homebrew bootstrap ownership requires the canonical deferred source tree", + "Homebrew bootstrap ownership requires its descriptor-owned deferred source tree", ); } - for (const path of HOMEBREW_BOOTSTRAP_MUTABLE_PATHS) { + const prefix = tree.descriptor.mount_prefix; + for (const path of homebrewBootstrapMutablePaths(prefix)) { ensureDirRecursive(fs, path); } // WHY: the guest package store belongs to the unprivileged Kandelo user. // Bottle composition initially creates structural prefix directories as // root; adopting the complete prefix here both avoids a false lazy-tree // collision and lets in-guest brew update Cellar, taps, links, and locks. - chownVfsTree(fs, HOMEBREW_BOOTSTRAP_PREFIX, 1000, 1000); + chownVfsTree(fs, prefix, 1000, 1000); chownVfsTree(fs, "/home/user/.cache", 1000, 1000); } +function homebrewBootstrapMutablePaths(prefix: string): string[] { + return [ + `${prefix}/Cellar`, + `${prefix}/Library/Taps`, + `${prefix}/var/homebrew/linked`, + `${prefix}/var/homebrew/locks`, + "/home/user/.cache/Homebrew", + ]; +} + function chownVfsTree( fs: MemoryFileSystem, root: string, @@ -2113,6 +2228,7 @@ function usage(message?: string, code = 2): never { [--homebrew-runtime-support ]] \\ [--package-tree-spec \\ --package-tree-archive \\ + [--homebrew-portable-ruby-archive ] \\ [--homebrew-bootstrap-env ] \\ [--materialize-package-tree]] \\ [--lazy-layer-out \\ diff --git a/libc/glue/dlopen.c b/libc/glue/dlopen.c index 4c73bd3b28..502fd6ba7a 100644 --- a/libc/glue/dlopen.c +++ b/libc/glue/dlopen.c @@ -186,10 +186,14 @@ typedef struct { int dladdr(const void *addr, Dl_info *info) { if (!addr || !info) return 0; - /* In Wasm there are no shared libraries with address ranges to look up. - * Return success with minimal info — the address belongs to the main - * program (the only "module"). */ - info->dli_fname = ""; + /* Wasm function pointers are table indexes, so this C shim cannot recover + * the host linker's per-module symbol record without a new host ABI. The + * address is nevertheless in the main program for ordinary callers of + * dladdr(), and /proc/self/exe is Kandelo's authoritative executable + * symlink. Returning that stable path lets portable runtimes derive their + * install prefix exactly as they do on Linux while leaving unavailable + * symbol-level fields explicitly empty. */ + info->dli_fname = "/proc/self/exe"; info->dli_fbase = (void *)0; info->dli_sname = NULL; info->dli_saddr = NULL; diff --git a/libc/musl-overlay/src/misc/wasm32posix/realpath.c b/libc/musl-overlay/src/misc/wasm32posix/realpath.c new file mode 100644 index 0000000000..e5edd09370 --- /dev/null +++ b/libc/musl-overlay/src/misc/wasm32posix/realpath.c @@ -0,0 +1,41 @@ +#include +#include +#include + +#include "syscall.h" + +char *realpath(const char *restrict filename, char *restrict resolved) +{ + char *output = resolved; + int allocated = 0; + + if (!filename) { + errno = EINVAL; + return 0; + } + if (!output) { + output = malloc(PATH_MAX); + if (!output) return 0; + allocated = 1; + } + + /* WHY: Kandelo's kernel already owns the authoritative namespace walker. + * Upstream musl resolves each component with a separate readlink syscall; + * across the Wasm syscall channel that repeats process/kernel handoffs for + * every ordinary non-symlink component. One existing realpath syscall keeps + * the same POSIX-visible lookup, permission, mount, and symlink semantics in + * the owning kernel while leaving one byte for libc's terminating NUL. */ + long length = __syscall(SYS_realpath, filename, output, PATH_MAX - 1); + if (length < 0) { + if (allocated) free(output); + errno = length == -ERANGE ? ENAMETOOLONG : (int)-length; + return 0; + } + if (length >= PATH_MAX) { + if (allocated) free(output); + errno = ENAMETOOLONG; + return 0; + } + output[length] = 0; + return output; +} diff --git a/packages/registry/homebrew-bootstrap/build-homebrew-bootstrap.sh b/packages/registry/homebrew-bootstrap/build-homebrew-bootstrap.sh index 28a53b8b74..421f2377ea 100755 --- a/packages/registry/homebrew-bootstrap/build-homebrew-bootstrap.sh +++ b/packages/registry/homebrew-bootstrap/build-homebrew-bootstrap.sh @@ -6,8 +6,10 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" OUT_DIR="${WASM_POSIX_DEP_OUT_DIR:-}" SOURCE_CHECKOUT="${WASM_POSIX_BUILD_GIT_HOMEBREW_BREW_DIR:-}" SOURCE_COMMIT="${WASM_POSIX_BUILD_GIT_HOMEBREW_BREW_COMMIT:-}" +RUBY_DEP="${WASM_POSIX_DEP_RUBY_DIR:-}" LOCK="$REPO_ROOT/homebrew/homebrew-bootstrap-source-lock.json" VERIFY="$REPO_ROOT/scripts/verify-homebrew-bootstrap-source-lock.mjs" +DETERMINISTIC_ZIP="$REPO_ROOT/images/vfs/scripts/create-deterministic-zip.sh" if [ -z "$OUT_DIR" ]; then echo "ERROR: homebrew-bootstrap is a resolver-owned build; WASM_POSIX_DEP_OUT_DIR is required" >&2 @@ -17,6 +19,16 @@ if [ -z "$SOURCE_CHECKOUT" ] || [ -z "$SOURCE_COMMIT" ]; then echo "ERROR: homebrew-bootstrap requires build.toml git input homebrew_brew (DIR and COMMIT)" >&2 exit 2 fi +if [ -z "$RUBY_DEP" ] || [ ! -d "$RUBY_DEP" ] || [ -L "$RUBY_DEP" ]; then + echo "ERROR: homebrew-bootstrap requires its resolved Ruby dependency" >&2 + exit 2 +fi +if [ ! -f "$RUBY_DEP/ruby.wasm" ] || [ -L "$RUBY_DEP/ruby.wasm" ] || + [ ! -f "$RUBY_DEP/ruby-runtime.zip" ] || + [ -L "$RUBY_DEP/ruby-runtime.zip" ]; then + echo "ERROR: Ruby dependency omits ruby.wasm or ruby-runtime.zip" >&2 + exit 2 +fi if [ ! -f "$LOCK" ] || [ -L "$LOCK" ]; then echo "ERROR: homebrew-bootstrap source lock must be a regular non-symlink file" >&2 exit 2 @@ -25,6 +37,10 @@ if [ ! -f "$VERIFY" ] || [ -L "$VERIFY" ]; then echo "ERROR: homebrew-bootstrap source-lock verifier must be a regular non-symlink file" >&2 exit 2 fi +if [ ! -x "$DETERMINISTIC_ZIP" ] || [ -L "$DETERMINISTIC_ZIP" ]; then + echo "ERROR: deterministic ZIP producer must be a regular executable" >&2 + exit 2 +fi # shellcheck source=/dev/null source "$REPO_ROOT/scripts/package-build-roots.sh" @@ -111,6 +127,7 @@ export LANG=C ARCHIVE="$BUILD_DIR/homebrew-bootstrap.zip" ENV_FILE="$BUILD_DIR/brew.env" PROVENANCE="$BUILD_DIR/homebrew-source.json" +PORTABLE_RUBY_ARCHIVE="$BUILD_DIR/homebrew-portable-ruby.zip" "$REPO_ROOT/scripts/prepare-homebrew-bootstrap-source.sh" \ --repository "$SOURCE_REPOSITORY" \ --revision "$LOCKED_REVISION" \ @@ -123,15 +140,67 @@ PROVENANCE="$BUILD_DIR/homebrew-source.json" --env "$ENV_FILE" \ --provenance "$PROVENANCE" +# Homebrew's own launcher selects vendor/portable-ruby/current and derives the +# version from this exact source tree. Build that ordinary upstream namespace +# from Kandelo's source-matched Ruby outputs; no deployed Homebrew prefix is +# involved in either the bytes or their eventual mount point. +PORTABLE_RUBY_VERSION="$({ + unzip -p "$ARCHIVE" Library/Homebrew/vendor/portable-ruby-version +} 2>/dev/null)" +if [[ ! "$PORTABLE_RUBY_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(_[0-9]+)?$ ]]; then + echo "ERROR: Homebrew source has an invalid portable Ruby version" >&2 + exit 1 +fi +RUBY_VERSION="${PORTABLE_RUBY_VERSION%%_*}" +RUBY_ABI="${RUBY_VERSION%.*}.0" +RUBY_RBCONFIG="usr/lib/ruby/$RUBY_ABI/wasm32-none/rbconfig.rb" +if ! unzip -p "$RUBY_DEP/ruby-runtime.zip" "$RUBY_RBCONFIG" 2>/dev/null | + grep -Fq "ruby lib version ($RUBY_VERSION)"; then + echo "ERROR: Ruby dependency does not match portable Ruby $RUBY_VERSION" >&2 + exit 1 +fi + +while IFS= read -r member; do + member_path="${member%/}" + if { [[ "$member_path" != usr ]] && [[ "$member_path" != usr/* ]]; } || + [[ "$member_path" == *\\* ]] || + [[ "/$member_path/" == *'/../'* ]] || + [[ "/$member_path/" == *'/./'* ]] || + [[ "/$member_path/" == *'//'* ]]; then + echo "ERROR: Ruby runtime has an unsafe member: $member" >&2 + exit 1 + fi +done < <(unzip -Z1 "$RUBY_DEP/ruby-runtime.zip") + +PORTABLE_STAGE="$BUILD_DIR/portable-ruby-stage" +RUNTIME_STAGE="$BUILD_DIR/ruby-runtime-stage" +mkdir -m 0700 "$PORTABLE_STAGE" "$RUNTIME_STAGE" +unzip -q "$RUBY_DEP/ruby-runtime.zip" -d "$RUNTIME_STAGE" +VERSION_STAGE="$PORTABLE_STAGE/$PORTABLE_RUBY_VERSION" +mkdir -p "$VERSION_STAGE" +cp -R "$RUNTIME_STAGE/usr/." "$VERSION_STAGE/" +if [ -e "$VERSION_STAGE/bin/ruby" ] || [ -L "$VERSION_STAGE/bin/ruby" ]; then + echo "ERROR: Ruby runtime unexpectedly contains bin/ruby" >&2 + exit 1 +fi +mkdir -p "$VERSION_STAGE/bin" +cp "$RUBY_DEP/ruby.wasm" "$VERSION_STAGE/bin/ruby" +chmod 0755 "$VERSION_STAGE/bin/ruby" +ln -s "$PORTABLE_RUBY_VERSION" "$PORTABLE_STAGE/current" +"$DETERMINISTIC_ZIP" "$PORTABLE_STAGE" "$PORTABLE_RUBY_ARCHIVE" + OUTPUT="$KANDELO_PACKAGE_OUT_DIR/homebrew-bootstrap.zip" ENV_OUTPUT="$KANDELO_PACKAGE_OUT_DIR/homebrew-brew.env" +PORTABLE_RUBY_OUTPUT="$KANDELO_PACKAGE_OUT_DIR/homebrew-portable-ruby.zip" if [ -e "$OUTPUT" ] || [ -L "$OUTPUT" ] || - [ -e "$ENV_OUTPUT" ] || [ -L "$ENV_OUTPUT" ]; then + [ -e "$ENV_OUTPUT" ] || [ -L "$ENV_OUTPUT" ] || + [ -e "$PORTABLE_RUBY_OUTPUT" ] || [ -L "$PORTABLE_RUBY_OUTPUT" ]; then echo "ERROR: homebrew-bootstrap output already exists" >&2 exit 1 fi cp "$ARCHIVE" "$OUTPUT" cp "$ENV_FILE" "$ENV_OUTPUT" +cp "$PORTABLE_RUBY_ARCHIVE" "$PORTABLE_RUBY_OUTPUT" node "$VERIFY" \ --lock "$LOCK" \ --package-name "$PACKAGE_NAME" \ @@ -147,4 +216,4 @@ node "$VERIFY" \ --provenance "$PROVENANCE" \ --archive "$OUTPUT" -echo "==> Built provenance-locked Homebrew bootstrap: $OUTPUT + $ENV_OUTPUT" +echo "==> Built provenance-locked Homebrew bootstrap: $OUTPUT + $ENV_OUTPUT + $PORTABLE_RUBY_OUTPUT" diff --git a/packages/registry/homebrew-bootstrap/build.toml b/packages/registry/homebrew-bootstrap/build.toml index 767e5ade43..dd3e2457b7 100644 --- a/packages/registry/homebrew-bootstrap/build.toml +++ b/packages/registry/homebrew-bootstrap/build.toml @@ -2,6 +2,7 @@ script_path = "packages/registry/homebrew-bootstrap/build-homebrew-bootstrap.sh" inputs = [ "packages/registry/homebrew-bootstrap/build-homebrew-bootstrap.sh", "scripts/package-build-roots.sh", + "images/vfs/scripts/create-deterministic-zip.sh", "scripts/prepare-homebrew-bootstrap-source.sh", "scripts/verify-homebrew-bootstrap-source-lock.mjs", "homebrew/homebrew-bootstrap-source-lock.json", @@ -10,7 +11,7 @@ inputs = [ ] repo_url = "https://github.com/Automattic/kandelo.git" commit = "UNPUBLISHED" -revision = 5 +revision = 6 [[git_inputs]] name = "homebrew_brew" diff --git a/packages/registry/homebrew-bootstrap/package.toml b/packages/registry/homebrew-bootstrap/package.toml index e667af9e08..620aa96f6f 100644 --- a/packages/registry/homebrew-bootstrap/package.toml +++ b/packages/registry/homebrew-bootstrap/package.toml @@ -3,7 +3,7 @@ name = "homebrew-bootstrap" version = "6.0.4-3-gd6c1be4" kernel_abi = 41 arches = ["wasm32"] -depends_on = [] +depends_on = ["ruby@4.0.5"] # The portable recipe identifies Homebrew's immutable upstream archive. The # Kandelo project build additionally consumes the same commit through a sealed @@ -32,6 +32,15 @@ name = "homebrew-brew" wasm = "homebrew-brew.env" fork_instrumentation = "disabled" +# Homebrew normally downloads this versioned tree through `vendor-install`. +# Kandelo publishes the source-matched Ruby dependency in the same support +# package so first-use activation can expose the stock upstream layout without +# a network bootstrap or a system-Ruby compatibility path. +[[outputs]] +name = "homebrew-portable-ruby" +wasm = "homebrew-portable-ruby.zip" +fork_instrumentation = "disabled" + [[host_tools]] name = "git" version_constraint = ">=2.30" @@ -43,3 +52,15 @@ name = "node" version_constraint = ">=20.0" probe = { args = ["--version"], version_regex = "v(\\d+\\.\\d+(?:\\.\\d+)?)" } install_hints = { darwin = "run through scripts/dev-shell.sh", linux = "run through scripts/dev-shell.sh" } + +[[host_tools]] +name = "unzip" +version_constraint = ">=6.0" +probe = { args = ["-v"], version_regex = "UnZip ([0-9]+\\.[0-9]+)" } +install_hints = { darwin = "run through scripts/dev-shell.sh", linux = "run through scripts/dev-shell.sh" } + +[[host_tools]] +name = "zip" +version_constraint = ">=3.0" +probe = { args = ["-v"], version_regex = "Zip ([0-9]+\\.[0-9]+)" } +install_hints = { darwin = "run through scripts/dev-shell.sh", linux = "run through scripts/dev-shell.sh" } diff --git a/packages/registry/icu/build-icu.sh b/packages/registry/icu/build-icu.sh index 79ea14e6bb..562722d930 100755 --- a/packages/registry/icu/build-icu.sh +++ b/packages/registry/icu/build-icu.sh @@ -132,11 +132,29 @@ run_logged() { # On Linux, statically fold the GNU C++/GCC runtime into the data tools: the Nix # CI runner has no libstdc++.so.6 on its loader path, so a dynamically linked # icupkg/pkgdata (run here by Stage 2's make) aborts at exec with "cannot open -# shared object file". macOS clang links a self-contained libc++ and rejects the -# flags. LDFLAGS set here is honored: runConfigureICU re-exports it to configure. +# shared object file". On macOS, the declared Nix clang's real installed +# directory is outside LLVM_PREFIX, so clang++ does not discover the unified +# tree's libc++ headers automatically. Select those declared headers and +# libraries explicitly rather than falling back to an ambient Xcode toolchain. +# CXXFLAGS/LDFLAGS set here are honored: runConfigureICU re-exports them to +# configure. +HOST_CXXFLAGS="-O2" case "$(uname -s)" in Linux) HOST_LDFLAGS="-static-libstdc++ -static-libgcc" ;; - *) HOST_LDFLAGS="" ;; + Darwin) + : "${LLVM_PREFIX:?ERROR: LLVM_PREFIX is required for the ICU host build on macOS}" + [ -f "$LLVM_PREFIX/include/c++/v1/memory" ] || { + echo "ERROR: declared host libc++ headers missing below $LLVM_PREFIX" >&2 + exit 1 + } + [ -f "$LLVM_PREFIX/lib/libc++.dylib" ] || { + echo "ERROR: declared host libc++ library missing below $LLVM_PREFIX" >&2 + exit 1 + } + HOST_CXXFLAGS="$HOST_CXXFLAGS -nostdinc++ -isystem $LLVM_PREFIX/include/c++/v1" + HOST_LDFLAGS="-L$LLVM_PREFIX/lib" + ;; + *) HOST_LDFLAGS="" ;; esac if [ ! -x "$HOST_BUILD/bin/icupkg" ] && [ ! -x "$HOST_BUILD/bin/genccode" ]; then echo "==> Stage 1: building ICU natively for host tools + data..." @@ -144,6 +162,7 @@ if [ ! -x "$HOST_BUILD/bin/icupkg" ] && [ ! -x "$HOST_BUILD/bin/genccode" ]; the mkdir -p "$HOST_BUILD" ( cd "$HOST_BUILD" CC="${HOST_CC:-clang}" CXX="${HOST_CXX:-clang++}" \ + CXXFLAGS="$HOST_CXXFLAGS" \ LDFLAGS="$HOST_LDFLAGS" \ "$ICU_SRC/runConfigureICU" MacOSX \ --enable-static --disable-shared \ diff --git a/packages/registry/libcurl/build-libcurl.sh b/packages/registry/libcurl/build-libcurl.sh index 5302a0d7b5..a53791f178 100755 --- a/packages/registry/libcurl/build-libcurl.sh +++ b/packages/registry/libcurl/build-libcurl.sh @@ -238,6 +238,7 @@ mkdir -p "$INSTALL_DIR" audit_wasm_imports() { local wasm_path="$1" local label="$2" + local platform_control="${3:-}" local import_dump declared_import_count wasm_imports parsed_import_count local wasm_import required_import allowed candidate local unexpected_imports=() @@ -259,6 +260,31 @@ audit_wasm_imports() { kernel.kernel_push_argv ) + if [ -n "$platform_control" ]; then + # WHY: this audit owns unresolved references introduced by libcurl, + # not imports intentionally owned by Kandelo's executable SDK. Compare + # against a minimal -ldl executable built by the same toolchain so a + # new process-libc capability does not require duplicating the SDK's + # complete import surface in this package recipe. + while IFS= read -r candidate; do + [ -n "$candidate" ] || continue + case "$candidate" in + env.*|kernel.*) allowed_imports+=("$candidate") ;; + *) + echo "ERROR: libcurl platform control has unsupported import module: $candidate" >&2 + exit 1 + ;; + esac + done < <( + wasm-objdump -x "$platform_control" | + awk ' + /^Import\[[0-9]+\]:$/ { inside = 1; next } + inside && /^[[:alpha:]_][[:alnum:]_]*\[/ { exit } + inside && / <- / { sub(/^.* <- /, ""); print } + ' + ) + fi + import_dump="$(wasm-objdump -x "$wasm_path")" declared_import_count="$( sed -n 's/^Import\[\([0-9][0-9]*\)\]:$/\1/p' <<<"$import_dump" | head -n 1 @@ -304,6 +330,12 @@ audit_wasm_imports() { echo "==> Validated $label import closure ($declared_import_count imports)" } +PLATFORM_CONTROL_SOURCE="$WORK_DIR/kandelo-dlopen-control.c" +PLATFORM_CONTROL_WASM="$WORK_DIR/kandelo-dlopen-control.wasm" +printf '%s\n' 'int main(void) { return 0; }' > "$PLATFORM_CONTROL_SOURCE" +"$CC" -O2 "$PLATFORM_CONTROL_SOURCE" -Wl,--no-gc-sections -ldl \ + -o "$PLATFORM_CONTROL_WASM" + if [ "$RESOLVER_MODE" = "1" ]; then case "$PACKAGE_NAME" in libcurl) @@ -426,7 +458,9 @@ EOF "$OPENSSL_PREFIX/lib/libcrypto.a" \ -ldl \ -o "$SMOKE_WASM" - audit_wasm_imports "$SMOKE_WASM" "libcurl full-member smoke Wasm" + audit_wasm_imports \ + "$SMOKE_WASM" "libcurl full-member smoke Wasm" \ + "$PLATFORM_CONTROL_WASM" for forbidden in "$WORK_DIR" "$REPO_ROOT" "$ZLIB_PREFIX" "$OPENSSL_PREFIX" "$INSTALL_DIR"; do if grep -aFq "$forbidden" "${output_files[@]}"; then @@ -445,7 +479,9 @@ EOF find "$INSTALL_DIR" -type f -print >&2 exit 1 fi - audit_wasm_imports "$INSTALL_DIR/curl.wasm" "curl CLI Wasm" + audit_wasm_imports \ + "$INSTALL_DIR/curl.wasm" "curl CLI Wasm" \ + "$PLATFORM_CONTROL_WASM" for forbidden in "$WORK_DIR" "$REPO_ROOT" "$ZLIB_PREFIX" "$OPENSSL_PREFIX" "$INSTALL_DIR"; do if grep -aFq "$forbidden" "$INSTALL_DIR/curl.wasm"; then echo "ERROR: curl.wasm contains producer path: $forbidden" >&2 diff --git a/packages/registry/mariadb/build-mariadb.sh b/packages/registry/mariadb/build-mariadb.sh index 1cf30f85ce..6f2a929f62 100755 --- a/packages/registry/mariadb/build-mariadb.sh +++ b/packages/registry/mariadb/build-mariadb.sh @@ -150,6 +150,26 @@ if ! host_helpers_ready; then mkdir -p "$HOST_BUILD_DIR" cd "$HOST_BUILD_DIR" + HOST_CXX_FLAGS="${CXXFLAGS:-}" + HOST_EXE_LINKER_FLAGS="${LDFLAGS:-}" + if [ "$(uname -s)" = "Darwin" ]; then + : "${LLVM_PREFIX:?ERROR: LLVM_PREFIX is required for the MariaDB host build on macOS}" + [ -f "$LLVM_PREFIX/include/c++/v1/new" ] || { + echo "ERROR: declared host libc++ headers missing below $LLVM_PREFIX" >&2 + exit 1 + } + [ -f "$LLVM_PREFIX/lib/libc++.dylib" ] || { + echo "ERROR: declared host libc++ library missing below $LLVM_PREFIX" >&2 + exit 1 + } + # WHY: Nix clang's real installed directory is outside the unified + # LLVM_PREFIX tree, so CMake cannot discover that tree's libc++ by + # following the compiler symlink. Keep native build helpers on the + # declared toolchain instead of falling back to ambient Xcode headers. + HOST_CXX_FLAGS="${HOST_CXX_FLAGS:+$HOST_CXX_FLAGS }-nostdinc++ -isystem $LLVM_PREFIX/include/c++/v1" + HOST_EXE_LINKER_FLAGS="${HOST_EXE_LINKER_FLAGS:+$HOST_EXE_LINKER_FLAGS }-L$LLVM_PREFIX/lib" + fi + # `WITH_SSL=OFF` + `CONC_WITH_SSL=OFF`: the host build only # produces helper executables (the import_executables target). # None of those helpers need SSL, but libmariadb's @@ -162,6 +182,8 @@ if ! host_helpers_ready; then # ≥3.4.2 installed (Nix dev shell, fresh CI runner, etc.). cmake "$SRC_DIR" \ -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + "-DCMAKE_CXX_FLAGS=$HOST_CXX_FLAGS" \ + "-DCMAKE_EXE_LINKER_FLAGS=$HOST_EXE_LINKER_FLAGS" \ -DWITH_UNIT_TESTS=OFF \ -DWITH_MARIABACKUP=OFF \ -DPLUGIN_CONNECT=NO \ diff --git a/packages/registry/program-packages.json b/packages/registry/program-packages.json index fc218ce5f6..9395212c96 100644 --- a/packages/registry/program-packages.json +++ b/packages/registry/program-packages.json @@ -4,344 +4,344 @@ "bash": { "manifestSha256": "6478060f28d430d18a6ebe7c603392d35a41d9bcf6bc74322351d1450b9c5335", "cacheKeys": { - "wasm32": "012336da4d490e68d3e200ec8d29e641d5946d6bec4c9e309d6f3bccedf9985b", - "wasm64": "27d9160a2ce6406fb805393c38a0c323a852b705646fd3aca805554bed9ccbfa" + "wasm32": "8fc30f59035df55e9dc2b7ed2359c13faf8d63ec285bf44e6f52c7de1e6b8fca", + "wasm64": "0e787e43b7f21922caa563d14dfcc5e38c92d8b3112040cd68d4d329c5852346" } }, "bc": { "manifestSha256": "a65661463bb7047b91ff00153bd99fd962c96e571934ab4e923f5215fb6ecfbd", "cacheKeys": { - "wasm32": "df5529c0caa8288deff41a9a0a59c17d99e5bf1eea473ffc553f68087ad55ac4", - "wasm64": "667a1b5538258dca474f1f284a3c1494ceff00fa9c0547d68657a9d1a5a8482b" + "wasm32": "9a7242df42960d7d69dab831f169f20626aa10174ada963fce9f801c7df2b125", + "wasm64": "ca11b8397be2a91e3a8f8131dee85b89479c10f77767951421e6f4d186e2a536" } }, "bzip2": { "manifestSha256": "59e1e53f5675e7c9148634933ed0f4c7192743727025cae4e843b6924c489abf", "cacheKeys": { - "wasm32": "12294306f7aa60473f3d83d4e4e2b7796daaf3e8986e9e19704c6ef3826fa83a", - "wasm64": "9cd477784d6ec27e5346d3afe5d742e84ca6d8eb86ed18c457498374660a8c7e" + "wasm32": "a2d2f675316d600f206b8088f199de3683e4cf803e54e0145c49c0864b5330e5", + "wasm64": "cdedbf3aa9cb63cfdf6b63e22415c43f7eb3407fc996da346c78dccf472be143" } }, "coreutils": { "manifestSha256": "b8baabc9af9283434e0d80f7fdc0aaef242ffa31092ab0fc39a0603a12b3cca7", "cacheKeys": { - "wasm32": "c44171215c792087144a3dc8bd76f9a37f2f8311ed3d7b29e0ce1ae8fb79a76a", - "wasm64": "a244a238a2814ec1556b67723c51f1838b268c13e43ac70ecf027b99b86f2c73" + "wasm32": "8aa8d109e92b93b779708d5e28b59cb972e58591cd6aaebb96613d0b99825b38", + "wasm64": "c3c1620a49c925561c593851b7ea7b73935166cbcdbc3cb99a4ca9b8464fc189" } }, "cpython": { "manifestSha256": "7dd4f446697a73941ec940c2ccba4d53be73fe6947f1e701031a4d0aa964c4ff", "cacheKeys": { - "wasm32": "4c75fb0082fa302537b99fc954a66275a680fd86afdcc71df12d6da7ea570c4f", - "wasm64": "8dc337e36f18b48a360c9da15421e379241bdcb4f3f7a1ae9203a69b38b42661" + "wasm32": "f15ab782192b8bd17b2acd4a5ea4dc33a90fc3b9a813f91a919697822b113bc7", + "wasm64": "a65170cb94e27e6b6cc09c5eef34e23e1a63cd8ec8cf571012e43ba14892799e" } }, "curl": { "manifestSha256": "55523d50261f46dc4aaaff458d1cd87c6f96eaecd687a7540ead35c96906366e", "cacheKeys": { - "wasm32": "c84aaa895980c54d92a8b3c9340ce9e85dda9c7866e0ba235e1e38336efeb487", - "wasm64": "d572fff32881fcf6b307d73a50db1b6b109c5d40430e3a636e48b9ac918bffb6" + "wasm32": "84a2431436e708240bef54f2c9a01a8148024b53f90683bf00c663fe81bb8315", + "wasm64": "fa6fc103af1e7354c2be25483da824fcc3edb82e8949a0c7b943cc48d97dffc5" } }, "dash": { "manifestSha256": "10ce1bb611fefd78a6c9ec9038f6113e29a7062b1a73e08eccf9b33004cea9a8", "cacheKeys": { - "wasm32": "226bda53ce2d316d11744de02e555a09e870f3bb5a5ee3d3e79d8a47f0612bef", - "wasm64": "4eef9693a9055b740c4bdf1fff1e66b7973295eff2fae81f97509288a1a83ecf" + "wasm32": "0cb537cf7cc09bbadf377821dfb68c3694d3db2ddcd0d1645f7a61126c5efda4", + "wasm64": "db16863f590647f1526b4e8743d0e1d493d2dba4f8ea39a49411fb0c3f355080" } }, "diffutils": { "manifestSha256": "3a78f0a46bae43ce5ea235c6638c2cbd1d8559b0b62b1fb42443b35968c1aca3", "cacheKeys": { - "wasm32": "5ade22f17ebe371d663fd23ad99b1a8422da350d40e2576eba114a6d9e0276c9", - "wasm64": "fb4aafd32770befcc41f1f557313003949a019cdd98b2d1b7ed6575cf97c6ffd" + "wasm32": "ad399566e397f6fa75dabf30380ff57183ea7ad59a1bf30297080c23cd581abe", + "wasm64": "172c331dbcf97fc282b5bf2713979d02660e3e9944e680b51a29e469a428b319" } }, "dinit": { "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", "cacheKeys": { - "wasm32": "db68fc369364580e6690bb1097fa27d4eb7a1449574f24b6ab1202c60ff51f1b", - "wasm64": "b6cb0cfbac1f98938650b2b6a05c2daf131d7767e60de67258c438caec8eb27f" + "wasm32": "e986afadf2ced00ecefa5a80b79ab1063ede97d23943c078628e3d689854b276", + "wasm64": "970ae5ab98c626cedc9c708568ffb3e0d0547684ebf1de2bbfe233b843b41f3e" } }, "erlang": { "manifestSha256": "475d6037e73a0f1e2f4381067194b2422751206979ea17209e10efa5a2a93bbb", "cacheKeys": { - "wasm32": "833271b798874daa6a0bb0bbf47ea113a24408829734b847a2808dd12b294e94", - "wasm64": "e6871239c53b408c7d1581b3feade963d9d70e7e8fb60df1e3f6a7110bce1247" + "wasm32": "d0ab28208e0f0068097155e12f6bcfde1c568eb3a5c28edb9a8669d3da786980", + "wasm64": "70cf31ac8759a2aad309568ae918f1a87c344c9a2784f66e137267dfb139794a" } }, "erlang-vfs": { "manifestSha256": "15efb74f1825e2b85bedfeb8d98c19fa648c4be39cb9defb65330a8f21eb9141", "cacheKeys": { - "wasm32": "045291d840ad92dd9f388a310d60fa582d9cccd1870c6fc6b01761db2075f6f5", - "wasm64": "8bd45c17a38be600b77fb927da838d5baee15e90ca8f9e23c5402c31a372fee5" + "wasm32": "98ce82948fcdc266c464c81ad82b3b8e0a4438149c38717cb9385dc83ad0180d", + "wasm64": "ad252d1a06ccdc58b61fa4fa5aa13303b7150998408aaff4da1c3be6149e4e7f" } }, "fbdoom": { "manifestSha256": "a00e0d9c84fcdbb3bd95f296cb3422d60b86dcff4c40734eea1bb0bec4c7d902", "cacheKeys": { - "wasm32": "1bf36f090735afd713cec6752a60cc4a5a1cd95abf9774efa947b5e1de918e63", - "wasm64": "80e54febab71f5536d02f72130e8d4f8960fe5855280aee067fc73efaf7a1caf" + "wasm32": "1b6ccf0a23fd165495effd479ab117c68e55c723e8e0b21dddcb6de7e808be42", + "wasm64": "10dd2abd40f3074be30a1808be5c4370687409d5cbb677a0cd2eb1efef240e40" } }, "file": { "manifestSha256": "7874c1affcbbf2c8ab8c8d4beb087f275af57b5caae6a8947bf5a63a181552b2", "cacheKeys": { - "wasm32": "73f41f2eb367d94a91642c73568d97a78afc53bbb700f81f518b502dce5733fe", - "wasm64": "53aa4f002bb33a38cd1fd5f706818d0e5f946cffd7eb88a24de9b784f9799c9e" + "wasm32": "2cdf71f1ed37aa53ea355f7dadc12f8ae041343b6941c0476d58d2ce764581b3", + "wasm64": "54cd887cfc7ee65b2fa7e2f5948f2f3baa133aca5c8ff26ce377fe6f70ada66d" } }, "findutils": { "manifestSha256": "cceedf52aea67fbb0da03cfce6f2e9b1a7b5561fa1656c2762b43c651a625d02", "cacheKeys": { - "wasm32": "a40215f2320308d34213e516c8d27af1daa652489451983297ef64f937bff3eb", - "wasm64": "73a0639d607ccc6bb61f3eedbf95e6d61b5126e77b95f62bd662afcd30981b22" + "wasm32": "a83aa55d45a41dcc635cdcb8c52f4309903b5c3ec843717f55bbdf1f9772e62a", + "wasm64": "e8d16880d449e28fccc265a7f68df16083d3151504c3317bed5df78e9cf42eff" } }, "gawk": { "manifestSha256": "a2567b8b0778805e385e1d3a41ac8b5d74aaf9d293b222001b17d09b11e5a0ba", "cacheKeys": { - "wasm32": "bb92b0fa58c8f18aae36dcc635a47df0ca25c12ee94df12db3d8e854bec07b5b", - "wasm64": "8a8686860d750d3439073bebe3193895e273d1c5b08f87e0c1a0a3c3629f8db9" + "wasm32": "1996fcf770d124ed1355db80f34271a30676afed43e439c19dcf591f435800c6", + "wasm64": "96de51b61cc2531f1b3bcb2aa629626f14fae3a80a43fed18e44f349d0d39b42" } }, "git": { "manifestSha256": "c2bc79e62c9a4e840ae94a16af0f41070460eaf3976a990dc60d2db977bee952", "cacheKeys": { - "wasm32": "8f0b85ba323e76be9007adcb663ef3c6e3ed4ae3cb69b200b360406f4b72acae", - "wasm64": "d402063e5e715b3d4282f46360f6c603c73d3e8db786c31e55dfd9ef7745413a" + "wasm32": "ec535423cc99a1531ebe8f4114a28636d26164c936d4649f042d01a49f76c082", + "wasm64": "0d6b5de97c769b92c877c2c88172ba0fa508f394f0913d0efa816cc076f5874e" } }, "grep": { "manifestSha256": "59270d3bfb33167b32d13246bf855b49308f8c9c0a66d9635c804f702421fbc6", "cacheKeys": { - "wasm32": "95afdd724b558edb84d7d236bc1f00af8c2a7aff386641feefa96c49793a1f45", - "wasm64": "a48b6bd6ad896f0d476f897f8e9b0a32981332b9304c5d2ddf9360e4e4a71b9e" + "wasm32": "8a78952c49537c96e7a0fd6837d23abed09ebf4fa67c0879b143316a043b2d15", + "wasm64": "73a649eb99d1e2188e0464eb2cf83587806fc6fd95424dc4e8f75fc59da7968b" } }, "gzip": { "manifestSha256": "33853ebe2301caf2979b667830e6c5afc5256f8717137624f08079e6a27f370a", "cacheKeys": { - "wasm32": "c38775ccc4e9b7460cec5de0001df43867c459e87722d6d133242e439f330da8", - "wasm64": "776601a5b90a73a914fae4397288860fb3487aad28d97f2152bb1366d8109e44" + "wasm32": "60030e233799c2bf5af1ccdc0919768cf0a7a9408bbf8670969c696b2ef22b35", + "wasm64": "a7fb6a22732a61bbd33c5578447d5e2734bda8b1045c26c6bd5879c9604abc16" } }, "homebrew-bootstrap": { - "manifestSha256": "183004a8385ab5afc388cd43401341b82812c67f97b08ad349ebd7f4dbbebd0f", + "manifestSha256": "b76e60692c6b1aad8d2373f9439465797a0549156c65ba0e1efe03a80955e52d", "cacheKeys": { - "wasm32": "ff2d8b4e5d86a43c21047b30465abd91857f09faa5ea4be55908a22bf025ebd5", - "wasm64": "f4b2ef0b0d3abea01662d68a63308b9cee01235aa2b442e469f8422cc41a43f6" + "wasm32": "03723a898603bb0eec70e4fc57c70ee7590081db1ed7087454e92e75ee712786", + "wasm64": "17c254a4e8193267e81ba16987348f8dbc39e7cc978a63cd1ce1c43a76c3e6c0" } }, "icu": { "manifestSha256": "f5b1f02d169ec1108fc32d9da55be53c3efb96e3a7aeba6a03aec62e0175f0dc", "cacheKeys": { - "wasm32": "4ac7eecb23c1a8df06db4aed05430e068c0384c3f86fad423d7513225c456966", - "wasm64": "9bcecadb19e7d77c6b5e0393a68cbfdcd711179bac82f3fce0c6b5a8ddc1d81b" + "wasm32": "dccdd3d70beae9d23d22bbdebf95a4ab509ec713de30eb7d86d0193ecaea4c09", + "wasm64": "5bc4c577ac7b5014674e985e1eb3c826e4255634e052a11bcd518fc27ee99937" } }, "kandelo-sdk": { "manifestSha256": "c081879f1becd855917cff96f17429bcf2b9fd61bf95211eca9ff8fb625bb2eb", "cacheKeys": { - "wasm32": "5d2aa9c9dcc3383037d21566dea7e2b7b4cce340068e0daa8bcc868e315d239c", - "wasm64": "fb52587f0758ff62fce309f9d47957a020bed06575677b99bcdf2a288921dc8d" + "wasm32": "921c21d90f7b778e4f5a58d171e6e59cf92a3d1b4753388e285eff5d97668534", + "wasm64": "e75e4aadda806ebdb7c3b6553285552725da385e4dd4678fe740c3cc0c3bdc72" } }, "kernel": { "manifestSha256": "db1d66db8575562ac7b3e720dbaa06d7dd5eef577d80220ea4167dc2d69b863b", "cacheKeys": { - "wasm32": "7c99137ecc1ba603403044403f0c9ac40fd92b6313414cf44e9eb0f1aca9015b", - "wasm64": "c0af24ad2a86912c4bc1981e1cb1427c207caf81db2bfacd700a9b012cdab003" + "wasm32": "e5180ca2c3ea83218b416c1d691ab2b549017eee1848ee2432df9af6c7d2b570", + "wasm64": "4262c431ccfd91320d0fe0128999d07f0089d79b1bca6aa032ddcf85d712f5ec" } }, "lamp": { "manifestSha256": "250b64635d64f8537178188fb5488dbfd2980d79a1c2ffbf346af67008088ba0", "cacheKeys": { - "wasm32": "2c204bb8f4a17372f19dcd2960286471eb63f418813125f6b8eb2f6914b948cc", - "wasm64": "474098b120dba94b459c5a725c3f53ecb396b2e3259a43a11842294d7121135f" + "wasm32": "b210502fef09fd12552380a9a87ab749fa9dface329bdd43bd0b93b1672dc429", + "wasm64": "a3294383c8f69a3c0a56ef53750c2710973fd898226020b8836b744a86c8524b" } }, "less": { "manifestSha256": "996d61545cfe83dcb663a33e8763e0edbd4db4a605fd69a91b243cf79b1f9b17", "cacheKeys": { - "wasm32": "84d1739981ea64e5f9c23cd586c4840d412737b235ba41875c1d4cafaecbf88b", - "wasm64": "3797cfcf88521e4dff04b24a89db79fa1c162c215fd6ccdf622f4b9f2f45956a" + "wasm32": "d371625245526c280a91a49ec0a819fb8827655fcc896bf7d60836a71222a38c", + "wasm64": "b5c2d56fa4bec21cf5bb82a8983e9a440f18510f0adab5d288d13aaeb726764f" } }, "libcurl": { "manifestSha256": "c771e6cdc83b43840db4dd1fdce4b6189ba4f281596f7485bcda9c65e2ecda00", "cacheKeys": { - "wasm32": "5992711bf6f9c74f856802f2bb42dd089857daffa04ff15955c5a68562669b87", - "wasm64": "d5cd56d278270946676db028ae25fd6bf670081b1d324981fd982081c2afadf9" + "wasm32": "f4f2e193219fee442db65d63e8ff29b47b72caf11ca1b5cb81ea552b24c2263d", + "wasm64": "00d46deeff9e20152c70a1031ca5c2099876a7d66e9c9e3c8f22d3d9878d354d" } }, "libcxx": { "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", "cacheKeys": { - "wasm32": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2", - "wasm64": "6f624f5476751d07bb58b04608a389400c079fd74c7e62ef52306d89f5f8754d" + "wasm32": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7", + "wasm64": "cb7c9eadcc32d322fc339ddeb06eac0a5377423bd9d92a478c6cf1e71e0ec7b3" } }, "libiconv": { "manifestSha256": "fa60b386f4ba0cd5bbbd595f67477cbfb0cc401a14bbd4769630c502bb3f24b1", "cacheKeys": { - "wasm32": "46ac767af4b028b8ff540560f1b608b5c9ad75f9859055e322a3dd012eb5390f", - "wasm64": "8069cbedf0aa2663a94120fffa759a04a52a41f74d311454ed3ccaae67b49aaf" + "wasm32": "0a7f0c9fb38e897230d1798c43cded55bd9c8ae4e83cbd28b7526d677f8deac4", + "wasm64": "79ccfd673c953f0eb3136ab40d3eeb53bfd2f1f3e12633457cb503724cc46bbd" } }, "libpng": { "manifestSha256": "79ed5c5c072a0267cce5c7a2fe37d8494571b17e44b952f619e04ec1c4a9db10", "cacheKeys": { - "wasm32": "b9effc903d2078f80bbd232461d7cc43ce8162826fc5dbeed275ab2336a5bfbb", - "wasm64": "8a9ce636b362d5cc86e6c7e4da7d905c947322892cea29406da80251e123f9cb" + "wasm32": "7ff7620c938207726786eed2bb33a7af995be614c8b1f3bb31cb352bcdfafbf8", + "wasm64": "1467147e2ac99813d1ca17901141a850f90ef83c62c5513f100f48075e78bc68" } }, "libxml2": { "manifestSha256": "908490efcdc7783c76ce36af31d1db24bfcc7ba5f1e7b9b5298cc81cb110256c", "cacheKeys": { - "wasm32": "a36d4055cf9c608654dace57fe18209597eb630c9bc7a1f9d4f320f2e7b885d8", - "wasm64": "a82e674086362f2223452d689d84ddd95f2bf77efc096c7e433e068a83769c6e" + "wasm32": "934a6aaaedd65011653013293f4b87532f68068d58d72fbe5559097835739e4b", + "wasm64": "61ea74b05513b8753e23bff60277bd33115ca1def27b8b2849d2356147c0ac78" } }, "libzip": { "manifestSha256": "83c65d63a6416e79436bf4e792759a4701d568df0e8c010394a9732cf659f0cc", "cacheKeys": { - "wasm32": "94ea97db7a56b44127bf30e29ae40c7d6d069ac010d6295b654a04d8f8fcbbc0", - "wasm64": "9039c5771804411a6cae506b94ca2af76d92aca4e52912a0d38b4f5c04144128" + "wasm32": "e5adb5a22165e93c595754d059dba976f8e2600376d8c9da157205fa8cab614b", + "wasm64": "9e9d1f610471dfab7d3b1b9bc382b3b6ba0a8b6eb7f1d1cd2c337d017059c193" } }, "lsof": { "manifestSha256": "cfd199bbe082435f7a2e703e2738a368af1eeb5b76b6e93c376054f21ce94419", "cacheKeys": { - "wasm32": "36e3ef4f5bd7ad67194224ae62b77726e51849336f56b83f1d4d2d68badcab87", - "wasm64": "c83cc88835cf7004de1eefb5e2f8d9651b520d6872e6ecee71a12cbb0b9e6c27" + "wasm32": "26e86352794f5faf1f9580d48bdf4577db7c74df82f8c62a2efd72bc41468f0a", + "wasm64": "3a44fdc47adf33c696327705025060997c5845651c9e5b636aa2620779182771" } }, "m4": { "manifestSha256": "2c6582d99d6eabfb9da52badf49b899e9fdcba76a728536b25bc3741f3331543", "cacheKeys": { - "wasm32": "e26cfe10cb77fd905bcf69767d22824ee51bebef986fec841b78673e433e8e5f", - "wasm64": "75a8b8f0c089d37708351e3c9e864e8728822e83b169e8b00c12ee6ec4385f3d" + "wasm32": "8f298e218fac8d3cf3a7828d311703c6b232020086851c3bf1aae9021f7379bf", + "wasm64": "0d546dde6b05e407894bbbf284749bf180b520f1fcbb4b6a733832250af8b78f" } }, "make": { "manifestSha256": "f878d2d730f36a4c6dfe1fff1b4ccc704757ea22d8c4c8ccb95b11cd641e5fed", "cacheKeys": { - "wasm32": "6450f1427f20639676c13843ab492dacf77a00deac3bf3716e3ef0a49b10612a", - "wasm64": "d48908bf7ec8cd6f3bb1866d029883a32fb53dc6ab5f8ecd34174b14aac9fc09" + "wasm32": "715f3f5c96e4e5944bbc3f55ab3c6d8cc4b0133abc09265b6acae9be4cd863e7", + "wasm64": "dc69893477d1f3bf81d4d3ddef38efbf9e253c5c060baf31f14dcc066e6e6df4" } }, "mariadb": { "manifestSha256": "aeb221be233e4b57bccb4b3375020f94c3ec593dac2734631f00e3b1aec9f48c", "cacheKeys": { - "wasm32": "ed9391bd27da1f44cd6aea9b51f3e2431c964afc8c76bb45a1fc18b73ab79243", - "wasm64": "269af4d38b0809ba0854715d33e9327b0eafd55391992efa945d317e518af932" + "wasm32": "6880cf285ed278369b3f4b3959df2c2388a4a57d1e42ed179bdc88995aa90ec0", + "wasm64": "4391919dcf0bf9e8ed26d01db20388bafc26a3541e41528b05d4005305e0289e" } }, "mariadb-test": { "manifestSha256": "d4ccce161d659a5a87c80fa1117054bbccea870e0d4a46bd8a070d3930ad0e3f", "cacheKeys": { - "wasm32": "a967a54c3de402ea5d454121226967757209eef6f8dcfc439e90c35f0032ecbd", - "wasm64": "59d4c3ff096720d901b31a46457fec03635cc893a584f80608c0761bcfe04398" + "wasm32": "20d9f6ef8e7dcba5fae20a99b92216c09bfb5b7279081dc2f26b173585bf9307", + "wasm64": "b41c0f83874588ceda7d42c841ad78e1df4325b4845b4ad3f25b8b1e237dcfbe" } }, "mariadb-vfs": { "manifestSha256": "26e74ac84d89b2a839ed72783437061a23022ef2f88ad0f52b6aacd0442ee1cf", "cacheKeys": { - "wasm32": "2eb50e31f5a54ba1cb1d6ee0fcff087e91a281ee79a7cfbb1a3ef0ad1b833769", - "wasm64": "884db9a8d75a3282daf82529bfe5578d3c956fa7c6f9b41552a72832dc34a32c" + "wasm32": "fb7dee8aa0eea01b0eb992a272476a3c0ef57cc57e25c67414f6d57b48e8e441", + "wasm64": "711c8d6db694351d1af8958c90edb8c489976800a1f1a60f2584dc16c2b0172f" } }, "modeset": { "manifestSha256": "36a8683c1c7309701d361c5f77e543a6c1531397b26c72dbb864528c59c42954", "cacheKeys": { - "wasm32": "752ec74aad6b305d9334a56116273bc35a8854cd77274b11b84549636e7b239e", - "wasm64": "8b0aa9662fbe51c83116aabce621bfe216c6fb1573b0774949307ed068d0a97e" + "wasm32": "e78096787decb030ebad9f2e4d1f0f028bbef880843734ae976843587c592e37", + "wasm64": "32d026484876fa8bf447b5a604c59e7fa2306c954930303ad0ce3d354ccad538" } }, "msmtpd": { "manifestSha256": "09de04a422ddf631a29a259d816e081f8d317d1077808ede55d8c500baae748f", "cacheKeys": { - "wasm32": "0741e71f99d4979625742f3e1b20e0fdc0140ff90ffa8997303ad9c3901e6815", - "wasm64": "d9f48323b596ef6978f34ddaa46da151c1c82030e595e8473f0412626bf55912" + "wasm32": "428beb3e8153aada86ede76aa28800fd78f5d1cbb618707a1f713b43f5faf29b", + "wasm64": "07c7f785cef299c15d4ddad65685ce76b2b46fa7e31395ebdbe65ba39b3138cb" } }, "nano": { "manifestSha256": "1ba6d340c95581319982257afd6a3554b333b880a7e69991b8c573da883f86c4", "cacheKeys": { - "wasm32": "907e645176d86044e4f1885cde3c5b5cba075a0d3245c6d8f86c78194864e264", - "wasm64": "3afef51e5fd55512439072bbb0b67d851f5e13b793fac1edb36d4761c72ca226" + "wasm32": "95058978c71a4dcbce64748127628ba7273b5c33ab3eac4f455c7d1f28d36596", + "wasm64": "1adcc5793eab08001c869d9973ecffac3478e861720646e62adc3f2613fa18fd" } }, "ncurses": { "manifestSha256": "0a1c180eeade627e204aa47065dbfebfcdc9359d25e2db436932c7fa3a86fc1e", "cacheKeys": { - "wasm32": "f7007c22d22911bf948ea1756962b7f0bf2f4a4dc11770bbae6f05a4d9ac575a", - "wasm64": "89a96085cc8e56fb4e4b0ae3aca1a5d42f541fae99322b9bcb5af5cc08f10140" + "wasm32": "546b02fc45c5010047b62bcb89d6f7655ca508c76ca561ee596624cc1ce47a77", + "wasm64": "fecfc32c415a17c94db29d024b3518ed99968f512bd84646802ba2e9cd43c64a" } }, "netcat": { "manifestSha256": "4cf33cd1ab768b3ad0108da8b68c2ff72e98469cd86a0bf2d0da54a7d4f6fa16", "cacheKeys": { - "wasm32": "5ea9f997b6b09deb18285945073754364de5b96dc53b30ca95df2edbfa5f8531", - "wasm64": "ee2fd0a39ef7d4d33659111743040278cfcbdba1eba4b13ec8dd39afe886e5c0" + "wasm32": "04661bea89ce63e48c96e8bfece91da3beadbd5579cbecef9b65186b68a72729", + "wasm64": "f04460d021f2314daba269e4bc37e816d885daf2483b3be11596be2c547a9fec" } }, "nethack": { "manifestSha256": "1a2f12ec2770bd8d40006d6c878a3493b97c71c2bb63ad08c7f6ad99bfd53504", "cacheKeys": { - "wasm32": "ad3d224fee9951ce2ddf76e2d6beaf66c2464c39b1ae6218832d888314aafdc3", - "wasm64": "415cbee229ca6c6df368d42c5d78affde0b886da680c070af4c704089d506d51" + "wasm32": "62505f6d4ce2c11722939a2798358325cb4e248c0e32989ef9e098fb74aeb522", + "wasm64": "174006546a46df26e83199a69f98a3565cbecb0dd7a5a145477f459cc14aa569" } }, "nethack-browser-bundle": { "manifestSha256": "b8294981da7ce4299dfb271d4aecb90ababa97a4d1acc9b716ae05bbe52beb53", "cacheKeys": { - "wasm32": "a1cf0b1cae527d6503726fe5f72a7c9b3ae3fab21c5f3d868a7635c1a1797a82", - "wasm64": "52f9c7973b026df0c8d1e441c74e384bb0c2e4bbbbd58c0645ddfae1a09a2f1f" + "wasm32": "68df5b69319d99f616a2befb74fbde48d2bbcfc45a1c183954bdab2b2e6909f1", + "wasm64": "5972c13ad19d3ded640ded4a1aedd16ce8eed8f8998582264e85192b1dd6634b" } }, "nginx": { "manifestSha256": "41b0aaf05fdd0d8da42642703f9e60fd7b9545d8029896918b87c771c359147b", "cacheKeys": { - "wasm32": "6e5a5508c265809727e0d599185e7216be72aece52651b8cd1c48c3712770196", - "wasm64": "d990e681235ee40af2c37689cae31a7737cf4b45e23856e14c627de8b2ae1dc1" + "wasm32": "a55f4c991fb3f173d17ccab1cd25deaa5779e5cfbbae7141cb0c97381995f28f", + "wasm64": "f6436df89a2de907d9f7f6f5bf9d033e54ccef8222cdada93c2474c54d69b7bb" } }, "nginx-php-vfs": { "manifestSha256": "97976410cb02f8ba710d856b4ac904bcf976d677b50eefdee39fb64176070d4b", "cacheKeys": { - "wasm32": "0e23cc73004b33997f519bcdeceb05022a54b97a4bac30721d0919e194ad8802", - "wasm64": "6ce3eb4f5abee2dbe592417f39bb7b00429cd68a3aefd9e587a2c771e3b33c2a" + "wasm32": "ec9e76bf80c92f53c18cba90a0817f16bee4693e1fcee1b8c528324a2ac20250", + "wasm64": "cc2fe0de04740d010848a3c0cafb6093e060faa8126d98dcbddcd06d8000035f" } }, "nginx-vfs": { "manifestSha256": "46aa2d3250ac5cd0f102a85c3a36a086c7a50ba1f9e05fa1c2aae3d07ad16f10", "cacheKeys": { - "wasm32": "c23b7b53f5c9e46cbc48eef745814e9f897fb05ea23c152f991144492c3f7321", - "wasm64": "e0712504af613c23cbe1594e4b928e800dc1b000add03e3994eae8fce1775c23" + "wasm32": "82e9591d96d27a7118b0d5c2303d65dd9552dc406a0e06c4a1b68f23f3e51855", + "wasm64": "a93cea19097ec6b3872c54f65fb96c8abf2fbed99b1dad7d72df0b42ebf637e4" } }, "node": { "manifestSha256": "2131a24dfda8587860e57fc65b573086477d376b065b3b9ca4e1dfe4d79f5790", "cacheKeys": { - "wasm32": "151bf271eefec8ff95770cc8e4c7d62e3a65af271275a026c78ce3139d9f8b6d", - "wasm64": "d5cb05ac959df3ec869e6b4ae02618f490e6471c36843652ab3b6b11966895c4" + "wasm32": "76796a79ab26b1204fd6c4373529d34913efa28f3bb4f4ec46cb14db0d81b25d", + "wasm64": "98378cd89179099d487ce6c06a4f42e0e067a7e3317e4f0677b4431d3c606630" } }, "node-vfs": { "manifestSha256": "d377eedb72d2d18652b3422c04deaae11a6efee84ee042b204151cfd69c04656", "cacheKeys": { - "wasm32": "614398767ef68a4db27569c97c8dd8f4b4f7a1e196038efe4cc9f1dfc55e5578", - "wasm64": "ba902db2584351f57415dc4114d95c5ad4830890544864c9afb7bb7109b3fb91" + "wasm32": "764b880d42e792623e0f715ed4398e7404ee17abf2b7638166845dfb3072ec6d", + "wasm64": "eb951305f473c8b3e73ad66cd80128a6f618225b335eb2966f3a98b635297bbe" } }, "openssl": { "manifestSha256": "374f5dce6b2691b630b55cb82955b33d2305384e4bd64746c581621bb0793993", "cacheKeys": { - "wasm32": "359369fd3977d6583c001fc6c26e5caf54160f07cce7c9f33ae87bd85c212217", - "wasm64": "1fc18a819edeb2136350e576c4dc437e0c623d4f89067e27491562e945eafb71" + "wasm32": "880eadc0dad3a794dce96eb841868a9326541fcaaa7630bc4d7618840ba0a849", + "wasm64": "dcbbb8734572bf71fb91ee63ffddecb08b511c6247f01556672d3e6caed84425" } }, "pcre2-source": { @@ -354,197 +354,197 @@ "perl": { "manifestSha256": "6cdc4dbc54d0e4008cff41f82ec8918c0e44b4aef23903539c1bc0f538fe3ad2", "cacheKeys": { - "wasm32": "d761b986fbd1b508814857959fffee294ecbc7d17eaa7677115782757bca752a", - "wasm64": "78b67a142217507da41d36fd1e66953b0f754bae43073d03281801616c39ba1f" + "wasm32": "e25be2e35de9dfc135d79802a08d077938d8c33e815326aa353d72876beb8413", + "wasm64": "6b25d680fb19489a185484dde03dfcd3671f214d237d146ec80e0e8a40abc4e5" } }, "perl-vfs": { "manifestSha256": "1345cc102bc8c24a6bc276410c00b4fcc99ba5f6adc9b031f882aaa35d53c8bc", "cacheKeys": { - "wasm32": "11277eefb716c2bb52fa2474d3c58f33060ce46f30e3bd1472ce39da708cc56c", - "wasm64": "9e483c2865a5c335c066681a9fcc33b041ef481d65c06f80a0824187ee594e88" + "wasm32": "807f40dc841eabdb0b71f9947ecc5c384332ebbde89aa37fabdc826d29208220", + "wasm64": "08eff56117fd10e0425f67efda1dfda7deb3b6f522c9485cdd6c81ecc70237ed" } }, "php": { "manifestSha256": "fcd9d7915ed8935b05418e148edcb312adf0b1e4e627fe4aab75444b35b33bbc", "cacheKeys": { - "wasm32": "8583b755c9eb5a6877e101b3330473d7b5248c7f1e1a8983461811fb04f61476", - "wasm64": "533209cdc874201a11e93f104aaf16fcbcaace6353543b28dd363618a58416c8" + "wasm32": "a07a9e3bf8039cbe818a180217e3edd763b563b5a7461ade5b40e781a207c952", + "wasm64": "011dd75b70475483392cdbc12855f57bf98b4a5f0182b0140d2886b4c07cd3cc" } }, "posix-utils-lite": { "manifestSha256": "8fd7190b2848ef80143adc7b9268c79e13cd4db3430f7105faf49a4b4407a06f", "cacheKeys": { - "wasm32": "1819a7d6e4bd40577180e9dc8fcde01ca5a96c99b85e5941fc4541f9d8f54d10", - "wasm64": "dd9222d798275e3b8256f1269b776bf0b3eea68ab05abd8aa8277258c13e57b0" + "wasm32": "0b3456ba5cd5ff45833de8867c9b036734aab58f9a20240dc68bf8c54e5181c7", + "wasm64": "8d327b89a82b0674fda440364bcd75ce55ed8db34a1e3a58b6403144624cf87a" } }, "python-vfs": { "manifestSha256": "d1aa99feae65f06c0ca8cf52c2176534b2723fd4ee6eba6e72c205245e1c9c26", "cacheKeys": { - "wasm32": "b03f527c89153f331ce1f47a02649bc0e09e1c6623b7d242251f458aaa252661", - "wasm64": "2bfcea19fb22047b07677d61a37edae460edfd94bf1c12ed678e862a425e015c" + "wasm32": "97756a7c2b77d6ac165f6dbdc0a10d1b6f35dcd77f6bc53a2612de81cdca1272", + "wasm64": "f2c93f1e1d9d26abf9733992153e6c2b56bd35e91d39ffc165658a0caed315a3" } }, "redis": { "manifestSha256": "151fb507de953ba2ba94b8f881bc7d66b4c741c1359a2f590cc07f9a4cd368a3", "cacheKeys": { - "wasm32": "9134583aada35ad9d6b2ecf26522214c72a6514fb48f9ea4f7907384deaa64be", - "wasm64": "b191a82c47c68f5f1f98e97788896728527983b83d71955c46d9dd319c2e5579" + "wasm32": "c917fdf98c1afe799e6625f52182e9a00378e6c343f63fc7102b8f1090e0b5eb", + "wasm64": "b4b29398e0b4ebc1c0630b5b3407c0903829409a00f869c709653b749a6d4e7e" } }, "redis-vfs": { "manifestSha256": "234c45c40f94e2a89f98295313b82cb9fce15a412ac829d9e87394e0dc731813", "cacheKeys": { - "wasm32": "f16e5aa9291ffaa3afe627d39cac87981f6828dd72124623fbd82a99a5ace60b", - "wasm64": "1a46402eb9afce97a59a3d551ee6c4c2c1323294b89ce575fc356ed6dcac8c5f" + "wasm32": "5e872140fa47035b40735d573779f62c0b179046f8bf765f5bb886969f3a29a8", + "wasm64": "3aa870c5bb55711ae7a61d291f33f1ccbcf8f0777bcf3da39759227833666849" } }, "rootfs": { "manifestSha256": "0420201025170f48c32949ac62707d4577cdc13a53ad1f01f59d318ec07c8d6e", "cacheKeys": { - "wasm32": "8712ba10c358df63db093fdda5eaf2aa0207d10501207c0fa5a2dd5868dd7ec3", - "wasm64": "dbcc5e48b6c77ac8893d1e313c6dab32d863e47959b5603ff3c158a154de042c" + "wasm32": "151e91bfd484f088aeaf8aaade1c7d6458e2db4d36429b175dad2122e18a6c69", + "wasm64": "270b2d7ab05590eb70b8a367337c0340843dc8c711f09e4ebc274c813edff46a" } }, "ruby": { "manifestSha256": "6ea67a246c29cd927a19decbb36ae4c4d478ff6642d2c77e08a6b2cfeb8251d2", "cacheKeys": { - "wasm32": "8aee9a53772073f007fcb92e219ad0623e86999762ca4e0218686db652cb2b4d", - "wasm64": "02d35654ad6b622adb287e0a699ad91915a1cc3c3a87e17287055813cbf56f62" + "wasm32": "837fc3191253208c74d75869d7d030107b397c702fab1dc1c7d7ca916eb1c250", + "wasm64": "623dec16022a4b36dacc9325030b54a4650e4553f9a59e7e481608077dda4b6c" } }, "sed": { "manifestSha256": "2a08e9c5dacc5facc8983c1ff35ff2a72db328405e9a1f464cc7ad155c4d08af", "cacheKeys": { - "wasm32": "05e76414e569f8f79cf1e290f7afc3822f58ba29e5ef56b9588a3fcf57f7828c", - "wasm64": "f0847cb7f3b994c6daecd1bdb22807a8959c40ed44d182241e482aba2ed9781b" + "wasm32": "5819a38727f0e3ffc8c2bc21d1a3494bad9df3eecd88f31d45f55607eca7ba37", + "wasm64": "d14b0358ba8626d12ff6f2020c157ba775c6a1778e6ab34eb3cbbfeec6f47873" } }, "shell": { "manifestSha256": "28a871b7fcf72f3e289d5116c2d308e22fd4beffd6c5db4bffa262f9b16c1384", "cacheKeys": { - "wasm32": "07baaa2896902650f67a54a305ca931d280239a0bb74bcd0b62307f946ab8ff1", - "wasm64": "20a8faf1565db03f8b4dc40ae79e0244eb852950a51afe32f0003291a29adcfe" + "wasm32": "25dbd7f217f7d878277face79d108e8e04cce7efdd4e2f3e5298076e995d286d", + "wasm64": "5022d8f46cd96078a90318c1a1280515ad499677452ce9d4631451b0c6cef454" } }, "spidermonkey": { "manifestSha256": "c72ef4e43ee2a4fc74390d03a239fce3b545a6d3b64f02ccc4d44972cec626bd", "cacheKeys": { - "wasm32": "83ce9ad65c9abbd9bddeee7ff39f92da696c1c41848de40b47350d963f7764ed", - "wasm64": "d9a042448a1b7da4232b97471eb95b94f83895697a16adc5a82efba4dcf972f8" + "wasm32": "c568200194f122282abb2aafa62044bd9eb739594a7ff25ce37b4ba94082e8e6", + "wasm64": "567abb0688e5ecaf9edb157c45eabd9d4cc41ac83d17bc99b82665956f1e1969" } }, "spidermonkey-node": { "manifestSha256": "f156c4c5044d2a9447324e7a2a35f8c3e6fa367b3e44f674dcc30ab6b946fae7", "cacheKeys": { - "wasm32": "f746c7700ebd7ae25296f05095b44c89bf91be26cc49a0f4f38e49336ad381a6", - "wasm64": "7df1645030c66147e1a4c77b24c27a517933ea9d21ecfdea5b35ab5ce0f33d1d" + "wasm32": "50b646274527dc2c46a8a846267fc34b23f5ef22b903e3fc697534f5a323867d", + "wasm64": "05f10a6ce949f60bb029b4392e78eff4d0faadc216147324f603d1fc3b9d302e" } }, "sqlite": { "manifestSha256": "ab9560b4eab53445034e2a87a27ccee9d05ddb0db73fee28f5000325b0b1cba5", "cacheKeys": { - "wasm32": "74a3c1fc211e94e2b55538438802f15a3b7997f61efe2f033f0b58076d50038f", - "wasm64": "d4b1ec9b8a1967924ee2d0a2bbc3b0fcecfe26641314c1d2c42f76e9482cc2a7" + "wasm32": "3a191371817cb5c2113b031e561ae9615a164f20b87e9ecbbb7e15c88431692a", + "wasm64": "fe533e4b85c52156e1b1d79cc2ae5fb850d2f30a22e1add2848a0c5afee162b1" } }, "sqlite-cli": { "manifestSha256": "1cebb768f9473dc58fc6e72c9b24565760aedaec4cbdb29ad43b22a0a5fd7030", "cacheKeys": { - "wasm32": "558f50274c45faa2efbf8b4d3c2a5afd3468dd1d2430cbd87c2398f2f1c73503", - "wasm64": "da710fc9e49490b761180ad31a0143ef169c99e301ce1b58dd6e6989c2484367" + "wasm32": "837538ad9565e69d03221cd901c285cd15c0305b7e8ebe2d59370739bbbc3999", + "wasm64": "006f8271b87d962b9e209af89848a735827e6915457a46c715783dcf9bf3d447" } }, "tar": { "manifestSha256": "08fa090c122d3105c735d74560bdd7b8b083a6f9ca8bdec17de0b4993e1be7fd", "cacheKeys": { - "wasm32": "2ae2051ddf7439c7eda0f20911a5402c44603b05242161bfe8002dce4fa58390", - "wasm64": "29cb6bd7db45c137d41dade5a3176ffd1d2cd8a2861eca975a99fdef6cad584e" + "wasm32": "2b9fc9de4354ee7391daae45569d78b057a6a5eaf3b4fdf3503bdf157252713b", + "wasm64": "66ef16220a5658e9c9f5ed7b621a3f64be717006d966cd5dd04c125ea12bea4a" } }, "tcl": { "manifestSha256": "67253d47de7df9184e68ec3c49835455746941835a26db9e588aa8fbee7d4636", "cacheKeys": { - "wasm32": "fc021865eb1e8583b65940f3d6d9b3df0084a7951f1e8d386ecb2ec3d91e4d4d", - "wasm64": "6e4c39d11b57ec9e7b86fb1bbb8282331329289360c43adc8857f35a54517a7c" + "wasm32": "c23eaadb20042e2af5f41b009af003a2adb0adb0c3253c816a536717187a225a", + "wasm64": "5fc745fea0731e26f932541ba170512b23942e8a4838ff2317b1db52f9371a88" } }, "texlive": { "manifestSha256": "028ada023f8a8f9966c8a28575242a339aaaf0e8870fe4a221986133b381c3c0", "cacheKeys": { - "wasm32": "932bd4d32bcce12e290f2213e3879abe85c2ad11a257398e812ad3d9e2f5aa35", - "wasm64": "7822fa5cc3736df0970bb376d4248e31e5dd92ba0113ffa9efc2ad1f5e0b9557" + "wasm32": "7d6251dbe71a6af7ef83862e810c171610ab037e9c674b891b76454c95177d83", + "wasm64": "5e9bab15d180aca691da31de15b052205c4cf592b4059c2748b939c071945f4d" } }, "unzip": { "manifestSha256": "ef4e5e34258827ab3cbc1930da3fd9514f08f2d7d56c7c3e0d5c495a4d3a20e9", "cacheKeys": { - "wasm32": "562280d78261c438af4f5bf1142ef3e95ef866034799d421e4745282d7e240db", - "wasm64": "dbc0f9901645890f69607d915ffbaa662768c7ff41484ab584f7c1d6ca5e9f74" + "wasm32": "3306c9a85cf89e9f3d2728c63eea7991b9c76e26ffbe7fa6b9eb0100bf89445c", + "wasm64": "eef642a7484dece5596e7ed29e72931180faac548f3d27b9fff6eddd5efd7c4b" } }, "userspace": { "manifestSha256": "221176f2a096dee19bbefd89da5c7f50138ecd92d37ec9d7d6b35dbb25e86924", "cacheKeys": { - "wasm32": "6d8a139641e318347638e2b4a1c5f547c39bb720789980d30cce94a977972fdd", - "wasm64": "9add6fe8696eef65cb5d20daec9d6f2d8d1cced23cb5465bb7378e9cfd277929" + "wasm32": "e617a3a91f4d51b97c1326f391e34eb8c241d5a84c26b36416c41f86631c0d49", + "wasm64": "c4c34a4c10b8af574c6c609c0745c58f97d4da23d5e7b086a15207f2f01ef390" } }, "vim": { "manifestSha256": "c211660ef41d01f95f3fbdf675b74891e897e3f304e04f1bf5586f326007382c", "cacheKeys": { - "wasm32": "1def118e38431ca3b5ab7d3ee0e5683b2114c55b6bebc5153c506fca3cb5256a", - "wasm64": "e47ed1d6b4e3264b4d74f7928b930bb7d6be2b95d952630c8a494dca18ca595d" + "wasm32": "5da1386710e9baec1c9ca2ad190e067071f26c2a7c2cacb06f2ff9df7439df3a", + "wasm64": "f8ad110d715e40e43eb7485e4decaf87477c2cc27dff756665d4908deee2f225" } }, "vim-browser-bundle": { "manifestSha256": "e31d84057db49c3534381604d0c8f3c7d765e33ede560a7ea7b01a5a8f1aa0d3", "cacheKeys": { - "wasm32": "d67b9967f865c9dcc06abfdbc538b7a724c9908d3a035f46b83b45b6e510f28e", - "wasm64": "d1ee4c88db5ac2a34aed633d7e9307e41dbeeca8024bd8bbf52f0e46d5585722" + "wasm32": "cdc07020c6a281b39732e8c1aa5aef7fd150ebe48444d37423086e0c29271e35", + "wasm64": "906d49e5ad9d442f50e994b4c93a1ba23846e0d9b772ff8e7f5ef50950ca7b95" } }, "wget": { "manifestSha256": "d3c7ba9bc1ae708b99850a6eb2cae521c85bf1010c90135a935f5dfae57a53a0", "cacheKeys": { - "wasm32": "0ab3ac6fbb77925a949930bd9d5830a9d0277caa3aa3d6b427b735ff5ef54ce3", - "wasm64": "ca58c81b7d100a3e1e31e57ee011a469bbbf9bb19cd6370e9af39dcfb24fb4f7" + "wasm32": "d7f56b9d2c2a892e2888c609b9fcf25f490e97536456440709a731ff525c3d47", + "wasm64": "093d1051efdd82e6628a85b9ddcd05438d18c8a3e58ce27852a73ca9546e2180" } }, "wordpress": { "manifestSha256": "36465e0596a06e855a8524eecfd87da98643bc13b37ee12939f5aab44bf33418", "cacheKeys": { - "wasm32": "e749bb66e576baa2cf4919a91fbf414f5311946101044e658831ccf4e8068e88", - "wasm64": "57d5b46059fa7b6b2701cf3db69290e72c1c476059b60f58de041c4f6f7c1ec3" + "wasm32": "7ef244afa0da39737ec243a8918cd81f12c42fe983be9935fc587d81d8d2ca88", + "wasm64": "0d02244195905221598389c4a376149959514ccac0598e59dc33dfddcef5c3d8" } }, "xz": { "manifestSha256": "8707d35b8647b1af8fa165dcb6ce7b2a6a007e19ab2daca2680c39395864a737", "cacheKeys": { - "wasm32": "86b9dc3f926a72bc93da4624570c5047b9e4406f0b5c248f24fbd24a46a18a6d", - "wasm64": "dc3b338b0fca39aa84681b9834c66f5f2f7fd2be9fa762a2864eaaa9c897b658" + "wasm32": "201487250ddcc127b636db79702465158356bebd505dcbf7b3fbf77310ce8cc0", + "wasm64": "5abed08c189750f5db2f47dca33f40405d73990ea2c77d0659c8db97d6a1dc04" } }, "zip": { "manifestSha256": "9c9cfa8220aaeaea26736b55f7cd0a7517b8853c07f9f0d241ad67dd43592e36", "cacheKeys": { - "wasm32": "3cb8a25196713a8863b34a17372a8ebd5cdb168a89d8ef059b2cbfe4a68cd6c0", - "wasm64": "bfb3bfa7b97a32785ac89cd642cb35af53db030c7c5bdd69c0edda7f0470606c" + "wasm32": "cc384c33682211759704640e85688eec5affeb765e0a6044ff6db9fd3de32a42", + "wasm64": "0e9a25d9b36edf92a8ae56af3f223021041284ccf0f19e2c7183d867ecd0f4ba" } }, "zlib": { "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", "cacheKeys": { - "wasm32": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f", - "wasm64": "4381eab7e75277430a5ae8da79b007b15256eaf141f3b0b9846e474eefb115ba" + "wasm32": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890", + "wasm64": "14eb313f818021e43d8149e9ec10197982b66334568631b6cc314f27df963751" } }, "zstd": { "manifestSha256": "89f266938c2c253700a838e6352c9de66c976c84883325aaa979f72b732357e4", "cacheKeys": { - "wasm32": "a47a83e02cd18eccb2586fe034b5aa5312e5c7e8d67c8c68c2ad7a2e06f73946", - "wasm64": "6f0e99d156e4f543b782eb01c1ae0324cf7f1bf6c2a85dba63fff85b2cebbf28" + "wasm32": "f670f96ec5ace8362aee82256db3c4f4a25dbe00315ebaf649438e365501a7a7", + "wasm64": "152435558f069494d6426e8d55e4ef6e90c047d526d5de5939c67a04d7278f12" } } }, @@ -555,14 +555,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "012336da4d490e68d3e200ec8d29e641d5946d6bec4c9e309d6f3bccedf9985b" + "wasm32": "8fc30f59035df55e9dc2b7ed2359c13faf8d63ec285bf44e6f52c7de1e6b8fca" }, "dependencyClosures": { "wasm32": [ { "packageName": "ncurses", "manifestSha256": "0a1c180eeade627e204aa47065dbfebfcdc9359d25e2db436932c7fa3a86fc1e", - "cacheKey": "f7007c22d22911bf948ea1756962b7f0bf2f4a4dc11770bbae6f05a4d9ac575a" + "cacheKey": "546b02fc45c5010047b62bcb89d6f7655ca508c76ca561ee596624cc1ce47a77" } ] }, @@ -582,7 +582,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "df5529c0caa8288deff41a9a0a59c17d99e5bf1eea473ffc553f68087ad55ac4" + "wasm32": "9a7242df42960d7d69dab831f169f20626aa10174ada963fce9f801c7df2b125" }, "dependencyClosures": { "wasm32": [] @@ -603,7 +603,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "12294306f7aa60473f3d83d4e4e2b7796daaf3e8986e9e19704c6ef3826fa83a" + "wasm32": "a2d2f675316d600f206b8088f199de3683e4cf803e54e0145c49c0864b5330e5" }, "dependencyClosures": { "wasm32": [] @@ -624,7 +624,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "c44171215c792087144a3dc8bd76f9a37f2f8311ed3d7b29e0ce1ae8fb79a76a" + "wasm32": "8aa8d109e92b93b779708d5e28b59cb972e58591cd6aaebb96613d0b99825b38" }, "dependencyClosures": { "wasm32": [] @@ -645,14 +645,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "4c75fb0082fa302537b99fc954a66275a680fd86afdcc71df12d6da7ea570c4f" + "wasm32": "f15ab782192b8bd17b2acd4a5ea4dc33a90fc3b9a813f91a919697822b113bc7" }, "dependencyClosures": { "wasm32": [ { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -679,19 +679,19 @@ "wasm32" ], "cacheKeys": { - "wasm32": "c84aaa895980c54d92a8b3c9340ce9e85dda9c7866e0ba235e1e38336efeb487" + "wasm32": "84a2431436e708240bef54f2c9a01a8148024b53f90683bf00c663fe81bb8315" }, "dependencyClosures": { "wasm32": [ { "packageName": "openssl", "manifestSha256": "374f5dce6b2691b630b55cb82955b33d2305384e4bd64746c581621bb0793993", - "cacheKey": "359369fd3977d6583c001fc6c26e5caf54160f07cce7c9f33ae87bd85c212217" + "cacheKey": "880eadc0dad3a794dce96eb841868a9326541fcaaa7630bc4d7618840ba0a849" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -711,7 +711,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "226bda53ce2d316d11744de02e555a09e870f3bb5a5ee3d3e79d8a47f0612bef" + "wasm32": "0cb537cf7cc09bbadf377821dfb68c3694d3db2ddcd0d1645f7a61126c5efda4" }, "dependencyClosures": { "wasm32": [] @@ -732,7 +732,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "5ade22f17ebe371d663fd23ad99b1a8422da350d40e2576eba114a6d9e0276c9" + "wasm32": "ad399566e397f6fa75dabf30380ff57183ea7ad59a1bf30297080c23cd581abe" }, "dependencyClosures": { "wasm32": [] @@ -774,14 +774,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "db68fc369364580e6690bb1097fa27d4eb7a1449574f24b6ab1202c60ff51f1b" + "wasm32": "e986afadf2ced00ecefa5a80b79ab1063ede97d23943c078628e3d689854b276" }, "dependencyClosures": { "wasm32": [ { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" } ] }, @@ -815,7 +815,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "833271b798874daa6a0bb0bbf47ea113a24408829734b847a2808dd12b294e94" + "wasm32": "d0ab28208e0f0068097155e12f6bcfde1c568eb3a5c28edb9a8669d3da786980" }, "dependencyClosures": { "wasm32": [] @@ -843,14 +843,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "045291d840ad92dd9f388a310d60fa582d9cccd1870c6fc6b01761db2075f6f5" + "wasm32": "98ce82948fcdc266c464c81ad82b3b8e0a4438149c38717cb9385dc83ad0180d" }, "dependencyClosures": { "wasm32": [ { "packageName": "erlang", "manifestSha256": "475d6037e73a0f1e2f4381067194b2422751206979ea17209e10efa5a2a93bbb", - "cacheKey": "833271b798874daa6a0bb0bbf47ea113a24408829734b847a2808dd12b294e94" + "cacheKey": "d0ab28208e0f0068097155e12f6bcfde1c568eb3a5c28edb9a8669d3da786980" } ] }, @@ -870,7 +870,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "1bf36f090735afd713cec6752a60cc4a5a1cd95abf9774efa947b5e1de918e63" + "wasm32": "1b6ccf0a23fd165495effd479ab117c68e55c723e8e0b21dddcb6de7e808be42" }, "dependencyClosures": { "wasm32": [] @@ -891,7 +891,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "73f41f2eb367d94a91642c73568d97a78afc53bbb700f81f518b502dce5733fe" + "wasm32": "2cdf71f1ed37aa53ea355f7dadc12f8ae041343b6941c0476d58d2ce764581b3" }, "dependencyClosures": { "wasm32": [] @@ -919,7 +919,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "a40215f2320308d34213e516c8d27af1daa652489451983297ef64f937bff3eb" + "wasm32": "a83aa55d45a41dcc635cdcb8c52f4309903b5c3ec843717f55bbdf1f9772e62a" }, "dependencyClosures": { "wasm32": [] @@ -947,7 +947,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "bb92b0fa58c8f18aae36dcc635a47df0ca25c12ee94df12db3d8e854bec07b5b" + "wasm32": "1996fcf770d124ed1355db80f34271a30676afed43e439c19dcf591f435800c6" }, "dependencyClosures": { "wasm32": [] @@ -968,7 +968,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "8f0b85ba323e76be9007adcb663ef3c6e3ed4ae3cb69b200b360406f4b72acae" + "wasm32": "ec535423cc99a1531ebe8f4114a28636d26164c936d4649f042d01a49f76c082" }, "dependencyClosures": { "wasm32": [] @@ -996,7 +996,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "95afdd724b558edb84d7d236bc1f00af8c2a7aff386641feefa96c49793a1f45" + "wasm32": "8a78952c49537c96e7a0fd6837d23abed09ebf4fa67c0879b143316a043b2d15" }, "dependencyClosures": { "wasm32": [] @@ -1017,7 +1017,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "c38775ccc4e9b7460cec5de0001df43867c459e87722d6d133242e439f330da8" + "wasm32": "60030e233799c2bf5af1ccdc0919768cf0a7a9408bbf8670969c696b2ef22b35" }, "dependencyClosures": { "wasm32": [] @@ -1033,15 +1033,26 @@ ] }, "homebrew-bootstrap": { - "manifestSha256": "183004a8385ab5afc388cd43401341b82812c67f97b08ad349ebd7f4dbbebd0f", + "manifestSha256": "b76e60692c6b1aad8d2373f9439465797a0549156c65ba0e1efe03a80955e52d", "arches": [ "wasm32" ], "cacheKeys": { - "wasm32": "ff2d8b4e5d86a43c21047b30465abd91857f09faa5ea4be55908a22bf025ebd5" + "wasm32": "03723a898603bb0eec70e4fc57c70ee7590081db1ed7087454e92e75ee712786" }, "dependencyClosures": { - "wasm32": [] + "wasm32": [ + { + "packageName": "ruby", + "manifestSha256": "6ea67a246c29cd927a19decbb36ae4c4d478ff6642d2c77e08a6b2cfeb8251d2", + "cacheKey": "837fc3191253208c74d75869d7d030107b397c702fab1dc1c7d7ca916eb1c250" + }, + { + "packageName": "zlib", + "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" + } + ] }, "members": [ { @@ -1057,6 +1068,13 @@ "mirrorPath": "homebrew-bootstrap/homebrew-brew.env", "outputName": "homebrew-brew", "forkInstrumentation": "disabled" + }, + { + "kind": "output", + "sourceArtifact": "homebrew-portable-ruby.zip", + "mirrorPath": "homebrew-bootstrap/homebrew-portable-ruby.zip", + "outputName": "homebrew-portable-ruby", + "forkInstrumentation": "disabled" } ] }, @@ -1066,14 +1084,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "5d2aa9c9dcc3383037d21566dea7e2b7b4cce340068e0daa8bcc868e315d239c" + "wasm32": "921c21d90f7b778e4f5a58d171e6e59cf92a3d1b4753388e285eff5d97668534" }, "dependencyClosures": { "wasm32": [ { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" } ] }, @@ -1093,64 +1111,64 @@ "wasm32" ], "cacheKeys": { - "wasm32": "2c204bb8f4a17372f19dcd2960286471eb63f418813125f6b8eb2f6914b948cc" + "wasm32": "b210502fef09fd12552380a9a87ab749fa9dface329bdd43bd0b93b1672dc429" }, "dependencyClosures": { "wasm32": [ { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "db68fc369364580e6690bb1097fa27d4eb7a1449574f24b6ab1202c60ff51f1b" + "cacheKey": "e986afadf2ced00ecefa5a80b79ab1063ede97d23943c078628e3d689854b276" }, { "packageName": "icu", "manifestSha256": "f5b1f02d169ec1108fc32d9da55be53c3efb96e3a7aeba6a03aec62e0175f0dc", - "cacheKey": "4ac7eecb23c1a8df06db4aed05430e068c0384c3f86fad423d7513225c456966" + "cacheKey": "dccdd3d70beae9d23d22bbdebf95a4ab509ec713de30eb7d86d0193ecaea4c09" }, { "packageName": "libcurl", "manifestSha256": "c771e6cdc83b43840db4dd1fdce4b6189ba4f281596f7485bcda9c65e2ecda00", - "cacheKey": "5992711bf6f9c74f856802f2bb42dd089857daffa04ff15955c5a68562669b87" + "cacheKey": "f4f2e193219fee442db65d63e8ff29b47b72caf11ca1b5cb81ea552b24c2263d" }, { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "libiconv", "manifestSha256": "fa60b386f4ba0cd5bbbd595f67477cbfb0cc401a14bbd4769630c502bb3f24b1", - "cacheKey": "46ac767af4b028b8ff540560f1b608b5c9ad75f9859055e322a3dd012eb5390f" + "cacheKey": "0a7f0c9fb38e897230d1798c43cded55bd9c8ae4e83cbd28b7526d677f8deac4" }, { "packageName": "libxml2", "manifestSha256": "908490efcdc7783c76ce36af31d1db24bfcc7ba5f1e7b9b5298cc81cb110256c", - "cacheKey": "a36d4055cf9c608654dace57fe18209597eb630c9bc7a1f9d4f320f2e7b885d8" + "cacheKey": "934a6aaaedd65011653013293f4b87532f68068d58d72fbe5559097835739e4b" }, { "packageName": "libzip", "manifestSha256": "83c65d63a6416e79436bf4e792759a4701d568df0e8c010394a9732cf659f0cc", - "cacheKey": "94ea97db7a56b44127bf30e29ae40c7d6d069ac010d6295b654a04d8f8fcbbc0" + "cacheKey": "e5adb5a22165e93c595754d059dba976f8e2600376d8c9da157205fa8cab614b" }, { "packageName": "mariadb", "manifestSha256": "aeb221be233e4b57bccb4b3375020f94c3ec593dac2734631f00e3b1aec9f48c", - "cacheKey": "ed9391bd27da1f44cd6aea9b51f3e2431c964afc8c76bb45a1fc18b73ab79243" + "cacheKey": "6880cf285ed278369b3f4b3959df2c2388a4a57d1e42ed179bdc88995aa90ec0" }, { "packageName": "msmtpd", "manifestSha256": "09de04a422ddf631a29a259d816e081f8d317d1077808ede55d8c500baae748f", - "cacheKey": "0741e71f99d4979625742f3e1b20e0fdc0140ff90ffa8997303ad9c3901e6815" + "cacheKey": "428beb3e8153aada86ede76aa28800fd78f5d1cbb618707a1f713b43f5faf29b" }, { "packageName": "nginx", "manifestSha256": "41b0aaf05fdd0d8da42642703f9e60fd7b9545d8029896918b87c771c359147b", - "cacheKey": "6e5a5508c265809727e0d599185e7216be72aece52651b8cd1c48c3712770196" + "cacheKey": "a55f4c991fb3f173d17ccab1cd25deaa5779e5cfbbae7141cb0c97381995f28f" }, { "packageName": "openssl", "manifestSha256": "374f5dce6b2691b630b55cb82955b33d2305384e4bd64746c581621bb0793993", - "cacheKey": "359369fd3977d6583c001fc6c26e5caf54160f07cce7c9f33ae87bd85c212217" + "cacheKey": "880eadc0dad3a794dce96eb841868a9326541fcaaa7630bc4d7618840ba0a849" }, { "packageName": "pcre2-source", @@ -1160,22 +1178,22 @@ { "packageName": "php", "manifestSha256": "fcd9d7915ed8935b05418e148edcb312adf0b1e4e627fe4aab75444b35b33bbc", - "cacheKey": "8583b755c9eb5a6877e101b3330473d7b5248c7f1e1a8983461811fb04f61476" + "cacheKey": "a07a9e3bf8039cbe818a180217e3edd763b563b5a7461ade5b40e781a207c952" }, { "packageName": "shell", "manifestSha256": "28a871b7fcf72f3e289d5116c2d308e22fd4beffd6c5db4bffa262f9b16c1384", - "cacheKey": "07baaa2896902650f67a54a305ca931d280239a0bb74bcd0b62307f946ab8ff1" + "cacheKey": "25dbd7f217f7d878277face79d108e8e04cce7efdd4e2f3e5298076e995d286d" }, { "packageName": "sqlite", "manifestSha256": "ab9560b4eab53445034e2a87a27ccee9d05ddb0db73fee28f5000325b0b1cba5", - "cacheKey": "74a3c1fc211e94e2b55538438802f15a3b7997f61efe2f033f0b58076d50038f" + "cacheKey": "3a191371817cb5c2113b031e561ae9615a164f20b87e9ecbbb7e15c88431692a" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -1195,7 +1213,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "84d1739981ea64e5f9c23cd586c4840d412737b235ba41875c1d4cafaecbf88b" + "wasm32": "d371625245526c280a91a49ec0a819fb8827655fcc896bf7d60836a71222a38c" }, "dependencyClosures": { "wasm32": [] @@ -1216,7 +1234,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "36e3ef4f5bd7ad67194224ae62b77726e51849336f56b83f1d4d2d68badcab87" + "wasm32": "26e86352794f5faf1f9580d48bdf4577db7c74df82f8c62a2efd72bc41468f0a" }, "dependencyClosures": { "wasm32": [] @@ -1237,7 +1255,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "e26cfe10cb77fd905bcf69767d22824ee51bebef986fec841b78673e433e8e5f" + "wasm32": "8f298e218fac8d3cf3a7828d311703c6b232020086851c3bf1aae9021f7379bf" }, "dependencyClosures": { "wasm32": [] @@ -1258,7 +1276,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "6450f1427f20639676c13843ab492dacf77a00deac3bf3716e3ef0a49b10612a" + "wasm32": "715f3f5c96e4e5944bbc3f55ab3c6d8cc4b0133abc09265b6acae9be4cd863e7" }, "dependencyClosures": { "wasm32": [] @@ -1280,15 +1298,15 @@ "wasm64" ], "cacheKeys": { - "wasm32": "ed9391bd27da1f44cd6aea9b51f3e2431c964afc8c76bb45a1fc18b73ab79243", - "wasm64": "269af4d38b0809ba0854715d33e9327b0eafd55391992efa945d317e518af932" + "wasm32": "6880cf285ed278369b3f4b3959df2c2388a4a57d1e42ed179bdc88995aa90ec0", + "wasm64": "4391919dcf0bf9e8ed26d01db20388bafc26a3541e41528b05d4005305e0289e" }, "dependencyClosures": { "wasm32": [ { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "pcre2-source", @@ -1300,7 +1318,7 @@ { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "6f624f5476751d07bb58b04608a389400c079fd74c7e62ef52306d89f5f8754d" + "cacheKey": "cb7c9eadcc32d322fc339ddeb06eac0a5377423bd9d92a478c6cf1e71e0ec7b3" }, { "packageName": "pcre2-source", @@ -1332,34 +1350,34 @@ "wasm32" ], "cacheKeys": { - "wasm32": "a967a54c3de402ea5d454121226967757209eef6f8dcfc439e90c35f0032ecbd" + "wasm32": "20d9f6ef8e7dcba5fae20a99b92216c09bfb5b7279081dc2f26b173585bf9307" }, "dependencyClosures": { "wasm32": [ { "packageName": "coreutils", "manifestSha256": "b8baabc9af9283434e0d80f7fdc0aaef242ffa31092ab0fc39a0603a12b3cca7", - "cacheKey": "c44171215c792087144a3dc8bd76f9a37f2f8311ed3d7b29e0ce1ae8fb79a76a" + "cacheKey": "8aa8d109e92b93b779708d5e28b59cb972e58591cd6aaebb96613d0b99825b38" }, { "packageName": "dash", "manifestSha256": "10ce1bb611fefd78a6c9ec9038f6113e29a7062b1a73e08eccf9b33004cea9a8", - "cacheKey": "226bda53ce2d316d11744de02e555a09e870f3bb5a5ee3d3e79d8a47f0612bef" + "cacheKey": "0cb537cf7cc09bbadf377821dfb68c3694d3db2ddcd0d1645f7a61126c5efda4" }, { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "db68fc369364580e6690bb1097fa27d4eb7a1449574f24b6ab1202c60ff51f1b" + "cacheKey": "e986afadf2ced00ecefa5a80b79ab1063ede97d23943c078628e3d689854b276" }, { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "mariadb", "manifestSha256": "aeb221be233e4b57bccb4b3375020f94c3ec593dac2734631f00e3b1aec9f48c", - "cacheKey": "ed9391bd27da1f44cd6aea9b51f3e2431c964afc8c76bb45a1fc18b73ab79243" + "cacheKey": "6880cf285ed278369b3f4b3959df2c2388a4a57d1e42ed179bdc88995aa90ec0" }, { "packageName": "pcre2-source", @@ -1385,35 +1403,35 @@ "wasm64" ], "cacheKeys": { - "wasm32": "2eb50e31f5a54ba1cb1d6ee0fcff087e91a281ee79a7cfbb1a3ef0ad1b833769", - "wasm64": "884db9a8d75a3282daf82529bfe5578d3c956fa7c6f9b41552a72832dc34a32c" + "wasm32": "fb7dee8aa0eea01b0eb992a272476a3c0ef57cc57e25c67414f6d57b48e8e441", + "wasm64": "711c8d6db694351d1af8958c90edb8c489976800a1f1a60f2584dc16c2b0172f" }, "dependencyClosures": { "wasm32": [ { "packageName": "coreutils", "manifestSha256": "b8baabc9af9283434e0d80f7fdc0aaef242ffa31092ab0fc39a0603a12b3cca7", - "cacheKey": "c44171215c792087144a3dc8bd76f9a37f2f8311ed3d7b29e0ce1ae8fb79a76a" + "cacheKey": "8aa8d109e92b93b779708d5e28b59cb972e58591cd6aaebb96613d0b99825b38" }, { "packageName": "dash", "manifestSha256": "10ce1bb611fefd78a6c9ec9038f6113e29a7062b1a73e08eccf9b33004cea9a8", - "cacheKey": "226bda53ce2d316d11744de02e555a09e870f3bb5a5ee3d3e79d8a47f0612bef" + "cacheKey": "0cb537cf7cc09bbadf377821dfb68c3694d3db2ddcd0d1645f7a61126c5efda4" }, { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "db68fc369364580e6690bb1097fa27d4eb7a1449574f24b6ab1202c60ff51f1b" + "cacheKey": "e986afadf2ced00ecefa5a80b79ab1063ede97d23943c078628e3d689854b276" }, { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "mariadb", "manifestSha256": "aeb221be233e4b57bccb4b3375020f94c3ec593dac2734631f00e3b1aec9f48c", - "cacheKey": "ed9391bd27da1f44cd6aea9b51f3e2431c964afc8c76bb45a1fc18b73ab79243" + "cacheKey": "6880cf285ed278369b3f4b3959df2c2388a4a57d1e42ed179bdc88995aa90ec0" }, { "packageName": "pcre2-source", @@ -1425,27 +1443,27 @@ { "packageName": "coreutils", "manifestSha256": "b8baabc9af9283434e0d80f7fdc0aaef242ffa31092ab0fc39a0603a12b3cca7", - "cacheKey": "a244a238a2814ec1556b67723c51f1838b268c13e43ac70ecf027b99b86f2c73" + "cacheKey": "c3c1620a49c925561c593851b7ea7b73935166cbcdbc3cb99a4ca9b8464fc189" }, { "packageName": "dash", "manifestSha256": "10ce1bb611fefd78a6c9ec9038f6113e29a7062b1a73e08eccf9b33004cea9a8", - "cacheKey": "4eef9693a9055b740c4bdf1fff1e66b7973295eff2fae81f97509288a1a83ecf" + "cacheKey": "db16863f590647f1526b4e8743d0e1d493d2dba4f8ea39a49411fb0c3f355080" }, { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "b6cb0cfbac1f98938650b2b6a05c2daf131d7767e60de67258c438caec8eb27f" + "cacheKey": "970ae5ab98c626cedc9c708568ffb3e0d0547684ebf1de2bbfe233b843b41f3e" }, { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "6f624f5476751d07bb58b04608a389400c079fd74c7e62ef52306d89f5f8754d" + "cacheKey": "cb7c9eadcc32d322fc339ddeb06eac0a5377423bd9d92a478c6cf1e71e0ec7b3" }, { "packageName": "mariadb", "manifestSha256": "aeb221be233e4b57bccb4b3375020f94c3ec593dac2734631f00e3b1aec9f48c", - "cacheKey": "269af4d38b0809ba0854715d33e9327b0eafd55391992efa945d317e518af932" + "cacheKey": "4391919dcf0bf9e8ed26d01db20388bafc26a3541e41528b05d4005305e0289e" }, { "packageName": "pcre2-source", @@ -1470,7 +1488,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "752ec74aad6b305d9334a56116273bc35a8854cd77274b11b84549636e7b239e" + "wasm32": "e78096787decb030ebad9f2e4d1f0f028bbef880843734ae976843587c592e37" }, "dependencyClosures": { "wasm32": [] @@ -1491,7 +1509,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "0741e71f99d4979625742f3e1b20e0fdc0140ff90ffa8997303ad9c3901e6815" + "wasm32": "428beb3e8153aada86ede76aa28800fd78f5d1cbb618707a1f713b43f5faf29b" }, "dependencyClosures": { "wasm32": [] @@ -1512,7 +1530,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "907e645176d86044e4f1885cde3c5b5cba075a0d3245c6d8f86c78194864e264" + "wasm32": "95058978c71a4dcbce64748127628ba7273b5c33ab3eac4f455c7d1f28d36596" }, "dependencyClosures": { "wasm32": [] @@ -1533,7 +1551,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "f7007c22d22911bf948ea1756962b7f0bf2f4a4dc11770bbae6f05a4d9ac575a" + "wasm32": "546b02fc45c5010047b62bcb89d6f7655ca508c76ca561ee596624cc1ce47a77" }, "dependencyClosures": { "wasm32": [] @@ -1617,7 +1635,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "5ea9f997b6b09deb18285945073754364de5b96dc53b30ca95df2edbfa5f8531" + "wasm32": "04661bea89ce63e48c96e8bfece91da3beadbd5579cbecef9b65186b68a72729" }, "dependencyClosures": { "wasm32": [] @@ -1638,14 +1656,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "ad3d224fee9951ce2ddf76e2d6beaf66c2464c39b1ae6218832d888314aafdc3" + "wasm32": "62505f6d4ce2c11722939a2798358325cb4e248c0e32989ef9e098fb74aeb522" }, "dependencyClosures": { "wasm32": [ { "packageName": "ncurses", "manifestSha256": "0a1c180eeade627e204aa47065dbfebfcdc9359d25e2db436932c7fa3a86fc1e", - "cacheKey": "f7007c22d22911bf948ea1756962b7f0bf2f4a4dc11770bbae6f05a4d9ac575a" + "cacheKey": "546b02fc45c5010047b62bcb89d6f7655ca508c76ca561ee596624cc1ce47a77" } ] }, @@ -1665,19 +1683,19 @@ "wasm32" ], "cacheKeys": { - "wasm32": "a1cf0b1cae527d6503726fe5f72a7c9b3ae3fab21c5f3d868a7635c1a1797a82" + "wasm32": "68df5b69319d99f616a2befb74fbde48d2bbcfc45a1c183954bdab2b2e6909f1" }, "dependencyClosures": { "wasm32": [ { "packageName": "ncurses", "manifestSha256": "0a1c180eeade627e204aa47065dbfebfcdc9359d25e2db436932c7fa3a86fc1e", - "cacheKey": "f7007c22d22911bf948ea1756962b7f0bf2f4a4dc11770bbae6f05a4d9ac575a" + "cacheKey": "546b02fc45c5010047b62bcb89d6f7655ca508c76ca561ee596624cc1ce47a77" }, { "packageName": "nethack", "manifestSha256": "1a2f12ec2770bd8d40006d6c878a3493b97c71c2bb63ad08c7f6ad99bfd53504", - "cacheKey": "ad3d224fee9951ce2ddf76e2d6beaf66c2464c39b1ae6218832d888314aafdc3" + "cacheKey": "62505f6d4ce2c11722939a2798358325cb4e248c0e32989ef9e098fb74aeb522" } ] }, @@ -1697,7 +1715,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "6e5a5508c265809727e0d599185e7216be72aece52651b8cd1c48c3712770196" + "wasm32": "a55f4c991fb3f173d17ccab1cd25deaa5779e5cfbbae7141cb0c97381995f28f" }, "dependencyClosures": { "wasm32": [] @@ -1718,79 +1736,79 @@ "wasm32" ], "cacheKeys": { - "wasm32": "0e23cc73004b33997f519bcdeceb05022a54b97a4bac30721d0919e194ad8802" + "wasm32": "ec9e76bf80c92f53c18cba90a0817f16bee4693e1fcee1b8c528324a2ac20250" }, "dependencyClosures": { "wasm32": [ { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "db68fc369364580e6690bb1097fa27d4eb7a1449574f24b6ab1202c60ff51f1b" + "cacheKey": "e986afadf2ced00ecefa5a80b79ab1063ede97d23943c078628e3d689854b276" }, { "packageName": "icu", "manifestSha256": "f5b1f02d169ec1108fc32d9da55be53c3efb96e3a7aeba6a03aec62e0175f0dc", - "cacheKey": "4ac7eecb23c1a8df06db4aed05430e068c0384c3f86fad423d7513225c456966" + "cacheKey": "dccdd3d70beae9d23d22bbdebf95a4ab509ec713de30eb7d86d0193ecaea4c09" }, { "packageName": "kernel", "manifestSha256": "db1d66db8575562ac7b3e720dbaa06d7dd5eef577d80220ea4167dc2d69b863b", - "cacheKey": "7c99137ecc1ba603403044403f0c9ac40fd92b6313414cf44e9eb0f1aca9015b" + "cacheKey": "e5180ca2c3ea83218b416c1d691ab2b549017eee1848ee2432df9af6c7d2b570" }, { "packageName": "libcurl", "manifestSha256": "c771e6cdc83b43840db4dd1fdce4b6189ba4f281596f7485bcda9c65e2ecda00", - "cacheKey": "5992711bf6f9c74f856802f2bb42dd089857daffa04ff15955c5a68562669b87" + "cacheKey": "f4f2e193219fee442db65d63e8ff29b47b72caf11ca1b5cb81ea552b24c2263d" }, { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "libiconv", "manifestSha256": "fa60b386f4ba0cd5bbbd595f67477cbfb0cc401a14bbd4769630c502bb3f24b1", - "cacheKey": "46ac767af4b028b8ff540560f1b608b5c9ad75f9859055e322a3dd012eb5390f" + "cacheKey": "0a7f0c9fb38e897230d1798c43cded55bd9c8ae4e83cbd28b7526d677f8deac4" }, { "packageName": "libxml2", "manifestSha256": "908490efcdc7783c76ce36af31d1db24bfcc7ba5f1e7b9b5298cc81cb110256c", - "cacheKey": "a36d4055cf9c608654dace57fe18209597eb630c9bc7a1f9d4f320f2e7b885d8" + "cacheKey": "934a6aaaedd65011653013293f4b87532f68068d58d72fbe5559097835739e4b" }, { "packageName": "libzip", "manifestSha256": "83c65d63a6416e79436bf4e792759a4701d568df0e8c010394a9732cf659f0cc", - "cacheKey": "94ea97db7a56b44127bf30e29ae40c7d6d069ac010d6295b654a04d8f8fcbbc0" + "cacheKey": "e5adb5a22165e93c595754d059dba976f8e2600376d8c9da157205fa8cab614b" }, { "packageName": "nginx", "manifestSha256": "41b0aaf05fdd0d8da42642703f9e60fd7b9545d8029896918b87c771c359147b", - "cacheKey": "6e5a5508c265809727e0d599185e7216be72aece52651b8cd1c48c3712770196" + "cacheKey": "a55f4c991fb3f173d17ccab1cd25deaa5779e5cfbbae7141cb0c97381995f28f" }, { "packageName": "openssl", "manifestSha256": "374f5dce6b2691b630b55cb82955b33d2305384e4bd64746c581621bb0793993", - "cacheKey": "359369fd3977d6583c001fc6c26e5caf54160f07cce7c9f33ae87bd85c212217" + "cacheKey": "880eadc0dad3a794dce96eb841868a9326541fcaaa7630bc4d7618840ba0a849" }, { "packageName": "php", "manifestSha256": "fcd9d7915ed8935b05418e148edcb312adf0b1e4e627fe4aab75444b35b33bbc", - "cacheKey": "8583b755c9eb5a6877e101b3330473d7b5248c7f1e1a8983461811fb04f61476" + "cacheKey": "a07a9e3bf8039cbe818a180217e3edd763b563b5a7461ade5b40e781a207c952" }, { "packageName": "shell", "manifestSha256": "28a871b7fcf72f3e289d5116c2d308e22fd4beffd6c5db4bffa262f9b16c1384", - "cacheKey": "07baaa2896902650f67a54a305ca931d280239a0bb74bcd0b62307f946ab8ff1" + "cacheKey": "25dbd7f217f7d878277face79d108e8e04cce7efdd4e2f3e5298076e995d286d" }, { "packageName": "sqlite", "manifestSha256": "ab9560b4eab53445034e2a87a27ccee9d05ddb0db73fee28f5000325b0b1cba5", - "cacheKey": "74a3c1fc211e94e2b55538438802f15a3b7997f61efe2f033f0b58076d50038f" + "cacheKey": "3a191371817cb5c2113b031e561ae9615a164f20b87e9ecbbb7e15c88431692a" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -1810,29 +1828,29 @@ "wasm32" ], "cacheKeys": { - "wasm32": "c23b7b53f5c9e46cbc48eef745814e9f897fb05ea23c152f991144492c3f7321" + "wasm32": "82e9591d96d27a7118b0d5c2303d65dd9552dc406a0e06c4a1b68f23f3e51855" }, "dependencyClosures": { "wasm32": [ { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "db68fc369364580e6690bb1097fa27d4eb7a1449574f24b6ab1202c60ff51f1b" + "cacheKey": "e986afadf2ced00ecefa5a80b79ab1063ede97d23943c078628e3d689854b276" }, { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "nginx", "manifestSha256": "41b0aaf05fdd0d8da42642703f9e60fd7b9545d8029896918b87c771c359147b", - "cacheKey": "6e5a5508c265809727e0d599185e7216be72aece52651b8cd1c48c3712770196" + "cacheKey": "a55f4c991fb3f173d17ccab1cd25deaa5779e5cfbbae7141cb0c97381995f28f" }, { "packageName": "shell", "manifestSha256": "28a871b7fcf72f3e289d5116c2d308e22fd4beffd6c5db4bffa262f9b16c1384", - "cacheKey": "07baaa2896902650f67a54a305ca931d280239a0bb74bcd0b62307f946ab8ff1" + "cacheKey": "25dbd7f217f7d878277face79d108e8e04cce7efdd4e2f3e5298076e995d286d" } ] }, @@ -1852,29 +1870,29 @@ "wasm32" ], "cacheKeys": { - "wasm32": "151bf271eefec8ff95770cc8e4c7d62e3a65af271275a026c78ce3139d9f8b6d" + "wasm32": "76796a79ab26b1204fd6c4373529d34913efa28f3bb4f4ec46cb14db0d81b25d" }, "dependencyClosures": { "wasm32": [ { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "openssl", "manifestSha256": "374f5dce6b2691b630b55cb82955b33d2305384e4bd64746c581621bb0793993", - "cacheKey": "359369fd3977d6583c001fc6c26e5caf54160f07cce7c9f33ae87bd85c212217" + "cacheKey": "880eadc0dad3a794dce96eb841868a9326541fcaaa7630bc4d7618840ba0a849" }, { "packageName": "spidermonkey", "manifestSha256": "c72ef4e43ee2a4fc74390d03a239fce3b545a6d3b64f02ccc4d44972cec626bd", - "cacheKey": "83ce9ad65c9abbd9bddeee7ff39f92da696c1c41848de40b47350d963f7764ed" + "cacheKey": "c568200194f122282abb2aafa62044bd9eb739594a7ff25ce37b4ba94082e8e6" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -1894,39 +1912,39 @@ "wasm32" ], "cacheKeys": { - "wasm32": "614398767ef68a4db27569c97c8dd8f4b4f7a1e196038efe4cc9f1dfc55e5578" + "wasm32": "764b880d42e792623e0f715ed4398e7404ee17abf2b7638166845dfb3072ec6d" }, "dependencyClosures": { "wasm32": [ { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "node", "manifestSha256": "2131a24dfda8587860e57fc65b573086477d376b065b3b9ca4e1dfe4d79f5790", - "cacheKey": "151bf271eefec8ff95770cc8e4c7d62e3a65af271275a026c78ce3139d9f8b6d" + "cacheKey": "76796a79ab26b1204fd6c4373529d34913efa28f3bb4f4ec46cb14db0d81b25d" }, { "packageName": "openssl", "manifestSha256": "374f5dce6b2691b630b55cb82955b33d2305384e4bd64746c581621bb0793993", - "cacheKey": "359369fd3977d6583c001fc6c26e5caf54160f07cce7c9f33ae87bd85c212217" + "cacheKey": "880eadc0dad3a794dce96eb841868a9326541fcaaa7630bc4d7618840ba0a849" }, { "packageName": "shell", "manifestSha256": "28a871b7fcf72f3e289d5116c2d308e22fd4beffd6c5db4bffa262f9b16c1384", - "cacheKey": "07baaa2896902650f67a54a305ca931d280239a0bb74bcd0b62307f946ab8ff1" + "cacheKey": "25dbd7f217f7d878277face79d108e8e04cce7efdd4e2f3e5298076e995d286d" }, { "packageName": "spidermonkey", "manifestSha256": "c72ef4e43ee2a4fc74390d03a239fce3b545a6d3b64f02ccc4d44972cec626bd", - "cacheKey": "83ce9ad65c9abbd9bddeee7ff39f92da696c1c41848de40b47350d963f7764ed" + "cacheKey": "c568200194f122282abb2aafa62044bd9eb739594a7ff25ce37b4ba94082e8e6" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -1946,7 +1964,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "d761b986fbd1b508814857959fffee294ecbc7d17eaa7677115782757bca752a" + "wasm32": "e25be2e35de9dfc135d79802a08d077938d8c33e815326aa353d72876beb8413" }, "dependencyClosures": { "wasm32": [] @@ -1967,14 +1985,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "11277eefb716c2bb52fa2474d3c58f33060ce46f30e3bd1472ce39da708cc56c" + "wasm32": "807f40dc841eabdb0b71f9947ecc5c384332ebbde89aa37fabdc826d29208220" }, "dependencyClosures": { "wasm32": [ { "packageName": "perl", "manifestSha256": "6cdc4dbc54d0e4008cff41f82ec8918c0e44b4aef23903539c1bc0f538fe3ad2", - "cacheKey": "d761b986fbd1b508814857959fffee294ecbc7d17eaa7677115782757bca752a" + "cacheKey": "e25be2e35de9dfc135d79802a08d077938d8c33e815326aa353d72876beb8413" } ] }, @@ -1994,54 +2012,54 @@ "wasm32" ], "cacheKeys": { - "wasm32": "8583b755c9eb5a6877e101b3330473d7b5248c7f1e1a8983461811fb04f61476" + "wasm32": "a07a9e3bf8039cbe818a180217e3edd763b563b5a7461ade5b40e781a207c952" }, "dependencyClosures": { "wasm32": [ { "packageName": "icu", "manifestSha256": "f5b1f02d169ec1108fc32d9da55be53c3efb96e3a7aeba6a03aec62e0175f0dc", - "cacheKey": "4ac7eecb23c1a8df06db4aed05430e068c0384c3f86fad423d7513225c456966" + "cacheKey": "dccdd3d70beae9d23d22bbdebf95a4ab509ec713de30eb7d86d0193ecaea4c09" }, { "packageName": "libcurl", "manifestSha256": "c771e6cdc83b43840db4dd1fdce4b6189ba4f281596f7485bcda9c65e2ecda00", - "cacheKey": "5992711bf6f9c74f856802f2bb42dd089857daffa04ff15955c5a68562669b87" + "cacheKey": "f4f2e193219fee442db65d63e8ff29b47b72caf11ca1b5cb81ea552b24c2263d" }, { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "libiconv", "manifestSha256": "fa60b386f4ba0cd5bbbd595f67477cbfb0cc401a14bbd4769630c502bb3f24b1", - "cacheKey": "46ac767af4b028b8ff540560f1b608b5c9ad75f9859055e322a3dd012eb5390f" + "cacheKey": "0a7f0c9fb38e897230d1798c43cded55bd9c8ae4e83cbd28b7526d677f8deac4" }, { "packageName": "libxml2", "manifestSha256": "908490efcdc7783c76ce36af31d1db24bfcc7ba5f1e7b9b5298cc81cb110256c", - "cacheKey": "a36d4055cf9c608654dace57fe18209597eb630c9bc7a1f9d4f320f2e7b885d8" + "cacheKey": "934a6aaaedd65011653013293f4b87532f68068d58d72fbe5559097835739e4b" }, { "packageName": "libzip", "manifestSha256": "83c65d63a6416e79436bf4e792759a4701d568df0e8c010394a9732cf659f0cc", - "cacheKey": "94ea97db7a56b44127bf30e29ae40c7d6d069ac010d6295b654a04d8f8fcbbc0" + "cacheKey": "e5adb5a22165e93c595754d059dba976f8e2600376d8c9da157205fa8cab614b" }, { "packageName": "openssl", "manifestSha256": "374f5dce6b2691b630b55cb82955b33d2305384e4bd64746c581621bb0793993", - "cacheKey": "359369fd3977d6583c001fc6c26e5caf54160f07cce7c9f33ae87bd85c212217" + "cacheKey": "880eadc0dad3a794dce96eb841868a9326541fcaaa7630bc4d7618840ba0a849" }, { "packageName": "sqlite", "manifestSha256": "ab9560b4eab53445034e2a87a27ccee9d05ddb0db73fee28f5000325b0b1cba5", - "cacheKey": "74a3c1fc211e94e2b55538438802f15a3b7997f61efe2f033f0b58076d50038f" + "cacheKey": "3a191371817cb5c2113b031e561ae9615a164f20b87e9ecbbb7e15c88431692a" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -2117,7 +2135,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "1819a7d6e4bd40577180e9dc8fcde01ca5a96c99b85e5941fc4541f9d8f54d10" + "wasm32": "0b3456ba5cd5ff45833de8867c9b036734aab58f9a20240dc68bf8c54e5181c7" }, "dependencyClosures": { "wasm32": [] @@ -2390,19 +2408,19 @@ "wasm32" ], "cacheKeys": { - "wasm32": "b03f527c89153f331ce1f47a02649bc0e09e1c6623b7d242251f458aaa252661" + "wasm32": "97756a7c2b77d6ac165f6dbdc0a10d1b6f35dcd77f6bc53a2612de81cdca1272" }, "dependencyClosures": { "wasm32": [ { "packageName": "cpython", "manifestSha256": "7dd4f446697a73941ec940c2ccba4d53be73fe6947f1e701031a4d0aa964c4ff", - "cacheKey": "4c75fb0082fa302537b99fc954a66275a680fd86afdcc71df12d6da7ea570c4f" + "cacheKey": "f15ab782192b8bd17b2acd4a5ea4dc33a90fc3b9a813f91a919697822b113bc7" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -2422,7 +2440,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "9134583aada35ad9d6b2ecf26522214c72a6514fb48f9ea4f7907384deaa64be" + "wasm32": "c917fdf98c1afe799e6625f52182e9a00378e6c343f63fc7102b8f1090e0b5eb" }, "dependencyClosures": { "wasm32": [] @@ -2450,24 +2468,24 @@ "wasm32" ], "cacheKeys": { - "wasm32": "f16e5aa9291ffaa3afe627d39cac87981f6828dd72124623fbd82a99a5ace60b" + "wasm32": "5e872140fa47035b40735d573779f62c0b179046f8bf765f5bb886969f3a29a8" }, "dependencyClosures": { "wasm32": [ { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "db68fc369364580e6690bb1097fa27d4eb7a1449574f24b6ab1202c60ff51f1b" + "cacheKey": "e986afadf2ced00ecefa5a80b79ab1063ede97d23943c078628e3d689854b276" }, { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "redis", "manifestSha256": "151fb507de953ba2ba94b8f881bc7d66b4c741c1359a2f590cc07f9a4cd368a3", - "cacheKey": "9134583aada35ad9d6b2ecf26522214c72a6514fb48f9ea4f7907384deaa64be" + "cacheKey": "c917fdf98c1afe799e6625f52182e9a00378e6c343f63fc7102b8f1090e0b5eb" } ] }, @@ -2487,79 +2505,79 @@ "wasm32" ], "cacheKeys": { - "wasm32": "8712ba10c358df63db093fdda5eaf2aa0207d10501207c0fa5a2dd5868dd7ec3" + "wasm32": "151e91bfd484f088aeaf8aaade1c7d6458e2db4d36429b175dad2122e18a6c69" }, "dependencyClosures": { "wasm32": [ { "packageName": "bash", "manifestSha256": "6478060f28d430d18a6ebe7c603392d35a41d9bcf6bc74322351d1450b9c5335", - "cacheKey": "012336da4d490e68d3e200ec8d29e641d5946d6bec4c9e309d6f3bccedf9985b" + "cacheKey": "8fc30f59035df55e9dc2b7ed2359c13faf8d63ec285bf44e6f52c7de1e6b8fca" }, { "packageName": "bc", "manifestSha256": "a65661463bb7047b91ff00153bd99fd962c96e571934ab4e923f5215fb6ecfbd", - "cacheKey": "df5529c0caa8288deff41a9a0a59c17d99e5bf1eea473ffc553f68087ad55ac4" + "cacheKey": "9a7242df42960d7d69dab831f169f20626aa10174ada963fce9f801c7df2b125" }, { "packageName": "coreutils", "manifestSha256": "b8baabc9af9283434e0d80f7fdc0aaef242ffa31092ab0fc39a0603a12b3cca7", - "cacheKey": "c44171215c792087144a3dc8bd76f9a37f2f8311ed3d7b29e0ce1ae8fb79a76a" + "cacheKey": "8aa8d109e92b93b779708d5e28b59cb972e58591cd6aaebb96613d0b99825b38" }, { "packageName": "dash", "manifestSha256": "10ce1bb611fefd78a6c9ec9038f6113e29a7062b1a73e08eccf9b33004cea9a8", - "cacheKey": "226bda53ce2d316d11744de02e555a09e870f3bb5a5ee3d3e79d8a47f0612bef" + "cacheKey": "0cb537cf7cc09bbadf377821dfb68c3694d3db2ddcd0d1645f7a61126c5efda4" }, { "packageName": "diffutils", "manifestSha256": "3a78f0a46bae43ce5ea235c6638c2cbd1d8559b0b62b1fb42443b35968c1aca3", - "cacheKey": "5ade22f17ebe371d663fd23ad99b1a8422da350d40e2576eba114a6d9e0276c9" + "cacheKey": "ad399566e397f6fa75dabf30380ff57183ea7ad59a1bf30297080c23cd581abe" }, { "packageName": "file", "manifestSha256": "7874c1affcbbf2c8ab8c8d4beb087f275af57b5caae6a8947bf5a63a181552b2", - "cacheKey": "73f41f2eb367d94a91642c73568d97a78afc53bbb700f81f518b502dce5733fe" + "cacheKey": "2cdf71f1ed37aa53ea355f7dadc12f8ae041343b6941c0476d58d2ce764581b3" }, { "packageName": "findutils", "manifestSha256": "cceedf52aea67fbb0da03cfce6f2e9b1a7b5561fa1656c2762b43c651a625d02", - "cacheKey": "a40215f2320308d34213e516c8d27af1daa652489451983297ef64f937bff3eb" + "cacheKey": "a83aa55d45a41dcc635cdcb8c52f4309903b5c3ec843717f55bbdf1f9772e62a" }, { "packageName": "gawk", "manifestSha256": "a2567b8b0778805e385e1d3a41ac8b5d74aaf9d293b222001b17d09b11e5a0ba", - "cacheKey": "bb92b0fa58c8f18aae36dcc635a47df0ca25c12ee94df12db3d8e854bec07b5b" + "cacheKey": "1996fcf770d124ed1355db80f34271a30676afed43e439c19dcf591f435800c6" }, { "packageName": "grep", "manifestSha256": "59270d3bfb33167b32d13246bf855b49308f8c9c0a66d9635c804f702421fbc6", - "cacheKey": "95afdd724b558edb84d7d236bc1f00af8c2a7aff386641feefa96c49793a1f45" + "cacheKey": "8a78952c49537c96e7a0fd6837d23abed09ebf4fa67c0879b143316a043b2d15" }, { "packageName": "m4", "manifestSha256": "2c6582d99d6eabfb9da52badf49b899e9fdcba76a728536b25bc3741f3331543", - "cacheKey": "e26cfe10cb77fd905bcf69767d22824ee51bebef986fec841b78673e433e8e5f" + "cacheKey": "8f298e218fac8d3cf3a7828d311703c6b232020086851c3bf1aae9021f7379bf" }, { "packageName": "make", "manifestSha256": "f878d2d730f36a4c6dfe1fff1b4ccc704757ea22d8c4c8ccb95b11cd641e5fed", - "cacheKey": "6450f1427f20639676c13843ab492dacf77a00deac3bf3716e3ef0a49b10612a" + "cacheKey": "715f3f5c96e4e5944bbc3f55ab3c6d8cc4b0133abc09265b6acae9be4cd863e7" }, { "packageName": "ncurses", "manifestSha256": "0a1c180eeade627e204aa47065dbfebfcdc9359d25e2db436932c7fa3a86fc1e", - "cacheKey": "f7007c22d22911bf948ea1756962b7f0bf2f4a4dc11770bbae6f05a4d9ac575a" + "cacheKey": "546b02fc45c5010047b62bcb89d6f7655ca508c76ca561ee596624cc1ce47a77" }, { "packageName": "posix-utils-lite", "manifestSha256": "8fd7190b2848ef80143adc7b9268c79e13cd4db3430f7105faf49a4b4407a06f", - "cacheKey": "1819a7d6e4bd40577180e9dc8fcde01ca5a96c99b85e5941fc4541f9d8f54d10" + "cacheKey": "0b3456ba5cd5ff45833de8867c9b036734aab58f9a20240dc68bf8c54e5181c7" }, { "packageName": "sed", "manifestSha256": "2a08e9c5dacc5facc8983c1ff35ff2a72db328405e9a1f464cc7ad155c4d08af", - "cacheKey": "05e76414e569f8f79cf1e290f7afc3822f58ba29e5ef56b9588a3fcf57f7828c" + "cacheKey": "5819a38727f0e3ffc8c2bc21d1a3494bad9df3eecd88f31d45f55607eca7ba37" } ] }, @@ -2579,14 +2597,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "8aee9a53772073f007fcb92e219ad0623e86999762ca4e0218686db652cb2b4d" + "wasm32": "837fc3191253208c74d75869d7d030107b397c702fab1dc1c7d7ca916eb1c250" }, "dependencyClosures": { "wasm32": [ { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -2613,7 +2631,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "05e76414e569f8f79cf1e290f7afc3822f58ba29e5ef56b9588a3fcf57f7828c" + "wasm32": "5819a38727f0e3ffc8c2bc21d1a3494bad9df3eecd88f31d45f55607eca7ba37" }, "dependencyClosures": { "wasm32": [] @@ -2634,7 +2652,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "07baaa2896902650f67a54a305ca931d280239a0bb74bcd0b62307f946ab8ff1" + "wasm32": "25dbd7f217f7d878277face79d108e8e04cce7efdd4e2f3e5298076e995d286d" }, "dependencyClosures": { "wasm32": [] @@ -2655,24 +2673,24 @@ "wasm32" ], "cacheKeys": { - "wasm32": "83ce9ad65c9abbd9bddeee7ff39f92da696c1c41848de40b47350d963f7764ed" + "wasm32": "c568200194f122282abb2aafa62044bd9eb739594a7ff25ce37b4ba94082e8e6" }, "dependencyClosures": { "wasm32": [ { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "openssl", "manifestSha256": "374f5dce6b2691b630b55cb82955b33d2305384e4bd64746c581621bb0793993", - "cacheKey": "359369fd3977d6583c001fc6c26e5caf54160f07cce7c9f33ae87bd85c212217" + "cacheKey": "880eadc0dad3a794dce96eb841868a9326541fcaaa7630bc4d7618840ba0a849" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -2692,29 +2710,29 @@ "wasm32" ], "cacheKeys": { - "wasm32": "f746c7700ebd7ae25296f05095b44c89bf91be26cc49a0f4f38e49336ad381a6" + "wasm32": "50b646274527dc2c46a8a846267fc34b23f5ef22b903e3fc697534f5a323867d" }, "dependencyClosures": { "wasm32": [ { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "openssl", "manifestSha256": "374f5dce6b2691b630b55cb82955b33d2305384e4bd64746c581621bb0793993", - "cacheKey": "359369fd3977d6583c001fc6c26e5caf54160f07cce7c9f33ae87bd85c212217" + "cacheKey": "880eadc0dad3a794dce96eb841868a9326541fcaaa7630bc4d7618840ba0a849" }, { "packageName": "spidermonkey", "manifestSha256": "c72ef4e43ee2a4fc74390d03a239fce3b545a6d3b64f02ccc4d44972cec626bd", - "cacheKey": "83ce9ad65c9abbd9bddeee7ff39f92da696c1c41848de40b47350d963f7764ed" + "cacheKey": "c568200194f122282abb2aafa62044bd9eb739594a7ff25ce37b4ba94082e8e6" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -2734,7 +2752,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "558f50274c45faa2efbf8b4d3c2a5afd3468dd1d2430cbd87c2398f2f1c73503" + "wasm32": "837538ad9565e69d03221cd901c285cd15c0305b7e8ebe2d59370739bbbc3999" }, "dependencyClosures": { "wasm32": [] @@ -2755,7 +2773,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "2ae2051ddf7439c7eda0f20911a5402c44603b05242161bfe8002dce4fa58390" + "wasm32": "2b9fc9de4354ee7391daae45569d78b057a6a5eaf3b4fdf3503bdf157252713b" }, "dependencyClosures": { "wasm32": [] @@ -2776,7 +2794,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "fc021865eb1e8583b65940f3d6d9b3df0084a7951f1e8d386ecb2ec3d91e4d4d" + "wasm32": "c23eaadb20042e2af5f41b009af003a2adb0adb0c3253c816a536717187a225a" }, "dependencyClosures": { "wasm32": [] @@ -2797,19 +2815,19 @@ "wasm32" ], "cacheKeys": { - "wasm32": "932bd4d32bcce12e290f2213e3879abe85c2ad11a257398e812ad3d9e2f5aa35" + "wasm32": "7d6251dbe71a6af7ef83862e810c171610ab037e9c674b891b76454c95177d83" }, "dependencyClosures": { "wasm32": [ { "packageName": "libpng", "manifestSha256": "79ed5c5c072a0267cce5c7a2fe37d8494571b17e44b952f619e04ec1c4a9db10", - "cacheKey": "b9effc903d2078f80bbd232461d7cc43ce8162826fc5dbeed275ab2336a5bfbb" + "cacheKey": "7ff7620c938207726786eed2bb33a7af995be614c8b1f3bb31cb352bcdfafbf8" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -2836,7 +2854,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "562280d78261c438af4f5bf1142ef3e95ef866034799d421e4745282d7e240db" + "wasm32": "3306c9a85cf89e9f3d2728c63eea7991b9c76e26ffbe7fa6b9eb0100bf89445c" }, "dependencyClosures": { "wasm32": [] @@ -2857,7 +2875,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "1def118e38431ca3b5ab7d3ee0e5683b2114c55b6bebc5153c506fca3cb5256a" + "wasm32": "5da1386710e9baec1c9ca2ad190e067071f26c2a7c2cacb06f2ff9df7439df3a" }, "dependencyClosures": { "wasm32": [] @@ -2878,14 +2896,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "d67b9967f865c9dcc06abfdbc538b7a724c9908d3a035f46b83b45b6e510f28e" + "wasm32": "cdc07020c6a281b39732e8c1aa5aef7fd150ebe48444d37423086e0c29271e35" }, "dependencyClosures": { "wasm32": [ { "packageName": "vim", "manifestSha256": "c211660ef41d01f95f3fbdf675b74891e897e3f304e04f1bf5586f326007382c", - "cacheKey": "1def118e38431ca3b5ab7d3ee0e5683b2114c55b6bebc5153c506fca3cb5256a" + "cacheKey": "5da1386710e9baec1c9ca2ad190e067071f26c2a7c2cacb06f2ff9df7439df3a" } ] }, @@ -2905,7 +2923,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "0ab3ac6fbb77925a949930bd9d5830a9d0277caa3aa3d6b427b735ff5ef54ce3" + "wasm32": "d7f56b9d2c2a892e2888c609b9fcf25f490e97536456440709a731ff525c3d47" }, "dependencyClosures": { "wasm32": [] @@ -2926,79 +2944,79 @@ "wasm32" ], "cacheKeys": { - "wasm32": "e749bb66e576baa2cf4919a91fbf414f5311946101044e658831ccf4e8068e88" + "wasm32": "7ef244afa0da39737ec243a8918cd81f12c42fe983be9935fc587d81d8d2ca88" }, "dependencyClosures": { "wasm32": [ { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "db68fc369364580e6690bb1097fa27d4eb7a1449574f24b6ab1202c60ff51f1b" + "cacheKey": "e986afadf2ced00ecefa5a80b79ab1063ede97d23943c078628e3d689854b276" }, { "packageName": "icu", "manifestSha256": "f5b1f02d169ec1108fc32d9da55be53c3efb96e3a7aeba6a03aec62e0175f0dc", - "cacheKey": "4ac7eecb23c1a8df06db4aed05430e068c0384c3f86fad423d7513225c456966" + "cacheKey": "dccdd3d70beae9d23d22bbdebf95a4ab509ec713de30eb7d86d0193ecaea4c09" }, { "packageName": "libcurl", "manifestSha256": "c771e6cdc83b43840db4dd1fdce4b6189ba4f281596f7485bcda9c65e2ecda00", - "cacheKey": "5992711bf6f9c74f856802f2bb42dd089857daffa04ff15955c5a68562669b87" + "cacheKey": "f4f2e193219fee442db65d63e8ff29b47b72caf11ca1b5cb81ea552b24c2263d" }, { "packageName": "libcxx", "manifestSha256": "6e1ebfa6914043770eae2a6c156b915acf6a56fe763f07d215ffe6f6f2305007", - "cacheKey": "189c56c958bfab35fa4b361d15a29279626c2f785db1a3e7ea4b71bb25ede3f2" + "cacheKey": "5894feb8228f76b3cf4cb18125de8d1bedcffea0234d3a5c06fef0387ab83fe7" }, { "packageName": "libiconv", "manifestSha256": "fa60b386f4ba0cd5bbbd595f67477cbfb0cc401a14bbd4769630c502bb3f24b1", - "cacheKey": "46ac767af4b028b8ff540560f1b608b5c9ad75f9859055e322a3dd012eb5390f" + "cacheKey": "0a7f0c9fb38e897230d1798c43cded55bd9c8ae4e83cbd28b7526d677f8deac4" }, { "packageName": "libxml2", "manifestSha256": "908490efcdc7783c76ce36af31d1db24bfcc7ba5f1e7b9b5298cc81cb110256c", - "cacheKey": "a36d4055cf9c608654dace57fe18209597eb630c9bc7a1f9d4f320f2e7b885d8" + "cacheKey": "934a6aaaedd65011653013293f4b87532f68068d58d72fbe5559097835739e4b" }, { "packageName": "libzip", "manifestSha256": "83c65d63a6416e79436bf4e792759a4701d568df0e8c010394a9732cf659f0cc", - "cacheKey": "94ea97db7a56b44127bf30e29ae40c7d6d069ac010d6295b654a04d8f8fcbbc0" + "cacheKey": "e5adb5a22165e93c595754d059dba976f8e2600376d8c9da157205fa8cab614b" }, { "packageName": "msmtpd", "manifestSha256": "09de04a422ddf631a29a259d816e081f8d317d1077808ede55d8c500baae748f", - "cacheKey": "0741e71f99d4979625742f3e1b20e0fdc0140ff90ffa8997303ad9c3901e6815" + "cacheKey": "428beb3e8153aada86ede76aa28800fd78f5d1cbb618707a1f713b43f5faf29b" }, { "packageName": "nginx", "manifestSha256": "41b0aaf05fdd0d8da42642703f9e60fd7b9545d8029896918b87c771c359147b", - "cacheKey": "6e5a5508c265809727e0d599185e7216be72aece52651b8cd1c48c3712770196" + "cacheKey": "a55f4c991fb3f173d17ccab1cd25deaa5779e5cfbbae7141cb0c97381995f28f" }, { "packageName": "openssl", "manifestSha256": "374f5dce6b2691b630b55cb82955b33d2305384e4bd64746c581621bb0793993", - "cacheKey": "359369fd3977d6583c001fc6c26e5caf54160f07cce7c9f33ae87bd85c212217" + "cacheKey": "880eadc0dad3a794dce96eb841868a9326541fcaaa7630bc4d7618840ba0a849" }, { "packageName": "php", "manifestSha256": "fcd9d7915ed8935b05418e148edcb312adf0b1e4e627fe4aab75444b35b33bbc", - "cacheKey": "8583b755c9eb5a6877e101b3330473d7b5248c7f1e1a8983461811fb04f61476" + "cacheKey": "a07a9e3bf8039cbe818a180217e3edd763b563b5a7461ade5b40e781a207c952" }, { "packageName": "shell", "manifestSha256": "28a871b7fcf72f3e289d5116c2d308e22fd4beffd6c5db4bffa262f9b16c1384", - "cacheKey": "07baaa2896902650f67a54a305ca931d280239a0bb74bcd0b62307f946ab8ff1" + "cacheKey": "25dbd7f217f7d878277face79d108e8e04cce7efdd4e2f3e5298076e995d286d" }, { "packageName": "sqlite", "manifestSha256": "ab9560b4eab53445034e2a87a27ccee9d05ddb0db73fee28f5000325b0b1cba5", - "cacheKey": "74a3c1fc211e94e2b55538438802f15a3b7997f61efe2f033f0b58076d50038f" + "cacheKey": "3a191371817cb5c2113b031e561ae9615a164f20b87e9ecbbb7e15c88431692a" }, { "packageName": "zlib", "manifestSha256": "d4f4701c0c30dc843ebe2111ed3e2284836cdfc4bc05a130c8fab4f31b8e6357", - "cacheKey": "dd57d8e8c32c0ab70166ddb3163b5f11ed875342deae2cd31ffb111ac78c027f" + "cacheKey": "81124d808e4bb5e84d0fbd519c5cc4dffced2b420e819cb26bd89d15af8a7890" } ] }, @@ -3018,7 +3036,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "86b9dc3f926a72bc93da4624570c5047b9e4406f0b5c248f24fbd24a46a18a6d" + "wasm32": "201487250ddcc127b636db79702465158356bebd505dcbf7b3fbf77310ce8cc0" }, "dependencyClosures": { "wasm32": [] @@ -3039,7 +3057,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "3cb8a25196713a8863b34a17372a8ebd5cdb168a89d8ef059b2cbfe4a68cd6c0" + "wasm32": "cc384c33682211759704640e85688eec5affeb765e0a6044ff6db9fd3de32a42" }, "dependencyClosures": { "wasm32": [] @@ -3060,7 +3078,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "a47a83e02cd18eccb2586fe034b5aa5312e5c7e8d67c8c68c2ad7a2e06f73946" + "wasm32": "f670f96ec5ace8362aee82256db3c4f4a25dbe00315ebaf649438e365501a7a7" }, "dependencyClosures": { "wasm32": [] diff --git a/packages/registry/ruby/build-ruby.sh b/packages/registry/ruby/build-ruby.sh index be848bcde3..fca5eba40d 100755 --- a/packages/registry/ruby/build-ruby.sh +++ b/packages/registry/ruby/build-ruby.sh @@ -4,7 +4,7 @@ set -euo pipefail # Build Ruby 4.0.5 for wasm32-posix-kernel. # # Two-phase build: -# 1. Host-native miniruby (generates C source files during cross-compilation) +# 1. Host-native Ruby (runs source-matched build and install tooling) # 2. Cross-compile Ruby for wasm32 using the SDK toolchain # # Output: packages/registry/ruby/bin/ruby.wasm and ruby-runtime.zip for a @@ -33,6 +33,13 @@ RUBY_VERSION="${WASM_POSIX_DEP_VERSION:-${RUBY_VERSION:-4.0.5}}" RUBY_MAJOR_MINOR="$(echo "$RUBY_VERSION" | cut -d. -f1-2)" SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://cache.ruby-lang.org/pub/ruby/${RUBY_MAJOR_MINOR}/ruby-${RUBY_VERSION}.tar.gz}" SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" +MSGPACK_VERSION="1.8.0" +MSGPACK_URL="https://rubygems.org/downloads/msgpack-${MSGPACK_VERSION}.gem" +MSGPACK_SHA256="e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732" +BOOTSNAP_VERSION="1.24.5" +BOOTSNAP_URL="https://rubygems.org/downloads/bootsnap-${BOOTSNAP_VERSION}.gem" +BOOTSNAP_SHA256="36b677448524d279b470469aabd5dff4a980e3fa4931a0df68da4a500eb1b6c4" +SOURCE_IDENTITY="ruby-${RUBY_VERSION}-msgpack-${MSGPACK_VERSION}-bootsnap-${BOOTSNAP_VERSION}-portable-v2" PACKAGE_NAME="${WASM_POSIX_DEP_NAME:-ruby}" GUEST_PREFIX="${WASM_POSIX_DEP_GUEST_PREFIX-/usr}" @@ -81,15 +88,15 @@ if [ -n "${WASM_POSIX_DEP_WORK_DIR:-}" ]; then fi export WASM_POSIX_SYSROOT="$SYSROOT" -if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$RUBY_VERSION" ]; then - echo "==> Existing Ruby source is not $RUBY_VERSION; cleaning Ruby build directories..." +if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$SOURCE_IDENTITY" ]; then + echo "==> Existing Ruby source does not match $SOURCE_IDENTITY; cleaning Ruby build directories..." rm -rf "$SRC_DIR" "$HOST_BUILD_DIR" "$CROSS_BUILD_DIR" "$INSTALL_DIR" "$BIN_DIR" fi -if [ -x "$HOST_BUILD_DIR/miniruby" ]; then - HOST_RUBY_VERSION="$("$HOST_BUILD_DIR/miniruby" -e 'print RUBY_VERSION' 2>/dev/null || true)" +if [ -x "$HOST_BUILD_DIR/ruby" ]; then + HOST_RUBY_VERSION="$("$HOST_BUILD_DIR/ruby" --disable=gems -e 'print RUBY_VERSION' 2>/dev/null || true)" if [ "$HOST_RUBY_VERSION" != "$RUBY_VERSION" ]; then - echo "==> Existing host miniruby is $HOST_RUBY_VERSION, expected $RUBY_VERSION; rebuilding..." + echo "==> Existing host Ruby is $HOST_RUBY_VERSION, expected $RUBY_VERSION; rebuilding..." rm -rf "$HOST_BUILD_DIR" fi fi @@ -116,11 +123,18 @@ echo "==> zlib at $ZLIB_PREFIX" # Build libyaml if not already built (Ruby needs it for psych/YAML) LIBYAML_DIR="$WORK_DIR/libyaml-install" -if [ ! -f "$LIBYAML_DIR/lib/libyaml.a" ]; then +LIBYAML_BUILD_ID="libyaml-0.2.5-pic-v1" +LIBYAML_BUILD_MARKER="$LIBYAML_DIR/.kandelo-build-id" +if [ ! -f "$LIBYAML_DIR/lib/libyaml.a" ] || \ + [ "$(cat "$LIBYAML_BUILD_MARKER" 2>/dev/null || true)" != "$LIBYAML_BUILD_ID" ]; then echo "==> Building libyaml for wasm32..." LIBYAML_VERSION="0.2.5" LIBYAML_SHA256="c642ae9b75fee120b2d96c712538bd2cf283228d2337df2cf2988e3c02678ef4" LIBYAML_SRC="$WORK_DIR/libyaml-src" + # A prior direct developer build may predate the PIC contract. Re-extract + # rather than trusting its configured Makefile, whose compile flags are not + # represented in libyaml's own output tree. + rm -rf "$LIBYAML_DIR" "$LIBYAML_SRC" if [ ! -d "$LIBYAML_SRC" ]; then curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "https://pyyaml.org/download/libyaml/yaml-${LIBYAML_VERSION}.tar.gz" \ -o "/tmp/yaml-${LIBYAML_VERSION}.tar.gz" @@ -131,10 +145,14 @@ if [ ! -f "$LIBYAML_DIR/lib/libyaml.a" ]; then fi cd "$LIBYAML_SRC" if [ ! -f Makefile ]; then - wasm32posix-configure --prefix="$LIBYAML_DIR" --disable-shared --enable-static + # psych.so absorbs libyaml.a, so every object in the archive must use + # the side-module-compatible position-independent relocation model. + CFLAGS="-O2 -fPIC" wasm32posix-configure \ + --prefix="$LIBYAML_DIR" --disable-shared --enable-static fi make -j"$(sysctl -n hw.ncpu 2>/dev/null || nproc)" make install + printf '%s\n' "$LIBYAML_BUILD_ID" > "$LIBYAML_BUILD_MARKER" cd "$REPO_ROOT" echo "==> libyaml built" fi @@ -167,10 +185,39 @@ if [ ! -d "$SRC_DIR" ]; then mkdir -p "$SRC_DIR" tar xzf "/tmp/${TARBALL}" -C "$SRC_DIR" --strip-components=1 rm "/tmp/${TARBALL}" - printf '%s\n' "$RUBY_VERSION" > "$SOURCE_MARKER" + printf '%s\n' "$SOURCE_IDENTITY" > "$SOURCE_MARKER" echo "==> Source extracted to $SRC_DIR" fi +stage_bundled_gem() { + local name="$1" + local version="$2" + local url="$3" + local sha256="$4" + local destination="$SRC_DIR/gems/${name}-${version}.gem" + local temporary="$destination.download" + + if [ ! -f "$destination" ] || + ! echo "$sha256 $destination" | shasum -a 256 -c - >/dev/null 2>&1; then + echo "==> Downloading Homebrew portable Ruby resource ${name} ${version}..." + curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors \ + -fsSL "$url" -o "$temporary" + echo "$sha256 $temporary" | shasum -a 256 -c - + mv "$temporary" "$destination" + fi + if ! grep -Eq "^${name}[[:space:]]+${version}([[:space:]]|$)" \ + "$SRC_DIR/gems/bundled_gems"; then + printf '%-19s %s\n' "$name" "$version" >> "$SRC_DIR/gems/bundled_gems" + fi +} + +# Match Homebrew's own portable-ruby Formula: these are the exact upstream +# bundled-gem resources that Homebrew adds before CRuby's normal build. +stage_bundled_gem \ + "msgpack" "$MSGPACK_VERSION" "$MSGPACK_URL" "$MSGPACK_SHA256" +stage_bundled_gem \ + "bootsnap" "$BOOTSNAP_VERSION" "$BOOTSNAP_URL" "$BOOTSNAP_SHA256" + # ─── Source patches for wasm32-posix ────────────────────────────────── # thread_none.c: missing thread_sched_atfork stub (called by thread.c unconditionally) if ! grep -q 'thread_sched_atfork' "$SRC_DIR/thread_none.c"; then @@ -498,23 +545,48 @@ EOF } # ─── Phase 1: Build host-native Ruby ───────────────────────────────── -if [ ! -x "$HOST_BUILD_DIR/miniruby" ]; then +if [ ! -x "$HOST_BUILD_DIR/ruby" ]; then echo "==> Building host Ruby (native build)..." mkdir -p "$HOST_BUILD_DIR" cd "$HOST_BUILD_DIR" - "$SRC_DIR/configure" \ + HOST_AR="$(command -v llvm-ar)" + HOST_RANLIB="$(command -v llvm-ranlib)" + # WHY: macOS /usr/bin/ar consults DEVELOPER_DIR and exits 255 when the + # repository's Nix SDK supplies that path. The dev shell's declared LLVM + # archiver is host-portable and also keeps this bootstrap independent of + # an ambient Xcode installation. + AR="$HOST_AR" RANLIB="$HOST_RANLIB" "$SRC_DIR/configure" \ --prefix="$HOST_BUILD_DIR/install" \ --disable-install-doc \ --disable-install-rdoc \ --disable-jit-support \ + --with-ext=monitor \ --with-out-ext=openssl,fiddle,readline - make miniruby -j"$(sysctl -n hw.ncpu 2>/dev/null || nproc)" + # CRuby's normal install runs RubyGems code from this exact source tree. + # A miniruby cannot load monitor, while an unrelated host Ruby can differ + # in core-library semantics. Build the smallest full, source-matched Ruby + # that can execute those host-side install tools faithfully. + make -j"$(sysctl -n hw.ncpu 2>/dev/null || nproc)" \ + AR="$HOST_AR" RANLIB="$HOST_RANLIB" cd "$REPO_ROOT" fi HOST_MINIRUBY="$HOST_BUILD_DIR/miniruby" +HOST_RUBY="$HOST_BUILD_DIR/ruby" echo "==> Host miniruby: $HOST_MINIRUBY" "$HOST_MINIRUBY" -e 'puts "Ruby #{RUBY_VERSION} miniruby OK"' +HOST_RUBY_ARCH="$( + "$HOST_RUBY" --disable=gems -I"$HOST_BUILD_DIR" -rrbconfig \ + -e 'print RbConfig::CONFIG.fetch("arch")' +)" +if [ -z "$HOST_RUBY_ARCH" ] || \ + [ ! -d "$HOST_BUILD_DIR/.ext/$HOST_RUBY_ARCH" ]; then + echo "ERROR: host Ruby did not produce a usable extension architecture" >&2 + exit 1 +fi +HOST_RUBY_COMMAND="$HOST_RUBY --disable=gems -I$HOST_BUILD_DIR -I$HOST_BUILD_DIR/.ext/common -I$HOST_BUILD_DIR/.ext/$HOST_RUBY_ARCH -I$SRC_DIR/lib" +echo "==> Host Ruby: $HOST_RUBY ($HOST_RUBY_ARCH)" +$HOST_RUBY_COMMAND -e 'require "monitor"; puts "source-matched host Ruby install tooling OK"' # ─── Phase 2: Cross-compile Ruby for wasm32-posix ──────────────────── echo "==> Cross-compiling Ruby for wasm32-posix..." @@ -707,9 +779,20 @@ ac_cv_func_sem_trywait=no ac_cv_func_sem_post=no ac_cv_func_posix_spawn=no ac_cv_func_posix_spawnp=no -ac_cv_func_dlopen=no -ac_cv_lib_dl_dlopen=no -ac_cv_func_dladdr=no +# Kandelo provides functional Wasm side modules through the SDK's explicit +# -ldl glue. Ruby uses HAVE_DLOPEN to compile its ordinary dln_load() path, so +# both the selected library and the function probe must describe that surface +# truthfully when cross-compilation cannot execute the Autoconf probe. +ac_cv_func_dlopen=yes +ac_cv_lib_dl_dlopen=yes +ac_cv_func_dladdr=yes +# WHY: CRuby derives this internal capability only from a closed list of +# native target operating systems. wasm32-unknown-none is deliberately not +# mislabelled as Linux, but Kandelo does provide dlopen() and dladdr(). Select +# CRuby's normal --enable-load-relative implementation just as portable Ruby +# does on supported native targets; this is a general Ruby relocation contract, +# not a Homebrew load-path override. +LOAD_RELATIVE=1 ac_cv_func_sigaltstack=no ac_cv_func_statvfs=no ac_cv_func_fstatvfs=no @@ -904,6 +987,7 @@ SITE_EOF CONFIG_SITE="$CONFIG_SITE" \ CC="$RUBY_CC_COMMAND" \ LD=wasm32posix-cc \ + LDSHARED="$RUBY_CC_COMMAND -shared" \ AR=wasm32posix-ar \ RANLIB=wasm32posix-ranlib \ NM=wasm32posix-nm \ @@ -915,6 +999,7 @@ SITE_EOF CFLAGS="-O2" \ CPPFLAGS="-DRUBY_KANDELO_POSIX=1 -I$ZLIB_PREFIX/include" \ LDFLAGS="-L$ZLIB_PREFIX/lib -Wl,-z,stack-size=1048576" \ + DLDFLAGS="-L$ZLIB_PREFIX/lib" \ "$SRC_DIR/configure" \ --host=wasm32-unknown-none \ --build="$(uname -m)-apple-darwin" \ @@ -934,15 +1019,34 @@ SITE_EOF --without-openssl \ --without-fiddle \ --without-readline \ - --with-static-linked-ext \ - --with-ext=stringio,zlib,monitor,psych,digest,digest/md5,digest/sha1,digest/sha2,json,json/parser,json/generator,strscan,date,etc,fcntl,io/console,pty,socket,continuation \ + --enable-load-relative \ + --with-ext=stringio,zlib,monitor,psych,digest,digest/md5,digest/sha1,digest/sha2,json,json/parser,json/generator,strscan,date,etc,fcntl,io/console,pty,socket,continuation,msgpack,bootsnap-* \ --with-out-ext=openssl,fiddle,readline,syslog,nkf,bigdecimal \ 2>&1 | tail -50 + if ! grep -Eq '^LIBRUBY_RELATIVE[[:space:]]*=[[:space:]]*yes$' Makefile; then + echo "ERROR: Ruby ignored its load-relative build contract" >&2 + exit 1 + fi echo "==> Configure complete." reject_asyncify_coroutine + # Ruby owns bundled-gem extraction and extension discovery. Run the same + # target as Homebrew portable-ruby before extmk computes the static graph. + # This target only unpacks the verified gem archives and needs the build + # environment's zlib extension. Keep it on the declared dev-shell Ruby; + # Ruby-version-sensitive install tooling below uses the source-matched + # full host Ruby instead. + make extract-gems "BASERUBY=$(command -v ruby) --disable=gems" + BOOTSNAP_SOURCE="$SRC_DIR/.bundle/gems/bootsnap-${BOOTSNAP_VERSION}" + if ! grep -q 'cygwin|wasm' \ + "$BOOTSNAP_SOURCE/lib/bootsnap/load_path_cache.rb"; then + echo "==> Patching Bootsnap platform support for Ruby on WebAssembly..." + patch -d "$BOOTSNAP_SOURCE" -p1 \ + < "$SCRIPT_DIR/patches/bootsnap-wasm-platform.patch" + fi + if [ -f Makefile ]; then echo "==> Ensuring Ruby generated POSTLINK is disabled (root-spill/fork instrumentation run explicitly after make)..." perl -0pi -e 's/^POSTLINK\s*=.*$/POSTLINK = :/mg' Makefile @@ -988,7 +1092,6 @@ content = re.sub( # Force-disable functions/features not available in wasm32-posix disable = { - 'HAVE_DLOPEN', 'HAVE_DYNAMIC_LOADING', 'HAVE_PTHREAD_CONDATTR_SETCLOCK', 'HAVE_PTHREAD_GETCPUCLOCKID', 'HAVE_PTHREAD_SETNAME_NP', 'HAVE_PTHREAD_SET_NAME_NP', 'HAVE_PTHREAD_GETATTR_NP', @@ -1087,7 +1190,7 @@ fi reject_asyncify_coroutine if [ -f Makefile ]; then - echo "==> Patching Ruby static archive rule for generated extension initializers..." + echo "==> Patching Ruby archive rule for generated objects..." perl -0pi -e 's/^(CPPFLAGS = )(?!.*RUBY_KANDELO_POSIX)/$1-DRUBY_KANDELO_POSIX=1 /m' Makefile perl -0pi -e 's/^\t\t\$\(Q\).*ARFLAGS.*LIBRUBY_A_OBJS.*$/\t\t\$(Q) \$(AR) \$(ARFLAGS) \$@ \$(LIBRUBY_A_OBJS)/m' Makefile rm -f libruby-static.a @@ -1135,34 +1238,33 @@ if [ ! -f exts.mk ]; then exit 1 fi -STATIC_EXTINITS="continuation date_core digest digest/md5 digest/sha1 digest/sha2 etc fcntl io/console json/ext/generator json/ext/parser monitor psych pty socket stringio strscan zlib" -STATIC_EXTOBJS="ext/extinit.o ext/continuation/continuation.a ext/date/date_core.a ext/digest/digest.a ext/digest/md5/md5.a ext/digest/sha1/sha1.a ext/digest/sha2/sha2.a ext/etc/etc.a ext/fcntl/fcntl.a ext/io/console/console.a ext/json/generator/generator.a ext/json/parser/parser.a ext/monitor/monitor.a ext/psych/psych.a ext/pty/pty.a ext/socket/socket.a ext/stringio/stringio.a ext/strscan/strscan.a ext/zlib/zlib.a" -STATIC_ENCOBJS="enc/encinit.o enc/libenc.a enc/libtrans.a" -STATIC_EXTLIBS="-lyaml -lz" -STATIC_LINK_PATHS="-L. -L$SYSROOT/lib -L$ZLIB_PREFIX/lib" -FINAL_RUBY_LDFLAGS="$STATIC_LINK_PATHS -Wl,-z,stack-size=1048576" - -echo "==> Relinking Ruby with static extensions and encodings..." -make -f exts.mk \ - libdir="$GUEST_PREFIX/lib" \ - LIBRUBY_EXTS=./.libruby-with-ext.time \ - "EXTENCS=$STATIC_ENCOBJS" \ - "BASERUBY=$BASERUBY_COMMAND" \ - "MINIRUBY=$BASERUBY_COMMAND -I. -rwasm32-none-fake" \ - static +FINAL_LINK_PATHS="-L. -L$SYSROOT/lib -L$ZLIB_PREFIX/lib" +# Ruby extensions resolve their public Ruby and libc imports from the one main +# process module. -ldl selects Kandelo's real loader glue, while +# --export-dynamic exposes the default-visibility API exactly as a native +# portable Ruby executable does for its extension shared objects. +FINAL_RUBY_LDFLAGS="$FINAL_LINK_PATHS -ldl -Wl,--export-dynamic -Wl,-z,stack-size=1048576" # Ruby's generated LDFLAGS include CFLAGS. Passing those compile flags through # the final wasm32 link produces a smaller executable shape that loses the # validated Ruby GC/root behavior. Keep the final link to linker paths plus the # explicit 1 MiB stack, matching the known-good fullmake package artifact. +# +# Keep CRuby's ordinary dynamic extension graph rather than folding its +# standard extensions into ruby.wasm. This is the layout in upstream portable +# Ruby, and Kandelo's general dlopen path now supports it. Besides preserving +# that compatibility contract, real extension files let ordinary Ruby loader +# caches index the runtime instead of repeatedly probing for statically linked +# features that cannot exist on disk. +# +# Re-enter through CRuby's `main` target rather than its leaf `ruby` target. +# The generated extension makefile passes dmyenc.o through EXTOBJS there; a +# direct leaf relink silently leaves Ruby's required Init_enc unresolved. +rm -f ruby make \ "LDFLAGS=$FINAL_RUBY_LDFLAGS" \ - "EXTOBJS=$STATIC_EXTOBJS $STATIC_ENCOBJS" \ - "EXTLIBS=$STATIC_EXTLIBS" \ - "EXTLDFLAGS=$STATIC_LINK_PATHS" \ - "EXTINITS=$STATIC_EXTINITS" \ SHOWFLAGS= \ - ruby + main # Collect binary mkdir -p "$BIN_DIR" @@ -1173,7 +1275,22 @@ else exit 1 fi -ROOT_SPILL="$REPO_ROOT/scripts/run-wasm-local-root-spill.sh" +# A sealed package build normally gives each root-spill invocation a fresh +# native-tool scratch directory. Ruby transforms the main executable and every +# installed dynamic extension in one package transaction, so rebuilding that +# identical helper for each file adds minutes without increasing isolation. +# Build it once inside this transaction's authoritative work directory and +# reuse only that immutable native executable for the remaining transforms. +ROOT_SPILL_TOOL_ROOT="$WASM_POSIX_DEP_WORK_DIR/ruby-local-root-spill" +mkdir -p "$ROOT_SPILL_TOOL_ROOT/target" "$ROOT_SPILL_TOOL_ROOT/bin" +CARGO_TARGET_DIR="$ROOT_SPILL_TOOL_ROOT/target" \ + WASM_POSIX_LOCAL_ROOT_SPILL_OUT_DIR="$ROOT_SPILL_TOOL_ROOT/bin" \ + bash "$REPO_ROOT/scripts/build-local-root-spill-tool.sh" +ROOT_SPILL="$ROOT_SPILL_TOOL_ROOT/bin/wasm-local-root-spill" +if [ ! -f "$ROOT_SPILL" ] || [ -L "$ROOT_SPILL" ] || [ ! -x "$ROOT_SPILL" ]; then + echo "ERROR: package-local wasm-local-root-spill is not a regular executable" >&2 + exit 1 +fi FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh" echo "==> Applying wasm-local-root-spill to ruby.wasm..." "$ROOT_SPILL" --profile ruby "$BIN_DIR/ruby.wasm" -o "$BIN_DIR/ruby.wasm.roots" @@ -1182,27 +1299,69 @@ echo "==> Applying wasm-fork-instrument to ruby.wasm..." "$FORK_INSTRUMENT" "$BIN_DIR/ruby.wasm" -o "$BIN_DIR/ruby.wasm.instr" mv "$BIN_DIR/ruby.wasm.instr" "$BIN_DIR/ruby.wasm" -# Install stdlib and default RubyGems/Bundler files under the selected guest -# prefix. Direct package builds retain /usr; Homebrew builds select their -# stable guest opt prefix so Ruby's built-in load path works after pouring. +# Install the relocatable stdlib and default RubyGems/Bundler tree. CRuby's +# load-relative install deliberately writes bin/ and lib/ at DESTDIR's root; +# the package archive adds /usr only as the direct-runtime mount location. +# Homebrew can instead place the same exact tree below vendor/portable-ruby. echo "==> Installing Ruby runtime..." mkdir -p "$INSTALL_DIR" -RUBY_INSTALL_ROOT="$INSTALL_DIR$GUEST_PREFIX" +RUBY_INSTALL_ROOT="$INSTALL_DIR" RUBY_LIB_DIR="$RUBY_INSTALL_ROOT/lib/ruby/${RUBY_MAJOR_MINOR}.0" -make install \ - DESTDIR="$INSTALL_DIR" 2>/dev/null || { - echo "==> make install failed, copying lib manually..." - mkdir -p "$RUBY_LIB_DIR" - cp -r "$SRC_DIR/lib/"* "$RUBY_LIB_DIR/" 2>/dev/null || true -} -if [ -d "$CROSS_BUILD_DIR/.ext/common" ]; then - cp -R "$CROSS_BUILD_DIR/.ext/common"/. "$RUBY_LIB_DIR"/ + +# Match Homebrew portable-ruby's public bundled-gem entrypoint. Ruby's +# out-of-tree build keeps generated gems below .bundle but installs library +# sources from srcdir/lib, so place the generated helper in that source tree. +# Its contents remain prefix-independent through RbConfig, exactly like the +# upstream Formula's helper. Kandelo's target binary cannot run on the build +# host, so use the full Ruby built from this exact source tree for CRuby's +# host-side install tools; the generated fake RbConfig keeps those tools on the +# target configuration. +HOST_INSTALL_RUBY="$HOST_RUBY_COMMAND" +TARGET_INSTALL_RUBY="$HOST_INSTALL_RUBY -I. -rwasm32-none-fake -I.ext/common" +RUBY_INSTALL_MAKE_ARGS=( + "BASERUBY=$HOST_INSTALL_RUBY" + "MINIRUBY=$TARGET_INSTALL_RUBY" +) +make ruby.pc "${RUBY_INSTALL_MAKE_ARGS[@]}" +PORTABLE_RUBY_ARCH="$( + wasm32posix-pkg-config \ + --variable=arch "$CROSS_BUILD_DIR/ruby-${RUBY_MAJOR_MINOR}.pc" +)" +if [ -z "$PORTABLE_RUBY_ARCH" ]; then + echo "ERROR: ruby.pc did not declare the portable Ruby architecture" >&2 + exit 1 fi -for ext_lib_dir in "$SRC_DIR/ext/monitor/lib" "$SRC_DIR/ext/socket/lib"; do - if [ -d "$ext_lib_dir" ]; then - cp -R "$ext_lib_dir"/. "$RUBY_LIB_DIR"/ - fi +PORTABLE_GEMS_FILE="$SRC_DIR/lib/$PORTABLE_RUBY_ARCH/portable_ruby_gems.rb" +mkdir -p "$(dirname "$PORTABLE_GEMS_FILE")" +: > "$PORTABLE_GEMS_FILE" +for require_path in \ + "$CROSS_BUILD_DIR"/.bundle/extensions/*/*/* \ + "$CROSS_BUILD_DIR"/.bundle/gems/*/lib; do + [ -d "$require_path" ] || continue + require_path="${require_path#"$CROSS_BUILD_DIR/.bundle/"}" + printf '%s\n' \ + '$:.unshift "#{RbConfig::CONFIG["rubylibprefix"]}/gems/#{RbConfig::CONFIG["ruby_version"]}/'"$require_path"'"' \ + >> "$PORTABLE_GEMS_FILE" done + +# Keep the same host-tool/target-configuration split for CRuby's ordinary +# install target. The target-generated common path supplies monitor.rb while +# the full host Ruby supplies the matching native monitor extension. +make install DESTDIR="$INSTALL_DIR" "${RUBY_INSTALL_MAKE_ARGS[@]}" + +# Ruby's native gems are ordinary upstream-style Wasm side modules loaded by +# dlopen. Their C frames have the same conservative-GC requirement as code in +# the main executable, so protecting only ruby.wasm leaves live VALUEs in an +# extension's Wasm locals invisible during collection. Transform the freshly +# installed copies rather than the build outputs: repeated local package builds +# then start from make install's authoritative unmodified modules and cannot +# accidentally stack the rewrite twice. +while IFS= read -r -d '' ruby_extension; do + echo "==> Applying wasm-local-root-spill to ${ruby_extension#"$RUBY_INSTALL_ROOT"/}..." + "$ROOT_SPILL" --profile ruby "$ruby_extension" -o "$ruby_extension.roots" + mv "$ruby_extension.roots" "$ruby_extension" +done < <(find "$RUBY_INSTALL_ROOT" -type f -name '*.so' -print0) + RUBY_ARCH_DIR="$RUBY_LIB_DIR/wasm32-none" mkdir -p "$RUBY_ARCH_DIR" cp rbconfig.rb "$RUBY_ARCH_DIR/rbconfig.rb" @@ -1240,12 +1399,19 @@ rm -f "$RUNTIME_ZIP" RUNTIME_STAGE="$(mktemp -d)" trap 'rm -rf "$RUNTIME_STAGE"' EXIT mkdir -p "$RUNTIME_STAGE/usr/lib" -# Keep the archive's existing /usr payload layout. The direct package VFS -# overlays it at /, while the Formula treats it as a portable staging tree and -# installs the contents into its keg. The generated rbconfig and executable -# still retain the selected guest prefix. +# Keep the direct package archive's existing /usr payload layout. Its contents +# are intrinsically relocatable: consumers may strip that staging component +# and install the unchanged bin/ and lib/ tree at any runtime prefix. cp -R "$RUBY_INSTALL_ROOT/lib/ruby" "$RUNTIME_STAGE/usr/lib/ruby" -cp -R "$RUBY_INSTALL_ROOT/bin" "$RUNTIME_STAGE/usr/bin" +mkdir -p "$RUNTIME_STAGE/usr/bin" +for runtime_executable in "$RUBY_INSTALL_ROOT"/bin/*; do + [ -e "$runtime_executable" ] || continue + # ruby.wasm is the separately validated, root-spilled, fork-instrumented + # package output. The binary emitted by `make install` is neither that + # authoritative artifact nor runtime data, so never duplicate it here. + [ "${runtime_executable##*/}" = "ruby" ] && continue + cp -R "$runtime_executable" "$RUNTIME_STAGE/usr/bin/" +done (cd "$RUNTIME_STAGE" && zip -r -q "$RUNTIME_ZIP" usr) echo "==> Ruby runtime archive: $RUNTIME_ZIP" diff --git a/packages/registry/ruby/patches/bootsnap-wasm-platform.patch b/packages/registry/ruby/patches/bootsnap-wasm-platform.patch new file mode 100644 index 0000000000..b10e3db7bb --- /dev/null +++ b/packages/registry/ruby/patches/bootsnap-wasm-platform.patch @@ -0,0 +1,21 @@ +diff --git a/lib/bootsnap/compile_cache.rb b/lib/bootsnap/compile_cache.rb +index 045917c..8628a49 100644 +--- a/lib/bootsnap/compile_cache.rb ++++ b/lib/bootsnap/compile_cache.rb +@@ -44 +44 @@ module Bootsnap +- RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/) ++ RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin|wasm/) +diff --git a/lib/bootsnap/load_path_cache.rb b/lib/bootsnap/load_path_cache.rb +index 7272a7d..ace7771 100644 +--- a/lib/bootsnap/load_path_cache.rb ++++ b/lib/bootsnap/load_path_cache.rb +@@ -57 +57 @@ module Bootsnap +- if RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/) ++ if RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin|wasm/) +diff --git a/lib/bootsnap/load_path_cache/path_scanner.rb b/lib/bootsnap/load_path_cache/path_scanner.rb +index c398b3e..4684b91 100644 +--- a/lib/bootsnap/load_path_cache/path_scanner.rb ++++ b/lib/bootsnap/load_path_cache/path_scanner.rb +@@ -30 +30 @@ module Bootsnap +- if RUBY_ENGINE == "ruby" && RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/) ++ if RUBY_ENGINE == "ruby" && RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin|wasm/) diff --git a/packages/registry/ruby/test/portable-ruby.test.ts b/packages/registry/ruby/test/portable-ruby.test.ts new file mode 100644 index 0000000000..53e62c3fe6 --- /dev/null +++ b/packages/registry/ruby/test/portable-ruby.test.ts @@ -0,0 +1,207 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; +import { type Zippable, zipSync } from "fflate"; + +import { tryResolveBinary } from "../../../../host/src/binary-resolver"; +import { + extractZipEntry, + parseZipCentralDirectory, +} from "../../../../host/src/vfs/zip"; +import { runCentralizedProgram } from + "../../../../host/test/centralized-test-helper"; +import { buildImage } from "../../../../tools/mkrootfs/src/builder"; + +const testDirectory = dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = join(testDirectory, "../../../.."); +const rubyBinary = tryResolveBinary("programs/ruby/ruby.wasm"); +const rubyRuntime = tryResolveBinary("programs/ruby/ruby-runtime.zip"); +const scratch = mkdtempSync(join(tmpdir(), "kandelo-portable-ruby-")); +const portableRubyVersion = "4.0.5_1"; +const portableRubyRoot = + "/opt/nonstandard-prefix/Library/Homebrew/vendor/portable-ruby"; +const portableRubyVersionRoot = `${portableRubyRoot}/${portableRubyVersion}`; +const portableRubyCurrent = `${portableRubyRoot}/current`; +const portableRubyBinary = `${portableRubyCurrent}/bin/ruby`; +const portableRubyRealBinary = `${portableRubyVersionRoot}/bin/ruby`; + +let rootfsImage: Uint8Array; + +function repoRelative(path: string): string { + return relative(repositoryRoot, path).replaceAll("\\", "/"); +} + +describe.skipIf(!rubyBinary || !rubyRuntime)( + "upstream-style portable Ruby on Kandelo", + () => { + beforeAll(async () => { + const portableArchive = join(scratch, "portable-ruby.zip"); + writeFileSync( + portableArchive, + createPortableRubyArchive(rubyBinary!, rubyRuntime!), + ); + const manifest = join(scratch, "MANIFEST"); + writeFileSync( + manifest, + [ + "/opt d 0755 0 0", + "/opt/nonstandard-prefix d 0755 0 0", + "/opt/nonstandard-prefix/Library d 0755 0 0", + "/opt/nonstandard-prefix/Library/Homebrew d 0755 0 0", + "/opt/nonstandard-prefix/Library/Homebrew/vendor d 0755 0 0", + `${portableRubyRoot} d 0755 0 0`, + `${portableRubyVersionRoot} d 0755 0 0`, + `archive url=${repoRelative(portableArchive)} ` + + `base=${portableRubyVersionRoot} ` + + "fmode=0644 fmode_policy=preserve-executable dmode=0755 uid=0 gid=0", + `${portableRubyCurrent} l 0777 0 0 target=${portableRubyVersion}`, + "", + ].join("\n"), + ); + rootfsImage = await buildImage({ + sourceTree: join(repositoryRoot, "images/rootfs"), + manifest, + repoRoot: repositoryRoot, + sabSize: 128 * 1024 * 1024, + maxSizeBytes: 256 * 1024 * 1024, + }); + }, 120_000); + + afterAll(() => { + rmSync(scratch, { recursive: true, force: true }); + }); + + it("loads the real msgpack and Bootsnap native extensions", async () => { + const program = String.raw` +require "rbconfig" +# Homebrew invokes Ruby with --disable=gems, then deliberately restores its +# controlled RubyGems environment in standalone/init.rb before loading the +# generated Bundler setup, portable Ruby gems, and Bootsnap. Keep this test on +# that upstream ordering so it does not rely on Ruby's automatic gem startup. +require "rubygems" +require "portable_ruby_gems" +require "msgpack" +require "bootsnap" + +# Keep representative CRuby standard extensions on their ordinary dynamic +# loading path too. This catches a portable archive that happens to carry the +# two Homebrew gems correctly but omits or statically substitutes the runtime +# extension tree that upstream Ruby expects. +%w[ + date digest/md5 etc fcntl io/console json psych socket stringio strscan zlib +].each { |feature| require feature } + +expected_prefix = ${JSON.stringify(portableRubyVersionRoot)} +unless RbConfig.ruby == ${JSON.stringify(portableRubyRealBinary)} + warn "Ruby did not retain its invoked portable path: #{RbConfig.ruby}" + exit 9 +end +unless RbConfig::CONFIG.fetch("prefix") == expected_prefix + warn "Ruby did not relocate its prefix: #{RbConfig::CONFIG.fetch("prefix")}" + exit 10 +end + +unless Bootsnap::LoadPathCache.supported? + warn "Bootsnap rejected #{RUBY_PLATFORM}" + exit 11 +end + +Bootsnap.setup( + cache_dir: "/tmp/portable-ruby-cache", + development_mode: false, + load_path_cache: true, + compile_cache_iseq: false, + compile_cache_yaml: false, +) + +decoded = MessagePack.unpack(MessagePack.pack("answer" => 42)) +unless decoded == { "answer" => 42 } + warn "msgpack round trip failed: #{decoded.inspect}" + exit 12 +end + +# Native Ruby extensions hold VALUEs in C locals across calls that can trigger +# collection. Force that exact boundary here: without local-root spilling on +# the Wasm side module itself, Bootsnap's two-array result can be reclaimed and +# replaced by an unrelated live object before scan_dir returns. +GC.stress = true +3.times do |iteration| + scan = Bootsnap::LoadPathCache::Native.scan_dir( + "#{expected_prefix}/lib/ruby/4.0.0", + ) + unless scan.is_a?(Array) && scan.length == 2 && scan.all?(Array) + warn "Bootsnap GC root corruption at iteration #{iteration}: #{scan.inspect}" + exit 13 + end +end +GC.stress = false + +require "set" +puts "portable-ruby=#{RUBY_VERSION}" +puts "portable-prefix=#{RbConfig::CONFIG.fetch("prefix")}" +puts "platform=#{RUBY_PLATFORM}" +puts "bootsnap-load-path-cache=#{Bootsnap::LoadPathCache.enabled?}" +puts "msgpack-answer=#{decoded.fetch("answer")}" +`; + + const result = await runCentralizedProgram({ + programPath: rubyBinary!, + argv: [portableRubyBinary, "--disable=gems,rubyopt", "-e", program], + env: ["HOME=/tmp", "TMPDIR=/tmp"], + rootfsImage, + timeout: 180_000, + }); + + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stderr).toBe(""); + expect(result.hostDiagnostics).toEqual([]); + expect(result.stdout).toBe([ + "portable-ruby=4.0.5", + `portable-prefix=${portableRubyVersionRoot}`, + "platform=wasm32-none", + "bootsnap-load-path-cache=true", + "msgpack-answer=42", + "", + ].join("\n")); + }, 240_000); + }, +); + +/** Compose the same versioned tree shape used by upstream portable Ruby. */ +function createPortableRubyArchive( + binaryPath: string, + runtimePath: string, +): Uint8Array { + const runtimeBytes = new Uint8Array(readFileSync(runtimePath)); + const archive: Zippable = {}; + for (const entry of parseZipCentralDirectory(runtimeBytes)) { + if (!entry.fileName.startsWith("usr/")) { + throw new Error(`Ruby runtime member escapes usr/: ${entry.fileName}`); + } + const portablePath = entry.fileName.slice("usr/".length); + if (portablePath.length === 0) continue; + archive[portablePath] = [extractZipEntry(runtimeBytes, entry), { + level: entry.isDirectory ? 0 : 9, + mtime: new Date(1980, 0, 1, 0, 0, 0), + os: 3, + attrs: ((entry.mode << 16) >>> 0), + }]; + } + if (archive["bin/ruby"] !== undefined) { + throw new Error("Ruby runtime unexpectedly already contains bin/ruby"); + } + archive["bin/ruby"] = [new Uint8Array(readFileSync(binaryPath)), { + level: 9, + mtime: new Date(1980, 0, 1, 0, 0, 0), + os: 3, + attrs: ((0o100755 << 16) >>> 0), + }]; + return zipSync(archive, { level: 9 }); +} diff --git a/packages/registry/shell/build.toml b/packages/registry/shell/build.toml index 97f3d96151..3203a6cfc6 100644 --- a/packages/registry/shell/build.toml +++ b/packages/registry/shell/build.toml @@ -48,6 +48,8 @@ inputs = [ "homebrew/kandelo-guest-layout.json", "host/src/homebrew-lazy-layer.ts", "host/src/homebrew-lazy-layer-descriptor.ts", + "host/src/homebrew-portable-ruby-contract.ts", + "host/src/homebrew-portable-ruby.ts", "host/src/homebrew-runtime-layer-consumer.ts", "host/src/homebrew-runtime-layer-limits.ts", "host/src/homebrew-runtime-layer-policy.ts", diff --git a/packages/registry/zlib/build-zlib.sh b/packages/registry/zlib/build-zlib.sh index 4dc4dd062d..47d78a26bb 100755 --- a/packages/registry/zlib/build-zlib.sh +++ b/packages/registry/zlib/build-zlib.sh @@ -52,7 +52,13 @@ tar xzf "$TARBALL" -C "$SRC_DIR" --strip-components=1 cd "$SRC_DIR" echo "==> Configuring zlib for $TARGET_ARCH..." -CC="$CC" AR="$AR" RANLIB="$RANLIB" \ +# WHY: libz.a is also consumed by dynamically loaded Wasm modules (Ruby's +# zlib extension is one example). wasm-ld requires every archive absorbed by a +# `-shared --experimental-pic` side module to contain position-independent +# objects. A PIC archive remains valid when statically linked into a main +# module, so publish one truthful general-purpose zlib artifact rather than a +# consumer-specific rebuild. +CFLAGS="-O2 -fPIC" CC="$CC" AR="$AR" RANLIB="$RANLIB" \ LDSHARED="$CC -shared" \ ./configure --static --prefix=/usr diff --git a/packages/registry/zlib/build.toml b/packages/registry/zlib/build.toml index 5a051064e4..1445a2f3a7 100644 --- a/packages/registry/zlib/build.toml +++ b/packages/registry/zlib/build.toml @@ -1,7 +1,9 @@ script_path = "packages/registry/zlib/build-zlib.sh" repo_url = "https://github.com/Automattic/kandelo.git" commit = "UNPUBLISHED" -revision = 2 +# Revision 3: compile libz.a as position-independent code so the same package +# can be consumed by both main modules and ordinary Wasm side modules. +revision = 3 [binary] index_url = "https://github.com/Automattic/kandelo/releases/download/binaries-abi-v{abi}/index.toml" diff --git a/run.sh b/run.sh index b24900bed4..b88664b689 100755 --- a/run.sh +++ b/run.sh @@ -2415,6 +2415,8 @@ prepare_browser_homebrew_bootstrap() { local prepare_args=( --browser-asset \ "$REPO_ROOT/apps/browser-demos/public/homebrew-bootstrap.zip" + --browser-portable-ruby-asset \ + "$REPO_ROOT/apps/browser-demos/public/homebrew-portable-ruby.zip" ) if [ "$REQUIRE_SEALED_HOMEBREW_SELECTION" -eq 1 ]; then prepare_args+=(--require-sealed) diff --git a/scripts/build-homebrew-main-shell-closure.sh b/scripts/build-homebrew-main-shell-closure.sh index a30ffe9fa1..82302b941b 100755 --- a/scripts/build-homebrew-main-shell-closure.sh +++ b/scripts/build-homebrew-main-shell-closure.sh @@ -12,6 +12,7 @@ REPORT="" BOTTLE_CACHE="" PACKAGE_TREE_SPEC="" PACKAGE_TREE_ARCHIVE="" +HOMEBREW_PORTABLE_RUBY_ARCHIVE="" HOMEBREW_BOOTSTRAP_ENV="" HOMEBREW_BOOTSTRAP_BOTTLE_REPORT="" HOMEBREW_BOOTSTRAP_SOURCE_LOCK="$REPO_ROOT/homebrew/homebrew-bootstrap-source-lock.json" @@ -67,6 +68,8 @@ Options: reviewed package-owned lazy-tree recipe --package-tree-archive exact dependency output named by the recipe + --homebrew-portable-ruby-archive + source-matched upstream portable Ruby tree --homebrew-bootstrap-env exact package-owned launcher environment --homebrew-bootstrap-bottle-report @@ -126,6 +129,10 @@ while [ "$#" -gt 0 ]; do PACKAGE_TREE_ARCHIVE="${2:-}" shift 2 ;; + --homebrew-portable-ruby-archive) + HOMEBREW_PORTABLE_RUBY_ARCHIVE="${2:-}" + shift 2 + ;; --homebrew-bootstrap-env) HOMEBREW_BOOTSTRAP_ENV="${2:-}" shift 2 @@ -200,11 +207,21 @@ if { [ -n "$PACKAGE_TREE_SPEC" ] && [ -z "$PACKAGE_TREE_ARCHIVE" ]; } || echo "build-homebrew-main-shell-closure: package-tree spec and archive must be provided together" >&2 exit 2 fi +if [ -n "$HOMEBREW_PORTABLE_RUBY_ARCHIVE" ] && + [ -z "$PACKAGE_TREE_SPEC" ]; then + echo "build-homebrew-main-shell-closure: portable Ruby requires a package tree" >&2 + exit 2 +fi if { [ -n "$PACKAGE_TREE_SPEC" ] && [ -z "$HOMEBREW_BOOTSTRAP_ENV" ]; } || { [ -z "$PACKAGE_TREE_SPEC" ] && [ -n "$HOMEBREW_BOOTSTRAP_ENV" ]; }; then echo "build-homebrew-main-shell-closure: Homebrew bootstrap environment and package tree must be provided together" >&2 exit 2 fi +if [ -n "$HOMEBREW_BOOTSTRAP_ENV" ] && + [ -z "$HOMEBREW_PORTABLE_RUBY_ARCHIVE" ]; then + echo "build-homebrew-main-shell-closure: Homebrew bootstrap environment requires portable Ruby" >&2 + exit 2 +fi if [ -z "$PACKAGE_TREE_SPEC" ] && [ -n "$HOMEBREW_BOOTSTRAP_BOTTLE_REPORT" ]; then echo "build-homebrew-main-shell-closure: Homebrew bootstrap bottle report requires a package tree" >&2 @@ -228,6 +245,12 @@ if [ -n "$PACKAGE_TREE_ARCHIVE" ] && echo "build-homebrew-main-shell-closure: package-tree archive must be a regular non-symlink file" >&2 exit 2 fi +if [ -n "$HOMEBREW_PORTABLE_RUBY_ARCHIVE" ] && + { [ ! -f "$HOMEBREW_PORTABLE_RUBY_ARCHIVE" ] || + [ -L "$HOMEBREW_PORTABLE_RUBY_ARCHIVE" ]; }; then + echo "build-homebrew-main-shell-closure: portable Ruby archive must be a regular non-symlink file" >&2 + exit 2 +fi if [ -n "$HOMEBREW_BOOTSTRAP_ENV" ] && { [ ! -f "$HOMEBREW_BOOTSTRAP_ENV" ] || [ -L "$HOMEBREW_BOOTSTRAP_ENV" ]; }; then echo "build-homebrew-main-shell-closure: Homebrew bootstrap environment must be a regular non-symlink file" >&2 @@ -525,6 +548,8 @@ PACKAGE_TREE_ARGS=() PACKAGE_TREE_JSON=null PACKAGE_TREE_ARCHIVE_SHA="" PACKAGE_TREE_ARCHIVE_BYTES=0 +HOMEBREW_PORTABLE_RUBY_ARCHIVE_SHA="" +HOMEBREW_PORTABLE_RUBY_ARCHIVE_BYTES=0 HOMEBREW_BOOTSTRAP_ENV_SHA="" HOMEBREW_BOOTSTRAP_ENV_BYTES=0 HOMEBREW_BOOTSTRAP_BOTTLE_JSON=null @@ -532,6 +557,7 @@ if [ -n "$PACKAGE_TREE_SPEC" ]; then PACKAGE_TREE_ARGS=( --package-tree-spec "$PACKAGE_TREE_SPEC" --package-tree-archive "$PACKAGE_TREE_ARCHIVE" + --homebrew-portable-ruby-archive "$HOMEBREW_PORTABLE_RUBY_ARCHIVE" --homebrew-bootstrap-env "$HOMEBREW_BOOTSTRAP_ENV" ) if [ "$MATERIALIZE_PACKAGE_TREE" = true ]; then @@ -541,6 +567,11 @@ if [ -n "$PACKAGE_TREE_SPEC" ]; then PACKAGE_TREE_ARCHIVE_SHA="$(sha256sum "$PACKAGE_TREE_ARCHIVE")" PACKAGE_TREE_ARCHIVE_SHA="${PACKAGE_TREE_ARCHIVE_SHA%% *}" PACKAGE_TREE_ARCHIVE_BYTES="$(wc -c <"$PACKAGE_TREE_ARCHIVE" | tr -d '[:space:]')" + HOMEBREW_PORTABLE_RUBY_ARCHIVE_SHA="$(sha256sum \ + "$HOMEBREW_PORTABLE_RUBY_ARCHIVE")" + HOMEBREW_PORTABLE_RUBY_ARCHIVE_SHA="${HOMEBREW_PORTABLE_RUBY_ARCHIVE_SHA%% *}" + HOMEBREW_PORTABLE_RUBY_ARCHIVE_BYTES="$(wc -c \ + <"$HOMEBREW_PORTABLE_RUBY_ARCHIVE" | tr -d '[:space:]')" HOMEBREW_BOOTSTRAP_ENV_SHA="$(sha256sum "$HOMEBREW_BOOTSTRAP_ENV")" HOMEBREW_BOOTSTRAP_ENV_SHA="${HOMEBREW_BOOTSTRAP_ENV_SHA%% *}" HOMEBREW_BOOTSTRAP_ENV_BYTES="$(wc -c <"$HOMEBREW_BOOTSTRAP_ENV" | tr -d '[:space:]')" @@ -569,6 +600,7 @@ if [ -n "$PACKAGE_TREE_SPEC" ]; then --report "$HOMEBREW_BOOTSTRAP_BOTTLE_REPORT" \ --output "archive=$PACKAGE_TREE_ARCHIVE" \ --output "environment=$HOMEBREW_BOOTSTRAP_ENV" \ + --output "portable_ruby=$HOMEBREW_PORTABLE_RUBY_ARCHIVE" \ "${SUPPORT_SELECTION_ARGS[@]}" \ --verified-report-out "$VERIFIED_BOOTSTRAP_REPORT" HOMEBREW_BOOTSTRAP_BOTTLE_JSON="$( @@ -675,6 +707,10 @@ jq -e \ --argjson homebrew_bootstrap_bottle "$HOMEBREW_BOOTSTRAP_BOTTLE_JSON" \ --arg package_tree_archive_sha "$PACKAGE_TREE_ARCHIVE_SHA" \ --argjson package_tree_archive_bytes "$PACKAGE_TREE_ARCHIVE_BYTES" \ + --arg homebrew_portable_ruby_archive_sha \ + "$HOMEBREW_PORTABLE_RUBY_ARCHIVE_SHA" \ + --argjson homebrew_portable_ruby_archive_bytes \ + "$HOMEBREW_PORTABLE_RUBY_ARCHIVE_BYTES" \ --arg homebrew_bootstrap_env_sha "$HOMEBREW_BOOTSTRAP_ENV_SHA" \ --argjson homebrew_bootstrap_env_bytes "$HOMEBREW_BOOTSTRAP_ENV_BYTES" \ --argjson materialize_package_tree "$MATERIALIZE_PACKAGE_TREE" \ @@ -895,8 +931,13 @@ jq -e \ (.homebrew_bootstrap_bottle == null) else (.homebrew_bootstrap_bottle == $homebrew_bootstrap_bottle) and - (.package_deferred_trees | length == 1) and - (.package_deferred_trees[0] as $tree | + (.package_deferred_trees | length == 2) and + ([.package_deferred_trees[] | + select(.id == $package_tree_spec.id)] | length == 1) and + ([.package_deferred_trees[] | + select(.id == "homebrew-bootstrap/portable-ruby")] | length == 1) and + ([.package_deferred_trees[] | + select(.id == $package_tree_spec.id)][0] as $tree | $tree.schema == $package_tree_spec.schema and $tree.kind == $package_tree_spec.kind and $tree.id == $package_tree_spec.id and @@ -923,7 +964,43 @@ jq -e \ $tree.state == (if $materialize_package_tree then "materialized" else "deferred" - end) and + end)) and + ([.package_deferred_trees[] | + select(.id == "homebrew-bootstrap/portable-ruby")][0] as $ruby | + $ruby.schema == 1 and + $ruby.kind == "kandelo-package-deferred-zip-tree" and + $ruby.content_role == "runtime-tree" and + $ruby.package == { + name: $package_tree_spec.package.name, + output: "homebrew-portable-ruby.zip" + } and + $ruby.archive.output == "homebrew-portable-ruby.zip" and + $ruby.archive.url == "homebrew-portable-ruby.zip" and + $ruby.archive.sha256 == $homebrew_portable_ruby_archive_sha and + $ruby.archive.bytes == $homebrew_portable_ruby_archive_bytes and + $ruby.archive.expanded_bytes > 0 and + $ruby.archive.source_entry_count > 0 and + ($ruby.descriptor.sha256 | test("^[0-9a-f]{64}$")) and + $ruby.descriptor.bytes > 0 and + $ruby.mount_prefix == + ((if $package_tree_spec.mount_prefix == "/" then "" + else $package_tree_spec.mount_prefix end) + + "/Library/Homebrew/vendor/portable-ruby") and + $ruby.owner == $package_tree_spec.owner and + $ruby.activation.mode == "first-use" and + $ruby.activation.capabilities == ["homebrew:runtime"] and + ($ruby.activation.roots | length == 1) and + ($ruby.activation.roots[0] | + startswith($ruby.mount_prefix + "/")) and + ($ruby.activation.roots[0] | endswith("/bin/ruby")) and + $ruby.activation.atomicGroup == { + id: $package_tree_spec.activation.atomic_group, + member: "homebrew-bootstrap/portable-ruby" + } and + $ruby.state == (if $materialize_package_tree + then "materialized" + else "deferred" + end)) and .homebrew_bootstrap == { environment: { path: "/etc/homebrew/brew.env", @@ -947,7 +1024,6 @@ jq -e \ ] } } - ) end) ' "$REPORT" >/dev/null diff --git a/scripts/build-homebrew-main-shell-product.sh b/scripts/build-homebrew-main-shell-product.sh index d2b0333181..e73e9b5a7b 100755 --- a/scripts/build-homebrew-main-shell-product.sh +++ b/scripts/build-homebrew-main-shell-product.sh @@ -59,6 +59,7 @@ for required in \ "$RECEIPT" \ "$BOOTSTRAP/homebrew-bootstrap.zip" \ "$BOOTSTRAP/homebrew-brew.env" \ + "$BOOTSTRAP/homebrew-portable-ruby.zip" \ "$BOOTSTRAP/report.json"; do [ -e "$required" ] && [ ! -L "$required" ] || { echo "build-homebrew-main-shell-product: prepared inputs omit $required" >&2 @@ -77,6 +78,8 @@ arguments=( --package-tree-spec \ "$REPO_ROOT/homebrew/main-shell-brew-package-tree.json" --package-tree-archive "$BOOTSTRAP/homebrew-bootstrap.zip" + --homebrew-portable-ruby-archive \ + "$BOOTSTRAP/homebrew-portable-ruby.zip" --homebrew-bootstrap-env "$BOOTSTRAP/homebrew-brew.env" --homebrew-bootstrap-bottle-report "$BOOTSTRAP/report.json" ) diff --git a/scripts/build-musl.sh b/scripts/build-musl.sh index b1da31dbe1..1f95fc06a4 100755 --- a/scripts/build-musl.sh +++ b/scripts/build-musl.sh @@ -57,9 +57,19 @@ export LLVM_BIN CC="$LLVM_BIN/clang" AR="$LLVM_BIN/llvm-ar" RANLIB="$LLVM_BIN/llvm-ranlib" +NM="$LLVM_BIN/llvm-nm" +READOBJ="$LLVM_BIN/llvm-readobj" +LD="$LLVM_BIN/wasm-ld" +LD_ARCH_ARGS=() +if [ "$ARCH" = "wasm64posix" ]; then + # WHY: unlike clang, wasm-ld does not infer the memory model from a + # relocatable input. Without this explicit emulation it rejects the + # wasm64 process-libc objects after musl has otherwise built successfully. + LD_ARCH_ARGS=(-mwasm64) +fi # Verify toolchain exists -for tool in "$CC" "$AR" "$RANLIB"; do +for tool in "$CC" "$AR" "$RANLIB" "$NM" "$READOBJ" "$LD"; do if [ ! -x "$tool" ]; then echo "Error: $tool not found. Run scripts/dev-shell.sh or set LLVM_BIN/LLVM_PREFIX." >&2 exit 1 @@ -298,13 +308,125 @@ echo "==> Building sigsetjmp helpers..." "$AR" rcs "$SYSROOT/lib/libc.a" "$SYSROOT/lib/sigsetjmp_helpers.o" # --------------------------------------------------------------- -# 9. Install override headers +# 9. Build the resolved process-libc fallback and retention anchor used by dlopen +# --------------------------------------------------------------- +echo "==> Building dynamic process-libc object and retention anchor..." +DYNAMIC_LIBC="$SYSROOT/lib/libc-dynamic.o" +DYNAMIC_LIBC_ARCHIVE="$MUSL_DIR/obj/libc-dynamic-strong.a" +DYNAMIC_LIBC_RESOLVED_STRONG="$MUSL_DIR/obj/libc-dynamic-resolved-strong.o" +DYNAMIC_LIBC_SYMBOLS="$MUSL_DIR/obj/dynamic-libc-symbols.txt" +DYNAMIC_LIBC_ANCHOR_SOURCE="$MUSL_DIR/obj/dynamic-libc-anchor.c" +DYNAMIC_LIBC_ANCHOR="$SYSROOT/lib/libc-dynamic-anchor.o" + +# A dlopen-capable Wasm executable must expose libc routines that only a +# future side module references. Resolve those routines from the strong musl +# archive first so mutually exclusive implementations (notably allocators) +# remain coherent, then weaken the resolved object so executable definitions +# interpose exactly as they do over a native shared libc. +cp "$SYSROOT/lib/libc.a" "$DYNAMIC_LIBC_ARCHIVE" +DYNAMIC_LIBC_EXCLUDED_MEMBERS=( + dladdr.o + dlclose.o + dlerror.o + dlopen.o + dlsym.o + _Fork.o + fork.o + vfork.o + __syscall_cp.o + sigsetjmp_helpers.o +) +for member in "${DYNAMIC_LIBC_EXCLUDED_MEMBERS[@]}"; do + if ! "$AR" t "$DYNAMIC_LIBC_ARCHIVE" | grep -Fx "$member" >/dev/null; then + echo "ERROR: expected $member in $DYNAMIC_LIBC_ARCHIVE" >&2 + exit 1 + fi +done +"$AR" d "$DYNAMIC_LIBC_ARCHIVE" "${DYNAMIC_LIBC_EXCLUDED_MEMBERS[@]}" +"$RANLIB" "$DYNAMIC_LIBC_ARCHIVE" + +"$READOBJ" --symbols "$DYNAMIC_LIBC_ARCHIVE" 2>/dev/null | + awk ' +function emit() { + if (name ~ /^[A-Za-z_][A-Za-z0-9_]*$/ && !undefined && !local && + (type == "FUNCTION" || type == "DATA")) print type, name +} +/^ Symbol \{$/ { + in_symbol=1; name=""; type=""; undefined=0; local=0; next +} +in_symbol && /^ Name: / { name=$2; next } +in_symbol && /^ Type: / { type=$2; next } +in_symbol && /UNDEFINED/ { undefined=1; next } +in_symbol && /BINDING_LOCAL/ { local=1; next } +in_symbol && /^ \}$/ { emit(); in_symbol=0 } +' | LC_ALL=C sort -u > "$DYNAMIC_LIBC_SYMBOLS" + +awk ' +BEGIN { + print "typedef void (*kandelo_dynamic_function)(void);" +} +$1 == "FUNCTION" { + functions[++function_count]=$2 + print "extern void " $2 "(void);" +} +$1 == "DATA" { + data[++data_count]=$2 + print "extern unsigned char " $2 ";" +} +END { + print "__attribute__((used, visibility(\"hidden\")))" + print "kandelo_dynamic_function const __kandelo_dynamic_libc_functions[] = {" + for (i=1; i<=function_count; i++) print " " functions[i] "," + print "};" + print "__attribute__((used, visibility(\"hidden\")))" + print "void const *const __kandelo_dynamic_libc_data[] = {" + for (i=1; i<=data_count; i++) print " &" data[i] "," + print "};" +} +' "$DYNAMIC_LIBC_SYMBOLS" > "$DYNAMIC_LIBC_ANCHOR_SOURCE" +"$CC" --target="$TARGET" -O2 -fPIC -fno-builtin -w \ + -matomics -mbulk-memory -mexception-handling \ + -c "$DYNAMIC_LIBC_ANCHOR_SOURCE" -o "$DYNAMIC_LIBC_ANCHOR" + +if ! "$NM" --undefined-only --format=posix "$DYNAMIC_LIBC_ANCHOR" \ + 2>/dev/null | grep -E '^ntohs U ' >/dev/null; then + echo "ERROR: $DYNAMIC_LIBC_ANCHOR does not retain ntohs" >&2 + exit 1 +fi + +# The anchor's strong undefined relocations make the ordinary archive +# resolver choose one definition for every process-libc symbol. A native +# shared libc then acts as a fallback to strong executable definitions. +"$LD" "${LD_ARCH_ARGS[@]}" -r --allow-undefined "$DYNAMIC_LIBC_ANCHOR" \ + "$DYNAMIC_LIBC_ARCHIVE" -o "$DYNAMIC_LIBC_RESOLVED_STRONG" + +# wasm-ld supports weak Wasm definitions, but llvm-objcopy does not implement +# Wasm symbol weakening. The repository transformer changes only the binding +# bits in the already-resolved object; code, data, and relocations stay exact. +HOST_TARGET="$(rustc -vV | awk '/^host:/ { print $2 }')" +if [ -z "$HOST_TARGET" ]; then + echo "ERROR: rustc -vV did not report a host target" >&2 + exit 1 +fi +( + cd "$REPO_ROOT" + cargo run --release -p xtask --target "$HOST_TARGET" --quiet -- \ + weaken-wasm-object "$DYNAMIC_LIBC_RESOLVED_STRONG" "$DYNAMIC_LIBC" +) +if ! "$NM" --defined-only --format=posix "$DYNAMIC_LIBC" 2>/dev/null | + grep -E '^malloc W ' >/dev/null; then + echo "ERROR: $DYNAMIC_LIBC does not provide weak malloc" >&2 + exit 1 +fi + +# --------------------------------------------------------------- +# 10. Install override headers # --------------------------------------------------------------- echo "==> Installing override headers..." bash "$REPO_ROOT/scripts/install-overlay-headers.sh" "$SYSROOT" # --------------------------------------------------------------- -# 10. Build platform graphics/DRI stub libraries +# 11. Build platform graphics/DRI stub libraries # --------------------------------------------------------------- if [ "$ARCH" = "wasm32posix" ]; then echo "==> Building platform graphics stubs..." @@ -316,4 +438,8 @@ echo "" echo "==> musl build complete!" echo " Sysroot: $SYSROOT" echo " libc.a: $SYSROOT/lib/libc.a" +echo " dynamic: $DYNAMIC_LIBC" +echo " anchor: $DYNAMIC_LIBC_ANCHOR" ls -la "$SYSROOT/lib/libc.a" 2>/dev/null || echo " WARNING: libc.a not found!" +ls -la "$DYNAMIC_LIBC" 2>/dev/null || echo " WARNING: libc-dynamic.o not found!" +ls -la "$DYNAMIC_LIBC_ANCHOR" 2>/dev/null || echo " WARNING: libc-dynamic-anchor.o not found!" diff --git a/scripts/check-homebrew-main-shell-brewfile.mjs b/scripts/check-homebrew-main-shell-brewfile.mjs index 9365fa2f22..aa7dbce70d 100755 --- a/scripts/check-homebrew-main-shell-brewfile.mjs +++ b/scripts/check-homebrew-main-shell-brewfile.mjs @@ -14,7 +14,6 @@ const provenanceDigestFlag = "--print-runtime-bottle-provenance-sha256"; const provenanceDigestDomain = "kandelo-homebrew-runtime-bottle-provenance-v1\u0000"; const runtimeSupportRootOrder = [ - `${tapName}/ruby`, `${tapName}/git`, `${tapName}/curl`, `${tapName}/findutils`, @@ -778,7 +777,11 @@ function readRuntimeSupport(path, lock) { !isRecord(activation.bootstrap_package) || activation.bootstrap_package.name !== "homebrew-bootstrap" || JSON.stringify(activation.bootstrap_package.outputs) !== - JSON.stringify(["homebrew-bootstrap.zip", "homebrew-brew.env"]) || + JSON.stringify([ + "homebrew-bootstrap.zip", + "homebrew-brew.env", + "homebrew-portable-ruby.zip", + ]) || activation.bootstrap_package.required_kernel_abi !== 42 ) { throw new Error( diff --git a/scripts/check-homebrew-main-shell-mirror-workflow.rb b/scripts/check-homebrew-main-shell-mirror-workflow.rb index dd58caf7b5..e8b73d15fe 100755 --- a/scripts/check-homebrew-main-shell-mirror-workflow.rb +++ b/scripts/check-homebrew-main-shell-mirror-workflow.rb @@ -13,11 +13,11 @@ File.join(ROOT, "homebrew/test/run_homebrew_guest_shipping_scope.sh") : File.expand_path(ARGV.fetch(1)) PUBLISH_JOB_DIGEST = - "5f38b593eeffd4cacf3d728baa64695e88fe2f0723757628dbc936b6b679c54b" + "3e8ae714ec2c2024c0413efc1f4489a3f094c0b971db2b41c8a64d286e0c247a" WORKFLOW_DIGEST = - "9dee4c5bb5a12cb06aa25f2c54fe884febf5762cb698772c0c6629283cf9af91" + "83a18a6935a098c1ba04c945a1876fcfdcf0a3d2563a3275b6da840af0c99b54" NODE_SCOPE_RUNNER_DIGEST = - "a351c57bba3b4ad05d58a346ccf2ffa22d6de194d1839c24a78d2b9bc07f1bf8" + "6cc77de083c1be972a20ef4d2a26e52a2ee1ff93969a3e6caac0d3a19e1fecbf" DOWNLOAD_ACTION = "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" UPLOAD_ACTION = @@ -28,6 +28,7 @@ main-shell.vfs.zst main-shell-brew-package-tree.json homebrew-bootstrap.zip + homebrew-portable-ruby.zip homebrew-brew.env ].freeze PUBLIC_CHROMIUM_PLAYWRIGHT_ENV = { @@ -400,7 +401,7 @@ def contract_digest(value) '.visibility == "public-anonymous-readback"' ).length == 2 && publication_run.include?('.operation == "verified-existing"') && - publication_run.include?('(.assets | length) == 4') && + publication_run.include?('(.assets | length) == 5') && publication_run.include?('"mirror-receipt=$mirror_receipt"') && publication_run.include?('"lifecycle-receipt=$lifecycle_receipt"'), "mirror verification and lifecycle publication receipts are not exact", @@ -527,6 +528,7 @@ def contract_digest(value) node_scope_env = { "IMAGE" => "${{ steps.public.outputs.image }}", "BOOTSTRAP" => "${{ steps.public.outputs.bootstrap }}", + "PORTABLE_RUBY" => "${{ steps.public.outputs.portable-ruby }}", "BOOTSTRAP_ENV" => "${{ steps.public.outputs.bootstrap-env }}", "TAP_CATALOG_REF" => "${{ inputs.tap-catalog-ref }}", "CANARY_REF" => "${{ inputs.canary-ref }}", @@ -663,6 +665,10 @@ def contract_digest(value) chromium_build_run.include?( 'cmp "${{ steps.public.outputs.bootstrap }}" "$browser_bootstrap"' ) && + chromium_build_run.include?( + 'cmp "${{ steps.public.outputs.portable-ruby }}"' + ) && + chromium_build_run.include?('dist/homebrew-portable-ruby.zip') && !chromium_build_run.include?( 'cp "${{ steps.public.outputs.bootstrap }}" "$browser_bootstrap"' ) && @@ -705,6 +711,9 @@ def contract_digest(value) chromium_resolution.include?( 'bootstrap="$PWD/apps/browser-demos/public/homebrew-bootstrap.zip"' ) && + chromium_resolution.include?( + 'portable_ruby="$PWD/apps/browser-demos/public/homebrew-portable-ruby.zip"' + ) && !chromium_resolution.include?("programs/homebrew-bootstrap/"), "Chromium proof must use the sealed shared bottle preparation path", ) diff --git a/scripts/ci-check-pages-deployment.sh b/scripts/ci-check-pages-deployment.sh index 22293355a9..e6c12a85e0 100755 --- a/scripts/ci-check-pages-deployment.sh +++ b/scripts/ci-check-pages-deployment.sh @@ -221,6 +221,9 @@ grep -Fq 'id: shell_product' <<<"$shell_product_block" && grep -Fq \ 'bootstrap="$PWD/apps/browser-demos/public/homebrew-bootstrap.zip"' \ <<<"$shell_product_block" && + grep -Fq \ + 'portable_ruby="$PWD/apps/browser-demos/public/homebrew-portable-ruby.zip"' \ + <<<"$shell_product_block" && grep -Fq 'scripts/verify-homebrew-main-shell-artifact-lock.sh' \ <<<"$shell_product_block" && grep -Fq 'scripts/inspect-homebrew-main-shell-public-product.ts' \ @@ -254,6 +257,8 @@ browser_build_block="$( )" grep -Fxq ' npm run build' <<<"$browser_build_block" && grep -Fq 'dist/homebrew-bootstrap.zip' <<<"$browser_build_block" && + grep -Fq 'dist/homebrew-portable-ruby.zip' \ + <<<"$browser_build_block" && grep -Fq 'bash ../../scripts/verify-browser-shell-vfs-asset.sh \' \ <<<"$browser_build_block" && grep -Fxq \ diff --git a/scripts/create-homebrew-guest-lifecycle-fixture.test.ts b/scripts/create-homebrew-guest-lifecycle-fixture.test.ts index 235c7c7a4f..0de957841f 100644 --- a/scripts/create-homebrew-guest-lifecycle-fixture.test.ts +++ b/scripts/create-homebrew-guest-lifecycle-fixture.test.ts @@ -34,6 +34,11 @@ test("creates one exact closed-browser lifecycle fixture", () => { const image = write(root, "main-shell.vfs.zst", new Uint8Array([1])); const spec = write(root, "tree.json", new Uint8Array([2])); const archive = write(root, "homebrew-bootstrap.zip", new Uint8Array([3])); + const portableRubyArchive = write( + root, + "homebrew-portable-ruby.zip", + new Uint8Array([7]), + ); const environment = write(root, "homebrew-brew.env", new Uint8Array([4])); const mirror = join(root, "mirror"); mkdirSync(mirror); @@ -50,6 +55,7 @@ test("creates one exact closed-browser lifecycle fixture", () => { image, bootstrapSpec: spec, bootstrapArchive: archive, + portableRubyArchive, bootstrapEnvironment: environment, bottleMirror: mirror, fixedAssetUrlRoot: "https://closed.example.test/run/", @@ -84,6 +90,7 @@ test("creates one exact closed-browser lifecycle fixture", () => { image, bootstrapSpec: spec, bootstrapArchive: archive, + portableRubyArchive, bootstrapEnvironment: environment, bottleMirror: mirror, fixedAssetUrlRoot: "https://closed.example.test/run/", @@ -107,6 +114,11 @@ test("creates one exact public-browser fixture without local bottle payloads", ( const image = write(root, "main-shell.vfs.zst", new Uint8Array([1])); const spec = write(root, "tree.json", new Uint8Array([2])); const archive = write(root, "homebrew-bootstrap.zip", new Uint8Array([3])); + const portableRubyArchive = write( + root, + "homebrew-portable-ruby.zip", + new Uint8Array([7]), + ); const environment = write(root, "homebrew-brew.env", new Uint8Array([4])); const mirror = join(root, "mirror"); mkdirSync(mirror); @@ -124,6 +136,7 @@ test("creates one exact public-browser fixture without local bottle payloads", ( image, bootstrapSpec: spec, bootstrapArchive: archive, + portableRubyArchive, bootstrapEnvironment: environment, bottleMirror: mirror, fixedAssetUrlRoot: diff --git a/scripts/create-homebrew-guest-lifecycle-fixture.ts b/scripts/create-homebrew-guest-lifecycle-fixture.ts index eab7fbf590..96a4d12778 100644 --- a/scripts/create-homebrew-guest-lifecycle-fixture.ts +++ b/scripts/create-homebrew-guest-lifecycle-fixture.ts @@ -27,6 +27,7 @@ export interface CreateHomebrewGuestLifecycleFixtureOptions { image: string; bootstrapSpec: string; bootstrapArchive: string; + portableRubyArchive: string; bootstrapEnvironment: string; bottleMirror: string; fixedAssetUrlRoot: string; @@ -75,6 +76,11 @@ export function createHomebrewGuestLifecycleFixture( fixedAssetUrl(fixedAssetUrlRoot, options.bootstrapArchive), "Homebrew bootstrap archive", ); + const portableRubyArchive = exactLocalAsset( + options.portableRubyArchive, + fixedAssetUrl(fixedAssetUrlRoot, options.portableRubyArchive), + "Homebrew portable Ruby archive", + ); const bootstrapEnvironment = exactLocalAsset( options.bootstrapEnvironment, fixedAssetUrl( @@ -143,6 +149,7 @@ export function createHomebrewGuestLifecycleFixture( bootstrap: { spec: bootstrapSpec, archive: bootstrapArchive, + portableRuby: portableRubyArchive, environment: bootstrapEnvironment, }, bottleMirror: { @@ -167,6 +174,7 @@ function parseOptions( "--image", "--homebrew-bootstrap-spec", "--homebrew-bootstrap-archive", + "--homebrew-portable-ruby-archive", "--homebrew-bootstrap-env", "--bottle-mirror", "--fixed-asset-url-root", @@ -230,6 +238,9 @@ function parseOptions( bootstrapArchive: resolve( values.get("--homebrew-bootstrap-archive")!, ), + portableRubyArchive: resolve( + values.get("--homebrew-portable-ruby-archive")!, + ), bootstrapEnvironment: resolve( values.get("--homebrew-bootstrap-env")!, ), @@ -360,6 +371,7 @@ function usage(): never { "--image " + "--homebrew-bootstrap-spec " + "--homebrew-bootstrap-archive " + + "--homebrew-portable-ruby-archive " + "--homebrew-bootstrap-env " + "--bottle-mirror --fixed-asset-url-root " + "--core-revision --canary-revision " + diff --git a/scripts/homebrew-guest-lifecycle-publication.test.ts b/scripts/homebrew-guest-lifecycle-publication.test.ts index c0517aa6d0..ea5f3325a3 100644 --- a/scripts/homebrew-guest-lifecycle-publication.test.ts +++ b/scripts/homebrew-guest-lifecycle-publication.test.ts @@ -86,6 +86,7 @@ test("creates and revalidates one separate immutable lifecycle-input handoff", ( "main-shell.vfs.zst", "main-shell-brew-package-tree.json", "homebrew-bootstrap.zip", + "homebrew-portable-ruby.zip", "homebrew-brew.env", ], ); @@ -253,6 +254,11 @@ function createInputs(root: string) { new Uint8Array([3]), ); const bootstrapArchive = write(inputRoot, "source.zip", new Uint8Array([4])); + const portableRubyArchive = write( + inputRoot, + "portable-ruby.zip", + new Uint8Array([8]), + ); const bootstrapEnvironment = write( inputRoot, "source.env", @@ -299,6 +305,7 @@ function createInputs(root: string) { image, bootstrapSpec, bootstrapArchive, + portableRubyArchive, bootstrapEnvironment, bottleMirrorPlan, }; diff --git a/scripts/homebrew-guest-lifecycle-publication.ts b/scripts/homebrew-guest-lifecycle-publication.ts index b101a8a0cd..821cefa703 100644 --- a/scripts/homebrew-guest-lifecycle-publication.ts +++ b/scripts/homebrew-guest-lifecycle-publication.ts @@ -47,6 +47,11 @@ const ASSETS = [ option: "bootstrapArchive", label: "Homebrew bootstrap archive", }, + { + name: "homebrew-portable-ruby.zip", + option: "portableRubyArchive", + label: "Homebrew portable Ruby archive", + }, { name: "homebrew-brew.env", option: "bootstrapEnvironment", @@ -78,6 +83,7 @@ export interface CreateHomebrewGuestLifecyclePublicationOptions extends ExactRef image: string; bootstrapSpec: string; bootstrapArchive: string; + portableRubyArchive: string; bootstrapEnvironment: string; bottleMirrorPlan: string; out: string; @@ -99,9 +105,10 @@ export interface HomebrewGuestLifecyclePublicationIdentity { * * The bottle mirror remains a separate release and source of truth. This * handoff owns only the fixed shell/bootstrap inputs. The shell image is a - * package-archive member. The bootstrap ZIP and environment still come from - * the transitional Kandelo registry package; none is a direct lazy-VFS URL. - * The source spec and transitional ownership are bound beside them. + * package-archive member. The source, environment, and source-matched portable + * Ruby come from one verified support-data bottle; none is accepted as an + * ambient lazy-VFS URL. The source spec and Formula ownership are bound beside + * them. */ export function createHomebrewGuestLifecyclePublication( options: CreateHomebrewGuestLifecyclePublicationOptions, @@ -545,6 +552,7 @@ function main(args: readonly string[]): void { "--image", "--homebrew-bootstrap-spec", "--homebrew-bootstrap-archive", + "--homebrew-portable-ruby-archive", "--homebrew-bootstrap-env", "--bottle-mirror-plan", "--kandelo-ref", @@ -558,6 +566,9 @@ function main(args: readonly string[]): void { image: values.get("--image")!, bootstrapSpec: values.get("--homebrew-bootstrap-spec")!, bootstrapArchive: values.get("--homebrew-bootstrap-archive")!, + portableRubyArchive: values.get( + "--homebrew-portable-ruby-archive", + )!, bootstrapEnvironment: values.get("--homebrew-bootstrap-env")!, bottleMirrorPlan: values.get("--bottle-mirror-plan")!, out: values.get("--out")!, @@ -592,6 +603,7 @@ function usage(): never { "usage: homebrew-guest-lifecycle-publication.ts create " + "--image --homebrew-bootstrap-spec " + "--homebrew-bootstrap-archive " + + "--homebrew-portable-ruby-archive " + "--homebrew-bootstrap-env --bottle-mirror-plan " + "--kandelo-ref --tap-catalog-ref " + "--tap-mirror-authority-ref --tap-caller-authority-ref " + diff --git a/scripts/homebrew-main-shell-image-contract.test.ts b/scripts/homebrew-main-shell-image-contract.test.ts index 9a044cb8ce..654ef469ea 100644 --- a/scripts/homebrew-main-shell-image-contract.test.ts +++ b/scripts/homebrew-main-shell-image-contract.test.ts @@ -251,7 +251,7 @@ test("bounds operational Homebrew downloads to the reviewed support closure", () assert.doesNotThrow(() => assertMainShellOperationalRuntimeFetches(runtimeSupport, [ "kandelo-dev/tap-core/coreutils", - "kandelo-dev/tap-core/ruby", + "kandelo-dev/tap-core/git", ]), ); assert.throws( diff --git a/scripts/homebrew-main-shell-node-smoke.ts b/scripts/homebrew-main-shell-node-smoke.ts index 7f7d575ebd..85739ccd3e 100755 --- a/scripts/homebrew-main-shell-node-smoke.ts +++ b/scripts/homebrew-main-shell-node-smoke.ts @@ -16,6 +16,8 @@ import { derivePackageDeferredZipTree, type DerivedPackageDeferredZipTree, } from "../host/src/vfs/package-deferred-tree"; +import { deriveHomebrewPortableRubyTree } from + "../host/src/homebrew-portable-ruby"; import { HOMEBREW_BOTTLE_MIRROR_PLAN_VFS_PATH, type HomebrewBottleMirrorPlan, @@ -48,6 +50,7 @@ const { migrationLockPath, homebrewBootstrapSpecPath, homebrewBootstrapArchivePath, + homebrewPortableRubyArchivePath, homebrewBootstrapEnvPath, homebrewBootstrapState, homebrewRuntimeSupportPath, @@ -80,6 +83,10 @@ const homebrewBootstrapArchiveBytes = readRegularFile( homebrewBootstrapArchivePath, "Homebrew bootstrap package output", ); +const homebrewPortableRubyArchiveBytes = readRegularFile( + homebrewPortableRubyArchivePath, + "Homebrew portable Ruby package output", +); const homebrewBootstrapEnvBytes = readRegularFile( homebrewBootstrapEnvPath, "Homebrew bootstrap launcher environment", @@ -94,6 +101,11 @@ const homebrewBootstrapTree = derivePackageDeferredZipTree( ), homebrewBootstrapArchiveBytes, ); +const homebrewPortableRubyTree = deriveHomebrewPortableRubyTree( + homebrewBootstrapTree, + homebrewBootstrapArchiveBytes, + homebrewPortableRubyArchiveBytes, +); const metadata = MemoryFileSystem.readImageMetadata(imageBytes); const capacity = MemoryFileSystem.readImageCapacity(imageBytes); assertVfsImageFitsProfile( @@ -114,9 +126,15 @@ assertPackageDeferredZipTreeState( homebrewBootstrapTree, homebrewBootstrapState, ); +assertPackageDeferredZipTreeState( + fs, + homebrewPortableRubyTree, + homebrewBootstrapState, +); assertHomebrewBootstrapTreeMetadata( metadata, homebrewBootstrapTree, + homebrewPortableRubyTree, homebrewBootstrapState, ); assertHomebrewBootstrapConsumerContract( @@ -166,19 +184,35 @@ const pendingTrees = allPendingTrees.filter((tree) => const pendingBootstrapTrees = allPendingTrees.filter((tree) => tree.activation?.capabilities.includes("homebrew:bootstrap"), ); +const pendingPortableRubyTrees = allPendingTrees.filter((tree) => + tree.activation?.capabilities.includes("homebrew:runtime") && + !pendingTrees.includes(tree) && + !pendingBootstrapTrees.includes(tree), +); const pendingRuntimeSupportTrees = pendingTrees.filter( (tree) => tree.activation?.atomicGroup?.id === runtimeSupport.activation.atomicGroup, ); const unknownPendingTrees = allPendingTrees.filter( (tree) => - !pendingTrees.includes(tree) && !pendingBootstrapTrees.includes(tree), + !pendingTrees.includes(tree) && + !pendingBootstrapTrees.includes(tree) && + !pendingPortableRubyTrees.includes(tree), ); if (unknownPendingTrees.length !== 0) { throw new Error( `main-shell image has ${unknownPendingTrees.length} unclassified pending package trees`, ); } +if ( + pendingPortableRubyTrees.length !== + (homebrewBootstrapState === "deferred" ? 1 : 0) +) { + throw new Error( + `main-shell image has ${pendingPortableRubyTrees.length} pending portable ` + + `Ruby trees; expected ${homebrewBootstrapState === "deferred" ? 1 : 0}`, + ); +} if ( pendingBootstrapTrees.length !== (homebrewBootstrapState === "deferred" ? 1 : 0) @@ -198,7 +232,7 @@ if ( ); } if ( - pendingBootstrapTrees.some( + [...pendingBootstrapTrees, ...pendingPortableRubyTrees].some( (tree) => tree.activation?.atomicGroup?.id !== runtimeSupport.activation.atomicGroup, @@ -263,6 +297,10 @@ const homebrewBootstrapTransportUrl = new URL( homebrewBootstrapTree.descriptor.archive.url, homebrewBootstrapLazyBase, ).toString(); +const homebrewPortableRubyTransportUrl = new URL( + homebrewPortableRubyTree.descriptor.archive.url, + homebrewBootstrapLazyBase, +).toString(); const closedLazyAssets = transportMode === "closed" ? [ @@ -279,6 +317,13 @@ const closedLazyAssets = size: homebrewBootstrapTree.descriptor.archive.bytes, bytes: homebrewBootstrapArchiveBytes, } satisfies ClosedLazyAsset, + { + url: homebrewPortableRubyTransportUrl, + sha256: + homebrewPortableRubyTree.descriptor.archive.sha256, + size: homebrewPortableRubyTree.descriptor.archive.bytes, + bytes: homebrewPortableRubyArchiveBytes, + } satisfies ClosedLazyAsset, ] : []), ] @@ -451,6 +496,11 @@ printf 'homebrew-m4-ok\\n' homebrewBootstrapTransportUrl, "base shell proof", ); + assertNoTransportForUrl( + lazyDownloads, + homebrewPortableRubyTransportUrl, + "base shell proof", + ); const runtimeActivationEventStart = lazyDownloads.length; const runtimeActivationStdoutStart = stdout.length; @@ -491,15 +541,20 @@ printf 'homebrew-atomic-runtime-activated\n' const runtimeActivationEvents = lazyDownloads.slice( runtimeActivationEventStart, ); - assertHomebrewBootstrapTransport( + assertPackageTreeTransport( runtimeActivationEvents, homebrewBootstrapTree, homebrewBootstrapTransportUrl, ); + assertPackageTreeTransport( + runtimeActivationEvents, + homebrewPortableRubyTree, + homebrewPortableRubyTransportUrl, + ); assertFetchedPackageSet( - withoutTransportUrl( + withoutTransportUrls( runtimeActivationEvents, - homebrewBootstrapTransportUrl, + [homebrewBootstrapTransportUrl, homebrewPortableRubyTransportUrl], ), pendingTrees, mirrorPlan, @@ -575,6 +630,11 @@ printf 'homebrew-operational-runtime-ok\n' homebrewBootstrapTransportUrl, "operational Homebrew runtime", ); + assertNoTransportForUrl( + brewOperationEvents, + homebrewPortableRubyTransportUrl, + "operational Homebrew runtime", + ); const operationalRuntimePackages = fetchedPackagesForEvents( brewOperationEvents, pendingTrees, @@ -605,14 +665,20 @@ printf 'homebrew-operational-runtime-ok\n' ...operationalRuntimePackages, ])].sort(); assertFetchedPackageSet( - withoutTransportUrl(lazyDownloads, homebrewBootstrapTransportUrl), + withoutTransportUrls(lazyDownloads, [ + homebrewBootstrapTransportUrl, + homebrewPortableRubyTransportUrl, + ]), pendingTrees, mirrorPlan, expectedFetchedPackages, "complete lazy shell and Homebrew runtime surface", ); const transportEvidence = assertBottleTransportEvents( - withoutTransportUrl(lazyDownloads, homebrewBootstrapTransportUrl), + withoutTransportUrls(lazyDownloads, [ + homebrewBootstrapTransportUrl, + homebrewPortableRubyTransportUrl, + ]), pendingTrees, mirrorPlan, expectedFetchedPackages, @@ -637,16 +703,17 @@ printf 'homebrew-operational-runtime-ok\n' function assertHomebrewBootstrapTreeMetadata( metadata: unknown, - tree: DerivedPackageDeferredZipTree, + sourceTree: DerivedPackageDeferredZipTree, + portableRubyTree: DerivedPackageDeferredZipTree, state: "deferred" | "materialized", ): void { const imageMetadata = asRecord(metadata, "main-shell image metadata"); if (!Array.isArray(imageMetadata.packageDeferredTrees)) { throw new Error("main-shell image metadata omits packageDeferredTrees"); } - const descriptor = tree.descriptor; - const expected = [ - { + const expected = [sourceTree, portableRubyTree].map((tree) => { + const descriptor = tree.descriptor; + return { schema: descriptor.schema, kind: descriptor.kind, id: descriptor.id, @@ -668,8 +735,8 @@ function assertHomebrewBootstrapTreeMetadata( owner: descriptor.owner, activation: descriptor.activation, state, - }, - ]; + }; + }); if ( canonicalJson(imageMetadata.packageDeferredTrees) !== canonicalJson(expected) @@ -788,19 +855,20 @@ function assertNoTransportForUrl( const event = events.find((candidate) => candidate.url === url); if (event !== undefined) { throw new Error( - `${label} unexpectedly fetched the Homebrew source tree from ${url}`, + `${label} unexpectedly fetched a Homebrew support tree from ${url}`, ); } } -function withoutTransportUrl( +function withoutTransportUrls( events: readonly LazyDownloadEvent[], - url: string, + urls: readonly string[], ): LazyDownloadEvent[] { - return events.filter((event) => event.url !== url); + const ignored = new Set(urls); + return events.filter((event) => !ignored.has(event.url)); } -function assertHomebrewBootstrapTransport( +function assertPackageTreeTransport( events: readonly LazyDownloadEvent[], tree: DerivedPackageDeferredZipTree, url: string, @@ -830,7 +898,7 @@ function assertHomebrewBootstrapTransport( ) ) { throw new Error( - "first brew use did not fetch the exact bootstrap tree once", + `first brew use did not fetch ${tree.descriptor.id} exactly once`, ); } } @@ -1213,6 +1281,7 @@ function parseArgs(args: string[]): { migrationLockPath: string; homebrewBootstrapSpecPath: string; homebrewBootstrapArchivePath: string; + homebrewPortableRubyArchivePath: string; homebrewBootstrapEnvPath: string; homebrewBootstrapState: "deferred" | "materialized"; homebrewRuntimeSupportPath: string; @@ -1226,6 +1295,7 @@ function parseArgs(args: string[]): { "--migration-lock", "--homebrew-bootstrap-spec", "--homebrew-bootstrap-archive", + "--homebrew-portable-ruby-archive", "--homebrew-bootstrap-env", "--homebrew-bootstrap-state", "--homebrew-runtime-support", @@ -1250,6 +1320,9 @@ function parseArgs(args: string[]): { const migrationLock = values.get("--migration-lock"); const homebrewBootstrapSpec = values.get("--homebrew-bootstrap-spec"); const homebrewBootstrapArchive = values.get("--homebrew-bootstrap-archive"); + const homebrewPortableRubyArchive = values.get( + "--homebrew-portable-ruby-archive", + ); const homebrewBootstrapEnv = values.get("--homebrew-bootstrap-env"); const homebrewBootstrapState = values.get("--homebrew-bootstrap-state"); const homebrewRuntimeSupport = values.get("--homebrew-runtime-support"); @@ -1261,6 +1334,7 @@ function parseArgs(args: string[]): { !migrationLock || !homebrewBootstrapSpec || !homebrewBootstrapArchive || + !homebrewPortableRubyArchive || !homebrewBootstrapEnv || !homebrewRuntimeSupport || !demoConfig || @@ -1277,6 +1351,7 @@ function parseArgs(args: string[]): { migrationLockPath: resolve(migrationLock), homebrewBootstrapSpecPath: resolve(homebrewBootstrapSpec), homebrewBootstrapArchivePath: resolve(homebrewBootstrapArchive), + homebrewPortableRubyArchivePath: resolve(homebrewPortableRubyArchive), homebrewBootstrapEnvPath: resolve(homebrewBootstrapEnv), homebrewBootstrapState, homebrewRuntimeSupportPath: resolve(homebrewRuntimeSupport), @@ -1292,6 +1367,7 @@ function smokeUsage(): never { "--image --migration-lock " + "--homebrew-bootstrap-spec " + "--homebrew-bootstrap-archive " + + "--homebrew-portable-ruby-archive " + "--homebrew-bootstrap-env " + "--homebrew-bootstrap-state " + "--homebrew-runtime-support " + diff --git a/scripts/homebrew-prefix-campaign.py b/scripts/homebrew-prefix-campaign.py index 1f75c4cb47..e229a9c6c4 100755 --- a/scripts/homebrew-prefix-campaign.py +++ b/scripts/homebrew-prefix-campaign.py @@ -3446,11 +3446,14 @@ def validate_required_entrant_recipe( if prepared["archive_format"] != "kandelo-deterministic-zip-v1": fail(f"{name} lock archive format is unsupported") outputs = exact_keys( - lock["outputs"], {"archive", "environment"}, f"{name} lock outputs" + lock["outputs"], + {"archive", "environment", "portable_ruby"}, + f"{name} lock outputs", ) for output_name, expected_path in ( ("archive", "homebrew-bootstrap.zip"), ("environment", "homebrew-brew.env"), + ("portable_ruby", "homebrew-portable-ruby.zip"), ): output = exact_keys( outputs[output_name], diff --git a/scripts/inspect-homebrew-main-shell-public-product.test.ts b/scripts/inspect-homebrew-main-shell-public-product.test.ts index 093ba9d71b..967620d8b7 100644 --- a/scripts/inspect-homebrew-main-shell-public-product.test.ts +++ b/scripts/inspect-homebrew-main-shell-public-product.test.ts @@ -15,6 +15,8 @@ import { type HomebrewBottleMirrorPlan, } from "../host/src/homebrew-bottle-mirror-plan"; import { homebrewRuntimeLayerPayloadAsset } from "../host/src/homebrew-runtime-layer-limits"; +import { deriveHomebrewPortableRubyTree } from + "../host/src/homebrew-portable-ruby"; import { parseHomebrewRuntimeSupportContract } from "../host/src/homebrew-runtime-support"; import { ensureDirRecursive, @@ -62,6 +64,7 @@ test("binds the sealed image, deferred brew source, and public mirror plan", asy const result = await inspectHomebrewMainShellPublicProduct({ imageBytes: fixture.imageBytes, homebrewBootstrapArchiveBytes: fixture.bootstrapArchive, + homebrewPortableRubyArchiveBytes: fixture.portableRubyArchive, homebrewBootstrapSpec: bootstrapSpec, homebrewRuntimeSupport: runtimeSupportValue, }); @@ -78,6 +81,10 @@ test("binds the sealed image, deferred brew source, and public mirror plan", asy bytes: fixture.bootstrapArchive.byteLength, activation_root: "/usr/bin/brew", }, + homebrew_portable_ruby: { + sha256: sha256(fixture.portableRubyArchive), + bytes: fixture.portableRubyArchive.byteLength, + }, bottle_mirror: { repository: runtimeSupport.catalog.tapRepository, collection_sha256: fixture.plan.collection_sha256, @@ -98,6 +105,7 @@ test("rejects bootstrap bytes that are not the deferred tree in the image", asyn inspectHomebrewMainShellPublicProduct({ imageBytes: fixture.imageBytes, homebrewBootstrapArchiveBytes: changedArchive, + homebrewPortableRubyArchiveBytes: fixture.portableRubyArchive, homebrewBootstrapSpec: bootstrapSpec, homebrewRuntimeSupport: runtimeSupportValue, }), @@ -114,6 +122,7 @@ test("rejects a public mirror outside the runtime-support catalog", async () => inspectHomebrewMainShellPublicProduct({ imageBytes: fixture.imageBytes, homebrewBootstrapArchiveBytes: fixture.bootstrapArchive, + homebrewPortableRubyArchiveBytes: fixture.portableRubyArchive, homebrewBootstrapSpec: bootstrapSpec, homebrewRuntimeSupport: runtimeSupportValue, }), @@ -128,6 +137,7 @@ test("rejects an unclassified deferred package tree", async () => { inspectHomebrewMainShellPublicProduct({ imageBytes: fixture.imageBytes, homebrewBootstrapArchiveBytes: fixture.bootstrapArchive, + homebrewPortableRubyArchiveBytes: fixture.portableRubyArchive, homebrewBootstrapSpec: bootstrapSpec, homebrewRuntimeSupport: runtimeSupportValue, }), @@ -141,6 +151,7 @@ async function productFixture(options?: { }): Promise<{ imageBytes: Uint8Array; bootstrapArchive: Uint8Array; + portableRubyArchive: Uint8Array; plan: HomebrewBottleMirrorPlan; }> { const repository = @@ -150,6 +161,12 @@ async function productFixture(options?: { bootstrapSpec, bootstrapArchive, ); + const portableRubyArchive = homebrewPortableRubyArchive(); + const portableRuby = deriveHomebrewPortableRubyTree( + bootstrap, + bootstrapArchive, + portableRubyArchive, + ); const fs = MemoryFileSystem.create( new SharedArrayBuffer(8 * MiB, { maxByteLength: 32 * MiB }), 32 * MiB, @@ -158,8 +175,9 @@ async function productFixture(options?: { prepareHomebrewBootstrapConsumerNamespace(fs, bootstrap); registerPackageDeferredZipTree(fs, bootstrap); installHomebrewBootstrapConsumerState(fs, bootstrap, bootstrapEnvironment); + registerPackageDeferredZipTree(fs, portableRuby); - const identities = runtimeSupport.additionalFormulaOrder.map( + const identities = runtimeSupport.baseFormulaOrder.map( (packageName, index) => { const id = `fixture-${String(index).padStart(2, "0")}`; const payload = new Uint8Array([index + 1]); @@ -219,17 +237,13 @@ async function productFixture(options?: { mode: "first-use", capabilities: [`homebrew-bottle:${asset.id}`], roots: [`${mountPrefix}/bin/tool`], - atomicGroup: { - id: runtimeSupport.activation.atomicGroup, - member: asset.id, - }, }, { uid: 1000, gid: 1000 }, ); } await fs.sealLazyAtomicGroup(runtimeSupport.activation.atomicGroup, [ bootstrap.descriptor.activation.atomicGroup!.member, - ...plan.assets.map((asset) => asset.id), + portableRuby.descriptor.activation.atomicGroup!.member, ]); if (options?.includeUnknownTree) { fs.registerLazyTreeWithMaterializationHandle( @@ -272,6 +286,7 @@ async function productFixture(options?: { return { imageBytes: await fs.saveImage(), bootstrapArchive, + portableRubyArchive, plan, }; } @@ -282,6 +297,11 @@ function homebrewBootstrapArchive(contents: string): Uint8Array { "bin/": zipEntry(new Uint8Array(), 0o040755), "Library/": zipEntry(new Uint8Array(), 0o040755), "Library/Homebrew/": zipEntry(new Uint8Array(), 0o040755), + "Library/Homebrew/vendor/": zipEntry(new Uint8Array(), 0o040755), + "Library/Homebrew/vendor/portable-ruby-version": zipEntry( + encoder.encode("4.0.5\n"), + 0o100644, + ), "Library/Homebrew/global.rb": zipEntry( encoder.encode(contents), 0o100644, @@ -292,6 +312,21 @@ function homebrewBootstrapArchive(contents: string): Uint8Array { ); } +function homebrewPortableRubyArchive(): Uint8Array { + return zipSync( + { + "4.0.5/": zipEntry(new Uint8Array(), 0o040755), + "4.0.5/bin/": zipEntry(new Uint8Array(), 0o040755), + "4.0.5/bin/ruby": zipEntry( + encoder.encode("ruby-wasm\n"), + 0o100755, + ), + current: zipEntry(encoder.encode("4.0.5"), 0o120777), + } satisfies Zippable, + { level: 9 }, + ); +} + function zipEntry(bytes: Uint8Array, mode: number): Zippable[string] { return [bytes, { os: 3, attrs: (mode << 16) >>> 0 }]; } diff --git a/scripts/inspect-homebrew-main-shell-public-product.ts b/scripts/inspect-homebrew-main-shell-public-product.ts index 8dbb4c83c4..3e2b7ade85 100644 --- a/scripts/inspect-homebrew-main-shell-public-product.ts +++ b/scripts/inspect-homebrew-main-shell-public-product.ts @@ -6,6 +6,8 @@ import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseHomebrewRuntimeSupportContract } from "../host/src/homebrew-runtime-support"; +import { deriveHomebrewPortableRubyTree } from + "../host/src/homebrew-portable-ruby"; import { HOMEBREW_BOTTLE_MIRROR_PLAN_VFS_PATH } from "../host/src/homebrew-bottle-mirror-plan"; import { MemoryFileSystem } from "../host/src/vfs/memory-fs"; import { @@ -32,6 +34,10 @@ export interface HomebrewMainShellPublicProduct { bytes: number; activation_root: "/usr/bin/brew"; }; + homebrew_portable_ruby: { + sha256: string; + bytes: number; + }; bottle_mirror: { repository: string; collection_sha256: string; @@ -46,6 +52,7 @@ export interface HomebrewMainShellPublicProduct { export async function inspectHomebrewMainShellPublicProduct(input: { imageBytes: Uint8Array; homebrewBootstrapArchiveBytes: Uint8Array; + homebrewPortableRubyArchiveBytes: Uint8Array; homebrewBootstrapSpec: unknown; homebrewRuntimeSupport: unknown; }): Promise { @@ -54,6 +61,10 @@ export async function inspectHomebrewMainShellPublicProduct(input: { input.homebrewBootstrapArchiveBytes, "Homebrew bootstrap archive", ); + const portableRubyBytes = nonemptyBytes( + input.homebrewPortableRubyArchiveBytes, + "Homebrew portable Ruby archive", + ); const runtimeSupport = parseHomebrewRuntimeSupportContract( input.homebrewRuntimeSupport, ); @@ -61,6 +72,11 @@ export async function inspectHomebrewMainShellPublicProduct(input: { input.homebrewBootstrapSpec, bootstrapBytes, ); + const portableRuby = deriveHomebrewPortableRubyTree( + bootstrap, + bootstrapBytes, + portableRubyBytes, + ); if ( bootstrap.descriptor.activation.atomicGroup?.id !== runtimeSupport.activation.atomicGroup || @@ -80,6 +96,7 @@ export async function inspectHomebrewMainShellPublicProduct(input: { // fails closed before that trust boundary. await fs.verifyImportedLazyAtomicGroupSeals(); assertPackageDeferredZipTreeState(fs, bootstrap, "deferred"); + assertPackageDeferredZipTreeState(fs, portableRuby, "deferred"); const activationRoot = runtimeSupport.activation.root; if (!fs.isPathDeferred(activationRoot)) { throw new Error( @@ -107,10 +124,24 @@ export async function inspectHomebrewMainShellPublicProduct(input: { const bootstrapTrees = allPendingTrees.filter((tree) => tree.activation?.capabilities.includes("homebrew:bootstrap"), ); + const portableRubyTrees = allPendingTrees.filter((tree) => + tree.activation?.capabilities.includes("homebrew:runtime") && + !tree.activation.capabilities.includes("homebrew:bootstrap") && + !tree.activation.capabilities.some((capability) => + capability.startsWith("homebrew-bottle:") + ) + ); const unknownTrees = allPendingTrees.filter( - (tree) => !bottleTrees.includes(tree) && !bootstrapTrees.includes(tree), + (tree) => + !bottleTrees.includes(tree) && + !bootstrapTrees.includes(tree) && + !portableRubyTrees.includes(tree), ); - if (bootstrapTrees.length !== 1 || unknownTrees.length !== 0) { + if ( + bootstrapTrees.length !== 1 || + portableRubyTrees.length !== 1 || + unknownTrees.length !== 0 + ) { throw new Error( "main-shell image has an unexpected deferred package-tree inventory", ); @@ -158,6 +189,10 @@ export async function inspectHomebrewMainShellPublicProduct(input: { bytes: bootstrapBytes.byteLength, activation_root: activationRoot, }, + homebrew_portable_ruby: { + sha256: sha256(portableRubyBytes), + bytes: portableRubyBytes.byteLength, + }, bottle_mirror: { repository: plan.repository, collection_sha256: plan.collection_sha256, @@ -228,6 +263,7 @@ function parseJsonFile(path: string, label: string): unknown { function parseArgs(args: string[]): { image: string; homebrewBootstrapArchive: string; + homebrewPortableRubyArchive: string; homebrewBootstrapSpec: string; homebrewRuntimeSupport: string; output: string; @@ -242,6 +278,7 @@ function parseArgs(args: string[]): { ![ "--image", "--homebrew-bootstrap-archive", + "--homebrew-portable-ruby-archive", "--homebrew-bootstrap-spec", "--homebrew-runtime-support", "--out", @@ -252,12 +289,15 @@ function parseArgs(args: string[]): { } values.set(key, value); } - if (values.size !== 5) usage(); + if (values.size !== 6) usage(); return { image: resolve(values.get("--image")!), homebrewBootstrapArchive: resolve( values.get("--homebrew-bootstrap-archive")!, ), + homebrewPortableRubyArchive: resolve( + values.get("--homebrew-portable-ruby-archive")!, + ), homebrewBootstrapSpec: resolve(values.get("--homebrew-bootstrap-spec")!), homebrewRuntimeSupport: resolve(values.get("--homebrew-runtime-support")!), output: resolve(values.get("--out")!), @@ -269,6 +309,7 @@ function usage(): never { "usage: npx tsx scripts/inspect-homebrew-main-shell-public-product.ts " + "--image " + "--homebrew-bootstrap-archive " + + "--homebrew-portable-ruby-archive " + "--homebrew-bootstrap-spec " + "--homebrew-runtime-support --out ", ); @@ -285,6 +326,10 @@ if ( options.homebrewBootstrapArchive, "Homebrew bootstrap archive", ), + homebrewPortableRubyArchiveBytes: readRegularFile( + options.homebrewPortableRubyArchive, + "Homebrew portable Ruby archive", + ), homebrewBootstrapSpec: parseJsonFile( options.homebrewBootstrapSpec, "Homebrew bootstrap package-tree spec", diff --git a/scripts/prepare-homebrew-browser-bootstrap.sh b/scripts/prepare-homebrew-browser-bootstrap.sh index e3c00d1212..1fdfd8be54 100755 --- a/scripts/prepare-homebrew-browser-bootstrap.sh +++ b/scripts/prepare-homebrew-browser-bootstrap.sh @@ -9,6 +9,7 @@ BOOTSTRAP_SPEC="$REPO_ROOT/homebrew/main-shell-brew-package-tree.json" OUTPUT_DIRECTORY="" BROWSER_ASSET="" +BROWSER_PORTABLE_RUBY_ASSET="" REQUIRE_SEALED=0 PENDING_SELECTION_ROOT="${WASM_POSIX_HOMEBREW_PENDING_SELECTION_ROOT:-}" @@ -21,7 +22,8 @@ usage() { cat >&2 <<'EOF' usage: prepare-homebrew-browser-bootstrap.sh \ [--output-directory ] \ - [--browser-asset ] [--require-sealed] \ + [--browser-asset \ + --browser-portable-ruby-asset ] [--require-sealed] \ [--pending-selection-root ] EOF exit 2 @@ -39,6 +41,11 @@ while [ "$#" -gt 0 ]; do BROWSER_ASSET="$2" shift 2 ;; + --browser-portable-ruby-asset) + [ "$#" -ge 2 ] || usage + BROWSER_PORTABLE_RUBY_ASSET="$2" + shift 2 + ;; --require-sealed) REQUIRE_SEALED=1 shift @@ -55,6 +62,9 @@ while [ "$#" -gt 0 ]; do done [ -n "$OUTPUT_DIRECTORY" ] || [ -n "$BROWSER_ASSET" ] || usage +if [ -n "$BROWSER_ASSET" ] || [ -n "$BROWSER_PORTABLE_RUBY_ASSET" ]; then + [ -n "$BROWSER_ASSET" ] && [ -n "$BROWSER_PORTABLE_RUBY_ASSET" ] || usage +fi [ "$REQUIRE_SEALED" -eq 0 ] || [ -z "$PENDING_SELECTION_ROOT" ] || fail "a pending selection cannot replace a sealed selection" @@ -86,6 +96,7 @@ temporary_parent="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" work_root="$(mktemp -d \ "$temporary_parent/kandelo-homebrew-browser-bootstrap.XXXXXX")" staged_browser_asset="" +staged_portable_ruby_asset="" cleanup() { local status=$? local cleanup_failed=0 @@ -94,6 +105,11 @@ cleanup() { [ ! -L "$staged_browser_asset" ]; then rm -f -- "$staged_browser_asset" || cleanup_failed=1 fi + if [ -n "$staged_portable_ruby_asset" ] && + [ -f "$staged_portable_ruby_asset" ] && + [ ! -L "$staged_portable_ruby_asset" ]; then + rm -f -- "$staged_portable_ruby_asset" || cleanup_failed=1 + fi rm -rf -- "$work_root" || cleanup_failed=1 if [ "$status" -eq 0 ] && [ "$cleanup_failed" -ne 0 ]; then echo "prepare-homebrew-browser-bootstrap: cleanup failed" >&2 @@ -201,7 +217,12 @@ env -u GH_TOKEN -u GITHUB_TOKEN \ "$REPO_ROOT/scripts/extract-homebrew-support-data-bottle.ts" \ "${extract_args[@]}" -for name in homebrew-bootstrap.zip homebrew-brew.env report.json; do +for name in \ + homebrew-bootstrap.zip \ + homebrew-portable-ruby.zip \ + homebrew-brew.env \ + report.json +do [ -f "$extraction_root/$name" ] && [ ! -L "$extraction_root/$name" ] || fail "verified extraction omitted $name" done @@ -224,6 +245,25 @@ if [ -n "$BROWSER_ASSET" ]; then mv -f -- "$staged_browser_asset" "$BROWSER_ASSET" staged_browser_asset="" cmp "$extraction_root/homebrew-bootstrap.zip" "$BROWSER_ASSET" + + portable_parent="$(dirname "$BROWSER_PORTABLE_RUBY_ASSET")" + [ -d "$portable_parent" ] && [ ! -L "$portable_parent" ] || + fail "portable Ruby browser asset parent must be a real directory" + if [ -e "$BROWSER_PORTABLE_RUBY_ASSET" ] || + [ -L "$BROWSER_PORTABLE_RUBY_ASSET" ]; then + [ -f "$BROWSER_PORTABLE_RUBY_ASSET" ] && + [ ! -L "$BROWSER_PORTABLE_RUBY_ASSET" ] || + fail "portable Ruby browser asset must be absent or a regular file" + fi + staged_portable_ruby_asset="$(mktemp \ + "$portable_parent/.homebrew-portable-ruby.zip.XXXXXX")" + cp -- "$extraction_root/homebrew-portable-ruby.zip" \ + "$staged_portable_ruby_asset" + chmod 0644 "$staged_portable_ruby_asset" + mv -f -- "$staged_portable_ruby_asset" "$BROWSER_PORTABLE_RUBY_ASSET" + staged_portable_ruby_asset="" + cmp "$extraction_root/homebrew-portable-ruby.zip" \ + "$BROWSER_PORTABLE_RUBY_ASSET" fi echo "Homebrew browser bootstrap prepared from tap $locked_tap_sha" diff --git a/scripts/prepare-homebrew-main-shell-inputs.sh b/scripts/prepare-homebrew-main-shell-inputs.sh index 3816a21a01..d81587fe63 100755 --- a/scripts/prepare-homebrew-main-shell-inputs.sh +++ b/scripts/prepare-homebrew-main-shell-inputs.sh @@ -155,6 +155,7 @@ BOOTSTRAP_PACKAGE="$(jq -er '.package.name' \ for required in \ "$BOOTSTRAP/homebrew-bootstrap.zip" \ "$BOOTSTRAP/homebrew-brew.env" \ + "$BOOTSTRAP/homebrew-portable-ruby.zip" \ "$BOOTSTRAP/report.json"; do [ -f "$required" ] && [ ! -L "$required" ] || { echo "prepare-homebrew-main-shell-inputs: extractor omitted $required" >&2 diff --git a/scripts/resolve-binary.bundle.mjs b/scripts/resolve-binary.bundle.mjs index 0dfcfb1dd2..b859a040ea 100644 --- a/scripts/resolve-binary.bundle.mjs +++ b/scripts/resolve-binary.bundle.mjs @@ -1,13 +1,13 @@ // Generated by scripts/build-resolve-binary-bundle.sh; do not edit. Third-party notices: resolve-binary.bundle.LICENSES.txt -var Co=Object.defineProperty;var Pn=(r,e,t)=>()=>{if(t)throw t[0];try{return r&&(e=r(r=0)),e}catch(n){throw t=[n],n}};var Nr=(r,e)=>{for(var t in e)Co(r,t,{get:e[t],enumerable:!0})};import{createRequire as Cs}from"module";function Li(r,e){return ki(r,{i:2},e&&e.out,e&&e.dictionary)}var Bs,yt,Ns,Ms,te,ft,$s,wi,vi,Fs,Si,yt,Ei,Ds,zi,Us,rl,Jn,Ae,M,$t,Ft,M,M,M,M,bi,M,Gs,Ks,Yn,ge,Xn,Ai,fn,Ws,fe,ki,Zs,Hs,ht,xi,Vs,qs,Qn=Pn(()=>{Bs=Cs("/");try{yt=Bs("worker_threads"),Ns=yt.Worker,Ms=yt.isMarkedAsUntransferable}catch{}te=Uint8Array,ft=Uint16Array,$s=Int32Array,wi=new te([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),vi=new te([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),Fs=new te([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),Si=function(r,e){for(var t=new ft(31),n=0;n<31;++n)t[n]=e+=1<>1|(M&21845)<<1,Ae=(Ae&52428)>>2|(Ae&13107)<<2,Ae=(Ae&61680)>>4|(Ae&3855)<<4,Jn[M]=((Ae&65280)>>8|(Ae&255)<<8)>>1;$t=(function(r,e,t){for(var n=r.length,i=0,s=new ft(e);i>c]=l}else for(a=new ft(n),i=0;i>15-r[i]);return a}),Ft=new te(288);for(M=0;M<144;++M)Ft[M]=8;for(M=144;M<256;++M)Ft[M]=9;for(M=256;M<280;++M)Ft[M]=7;for(M=280;M<288;++M)Ft[M]=8;bi=new te(32);for(M=0;M<32;++M)bi[M]=5;Gs=$t(Ft,9,1),Ks=$t(bi,5,1),Yn=function(r){for(var e=r[0],t=1;te&&(e=r[t]);return e},ge=function(r,e,t){var n=e/8|0;return(r[n]|r[n+1]<<8)>>(e&7)&t},Xn=function(r,e){var t=e/8|0;return(r[t]|r[t+1]<<8|r[t+2]<<16)>>(e&7)},Ai=function(r){return(r+7)/8|0},fn=function(r,e,t){return(e==null||e<0)&&(e=0),(t==null||t>r.length)&&(t=r.length),new te(r.subarray(e,t))},Ws=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],fe=function(r,e,t){var n=new Error(e||Ws[r]);if(n.code=r,Error.captureStackTrace&&Error.captureStackTrace(n,fe),!t)throw n;return n},ki=function(r,e,t,n){var i=r.length,s=n?n.length:0;if(!i||e.f&&!e.l)return t||new te(0);var o=!t,a=o||e.i!=2,c=e.i;o&&(t=new te(i*3));var l=function(Oe){var Te=t.length;if(Oe>Te){var Qt=new te(Math.max(Te*2,Oe));Qt.set(t),t=Qt}},u=e.f||0,d=e.p||0,f=e.b||0,y=e.l,h=e.d,p=e.m,w=e.n,m=i*8;do{if(!y){u=ge(r,d,1);var g=ge(r,d+1,3);if(d+=3,g)if(g==1)y=Gs,h=Ks,p=9,w=5;else if(g==2){var v=ge(r,d,31)+257,A=ge(r,d+10,15)+4,k=v+ge(r,d+5,31)+1;d+=14;for(var x=new te(k),I=new te(19),_=0;_>4;if(S<16)x[_++]=S;else{var U=0,ae=0;for(S==16?(ae=3+ge(r,d,3),d+=2,U=x[_-1]):S==17?(ae=3+ge(r,d,7),d+=3):S==18&&(ae=11+ge(r,d,127),d+=7);ae--;)x[_++]=U}}var N=x.subarray(0,v),Z=x.subarray(v);p=Yn(N),w=Yn(Z),y=$t(N,p,1),h=$t(Z,w,1)}else fe(1);else{var S=Ai(d)+4,z=r[S-4]|r[S-3]<<8,E=S+z;if(E>i){c&&fe(0);break}a&&l(f+z),t.set(r.subarray(S,E),f),e.b=f+=z,e.p=d=E*8,e.f=u;continue}if(d>m){c&&fe(0);break}}a&&l(f+131072);for(var vt=(1<>4;if(d+=U&15,d>m){c&&fe(0);break}if(U||fe(2),Ee<256)t[f++]=Ee;else if(Ee==256){De=d,y=null;break}else{var St=Ee-254;if(Ee>264){var _=Ee-257,_e=wi[_];St=ge(r,d,(1<<_e)-1)+Ei[_],d+=_e}var Je=h[Xn(r,d)&Jt],pe=Je>>4;Je||fe(3),d+=Je&15;var Z=Us[pe];if(pe>3){var _e=vi[pe];Z+=Xn(r,d)&(1<<_e)-1,d+=_e}if(d>m){c&&fe(0);break}a&&l(f+131072);var Pe=f+St;if(f>3&1)+(e>>4&1);n>0;n-=!r[t++]);return t+(e&2)},ht=(function(){function r(e,t){typeof e=="function"&&(t=e,e={}),this.ondata=t;var n=e&&e.dictionary&&e.dictionary.subarray(-32768);this.s={i:0,b:n?n.length:0},this.o=new te(32768),this.p=new te(0),n&&this.o.set(n)}return r.prototype.e=function(e){if(this.ondata||fe(5),this.d&&fe(4),!this.p.length)this.p=e;else if(e.length){var t=new te(this.p.length+e.length);t.set(this.p),t.set(e,this.p.length),this.p=t}},r.prototype.c=function(e){this.s.i=+(this.d=e||!1);var t=this.s.b,n=ki(this.p,this.s,this.o);this.ondata(fn(n,t,this.s.b),this.d),this.o=fn(n,this.s.b-32768),this.s.b=this.o.length,this.p=fn(this.p,this.s.p/8|0),this.s.p&=7},r.prototype.push=function(e,t){this.e(e),this.c(t)},r})();xi=(function(){function r(e,t){this.v=1,this.r=0,ht.call(this,e,t)}return r.prototype.push=function(e,t){if(ht.prototype.e.call(this,e),this.r+=e.length,this.v){var n=this.p.subarray(this.v-1),i=n.length>3?Hs(n):4;if(i>n.length){if(!t)return}else this.v>1&&this.onmember&&this.onmember(this.r-n.length);this.p=n.subarray(i),this.v=0}ht.prototype.c.call(this,0),this.s.f&&!this.s.l?(this.v=Ai(this.s.p)+9,this.s={i:0},this.o=new te(0),this.push(new te(0),t)):t&&ht.prototype.c.call(this,t)},r})(),Vs=typeof TextDecoder<"u"&&new TextDecoder,qs=0;try{Vs.decode(Zs,{stream:!0}),qs=1}catch{}});var nr={};Nr(nr,{extractZipEntry:()=>na,extractZipEntryBounded:()=>ra,fetchZipCentralDirectory:()=>oa,parseZipCentralDirectory:()=>Dt});function Ri(r){let e=new DataView(r.buffer,r.byteOffset,r.byteLength),t=Math.max(0,r.length-Pi);for(let n=r.length-Xs;n>=t;n--)if(e.getUint32(n,!0)===js)return n;throw new Error("Zip EOCD record not found")}function Dt(r){let e=new DataView(r.buffer,r.byteOffset,r.byteLength),t=Ri(r),n=e.getUint16(t+10,!0),i=e.getUint32(t+16,!0),s=[],o=i;for(let a=0;a>8,z;S===Ii?z=p>>16&65535:g.startsWith("bin/")||g.startsWith("sbin/")||g.includes("/bin/")||g.includes("/sbin/")?z=493:z=420;let E=g.endsWith("/"),v=S===Ii&&(z&Qs)===Js;s.push({fileName:g,fileNameBytes:m,compressedSize:u,uncompressedSize:d,compressionMethod:l,localHeaderOffset:w,mode:z,isDirectory:E,isSymlink:v,externalAttrs:p,creatorOS:S}),o+=er+f+y+h}return s}function Ci(r,e){if(r.byteLength!==e.byteLength)return!1;for(let t=0;t{if(a.byteLength>t-s)throw new Error(`ZIP member ${e.fileName} expands beyond ${t} bytes`);i.set(a,s),s+=a.byteLength}).push(n,!0),s!==t)throw new Error(`ZIP member ${e.fileName} expanded ${s} bytes, expected ${t}`);return i}function ia(r,e){let t=new DataView(r.buffer,r.byteOffset,r.byteLength),n=e.localHeaderOffset;if(n<0||n>r.byteLength-tr||t.getUint32(n,!0)!==_i)throw new Error(`Invalid local file header signature at offset ${n}`);let i=t.getUint16(n+8,!0),s=t.getUint16(n+26,!0),o=t.getUint16(n+28,!0),a=n+tr,c=a+s+o,l=c+e.compressedSize;if(i!==e.compressionMethod||cr.byteLength||!Ci(r.subarray(a,a+s),e.fileNameBytes))throw new Error(`ZIP member ${e.fileName} has inconsistent local metadata`);return r.subarray(c,l)}async function oa(r){let e=await fetch(r,{method:"HEAD"});if(!e.ok)throw new Error(`HEAD request failed: ${e.status} ${e.statusText}`);let t=parseInt(e.headers.get("content-length")||"0",10),n=e.headers.get("accept-ranges");if(!t||n!=="bytes"){let m=await fetch(r);if(!m.ok)throw new Error(`Fetch failed: ${m.status} ${m.statusText}`);let g=new Uint8Array(await m.arrayBuffer());return{entries:Dt(g),totalSize:g.length}}let i=Math.min(t,Pi),s=t-i,o=await fetch(r,{headers:{Range:`bytes=${s}-${t-1}`}});if(o.status!==206){let m=await fetch(r);if(!m.ok)throw new Error(`Fetch failed: ${m.status} ${m.statusText}`);let g=new Uint8Array(await m.arrayBuffer());return{entries:Dt(g),totalSize:g.length}}let a=new Uint8Array(await o.arrayBuffer()),c=new DataView(a.buffer,a.byteOffset,a.byteLength),l=Ri(a),u=c.getUint32(l+12,!0),d=c.getUint32(l+16,!0);if(d>=s){let m=t,g=new Uint8Array(m);return g.set(a,s),{entries:Dt(g),totalSize:m}}let f=d+u-1,y=await fetch(r,{headers:{Range:`bytes=${d}-${f}`}});if(y.status!==206)throw new Error(`Range request for CD failed: ${y.status}`);let h=new Uint8Array(await y.arrayBuffer()),p=t,w=new Uint8Array(p);return w.set(h,d),w.set(a,s),{entries:Dt(w),totalSize:p}}var js,Ys,_i,Pi,Xs,er,tr,Oi,Ti,Ii,Js,Qs,ea,ta,rr=Pn(()=>{"use strict";Qn();js=101010256,Ys=33639248,_i=67324752,Pi=65557,Xs=22,er=46,tr=30,Oi=0,Ti=8,Ii=3,Js=40960,Qs=61440,ea=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}),ta=new TextEncoder});var Ui={};Nr(Ui,{DEFAULT_TAR_GZIP_LIMITS:()=>Di,TarParseError:()=>P,parseTarGzip:()=>la});function la(r,e={}){let t=e.label??"TAR gzip archive",n=ua(e.limits,t);if(r.byteLength===0||r.byteLength>n.maxCompressedBytes)throw new P(`${t}: compressed byte count ${r.byteLength} is outside 1..${n.maxCompressedBytes}`);let i=fa(r,t);if(i===0||i>n.maxUncompressedBytes)throw new P(`${t}: declared uncompressed byte count ${i} is outside 1..${n.maxUncompressedBytes}`);let s=ha(r,t,i);if(s.byteLength!==i)throw new P(`${t}: gzip expanded to ${s.byteLength} bytes, expected ${i}`);let o=new DataView(r.buffer,r.byteOffset,r.byteLength).getUint32(r.byteLength-8,!0);if(ya(s)!==o)throw new P(`${t}: gzip CRC32 mismatch`);return da(s,t,n)}function da(r,e,t){if(r.byteLength%ke!==0)throw new P(`${e}: TAR byte count is not block-aligned`);let n=[],i=0,s=0,o=0,a=null,c={},l=!1;for(;i+ke<=r.byteLength;){let u=r.subarray(i,i+ke);if(i+=ke,or(u)){if(i+ke>r.byteLength)throw new P(`${e}: TAR end marker is truncated`);let E=r.subarray(i,i+ke);if(!or(E))throw new P(`${e}: TAR has only one zero end block`);if(i+=ke,!or(r.subarray(i)))throw new P(`${e}: TAR has nonzero data after its end marker`);l=!0;break}wa(u,e);let d=Ut(u,156,1,e)||"0",f=ar(u,124,12,`${e}: TAR entry size`),y=ar(u,100,8,`${e}: TAR entry mode`)&sa,h=va(u,e,t.maxPathBytes),p=Ut(u,157,100,e);if(d==="x"||d==="g"){if(o+=1,o>t.maxEntries+1)throw new P(`${e}: TAR extension header count exceeds ${t.maxEntries+1}`);let E=Ni(r,i,f,e);i=Mi(i,f,r.byteLength,e);let v=ma(E,e,t);d==="x"?a=v:c={...c,...v};continue}if(s+=1,s>t.maxEntries)throw new P(`${e}: TAR entry count exceeds ${t.maxEntries}`);let w={...c,...a??{}};a=null;let m=w.size===void 0?f:ga(w.size,`${e}: PAX entry size`),g=Ni(r,i,m,e);i=Mi(i,m,r.byteLength,e);let S=sr(w.path??h,e,t.maxPathBytes),z=w.linkpath??p;switch(d){case"0":case"\0":n.push({path:S,type:"file",mode:y,data:g});break;case"5":ir(m,e,"directory",S),n.push({path:S,type:"directory",mode:y});break;case"2":ir(m,e,"symlink",S),$i(z,e,S,t.maxLinkBytes,!1),n.push({path:S,type:"symlink",mode:y,linkName:z});break;case"1":ir(m,e,"hardlink",S),$i(z,e,S,t.maxLinkBytes,!0),n.push({path:S,type:"hardlink",mode:y,linkName:sr(z,`${e}: hardlink target`,t.maxPathBytes)});break;case"3":case"4":case"6":throw new P(`${e}: unsupported TAR device/FIFO entry ${S}`);default:throw new P(`${e}: unsupported TAR entry type ${JSON.stringify(d)} for ${S}`)}}if(!l)throw new P(`${e}: TAR is missing its two-block end marker`);if(a!==null)throw new P(`${e}: local PAX header has no following entry`);return n}function ua(r,e){let t={...Di,...r};for(let[n,i]of Object.entries(t))if(!Number.isSafeInteger(i)||i<=0)throw new P(`${e}: ${n} must be a positive safe integer`);return t}function fa(r,e){if(r.byteLength<18||r[0]!==31||r[1]!==139||r[2]!==8)throw new P(`${e}: invalid gzip header`);return new DataView(r.buffer,r.byteOffset,r.byteLength).getUint32(r.byteLength-4,!0)}function ha(r,e,t){let n=new Uint8Array(t),i=0,s=!1,o=new xi(a=>{if(a.byteLength>t-i)throw new P(`${e}: gzip expansion exceeds its declared ${t} bytes`);n.set(a,i),i+=a.byteLength});o.onmember=()=>{throw s=!0,new P(`${e}: concatenated gzip members are unsupported`)};try{o.push(r,!0)}catch(a){throw a instanceof P?a:new P(`${e}: cannot gunzip archive: ${Ea(a)}`)}if(s)throw new P(`${e}: concatenated gzip members are unsupported`);return n.subarray(0,i)}function ya(r){let e=4294967295;for(let t of r)e=ca[(e^t)&255]^e>>>8;return(e^4294967295)>>>0}function pa(){let r=new Uint32Array(256);for(let e=0;e>>1^((t&1)===0?0:3988292384);r[e]=t>>>0}return r}function Ni(r,e,t,n){if(t>r.byteLength-e)throw new P(`${n}: TAR entry is truncated`);return r.subarray(e,e+t)}function Mi(r,e,t,n){let s=Math.ceil(e/ke)*ke;if(!Number.isSafeInteger(s)||s>t-r)throw new P(`${n}: TAR entry padding is truncated`);return r+s}function ma(r,e,t){let n={},i=0;for(;i9)throw new P(`${e}: invalid PAX record length`);if(o=o*10+p,!Number.isSafeInteger(o))throw new P(`${e}: invalid PAX record length`)}let a=i+o;if(o<=s-i+2||a>r.byteLength||r[a-1]!==10)throw new P(`${e}: truncated PAX record`);let c=s+1;for(;c=a-1)throw new P(`${e}: invalid PAX record`);let l=r.subarray(s+1,c);if(l.byteLength>256)throw new P(`${e}: PAX record key is too long`);let u=cr(l,`${e}: PAX record key`),d=r.subarray(c+1,a-1),f=u==="path"?t.maxPathBytes:u==="linkpath"?t.maxLinkBytes:u==="size"?32:0;if(f===0){i=a;continue}if(d.byteLength>f)throw new P(`${e}: PAX ${u} value is too long`);let y=cr(d,`${e}: PAX record value`);n[u]=y,i=a}return n}function ga(r,e){if(!/^(0|[1-9][0-9]*)$/.test(r))throw new P(`${e} is invalid`);let t=Number(r);if(!Number.isSafeInteger(t)||t<0)throw new P(`${e} is invalid`);return t}function wa(r,e){let t=ar(r,148,8,`${e}: TAR checksum`),n=0;for(let i=0;i=148&&i<156?32:r[i];if(t!==n)throw new P(`${e}: TAR checksum mismatch`)}function va(r,e,t){let n=Ut(r,0,100,e),i=Ut(r,345,155,e);return sr(i?`${i}/${n}`:n,e,t)}function sr(r,e,t){let n=r;for(;n.startsWith("./");)n=n.slice(2);return n=n.replace(/\/+$/g,""),Sa(n,`${e}: TAR path`,t),n}function Ut(r,e,t,n){let i=e,s=e+t;for(;in||r.includes("\0"))throw new P(`${e}: link target for ${t} is invalid`);if(i&&r.includes("\\"))throw new P(`${e}: hardlink target for ${t} is invalid`)}function Sa(r,e,t){if(r.length===0||r.startsWith("/")||r.includes("\0")||r.includes("\\")||Fi.encode(r).byteLength>t)throw new P(`${e} ${JSON.stringify(r)} must be a bounded relative POSIX path`);for(let n of r.split("/"))if(n.length===0||n==="."||n==="..")throw new P(`${e} ${JSON.stringify(r)} contains an unsafe path segment`)}function or(r){for(let e of r)if(e!==0)return!1;return!0}function cr(r,e){try{return aa.decode(r)}catch{throw new P(`${e} contains non-UTF-8 text`)}}function Ea(r){return r instanceof Error?r.message:String(r)}var ke,sa,Bi,aa,Fi,ca,Di,P,Gi=Pn(()=>{"use strict";Qn();ke=512,sa=4095,Bi=1024*1024,aa=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}),Fi=new TextEncoder,ca=pa(),Di=Object.freeze({maxCompressedBytes:256*Bi,maxUncompressedBytes:512*Bi,maxEntries:1e5,maxPathBytes:4096,maxLinkBytes:65536}),P=class extends Error{constructor(e){super(e),this.name="TarParseError"}}});import{existsSync as jt,lstatSync as In,readdirSync as sc,readFileSync as qe,realpathSync as ve,statSync as je}from"node:fs";import{createHash as bo}from"node:crypto";import{spawnSync as xr}from"node:child_process";import{basename as ac,dirname as Xt,isAbsolute as _n,join as K,relative as cc,resolve as we,sep as lc}from"node:path";import{fileURLToPath as dc}from"node:url";var Et="kandelo.wpk_fork.linked_frames";var Mr=[75,76,67,70],zt=24,$r=8,On=3,Fr=[{bytes:4,chunkHeaderSize:32,nodeHeaderSize:24},{bytes:8,chunkHeaderSize:56,nodeHeaderSize:32}],et=[{module:"env",name:"__wpk_fork_frame_commit",params:["ptr"],results:[]},{module:"env",name:"__wpk_fork_frame_next",params:["ptr"],results:["ptr"]},{module:"env",name:"__wpk_fork_frame_reserve",params:["ptr"],results:["ptr"]}],bt=[{name:"wpk_fork_abort_begin",params:["ptr"],results:[]},{name:"wpk_fork_abort_end",params:[],results:[]},{name:"wpk_fork_rewind_begin",params:["ptr"],results:[]},{name:"wpk_fork_rewind_end",params:[],results:[]},{name:"wpk_fork_state",params:[],results:["i32"]},{name:"wpk_fork_unwind_begin",params:["ptr"],results:[]},{name:"wpk_fork_unwind_end",params:[],results:[]}];var Dr=["__abi_version","kernel_alloc_scratch","kernel_create_process","kernel_create_process_with_stdio","kernel_dequeue_signal","kernel_exec_prepare","kernel_exec_setup_for_thread","kernel_fork_process","kernel_get_parent_pid","kernel_get_process_exit_signal","kernel_get_process_state","kernel_handle_channel","kernel_has_sa_nocldstop","kernel_host_adapter_manifest_len","kernel_host_adapter_manifest_ptr","kernel_ipc_shmat_for_process","kernel_ipc_shmat_for_task","kernel_ipc_shmdt_for_process","kernel_ipc_shmdt_for_task","kernel_mark_process_signaled","kernel_pipe_has_readers","kernel_posix_timer_fire","kernel_prepare_write_operation","kernel_reap_exited_child","kernel_remove_process","kernel_set_current_tid","kernel_spawn_process","kernel_thread_exit","kernel_validate_task","kernel_wait_child_poll"];var H={LINK_MAX:0,MAX_CANON:1,MAX_INPUT:2,NAME_MAX:3,PATH_MAX:4,PIPE_BUF:5,CHOWN_RESTRICTED:6,NO_TRUNC:7,VDISABLE:8,SYNC_IO:9,ASYNC_IO:10,PRIO_IO:11,SOCK_MAXBUF:12,FILESIZEBITS:13,REC_INCR_XFER_SIZE:14,REC_MAX_XFER_SIZE:15,REC_MIN_XFER_SIZE:16,REC_XFER_ALIGN:17,ALLOC_SIZE_MIN:18,SYMLINK_MAX:19,POSIX2_SYMLINKS:20,FALLOC:21,TEXTDOMAIN_MAX:22,TIMESTAMP_RESOLUTION:23};function L(r,e){let t=0,n=0,i=e;for(;;){let s=r[i++];if(t|=(s&127)<=21&&n<=34?At(e,t):n===84||n>=92&&n<=99||n>=112&&n<=123||n>=124&&n<=131||n>=156&&n<=159?t+1:t:r===254?n===0||n===1||n===2?At(e,t):n===3?t:n>=16&&n<=79?At(e,t):null:null}function Fo(r,e,t){let[n,i]=L(r,e);e+=i+n;let[s,o]=L(r,e);e+=o+s;let a=r[e++];if(a===0){t.funcImports++;let[,c]=L(r,e);e+=c}else if(a===1){e++;let c=r[e++],[,l]=L(r,e);if(e+=l,c&1){let[,u]=L(r,e);e+=u}}else if(a===2){let c=r[e++],[,l]=L(r,e);if(e+=l,c&1){let[,u]=L(r,e);e+=u}}else a===3&&(t.globalImports++,e+=2);return e}function en(r){return r.length>=8&&r[0]===0&&r[1]===97&&r[2]===115&&r[3]===109}function Re(r,e){let[t,n]=L(r,e);return e+=n,[new TextDecoder().decode(r.subarray(e,e+t)),e+t]}function Do(r,e){if(e.length===0)return!0;let t=new TextEncoder().encode(e);e:for(let n=0;n<=r.length-t.length;n++){for(let i=0;ir);function Ur(r,e,t){if(!t)throw new Error(`function ${e} refers to an unknown type`);let n=r.get(e)??[];n.push(t),r.set(e,n)}function Tn(r,e){let[t,n]=L(r,e);e+=n;let[,i]=L(r,e);if(e+=i,(t&1)!==0){let[,s]=L(r,e);e+=s}return{flags:t,next:e}}function Go(r){let e=new Uint8Array(r);if(!en(e))throw new Error("not a wasm binary");let t=[],n=[],i=[],s={functionImports:new Map,functionExports:new Map,memoryPointerWidths:[],linkedFrameDescriptors:[],importsKernelFork:!1},o=8;for(;oe.length)throw new Error("wasm section exceeds file size");let f=u,y=!1;if(a===0){let[h,p]=Re(e,f);h===Et&&s.linkedFrameDescriptors.push(e.slice(p,d))}else if(a===1){y=!0;let[h,p]=L(e,f);f+=p;for(let w=0;wr[c]===a))throw new Error("linked-frame descriptor has invalid magic");let e=new DataView(r.buffer,r.byteOffset,r.byteLength),t=e.getUint16(4,!0);if(t!==1)throw new Error(`linked-frame descriptor version ${t} is unsupported`);let n=e.getUint16(6,!0);if(n!==zt)throw new Error(`linked-frame descriptor declares size ${n}, expected ${zt}`);let i=e.getUint8(8),s=Fr.find(({bytes:a})=>a===i);if(!s)throw new Error(`linked-frame descriptor pointer width ${i} is unsupported`);if(e.getUint8(9)!==$r)throw new Error(`linked-frame descriptor alignment ${e.getUint8(9)} is unsupported`);let o=e.getUint16(10,!0);if(o!==On)throw new Error(`linked-frame descriptor flags 0x${o.toString(16)} do not equal required flags 0x${On.toString(16)}`);if(e.getUint32(12,!0)!==s.chunkHeaderSize||e.getUint32(16,!0)!==s.nodeHeaderSize)throw new Error(`linked-frame descriptor header sizes do not match its ${i}-byte pointer width`);return s.bytes}function Gr(r,e){return r==="i32"?127:e===8?126:127}function Kr(r,e,t,n){return r.params.length===e.length&&r.results.length===t.length&&r.params.every((i,s)=>i===Gr(e[s],n))&&r.results.every((i,s)=>i===Gr(t[s],n))}function Wr(r,e,t){let n=i=>i==="ptr"&&t===8?"i64":"i32";return`(${r.map(n).join(", ")}) -> (${e.map(n).join(", ")})`}function Wo(r){let e=[];for(let o of bt){let a=r.functionExports.get(o.name);a&&a.length!==1&&e.push(`duplicate ABI 42 wasm-fork-instrument export ${o.name}`)}let t=bt.filter(({name:o})=>!r.functionExports.has(o)).map(({name:o})=>o);t.length>0&&e.push(`incomplete wasm-fork-instrument exports; missing ${t.join(", ")}`);let n=null;if(r.linkedFrameDescriptors.length===0)e.push(`missing required ${Et} descriptor`);else if(r.linkedFrameDescriptors.length!==1)e.push(`has ${r.linkedFrameDescriptors.length} ${Et} descriptors, expected exactly one`);else try{n=Ko(r.linkedFrameDescriptors[0])}catch(o){e.push(o instanceof Error?o.message:String(o))}let i=et.filter(({module:o,name:a})=>r.functionImports.has(`${o}.${a}`)),s=r.importsKernelFork||i.length>0;if(s){let o=et.filter(({module:a,name:c})=>!r.functionImports.has(`${a}.${c}`)).map(({module:a,name:c})=>`${a}.${c}`);o.length>0&&e.push(`incomplete ABI 42 linked-frame imports; missing ${o.join(", ")}`);for(let a of et){let c=`${a.module}.${a.name}`,l=r.functionImports.get(c);l&&l.length!==1&&e.push(`duplicate ABI 42 linked-frame import ${c}`)}}if(n!==null){if(r.memoryPointerWidths.length!==1)e.push(`ABI 42 fork instrumentation requires exactly one module memory, found ${r.memoryPointerWidths.length}`);else if(r.memoryPointerWidths[0]!==n){let o=n===8?"an":"a";e.push(`ABI 42 linked-frame descriptor declares ${o} ${n}-byte pointer but the module memory uses ${r.memoryPointerWidths[0]}-byte addresses`)}for(let o of bt){let a=r.functionExports.get(o.name);a?.length===1&&!Kr(a[0],o.params,o.results,n)&&e.push(`ABI 42 wasm-fork-instrument export ${o.name} has the wrong signature; expected ${Wr(o.params,o.results,n)}`)}if(s)for(let o of et){let a=`${o.module}.${o.name}`,c=r.functionImports.get(a);c?.length===1&&!Kr(c[0],o.params,o.results,n)&&e.push(`ABI 42 linked-frame import ${a} has the wrong signature; expected ${Wr(o.params,o.results,n)}`)}}return e}function Zo(r){let e=new Uint8Array(r);if(!en(e))return[];let t=[],n=8;for(;nt.startsWith("reloc."))}function Hr(r,e={}){let t=[];if(Vo(r)&&t.push("contains asyncify_"),e.expectedAbi!==void 0&&e.expectedAbi!==null){let d=Yo(r);d!==null&&d!==e.expectedAbi&&t.push(`ABI ${d}, expected ${e.expectedAbi}`)}let n=new Set(Ho(r));if(e.requiredExports){let d=e.requiredExports.filter(f=>!n.has(f));d.length>0&&t.push(`missing required exports: ${d.join(", ")}`)}let i=Uo.filter(d=>n.has(d)),s=Zo(r),o=Zr(r),a=et.filter(({module:d,name:f})=>s.includes(`${d}.${f}`)),c=o.filter(d=>d===Et).length,l=i.length>0||a.length>0||c>0;if(e.forbidForkInstrumentation&&l&&t.push("contains ABI 42 wasm-fork-instrument metadata, imports, or exports"),(e.requireForkInstrumentation??!qo(r))&&(l||s.includes("kernel.kernel_fork")))try{t.push(...Wo(Go(r)))}catch(d){t.push(`cannot validate ABI 42 fork-artifact contract: ${d instanceof Error?d.message:String(d)}`)}return t}function jo(r,e){let t=new Uint8Array(r);if(t.length<8)return null;let n=0,i=null,s=null,o=8;for(;o=c)return null;let p=a;for(let g=0;g=h)return null;let[p,w]=L(t,y);y+=w;for(let m=0;mh)return null}return y}function f(y,h=0){if(h>4)return null;let p=u(y);if(!p)return null;let w=d(p.start,p.end);if(w===null)return null;let m=w,g=p.end;for(;m=32&&S<=38||S===208){let[,z]=L(t,m);m+=z}else if(S>=40&&S<=62)m=At(t,m);else if(S===63||S===64)m++;else if(S===66){let[,z]=No(t,m);m+=z}else if(S===67)m+=4;else if(S===68)m+=8;else if(S===252||S===253||S===254){let z=$o(S,t,m);if(z===null)return null;m=z}}return null}return f(i)}function Yo(r){return jo(r,"__abi_version")}var Xo=ArrayBuffer,j=Uint8Array,tn=Uint16Array,Jo=Int16Array;var nn=Int32Array,Cn=function(r,e,t){if(j.prototype.slice)return j.prototype.slice.call(r,e,t);(e==null||e<0)&&(e=0),(t==null||t>r.length)&&(t=r.length);var n=new j(t-e);return n.set(r.subarray(e,t)),n},Lt=function(r,e,t,n){if(j.prototype.fill)return j.prototype.fill.call(r,e,t,n);for((t==null||t<0)&&(t=0),(n==null||n>r.length)&&(n=r.length);tr.length)&&(n=r.length);t2046MB)","invalid block type","FSE accuracy too high","match distance too far back","unexpected EOF"],Y=function(r,e,t){var n=new Error(e||es[r]);if(n.code=r,Error.captureStackTrace&&Error.captureStackTrace(n,Y),!t)throw n;return n},Vr=function(r,e,t){for(var n=0,i=0;n>>0},ns=function(r,e){var t=r[0]|r[1]<<8|r[2]<<16;if(t==3126568&&r[3]==253){var n=r[4],i=n>>5&1,s=n>>2&1,o=n&3,a=n>>6;n&8&&Y(0);var c=6-i,l=o==3?4:o,u=Vr(r,c,l);c+=l;var d=a?1<>3);y=h+(h>>3)*(r[5]&7)}y>2145386496&&Y(1);var p=new j((e==1?f||y:e?0:y)+12);return p[0]=1,p[4]=4,p[8]=8,{b:c+d,y:0,l:0,d:u,w:e&&e!=1?e:p.subarray(12),e:y,o:new nn(p.buffer,0,3),u:f,c:s,m:Math.min(131072,y)}}else if((t>>4|r[3]<<20)==25481893)return ts(r,4)+8;Y(0)},Ge=function(r){for(var e=0;1<t&&Y(3);for(var s=1<0;){var g=Ge(o+1),S=n>>3,z=(1<>(n&7)&z,v=(1<v&&(E-=A)),f[++a]=--E,E==-1?(o+=E,w[--u]=a):o-=E,!E)do{var x=n>>3;c=(r[x]|r[x+1]<<8)>>(n&7)&3,n+=2,a+=c}while(c==3)}(a>255||o)&&Y(0);for(var I=0,_=(s>>1)+(s>>3)+3,D=s-1,V=0;V<=a;++V){var B=f[V];if(B<1){y[V]=-B;continue}for(l=0;l=u)}}for(I&&Y(0),l=0;l>3,{b:i,s:w,n:m,t:h}]},rs=function(r,e){var t=0,n=-1,i=new j(292),s=r[e],o=i.subarray(0,256),a=i.subarray(256,268),c=new tn(i.buffer,268);if(s<128){var l=xt(r,e+1,6),u=l[0],d=l[1];e+=s;var f=u<<3,y=r[e];y||Y(0);for(var h=0,p=0,w=d.b,m=w,g=(++e<<3)-8+Ge(y);g-=w,!(g>3;if(h+=(r[S]|r[S+1]<<8)>>(g&7)&(1<>3,p+=(r[S]|r[S+1]<<8)>>(g&7)&(1<255&&Y(0)}else{for(n=s-127;t>4,o[t+1]=z&15}++e}var E=0;for(t=0;t11&&Y(0),E+=v&&1<0;--t){var V=c[t];Lt(D,t,V,c[t-1]=V+a[t]*(1<a&&d>3,y=(r[f]|r[f+1]<<8|r[f+2]<<16)>>(u&7);c=(c<>2,o=s<<1,a=s+o;kt(r.subarray(n,n+=r[0]|r[1]<<8),e.subarray(0,s),t),kt(r.subarray(n,n+=r[2]|r[3]<<8),e.subarray(s,o),t),kt(r.subarray(n,n+=r[4]|r[5]<<8),e.subarray(o,a),t),kt(r.subarray(n),e.subarray(a),t)},ds=function(r,e,t){var n,i=e.b,s=r[i],o=s>>1&3;e.l=s&1;var a=s>>3|r[i+1]<<5|r[i+2]<<13,c=(i+=3)+a;if(o==1)return i>=r.length?void 0:(e.b=i+1,t?(Lt(t,r[i],e.y,e.y+=a),t):Lt(new j(a),r[i]));if(!(c>r.length)){if(o==0)return e.b=c,t?(t.set(r.subarray(i,c),e.y),e.y+=a,t):Cn(r,i,c);if(o==2){var l=r[i],u=l&3,d=l>>2&3,f=l>>4,y=0,h=0;u<2?d&1?f|=r[++i]<<4|(d&2&&r[++i]<<12):f=l>>3:(h=d,d<2?(f|=(r[++i]&63)<<4,y=r[i]>>6|r[++i]<<2):d==2?(f|=r[++i]<<4|(r[++i]&3)<<12,y=r[i]>>2|r[++i]<<6):(f|=r[++i]<<4|(r[++i]&63)<<12,y=r[i]>>6|r[++i]<<2|r[++i]<<10)),++i;var p=t?t.subarray(e.y,e.y+e.m):new j(e.m),w=p.length-f;if(u==0)p.set(r.subarray(i,i+=f),w);else if(u==1)Lt(p,r[i++],w);else{var m=e.h;if(u==2){var g=rs(r,i);y+=i-(i=g[0]),e.h=m=g[1]}else m||Y(0);(h?ls:kt)(r.subarray(i,i+=y),p.subarray(w),m)}var S=r[i++];if(S){S==255?S=(r[i++]|r[i++]<<8)+32512:S>127&&(S=S-128<<8|r[i++]);var z=r[i++];z&3&&Y(0);for(var E=[os,ss,is],v=2;v>-1;--v){var A=z>>(v<<1)+2&3;if(A==1){var k=new j([0,0,r[i++]]);E[v]={s:k.subarray(2,3),n:k.subarray(0,1),t:new tn(k.buffer,0,1),b:0}}else A==2?(n=xt(r,i,9-(v&1)),i=n[0],E[v]=n[1]):A==3&&(e.t||Y(0),E[v]=e.t[v])}var x=e.t=E,I=x[0],_=x[1],D=x[2],V=r[c-1];V||Y(0);var B=(c<<3)-8+Ge(V)-D.b,O=B>>3,U=0,ae=(r[O]|r[O+1]<<8)>>(B&7)&(1<>3;var N=(r[O]|r[O+1]<<8)>>(B&7)&(1<<_.b)-1;O=(B-=I.b)>>3;var Z=(r[O]|r[O+1]<<8)>>(B&7)&(1<>3;var Je=1<>>(B&7)&Je-1);O=(B-=Nn[De])>>3;var Pe=cs[De]+((r[O]|r[O+1]<<8|r[O+2]<<16)>>(B&7)&(1<>3;var Ue=as[vt]+((r[O]|r[O+1]<<8|r[O+2]<<16)>>(B&7)&(1<>3,ae=D.t[ae]+((r[O]|r[O+1]<<8)>>(B&7)&(1<>3,Z=I.t[Z]+((r[O]|r[O+1]<<8)>>(B&7)&(1<>3,N=_.t[N]+((r[O]|r[O+1]<<8)>>(B&7)&(1<<_e)-1),pe>3)e.o[2]=e.o[1],e.o[1]=e.o[0],e.o[0]=pe-=3;else{var Qe=pe-(Ue!=0);Qe?(pe=Qe==3?e.o[0]-1:e.o[Qe],Qe>1&&(e.o[2]=e.o[1]),e.o[1]=e.o[0],e.o[0]=pe):pe=e.o[0]}for(var v=0;vPe&&(Te=Pe);for(var v=0;v=i){let k=(y+1)*4096;try{e.grow(k)}catch{throw new b(ee)}if(i=Math.floor(e.byteLength/4096),y>=i)throw new b(ee)}new Uint8Array(e).fill(0);let h=new r(e);h.w32(Un,Fn),h.w32(Gn,Dn),h.w32(sn,4096),h.w32(rt,i),h.w32(Ce,o),h.w32(Ke,u),h.w32(cn,d),h.w32(Jr,f),h.w32(ln,y),h.w32(Es,a),h.w32(zs,c),h.w32(bs,l),h.w32(_t,s),h.w32(Qr,256);let p=d*4096;for(let k=0;k>2)+(k>>5);h.i32[x]|=1<<(k&31)}let w=i-y;Atomics.store(h.i32,it>>2,w),h.blockAllocHint=y;let m=u*4096;h.i32[m>>2]|=3,Atomics.store(h.i32,an>>2,o-2),h.inodeAllocHint=2;let g=h.inodeOffset(1);h.w32(g+C,W|493),h.w32(g+$,2),h.w64(g+ie,1);let S=h.blockAlloc();if(S<0)throw new b(ee);h.w32(g+Q,S);let z=S*4096,E=Ne(R+1),v=Ne(R+2);h.w32(z,1),h.view.setUint16(z+4,E,!0),h.view.setUint16(z+6,1,!0),h.u8[z+R]=46;let A=z+E;return h.w32(A,1),h.view.setUint16(A+4,v,!0),h.view.setUint16(A+6,2,!0),h.u8[A+R]=46,h.u8[A+R+1]=46,h.w64(g+T,E+v),Atomics.store(h.i32,Kn>>2,1),h}static inspectImageCapacity(e){if(e.byteLength<_t+4)throw new b(q,"SharedFS image is too small");let t=new DataView(e.buffer,e.byteOffset,e.byteLength);if(t.getUint32(Un,!0)!==Fn)throw new b(q,"Bad magic");if(t.getUint32(Gn,!0)!==Dn)throw new b(q,"Bad version");let n=t.getUint32(sn,!0);if(n!==4096)throw new b(q,"Bad block size");let i=t.getUint32(_t,!0)*n;return{byteLength:e.byteLength,maxByteLength:Math.max(e.byteLength,i)}}static mount(e,t){let n=new r(e);if(n.r32(Un)!==Fn)throw new b(q,"Bad magic");if(n.r32(Gn)!==Dn)throw new b(q,"Bad version");if(n.r32(sn)!==4096)throw new b(q,"Bad block size");return t?.restoreImage&&n.resetRestoredRuntimeState(),n.resetAllocationHints(),n}snapshotBytes(e){return this.withNamespaceLock(()=>this.snapshotBytesUnlocked(e))}snapshotState(e){return this.withNamespaceLock(()=>({bytes:this.snapshotBytesUnlocked(e),identities:this.collectIdentityStateUnlocked()}))}identityState(){return this.withNamespaceLock(()=>this.collectIdentityStateUnlocked())}snapshotBytesUnlocked(e){let t=e?.normalizeTimestampsMs;if(t!==void 0&&(!Number.isSafeInteger(t)||t<0))throw new b(q,"Snapshot timestamp must be a non-negative safe integer in milliseconds");let n=t===void 0?void 0:BigInt(t);for(let a=0;a>2)!==0)throw new b(Zn,"Cannot save a VFS image with open descriptors")}let i=this.r32(Ce);for(let a=0;a=1&&this.inodeIsAllocated(a)?n:0n;o.setBigUint64(c+Pt,l,!0),o.setBigUint64(c+ce,l,!0),o.setBigUint64(c+X,l,!0)}}return s}collectIdentityStateUnlocked(){let e=new Map,t=this.inodeOffset(1),n=this.r64(t+ie);e.set(`1:${n}`,{ino:1,generation:n,dataSequence:Atomics.load(this.i32,t+oe>>2)>>>0,mode:this.r32(t+C),linkCount:this.r32(t+$),size:this.r64(t+T),uid:this.r32(t+Tt),gid:this.r32(t+Rt),paths:["/"]});let i=[{ino:1,path:"/"}],s=new Set;for(;i.length>0;){let o=i.pop();if(s.has(o.ino))throw new b(F);s.add(o.ino);let a=this.inodeOffset(o.ino);if((this.r32(a+C)&G)!==W)throw new b(F);let c=this.r64(a+T),l=0;for(;l>2)>>>0,mode:I,linkCount:this.r32(v+$),size:this.r64(v+T),uid:this.r32(v+Tt),gid:this.r32(v+Rt),...(I&G)===tt?{symlinkTarget:this.readSymlinkInodeUnlocked(m)}:{},paths:[]},e.set(k,x)}x.paths.push(E),(this.r32(v+C)&G)===W&&i.push({ino:m,path:E})}}p+=g}l+=h}}return e}statfs(){let e=this.r32(sn),t=this.r32(rt),n=this.r32(_t),i=typeof this.buffer.maxByteLength=="number"?this.buffer.maxByteLength:this.buffer.byteLength,s=Math.floor(i/e),o=Math.max(t,Math.min(n,s)),a=Atomics.load(this.i32,it>>2),c=Math.max(0,o-t);return{blockSize:e,totalBlocks:o,freeBlocks:a+c,totalInodes:this.r32(Ce),freeInodes:Atomics.load(this.i32,an>>2),maxName:255}}r32(e){return this.view.getUint32(e,!0)}w32(e,t){this.view.setUint32(e,t,!0)}r64(e){return Number(this.view.getBigUint64(e,!0))}w64(e,t){this.view.setBigUint64(e,BigInt(t),!0)}waitForAtomicChange(e,t){if(this.atomicsWaitAllowed!==!1)try{Atomics.wait(this.i32,e,t),this.atomicsWaitAllowed=!0;return}catch(n){if(!(n instanceof TypeError))throw n;this.atomicsWaitAllowed=!1}for(;Atomics.load(this.i32,e)===t;);}resetAllocationHints(){this.blockAllocHint=this.findNextFreeBlockHint(),this.inodeAllocHint=this.findNextFreeInodeHint()}findNextFreeBlockHint(){let e=this.r32(rt),t=this.r32(ln),n=this.r32(cn)*4096;for(let i=t;i>2)+(i>>5),o=i&31;if((Atomics.load(this.i32,s)&1<>2)+(n>>5),s=n&31;if((Atomics.load(this.i32,i)&1<>2;for(;;){if(Atomics.compareExchange(this.i32,e,0,1)===0)return;this.waitForAtomicChange(e,1)}}sbUnlock(){let e=dn>>2;Atomics.store(this.i32,e,0),Atomics.notify(this.i32,e,1/0)}namespaceLock(){let e=un>>2;for(;;){if(Atomics.compareExchange(this.i32,e,0,1)===0)return;this.waitForAtomicChange(e,1)}}namespaceUnlock(){let e=un>>2;Atomics.store(this.i32,e,0),Atomics.notify(this.i32,e,1/0)}withNamespaceLock(e){this.namespaceLock();try{return e()}finally{this.namespaceUnlock()}}resetRestoredRuntimeState(){Atomics.store(this.i32,dn>>2,0),Atomics.store(this.i32,un>>2,0),this.u8.fill(0,256,4096);let e=this.r32(Ce),t=this.r32(Ke)*4096;for(let n=0;n>5)*4)&1<<(n&31))===0||this.r32(i+$)!==0)continue;let o=this.r32(i+C),a=this.r64(i+T);(o&G)===tt&&a<=40?(this.u8.fill(0,i+Q,i+Q+40),this.w64(i+T,0)):this.inodeTruncate(n,0),this.inodeFree(n)}}blockAlloc(){let e=this.r32(rt),t=this.r32(cn)*4096,n=this.r32(ln),i=this.blockAllocHint>=n&&this.blockAllocHint>2)+(a>>5),l=a&31,u=Atomics.load(this.i32,c);if(u&1<>2,1),this.blockAllocHint=a+1>2)+(e>>5),i=e&31;for(;;){let s=Atomics.load(this.i32,n),o=s&~(1<>2,1),e>=this.r32(ln)&&e>2)>0)return 0;let e=this.r32(rt),t=this.r32(_t),n=this.r32(Qr),i=e+n;if(i>t&&(i=t,n=i-e,n===0))return ee;let s=i*4096;if(this.buffer.byteLength>2,n),Atomics.add(this.i32,Kn>>2,1),this.blockAllocHint=e,0}finally{this.sbUnlock()}}inodeOffset(e){let n=this.r32(Jr)+Math.floor(e/32),i=e%32*128;return n*4096+i}inodeAlloc(){let e=this.r32(Ce),t=this.r32(Ke)*4096,n=this.inodeAllocHint>=2&&this.inodeAllocHint>2)+(o>>5),c=o&31,l=Atomics.load(this.i32,a);if(l&1<>2,1),this.inodeAllocHint=o+1>2,1)+1}inodeFree(e){let n=(this.r32(Ke)*4096>>2)+(e>>5),i=e&31;for(;;){let s=Atomics.load(this.i32,n);if((s&1<>2,1),e>=2&&e0&&this.w32(n+Be,i-1),i<=1&&this.r32(n+$)===0&&(this.inodeTruncate(e,0),t=!0)}finally{this.inodeWriteUnlock(e)}t&&this.inodeFree(e)}inodeDropLinkRefLocked(e){let t=this.inodeOffset(e),n=this.r32(t+$);return n>1?(this.w32(t+$,n-1),this.w64(t+X,Date.now()),!1):this.inodeOrphanLocked(e)}inodeOrphanLocked(e){let t=this.inodeOffset(e);if(this.w32(t+$,0),this.w64(t+X,Date.now()),this.r32(t+Be)>0)return!1;let n=this.r32(t+C),i=this.r64(t+T);return(n&G)===tt&&i<=40?(this.u8.fill(0,t+Q,t+Q+40),this.w64(t+T,0)):this.inodeTruncate(e,0),!0}inodeReadLock(e){let t=this.inodeOffset(e)+ot>>2;for(;;){let n=Atomics.load(this.i32,t);if(n&ii){this.waitForAtomicChange(t,n);continue}if(Atomics.compareExchange(this.i32,t,n,n+1)===n)return}}inodeReadUnlock(e){let t=this.inodeOffset(e)+ot>>2;(Atomics.sub(this.i32,t,1)&As)===1&&Atomics.notify(this.i32,t,1)}inodeWriteLock(e){let t=this.inodeOffset(e)+ot>>2;for(;;){let n=Atomics.load(this.i32,t);if(n!==0){this.waitForAtomicChange(t,n);continue}if(Atomics.compareExchange(this.i32,t,0,ii)===0)return}}inodeWriteUnlock(e){let t=this.inodeOffset(e)+ot>>2;Atomics.store(this.i32,t,0),Atomics.notify(this.i32,t,1/0)}inodeBlockMap(e,t,n){let i=this.inodeOffset(e);if(t<10){let s=this.r32(i+Q+t*4);if(s!==0)return s;if(!n)return 0;let o=this.blockAllocWithGrow();return o<0||this.w32(i+Q+t*4,o),o}if(t-=10,t<1024){let s=this.r32(i+Ot),o=!1;if(s===0){if(!n)return 0;if(s=this.blockAllocWithGrow(),s<0)return s;this.w32(i+Ot,s),o=!0}let a=s*4096+t*4,c=this.r32(a);if(c!==0)return c;if(!n)return 0;let l=this.blockAllocWithGrow();return l<0?(o&&(this.w32(i+Ot,0),this.blockFree(s)),l):(this.w32(a,l),l)}if(t-=1024,t<1024*1024){let s=Math.floor(t/1024),o=t%1024,a=this.r32(i+st),c=!1;if(a===0){if(!n)return 0;if(a=this.blockAllocWithGrow(),a<0)return a;this.w32(i+st,a),c=!0}let l=a*4096+s*4,u=this.r32(l),d=!1;if(u===0){if(!n)return 0;if(u=this.blockAllocWithGrow(),u<0)return c&&(this.w32(i+st,0),this.blockFree(a)),u;this.w32(l,u),d=!0}let f=u*4096+o*4,y=this.r32(f);if(y!==0)return y;if(!n)return 0;let h=this.blockAllocWithGrow();return h<0?(d&&(this.w32(l,0),this.blockFree(u)),c&&(this.w32(i+st,0),this.blockFree(a)),h):(this.w32(f,h),h)}return q}inodeReadData(e,t,n,i){let s=this.inodeOffset(e),o=this.r64(s+T);if(t>=o)return 0;t+i>o&&(i=o-t);let a=0,c=0;for(;i>0;){let l=Math.floor(t/4096),u=t%4096,d=4096-u;d>i&&(d=i);let f=this.inodeBlockMap(e,l,!1);if(f<=0)n.fill(0,c,c+d);else{let y=f*4096+u;n.set(this.u8.subarray(y,y+d),c)}c+=d,t+=d,i-=d,a+=d}return a}inodeWriteData(e,t,n,i){let s=this.inodeOffset(e),o=this.r64(s+T);t>o&&this.zeroOldEofTail(e,o);let a=0,c=0;for(;i>0;){let l=Math.floor(t/4096),u=t%4096,d=4096-u;d>i&&(d=i);let f=this.inodeBlockMap(e,l,!0);if(f<0){if(a===0)return f;break}let y=f*4096+u;this.u8.set(n.subarray(c,c+d),y),c+=d,t+=d,i-=d,a+=d}if(a>0&&t>this.r64(s+T)&&this.w64(s+T,t),a>0){let l=Date.now();this.w64(s+ce,l),this.w64(s+X,l),Atomics.add(this.i32,s+oe>>2,1)}return a}zeroInodeRange(e,t,n){for(;t0){let c=a*4096+s;this.u8.fill(0,c,c+o)}t+=o}}zeroOldEofTail(e,t){let n=t%4096;if(n===0)return;let i=Math.floor(t/4096),s=this.inodeBlockMap(e,i,!1);if(s<=0)return;let o=s*4096+n;this.u8.fill(0,o,s*4096+4096)}freeBlocksFrom(e,t){let n=this.inodeOffset(e);for(let o=t;o<10;o++){let a=this.r32(n+Q+o*4);a&&(this.blockFree(a),this.w32(n+Q+o*4,0))}let i=this.r32(n+Ot);if(i){let o=t>10?t-10:0;for(let a=o;a<1024;a++){let c=i*4096+a*4,l=this.r32(c);l&&(this.blockFree(l),this.w32(c,0))}o===0&&(this.blockFree(i),this.w32(n+Ot,0))}let s=this.r32(n+st);if(s){let o=t>1034?t-10-1024:0,a=Math.floor(o/1024);for(let c=a;c<1024;c++){let l=s*4096+c*4,u=this.r32(l);if(!u)continue;let d=c===a?o%1024:0;for(let f=d;f<1024;f++){let y=u*4096+f*4,h=this.r32(y);h&&(this.blockFree(h),this.w32(y,0))}d===0&&(this.blockFree(u),this.w32(l,0))}a===0&&(this.blockFree(s),this.w32(n+st,0))}}inodeTruncate(e,t,n=!1){let i=this.inodeOffset(e),s=this.r64(i+T),o=t!==s;if(t>=s){if(t>s&&this.zeroOldEofTail(e,s),this.w64(i+T,t),o||n){let c=Date.now();this.w64(i+ce,c),this.w64(i+X,c),Atomics.add(this.i32,i+oe>>2,1)}return}t%4096!==0&&this.zeroInodeRange(e,t,Math.ceil(t/4096)*4096);let a=Math.ceil(t/4096);if(this.freeBlocksFrom(e,a),this.w64(i+T,t),o||n){let c=Date.now();this.w64(i+ce,c),this.w64(i+X,c),Atomics.add(this.i32,i+oe>>2,1)}}validateFileSize(e){if(!Number.isSafeInteger(e)||e<0)throw new b(q);if(e>at)throw new b(Ct)}validateSeekPosition(e){if(!Number.isSafeInteger(e))throw new b(ci);if(e<0)throw new b(q);if(e>at)throw new b(Ct)}touchDirectoryMutation(e){let t=this.inodeOffset(e),n=Date.now();this.w64(t+ce,n),this.w64(t+X,n);let i=Atomics.add(this.i32,t+ei>>2,1)+1>>>0,s=this.dirIndexes.get(e);s&&(s.mutationSequence=i,s.size=this.r64(t+T))}dirNameKey(e){return ct(e)}dirEntryNameMatches(e,t){if(this.view.getUint16(e+6,!0)!==t.length)return!1;for(let i=0;i=R&&n%4===0&&e+n<=t&&i<=n-R}inodeIsAllocated(e){let t=this.r32(Ce);if(e<=0||e>=t)return!1;let n=this.r32(Ke)*4096;return(Atomics.load(this.i32,(n>>2)+(e>>5))&1<<(e&31))!==0}rebuildDirIndex(e,t,n,i){let s=new Map,o=[],a=0;for(;a4096-u&&(y=4096-u);let h=u;for(;h=R&&o.push({abs:p,recLen:m});h+=m}a+=y}let c={generation:t,mutationSequence:n,size:i,entries:s,free:o};return this.dirIndexes.set(e,c),c}getDirIndex(e){let t=this.inodeOffset(e),n=this.r64(t+T),i=this.r64(t+ie),s=Atomics.load(this.i32,t+ei>>2)>>>0,o=this.dirIndexes.get(e);return o&&o.generation===i&&o.mutationSequence===s&&o.size===n?o:(o&&this.dirIndexes.delete(e),n=0;o--){let a=e.free[o];if(!(a.recLen4096-c&&(d=4096-c);let f=c;for(;fn)return-1;a=c,o+=l}return o===n?a:-1}dirAppendEntry(e,t,n,i=-1){let s=this.inodeOffset(e),o=this.r64(s+T),a=Ne(R+t.length),c=o,l=Math.floor(c/4096),u=c%4096,d=0;if(u!==0&&u+a>4096){let h=4096-u,p=0;if(h>=R){if(p=this.inodeBlockMap(e,l,!1),p<=0)return F}else if(i<0&&(i=this.findLastDirEntryInBlock(e,l,u)),i<0)return F;if(d=this.inodeBlockMap(e,l+1,!0),d<0)return d;if(h>=R){let w=p*4096+u;this.w32(w,0),this.view.setUint16(w+4,h,!0),this.view.setUint16(w+6,0,!0)}else{let m=this.view.getUint16(i+4,!0)+h;this.view.setUint16(i+4,m,!0),this.updateDirIndexRecLen(e,i,m)}c=(l+1)*4096,l++,u=0}let f;if(u===0){if(f=d||this.inodeBlockMap(e,l,!0),f<0)return f}else if(f=this.inodeBlockMap(e,l,!1),f<=0)return F;let y=f*4096+u;return this.w32(y,n),this.view.setUint16(y+4,a,!0),this.view.setUint16(y+6,t.length,!0),this.u8.set(t,y+R),this.w64(s+T,c+a),this.touchDirectoryMutation(e),this.updateDirIndexAdd(e,t,n,y,a),0}dirAddEntry(e,t,n){let i=this.getDirIndex(e);if(typeof i=="number")return i;if(i)return this.useDirIndexFreeSlot(i,e,t,n)?0:this.dirAppendEntry(e,t,n);let s=this.inodeOffset(e),o=this.r64(s+T),a=Ne(R+t.length),c=-1,l=0;for(;l4096-d&&(h=4096-d);let p=d;for(;pd+h||S>g-R)return F;if(m===0&&g>=a)return this.w32(w,n),this.view.setUint16(w+6,t.length,!0),this.u8.set(t,w+R),this.touchDirectoryMutation(e),this.updateDirIndexAdd(e,t,n,w,g),0;let z=Ne(R+S),E=g-z;if(m!==0&&E>=a){this.view.setUint16(w+4,z,!0);let v=w+z;return this.w32(v,n),this.view.setUint16(v+4,E,!0),this.view.setUint16(v+6,t.length,!0),this.u8.set(t,v+R),this.touchDirectoryMutation(e),this.updateDirIndexAdd(e,t,n,v,E),0}c=w,p+=g}l+=h}return this.dirAppendEntry(e,t,n,c)}dirRemoveEntry(e,t){let n=this.getDirIndex(e);if(typeof n=="number")return n;if(n){let a=this.dirNameKey(t),c=n.entries.get(a);if(!c)return me;if(this.r32(c.abs)===c.ino&&this.view.getUint16(c.abs+4,!0)===c.recLen&&this.view.getUint16(c.abs+6,!0)===c.nameLen&&this.dirEntryNameMatches(c.abs,t))return this.w32(c.abs,0),n.entries.delete(a),n.free.push({abs:c.abs,recLen:c.recLen}),this.touchDirectoryMutation(e),0;n.entries.delete(a)}let i=this.inodeOffset(e),s=this.r64(i+T),o=0;for(;o4096-c&&(d=4096-c);let f=c;for(;f4096-l&&(f=4096-l);let y=l;for(;y4096-o&&(l=4096-o);let u=o;for(;uo+l||h>y-R)throw new b(F);if(f!==0){if(h===1&&this.u8[d+R]===46){u+=y;continue}if(h===2&&this.u8[d+R]===46&&this.u8[d+R+1]===46){u+=y;continue}return!1}u+=y}i+=l}return!0}dirIsAncestor(e,t){let n=t;for(let i=0;i<8*1024;i++){if(n===e)return!0;if(n===1)return!1;let s=this.dirLookup(n,oi);if(s<0||s===n)throw new b(F);n=s}throw new b(F)}pathResolve(e,t){if(!e.startsWith("/"))return me;let n=1,i=e.split("/").filter(o=>o.length>0),s=0;for(let o=0;o255)return Hn;let c=ue.encode(a),l;this.inodeReadLock(n);try{let f=this.inodeOffset(n);if((this.r32(f+C)&G)!==W)return ze;l=this.dirLookup(n,c)}finally{this.inodeReadUnlock(n)}if(l<0)return l;let u=this.inodeOffset(l);if((this.r32(u+C)&G)===tt&&(!(o===i.length-1)||t)){if(++s>8)return ai;let y=this.r64(u+T),h;if(y<=40)h=ct(this.u8.subarray(u+Q,u+Q+y));else{let p=new Uint8Array(y);this.inodeReadData(l,0,p,y),h=Nt.decode(p)}if(h.startsWith("/")){n=1;let p=h.split("/").filter(m=>m.length>0),w=i.slice(o+1);i.length=0,i.push(...p,...w),o=-1}else{let p=h.split("/").filter(m=>m.length>0),w=i.slice(o+1);i.length=o,i.push(...p,...w),o--}continue}n=l}return n}pathResolveParent(e){if(!e.startsWith("/"))throw new b(q,"Path must be absolute");let t=e.split("/").filter(c=>c.length>0);if(t.length===0)throw new b(q,"Cannot operate on /");let n=t.pop();if(n.length>255)throw new b(Hn);let i="/"+t.join("/"),s=this.pathResolve(i,!0);if(s<0)throw new b(s);let o=this.inodeOffset(s);if((this.r32(o+C)&G)!==W)throw new b(ze);return{parentIno:s,name:n}}fdAlloc(e,t,n){for(let i=0;i>2;if(Atomics.compareExchange(this.i32,o,0,1)===0)return this.w32(s+ti,e),this.w64(s+We,0),this.w32(s+ni,t),this.w32(s+ri,n?1:0),this.inodeAddOpenRef(e)?i:(Atomics.store(this.i32,o,0),me)}return si}fdGet(e){if(e<0||e>=rn)return null;let t=256+e*24;return Atomics.load(this.i32,t>>2)?{base:t,ino:this.r32(t+ti),offset:this.r64(t+We),flags:this.r32(t+ni),isDir:this.r32(t+ri)!==0}:null}fdFree(e){if(e>=0&&e>2,0)}}buildStat(e){let t=this.inodeOffset(e);return{ino:e,generation:this.r64(t+ie),dataSequence:this.r32(t+oe),mode:this.r32(t+C),linkCount:this.r32(t+$),size:this.r64(t+T),mtime:this.r64(t+ce),ctime:this.r64(t+X),atime:this.r64(t+Pt),uid:this.r32(t+Tt),gid:this.r32(t+Rt)}}namespaceEntryIdentity(e){let t=this.inodeOffset(e);return{ino:e,generation:this.r64(t+ie),linkCount:this.r32(t+$),mode:this.r32(t+C)}}open(e,t,n=420){return this.withNamespaceLock(()=>this.openUnlocked(e,t,n))}createLazyStub(e,t){return this.withNamespaceLock(()=>{let n=this.openUnlocked(e,Xr|Bt,t);try{let i=this.fdGet(n);if(!i)throw new b(ne);this.inodeWriteLock(i.ino);try{return this.inodeTruncate(i.ino,0,!0),this.buildStat(i.ino)}finally{this.inodeWriteUnlock(i.ino)}}finally{this.closeUnlocked(n)}})}replaceIfIdentity(e,t,n,i,s){return this.withNamespaceLock(()=>{let o=this.pathResolve(e,!0);if(o<0||o!==t)return!1;let a=this.inodeOffset(o);if(this.r64(a+ie)!==n||this.r32(a+oe)!==i||(this.r32(a+C)&G)!==It)return!1;this.validateFileSize(s.byteLength),this.inodeWriteLock(o);try{if(this.r64(a+ie)!==n||this.r32(a+oe)!==i||this.r64(a+T)!==0)return!1;let c=this.r64(a+ce),l=this.r64(a+X);this.inodeTruncate(o,0,!0);let u=s.byteLength>0?this.inodeWriteData(o,0,s,s.byteLength):0;if(u!==s.byteLength)throw this.inodeTruncate(o,0,!0),Atomics.store(this.i32,a+oe>>2,i),this.w64(a+ce,c),this.w64(a+X,l),new b(u<0?u:ee);return!0}finally{this.inodeWriteUnlock(o)}})}replaceManyIfIdentities(e,t=[]){return e.length===0&&t.length===0?!0:this.withNamespaceLock(()=>{let n=[],i=new Set,s=a=>{let c=this.pathResolve(a.path,!1);if(c<0||c!==a.expectedIno)return!1;let l=this.inodeOffset(c);return this.r64(l+ie)===a.expectedGeneration&&this.r32(l+oe)===a.expectedDataSequence&&this.r32(l+C)===a.expectedMode&&this.r32(l+$)===a.expectedLinkCount&&this.r64(l+T)===a.expectedSize&&this.r32(l+Tt)===a.expectedUid&&this.r32(l+Rt)===a.expectedGid};for(let a of t)if(!s(a))return!1;for(let a of e){this.validateFileSize(a.data.byteLength);let c=-1;for(let l of a.paths){let u=this.pathResolve(l,!0);if(u!==a.expectedIno)continue;let d=this.inodeOffset(u);if(this.r64(d+ie)===a.expectedGeneration&&this.r32(d+oe)===a.expectedDataSequence&&(this.r32(d+C)&G)===It&&this.r64(d+T)===0){c=u;break}}if(c<0)return!1;if(i.has(c))throw new b(q,"duplicate conditional replacement inode");i.add(c),n.push({...a,ino:c})}let o=[...i].sort((a,c)=>a-c);for(let a of o)this.inodeWriteLock(a);try{for(let l of n){let u=this.inodeOffset(l.ino);if(this.r64(u+ie)!==l.expectedGeneration||this.r32(u+oe)!==l.expectedDataSequence||(this.r32(u+C)&G)!==It||this.r64(u+T)!==0)return!1}for(let l of t)if(!s(l))return!1;let a=n.map(l=>{let u=this.inodeOffset(l.ino);return{ino:l.ino,dataSequence:this.r32(u+oe),mtime:this.r64(u+ce),ctime:this.r64(u+X)}}),c=0;try{for(let l of n){c++,this.inodeTruncate(l.ino,0,!0);let u=l.data.byteLength>0?this.inodeWriteData(l.ino,0,l.data,l.data.byteLength):0;if(u!==l.data.byteLength)throw new b(u<0?u:ee)}}catch(l){for(let u=c-1;u>=0;u--){let d=a[u],f=this.inodeOffset(d.ino);this.inodeTruncate(d.ino,0,!0),Atomics.store(this.i32,f+oe>>2,d.dataSequence),this.w64(f+ce,d.mtime),this.w64(f+X,d.ctime)}throw l}return!0}finally{for(let a=o.length-1;a>=0;a--)this.inodeWriteUnlock(o[a])}})}openUnlocked(e,t,n=420){let i=t&on,s=(t&Bt)!==0,o=(t&qn)!==0;if(s&&o){let d=this.pathResolve(e,!1);if(d>=0)throw new b(lt);if(d!==me)throw new b(d)}let a=this.pathResolve(e,!0);if(a<0&&a===me&&s){let{parentIno:d,name:f}=this.pathResolveParent(e);this.inodeWriteLock(d);try{let y=ue.encode(f),h=this.dirLookup(d,y);if(h>=0){if(o)throw new b(lt);a=h}else{let p=this.inodeAlloc();if(p<0)throw new b(ee);let w=this.inodeOffset(p);this.w32(w+C,It|n&4095),this.w32(w+$,1),this.w64(w+T,0);let m=Date.now();this.w64(w+Pt,m),this.w64(w+ce,m),this.w64(w+X,m);let g=this.dirAddEntry(d,y,p);if(g<0)throw this.inodeFree(p),new b(g);a=p}}finally{this.inodeWriteUnlock(d)}}if(a<0)throw new b(a);let c=this.inodeOffset(a),l=this.r32(c+C);if((l&G)===W&&i!==nt)throw new b(Ze);if(t&ms&&(l&G)!==W)throw new b(ze);if(t&Mt){if((l&G)===W)throw new b(Ze);this.inodeWriteLock(a),this.inodeTruncate(a,0,!0),this.inodeWriteUnlock(a)}let u=this.fdAlloc(a,t,!1);if(u<0)throw new b(u);return u}close(e){this.withNamespaceLock(()=>this.closeUnlocked(e))}closeUnlocked(e){let t=this.fdGet(e);if(!t)throw new b(ne);this.fdFree(e),this.inodeDropOpenRef(t.ino)}read(e,t){let n=this.fdGet(e);if(!n)throw new b(ne);let i=this.inodeOffset(n.ino);if((this.r32(i+C)&G)===W)throw new b(Ze);this.inodeReadLock(n.ino);try{let o=this.inodeReadData(n.ino,n.offset,t,t.length),a=256+e*24;return this.w64(a+We,n.offset+o),o}finally{this.inodeReadUnlock(n.ino)}}readAt(e,t,n){let i=this.fdGet(e);if(!i)throw new b(ne);let s=this.inodeOffset(i.ino);if((this.r32(s+C)&G)===W)throw new b(Ze);this.validateSeekPosition(n),this.inodeReadLock(i.ino);try{return this.inodeReadData(i.ino,n,t,t.length)}finally{this.inodeReadUnlock(i.ino)}}write(e,t){let n=this.fdGet(e);if(!n)throw new b(ne);if((n.flags&on)===nt)throw new b(ne);this.inodeWriteLock(n.ino);try{let s=n.offset;if(n.flags&ps){let c=this.inodeOffset(n.ino);s=this.r64(c+T)}if(!Number.isSafeInteger(s)||s<0)throw new b(q);if(s>at||t.length>at-s)throw new b(Ct);let o=this.inodeWriteData(n.ino,s,t,t.length);if(o<0)return o;let a=256+e*24;return this.w64(a+We,s+o),o}finally{this.inodeWriteUnlock(n.ino)}}writeAt(e,t,n){let i=this.fdGet(e);if(!i)throw new b(ne);if((i.flags&on)===nt)throw new b(ne);this.validateSeekPosition(n),this.inodeWriteLock(i.ino);try{if(n>at||t.length>at-n)throw new b(Ct);return this.inodeWriteData(i.ino,n,t,t.length)}finally{this.inodeWriteUnlock(i.ino)}}lseek(e,t,n){let i=this.fdGet(e);if(!i)throw new b(ne);let s;if(n===gs)s=t;else if(n===ws)s=i.offset+t;else if(n===vs){let a=this.inodeOffset(i.ino);s=this.r64(a+T)+t}else throw new b(q);this.validateSeekPosition(s);let o=256+e*24;return this.w64(o+We,s),s}ftruncate(e,t){let n=this.fdGet(e);if(!n)throw new b(ne);if((n.flags&on)===nt)throw new b(ne);this.validateFileSize(t),this.inodeWriteLock(n.ino);try{this.inodeTruncate(n.ino,t,!0)}finally{this.inodeWriteUnlock(n.ino)}}fstat(e){let t=this.fdGet(e);if(!t)throw new b(ne);this.inodeReadLock(t.ino);try{return this.buildStat(t.ino)}finally{this.inodeReadUnlock(t.ino)}}stat(e){return this.withNamespaceLock(()=>this.statUnlocked(e))}statUnlocked(e){let t=this.pathResolve(e,!0);if(t<0)throw new b(t);this.inodeReadLock(t);try{return this.buildStat(t)}finally{this.inodeReadUnlock(t)}}lstat(e){return this.withNamespaceLock(()=>this.lstatUnlocked(e))}lstatUnlocked(e){let t=this.pathResolve(e,!1);if(t<0)throw new b(t);this.inodeReadLock(t);try{return this.buildStat(t)}finally{this.inodeReadUnlock(t)}}unlink(e){return this.withNamespaceLock(()=>this.unlinkUnlocked(e))}unlinkUnlocked(e){let{parentIno:t,name:n}=this.pathResolveParent(e),i=ue.encode(n),s=e.length>1&&e.endsWith("/");this.inodeWriteLock(t);try{let o=this.dirLookup(t,i);if(o<0)throw new b(o);let a=this.inodeOffset(o),c=this.r32(a+C);if(s&&(c&G)!==W)throw new b(ze);if((c&G)===W)throw new b(Ze);let l=this.namespaceEntryIdentity(o),u=this.dirRemoveEntry(t,i);if(u<0)throw new b(u);let d=!1;this.inodeWriteLock(o);try{d=this.inodeDropLinkRefLocked(o)}finally{this.inodeWriteUnlock(o)}return d&&this.inodeFree(o),l}finally{this.inodeWriteUnlock(t)}}rename(e,t){return this.withNamespaceLock(()=>this.renameUnlocked(e,t))}renameUnlocked(e,t){let{parentIno:n,name:i}=this.pathResolveParent(e),{parentIno:s,name:o}=this.pathResolveParent(t);if(Wn(i)||Wn(o))throw new b(q);let a=ue.encode(i),c=ue.encode(o),l=e.length>1&&e.endsWith("/"),u=t.length>1&&t.endsWith("/"),d=Math.min(n,s),f=Math.max(n,s);this.inodeWriteLock(d),d!==f&&this.inodeWriteLock(f);try{let y=this.dirLookup(n,a);if(y<0)throw new b(y);let h=this.inodeOffset(y),w=this.r32(h+C)&G,m=this.namespaceEntryIdentity(y);if((l||u)&&w!==W)throw new b(ze);if(w===W&&this.dirIsAncestor(y,s))throw new b(q);let g=this.dirLookup(s,c),S=!1,z;if(g>=0){if(g===y)return{source:m,replaced:m};z=this.namespaceEntryIdentity(g);let v=this.inodeOffset(g),k=this.r32(v+C)&G;if(w===W&&k!==W)throw new b(ze);if(w!==W&&k===W)throw new b(Ze);let x=!1,I=g===n||g===s;I||this.inodeWriteLock(g);try{if(k===W&&!this.dirIsEmpty(g))throw new b(Vn);let _=this.dirReplaceEntryIno(s,c,y);if(_<0)throw new b(_);x=k===W?this.inodeOrphanLocked(g):this.inodeDropLinkRefLocked(g)}finally{I||this.inodeWriteUnlock(g)}x&&this.inodeFree(g),S=k===W}else{let v=this.dirAddEntry(s,c,y);if(v<0)throw new b(v)}let E=this.dirRemoveEntry(n,a);if(E<0)throw new b(E);if(w===W){if(n!==s){let v=this.inodeOffset(n);this.w32(v+$,this.r32(v+$)-1);let A=this.inodeOffset(s);this.w32(A+$,this.r32(A+$)+1),this.inodeWriteLock(y);try{let k=this.dirReplaceEntryIno(y,oi,s);if(k<0)throw new b(k);this.w64(h+X,Date.now())}finally{this.inodeWriteUnlock(y)}}if(S){let v=this.inodeOffset(s);this.w32(v+$,this.r32(v+$)-1)}}else if(S){let v=this.inodeOffset(s);this.w32(v+$,this.r32(v+$)-1)}return{source:m,replaced:z}}finally{d!==f&&this.inodeWriteUnlock(f),this.inodeWriteUnlock(d)}}mkdir(e,t=493){this.withNamespaceLock(()=>this.mkdirUnlocked(e,t))}mkdirUnlocked(e,t=493){let{parentIno:n,name:i}=this.pathResolveParent(e),s=ue.encode(i);this.inodeWriteLock(n);try{if(this.dirLookup(n,s)>=0)throw new b(lt);let a=this.inodeAlloc();if(a<0)throw new b(ee);let c=this.inodeOffset(a);this.w32(c+C,W|t),this.w32(c+$,2),this.w64(c+T,0);let l=Date.now();this.w64(c+Pt,l),this.w64(c+ce,l),this.w64(c+X,l);let u=this.blockAllocWithGrow();if(u<0)throw this.inodeFree(a),new b(ee);this.w32(c+Q,u);let d=u*4096,f=Ne(R+1),y=Ne(R+2);this.w32(d,a),this.view.setUint16(d+4,f,!0),this.view.setUint16(d+6,1,!0),this.u8[d+R]=46;let h=d+f;this.w32(h,n),this.view.setUint16(h+4,y,!0),this.view.setUint16(h+6,2,!0),this.u8[h+R]=46,this.u8[h+R+1]=46,this.w64(c+T,f+y);let p=this.dirAddEntry(n,s,a);if(p<0)throw this.blockFree(u),this.inodeFree(a),new b(p);let w=this.inodeOffset(n);this.w32(w+$,this.r32(w+$)+1)}finally{this.inodeWriteUnlock(n)}}rmdir(e){this.withNamespaceLock(()=>this.rmdirUnlocked(e))}rmdirUnlocked(e){let{parentIno:t,name:n}=this.pathResolveParent(e);if(Wn(n))throw new b(q);let i=ue.encode(n);this.inodeWriteLock(t);try{let s=this.dirLookup(t,i);if(s<0)throw new b(s);let o=this.inodeOffset(s);if((this.r32(o+C)&G)!==W)throw new b(ze);let c=!1;this.inodeWriteLock(s);try{if(!this.dirIsEmpty(s))throw new b(Vn);let u=this.dirRemoveEntry(t,i);if(u<0)throw new b(u);c=this.inodeOrphanLocked(s)}finally{this.inodeWriteUnlock(s)}c&&this.inodeFree(s);let l=this.inodeOffset(t);this.w32(l+$,this.r32(l+$)-1)}finally{this.inodeWriteUnlock(t)}}symlink(e,t){this.withNamespaceLock(()=>this.symlinkUnlocked(e,t))}symlinkUnlocked(e,t){let{parentIno:n,name:i}=this.pathResolveParent(t),s=ue.encode(i),o=ue.encode(e);this.inodeWriteLock(n);try{if(this.dirLookup(n,s)>=0)throw new b(lt);let c=this.inodeAlloc();if(c<0)throw new b(ee);let l=this.inodeOffset(c);if(this.w32(l+C,tt|511),this.w32(l+$,1),o.length<=40)this.u8.set(o,l+Q),this.w64(l+T,o.length);else{this.w64(l+T,0);let d=this.inodeWriteData(c,0,o,o.length);if(d!==o.length)throw d>0&&this.inodeTruncate(c,0),this.inodeFree(c),new b(d<0?d:ee)}let u=this.dirAddEntry(n,s,c);if(u<0)throw o.length<=40?(this.u8.fill(0,l+Q,l+Q+40),this.w64(l+T,0)):this.inodeTruncate(c,0),this.inodeFree(c),new b(u)}finally{this.inodeWriteUnlock(n)}}chmod(e,t){this.withNamespaceLock(()=>this.chmodUnlocked(e,t))}chmodUnlocked(e,t){let n=this.pathResolve(e,!0);if(n<0)throw new b(n);this.inodeWriteLock(n);try{let i=this.inodeOffset(n),s=this.r32(i+C);this.w32(i+C,s&G|t&4095),this.w64(i+X,Date.now())}finally{this.inodeWriteUnlock(n)}}fchmod(e,t){let n=this.fdGet(e);if(!n)throw new b(ne);this.inodeWriteLock(n.ino);try{let i=this.inodeOffset(n.ino),s=this.r32(i+C);this.w32(i+C,s&G|t&4095),this.w64(i+X,Date.now())}finally{this.inodeWriteUnlock(n.ino)}}chown(e,t,n){this.withNamespaceLock(()=>this.chownUnlocked(e,t,n))}chownUnlocked(e,t,n){let i=this.pathResolve(e,!0);if(i<0)throw new b(i);this.inodeWriteLock(i);try{this.chownInodeUnlocked(i,t,n)}finally{this.inodeWriteUnlock(i)}}fchown(e,t,n){let i=this.fdGet(e);if(!i)throw new b(ne);this.inodeWriteLock(i.ino);try{this.chownInodeUnlocked(i.ino,t,n)}finally{this.inodeWriteUnlock(i.ino)}}lchown(e,t,n){this.withNamespaceLock(()=>this.lchownUnlocked(e,t,n))}lchownUnlocked(e,t,n){let i=this.pathResolve(e,!1);if(i<0)throw new b(i);this.inodeWriteLock(i);try{this.chownInodeUnlocked(i,t,n)}finally{this.inodeWriteUnlock(i)}}chownInodeUnlocked(e,t,n){let i=this.inodeOffset(e);t!==Yr&&this.w32(i+Tt,t),n!==Yr&&this.w32(i+Rt,n);let s=this.r32(i+C);(s&G)===It&&(s&ys)!==0&&this.w32(i+C,s&~(fs|hs)),this.w64(i+X,Date.now())}utimens(e,t,n,i,s){this.withNamespaceLock(()=>this.utimensUnlocked(e,t,n,i,s))}utimensUnlocked(e,t,n,i,s){let o=this.pathResolve(e,!0);if(o<0)throw new b(o);this.inodeWriteLock(o);try{let a=this.inodeOffset(o),c=1073741823,l=1073741822,u=Date.now();if(n!==l){let d=n===c?u:t*1e3+Math.floor(n/1e6);this.w64(a+Pt,d)}if(s!==l){let d=s===c?u:i*1e3+Math.floor(s/1e6);this.w64(a+ce,d)}this.w64(a+X,u)}finally{this.inodeWriteUnlock(o)}}link(e,t){return this.withNamespaceLock(()=>this.linkUnlocked(e,t))}linkUnlocked(e,t){let n=this.pathResolve(e,!1);if(n<0)throw new b(n);let i=this.inodeOffset(n);if((this.r32(i+C)&G)===W)throw new b(Ss);let{parentIno:o,name:a}=this.pathResolveParent(t),c=ue.encode(a);this.inodeWriteLock(o);try{if(this.dirLookup(o,c)>=0)throw new b(lt);let u=this.dirAddEntry(o,c,n);if(u<0)throw new b(u);this.inodeWriteLock(n);try{let d=this.r32(i+$);this.w32(i+$,d+1),this.w64(i+X,Date.now())}finally{this.inodeWriteUnlock(n)}return{...this.namespaceEntryIdentity(n),linkCount:this.r32(i+$)}}finally{this.inodeWriteUnlock(o)}}readlink(e){return this.withNamespaceLock(()=>this.readlinkUnlocked(e))}readlinkUnlocked(e){let t=this.pathResolve(e,!1);if(t<0)throw new b(t);return this.readSymlinkInodeUnlocked(t)}readSymlinkInodeUnlocked(e){let t=this.inodeOffset(e);if((this.r32(t+C)&G)!==tt)throw new b(q);let i=this.r64(t+T);if(i<=40)return ct(this.u8.subarray(t+Q,t+Q+i));this.inodeReadLock(e);try{let s=new Uint8Array(i);return this.inodeReadData(e,0,s,i),Nt.decode(s)}finally{this.inodeReadUnlock(e)}}opendir(e){return this.withNamespaceLock(()=>this.opendirUnlocked(e))}opendirUnlocked(e){let t=this.pathResolve(e,!0);if(t<0)throw new b(t);let n=this.inodeOffset(t);if((this.r32(n+C)&G)!==W)throw new b(ze);let s=this.fdAlloc(t,nt,!0);if(s<0)throw new b(s);return s}readdirEntry(e){return this.withNamespaceLock(()=>this.readdirEntryUnlocked(e))}readdirEntryUnlocked(e){let t=this.fdGet(e);if(!t||!t.isDir)throw new b(ne);let n=this.inodeOffset(t.ino),i=this.r64(n+T);for(;t.offset=this.r32(Ce))throw new b(F);let p=this.r32(Ke)*4096;if((this.r32(p+(u>>5)*4)&1<<(u&31))===0)throw new b(F);let m=ct(this.u8.subarray(l+R,l+R+f)),g=this.buildStat(u);return this.w64(h+We,y),t.offset=y,{name:m,stat:g}}return null}closedir(e){this.close(e)}readdir(e){let t=this.opendir(e),n=[];try{let i;for(;(i=this.readdirEntry(t))!==null;)i.name!=="."&&i.name!==".."&&n.push(i.name)}finally{this.closedir(t)}return n}writeFile(e,t){let n=typeof t=="string"?ue.encode(t):t,i=this.open(e,Xr|Bt|Mt);try{this.write(i,n)}finally{this.close(i)}}readFile(e){let t=this.open(e,nt);try{let n=this.fstat(t),i=new Uint8Array(n.size);return this.read(t,i),i}finally{this.close(t)}}readFileText(e){return Nt.decode(this.readFile(e))}};function li(r,e){let t=new Map,n=new Map;for(let o of r){if(t.has(o.path))throw new Error(`${e} duplicates path ${o.path}`);if(t.set(o.path,o),o.type==="file"){if(!o.inodeGroup)throw new Error(`${e} file ${o.path} has no inode group`);if(n.has(o.inodeGroup))throw new Error(`${e} inode group ${o.inodeGroup} has multiple files`);n.set(o.inodeGroup,o)}}let i=new Set,s=new Map;for(let o of r){if(o.type!=="hardlink"||s.has(o.path))continue;let a=[],c=o,l;for(;c.type==="hardlink";){let d=s.get(c.path);if(d){l=d;break}if(i.has(c.path))throw new Error(`${e} hardlink cycle reaches ${c.path}`);if(i.add(c.path),a.push(c),!c.target)throw new Error(`${e} hardlink ${c.path} has no target`);let f=t.get(c.target);if(!f)throw new Error(`${e} hardlink ${c.path} target ${c.target} is missing`);if(f.type!=="file"&&f.type!=="hardlink"||!c.inodeGroup||f.inodeGroup!==c.inodeGroup||f.size!==c.size||f.mode!==c.mode)throw new Error(`${e} hardlink ${c.path} has an invalid target`);c=f}l??=c.type==="file"?c:void 0;let u=n.get(o.inodeGroup??"");if(!l||l!==u)throw new Error(`${e} hardlink ${o.path} does not resolve to its inode`);for(let d=a.length-1;d>=0;d-=1){let f=a[d];if(n.get(f.inodeGroup??"")!==l)throw new Error(`${e} hardlink ${f.path} does not resolve to its inode`);i.delete(f.path),s.set(f.path,l)}}return{canonicalByGroup:n,canonicalTargetByPath:s}}var le={maxArchiveBytes:268435456,maxExpandedBytes:268435456,maxPayloadBytes:268435456,maxEntries:1e5,maxPathBytes:4096,maxSymlinkTargetBytes:65536,maxStringBytes:8192,maxTransportsPerTree:8,maxActivationCapabilities:32,maxActivationRoots:64,maxActivationCapabilityBytes:255},Se={maxArchiveBytes:512*1024*1024,maxExpandedBytes:512*1024*1024,maxPayloadBytes:512*1024*1024,maxEntries:1e5,maxGroups:512};function di(r,e="Deferred tree collection"){for(let[t,n]of Object.entries(r))if(!Number.isSafeInteger(n)||n<0)throw new Error(`${e} ${t} usage is invalid`);if(r.groups>Se.maxGroups)throw new Error(`${e} exceeds the ${Se.maxGroups}-group cap`);if(r.archiveBytes>Se.maxArchiveBytes)throw new Error(`${e} exceeds the archive-byte cap`);if(r.expandedBytes>Se.maxExpandedBytes)throw new Error(`${e} exceeds the expansion cap`);if(r.payloadBytes>Se.maxPayloadBytes)throw new Error(`${e} exceeds the payload-byte cap`);if(r.entries>Se.maxEntries)throw new Error(`${e} exceeds the entry-count cap`)}var ui=Object.freeze({prefix:"/opt/kandelo/homebrew",cellar:"/opt/kandelo/homebrew/Cellar",repository:"/opt/kandelo/homebrew",stableEntrypoint:"/usr/bin/brew"});var fi=1e5,xs=4096,dt=ui.prefix,pi=[["@@HOMEBREW_PREFIX@@",dt],["@@HOMEBREW_CELLAR@@",`${dt}/Cellar`],["@@HOMEBREW_REPOSITORY@@",dt],["@@HOMEBREW_LIBRARY@@",`${dt}/Library`],["@@HOMEBREW_PERL@@",`${dt}/opt/perl/bin/perl`]],jn="@@HOMEBREW_JAVA@@",Is=/^openjdk(?:@\d+(?:\.\d+)*)?/,ut=new TextEncoder,_s=[...pi.map(([r])=>r),jn].map(r=>({placeholder:r,bytes:ut.encode(r)}));function mi(r){let e;try{e=JSON.parse(new TextDecoder("utf-8",{fatal:!0}).decode(r))}catch(a){throw new Error("INSTALL_RECEIPT.json is not valid UTF-8 JSON: "+Rs(a))}if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("INSTALL_RECEIPT.json must contain an object");let t=e,n=t.changed_files;if(n!=null&&!Array.isArray(n))throw new Error("INSTALL_RECEIPT.json changed_files must be an array or null when present");let i=Array.isArray(n)?n:[];if(i.length>fi)throw new Error(`INSTALL_RECEIPT.json declares ${i.length} changed files, limit ${fi}`);let s=[],o=new Set;for(let[a,c]of i.entries()){if(typeof c!="string")throw new Error(`INSTALL_RECEIPT.json changed_files[${a}] is not a string`);if(Os(c,"Homebrew changed file"),o.has(c))throw new Error(`INSTALL_RECEIPT.json repeats changed file ${c}`);o.add(c),s.push(c)}return{changedFiles:s,runtimeDependencies:t.runtime_dependencies}}function gi(r,e,t){let n=r;for(let[o,a]of pi)n=yi(n,ut.encode(o),ut.encode(a));let i=ut.encode(jn);if(hi(n,i)){let o=Ps(e.runtimeDependencies);if(o===void 0)throw new Error(`Homebrew changed file ${t} uses ${jn} without exactly one OpenJDK runtime dependency`);n=yi(n,i,ut.encode(o))}let s=_s.find(({bytes:o})=>hi(n,o));if(s!==void 0)throw new Error(`Homebrew changed file ${t} retains ${s.placeholder}`);return n}function Ps(r){if(!Array.isArray(r))return;let e=[];for(let n of r){if(typeof n!="object"||n===null||Array.isArray(n))continue;let i=n,s=typeof i.full_name=="string"?i.full_name.split("/").at(-1):typeof i.name=="string"?i.name.split("/").at(-1):void 0,o=s===void 0?null:Is.exec(s);s!==void 0&&o?.[0]===s&&e.push(s)}let t=[...new Set(e)];return t.length===1?`${dt}/opt/${t[0]}/libexec`:void 0}function Os(r,e){if(r.length===0||r.startsWith("/")||r.includes("\\")||r.includes("\0")||Ts(r)||ut.encode(r).byteLength>xs||r.split("/").some(t=>t===""||t==="."||t===".."))throw new Error(`${e} has an unsafe path segment: ${r}`)}function Ts(r){for(let e=0;e57343)){if(t<=56319&&e+1=56320&&r.charCodeAt(e+1)<=57343){e+=1;continue}return!0}}return!1}function hi(r,e){if(e.byteLength===0||e.byteLength>r.byteLength)return!1;e:for(let t=0;t<=r.byteLength-e.byteLength;t+=1){for(let n=0;nAn||r.includes("\0")||r.includes("\\"))throw new Error(`Lazy archive mount prefix must be an absolute POSIX path: ${JSON.stringify(r)}`);let e=r.replace(/\/+$/,"");if(e==="")return"/";if(e.slice(1).split("/").some(n=>n===""||n==="."||n===".."))throw new Error(`Lazy archive mount prefix is not canonical: ${JSON.stringify(r)}`);return e}function Ca(r,e,t,n){let i=Vt(t),s=new Map,o=e.map(a=>{let c=a.fileName,l=`Lazy archive ${JSON.stringify(r)} member ${JSON.stringify(c)}`;if(c.length===0)throw new Error(`${l} has an empty path`);if(c.includes("\0"))throw new Error(`${l} contains a NUL byte`);if(c.includes("\\"))throw new Error(`${l} contains a backslash`);if(c.startsWith("/")||/^[A-Za-z]:\//.test(c))throw new Error(`${l} must be relative, not absolute`);if(a.isDirectory&&a.isSymlink)throw new Error(`${l} has conflicting directory and symlink types`);if(a.isDirectory!==c.endsWith("/"))throw new Error(`${l} has inconsistent directory metadata`);let u=a.isDirectory?c.slice(0,-1):c,d=u.split("/");if(u.length===0||d.some(f=>f===""||f==="."||f===".."))throw new Error(`${l} is not a canonical relative POSIX path`);if(s.has(u))throw new Error(`${l} collides with another member at ${JSON.stringify(u)}`);if(a.isSymlink&&!n?.has(c))throw new Error(`Lazy archive symlink target was not provided: ${c}`);return s.set(u,a),{entry:a,archivePath:u,vfsPath:i==="/"?`/${u}`:`${i}/${u}`}});for(let{archivePath:a}of o){let c=a.split("/");for(let l=1;lmt)throw new Error(`VFS image metadata exceeds ${mt} bytes`);let e;try{e=JSON.parse(new TextDecoder().decode(r))}catch(t){let n=t instanceof Error?t.message:String(t);throw new Error(`Invalid VFS image metadata JSON: ${n}`)}return zr(e)}function Ma(r){if(r===null)return new Uint8Array(0);let e=zr(r),t=new TextEncoder().encode(JSON.stringify(e));if(t.byteLength>mt)throw new Error(`VFS image metadata exceeds ${mt} bytes`);return t}function $a(r){return r.byteLength>=Gt.length&&r[0]===Gt[0]&&r[1]===Gt[1]&&r[2]===Gt[2]&&r[3]===Gt[3]?oc(r):r}function yn(r){let e=$a(r);if(e.byteLengthgn)throw new Error(`VFS image lazy metadata exceeds ${gn} bytes`);if(r.byteLengthwn)throw new Error(`VFS image lazy archive metadata exceeds ${wn} bytes`);if(r.byteLength=0?n:void 0}function Ua(r){return r===408||r===429||r>=500&&r<=599}function Ga(r,e=Date.now()){let t=r?.get("retry-after")?.trim();if(!t)return;let n;if(/^\d+$/.test(t))n=Number(t)*1e3;else{let i=Date.parse(t);if(!Number.isFinite(i))return;n=Math.max(0,i-e)}if(!(!Number.isSafeInteger(n)||n<0))return Math.min(n,ro)}function Ka(r){if(!(typeof r!="object"||r===null||!("cause"in r)))return r.cause}function io(r){if(!(typeof r!="object"||r===null||!("name"in r)))return typeof r.name=="string"?r.name:void 0}function oo(r){if(!(typeof r!="object"||r===null||!("code"in r)))return typeof r.code=="string"?r.code:void 0}function so(r,e){let t=new Set,n=r;for(let i=0;n!==void 0&&i<8;i+=1){if(t.has(n))return!1;if(t.add(n),e(n))return!0;n=Ka(n)}return!1}function ao(r){return so(r,e=>io(e)==="AbortError"||oo(e)==="ABORT_ERR")}function Wa(r){return ao(r)?!1:so(r,e=>{let t=io(e),n=oo(e);return e instanceof TypeError||t==="NetworkError"||t==="TimeoutError"||n!==void 0&&Ra.has(n)})}function Za(r,e){if(r instanceof Sn){if(!Ua(r.status))return null;if(r.retryAfterMs!==void 0)return r.retryAfterMs}else if(!Wa(r))return null;return Math.min(Ta*2**e,ro)}function J(r){if(r?.aborted)throw r.reason}function Ha(r,e){return J(e),r===0?Promise.resolve():new Promise((t,n)=>{let i=setTimeout(()=>a(!1),r),s=()=>a(!0,e.reason),o=!1;function a(c,l){o||(o=!0,clearTimeout(i),e?.removeEventListener("abort",s),c?n(l):t())}e?.addEventListener("abort",s,{once:!0}),e?.aborted&&s()})}async function fr(r,e){try{await r.body?.cancel(e)}catch{}}function Va(r,e){if(r.length===1)return r[0];let t=new Uint8Array(e),n=0;for(let i of r)t.set(i,n),n+=i.byteLength;return t}function qt(r){if(r===void 0)return;if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("Lazy archive integrity must be an object");let e=r;if(Object.keys(e).length!==2||!("sha256"in e)||!("bytes"in e))throw new Error("Lazy archive integrity has unexpected fields");if(typeof e.sha256!="string"||!mr.test(e.sha256))throw new Error("Lazy archive integrity has an invalid SHA-256 digest");if(!Number.isSafeInteger(e.bytes)||Number(e.bytes)<=0||Number(e.bytes)>Ki)throw new Error(`Lazy archive integrity byte count must be between 1 and ${Ki}`);return{sha256:e.sha256,bytes:Number(e.bytes)}}function Me(r,e,t){if(typeof r!="object"||r===null||Array.isArray(r))throw new Error(`${t} must be an object`);let n=r;if(Object.keys(n).length!==e.length||e.some(s=>!Object.prototype.hasOwnProperty.call(n,s)))throw new Error(`${t} has unexpected or missing fields`);return n}function wr(r,e,t,n){if(typeof r!="object"||r===null||Array.isArray(r))throw new Error(`${n} must be an object`);let i=r,s=new Set(e);if(Object.keys(i).some(o=>!s.has(o))||t.some(o=>!Object.prototype.hasOwnProperty.call(i,o)))throw new Error(`${n} has unexpected or missing fields`);return i}function xe(r,e,t,n){if(!Array.isArray(r)||r.lengthn)throw new Error(`${e} must contain ${t} to ${n} items`);return r}function he(r,e,t){if(typeof r!="string"||r.length===0||r.includes("\0")||new TextEncoder().encode(r).byteLength>t)throw new Error(`${e} is invalid or exceeds ${t} bytes`);return r}function re(r,e,t,n){if(!Number.isSafeInteger(r)||Number(r)n)throw new Error(`${e} must be an integer between ${t} and ${n}`);return Number(r)}function En(r,e=1){let t=r,n=typeof t=="object"&&t!==null&&!Array.isArray(t)&&t.source!==void 0,i=typeof t=="object"&&t!==null&&!Array.isArray(t)&&t.modePolicy!==void 0,s=Me(r,["decoder","mediaType","sha256","bytes","expandedBytes","sourceEntryCount","transports",...i?["modePolicy"]:[],...n?["source"]:[]],"Lazy tree content"),o=s.decoder==="zip-v1"?"application/zip":s.decoder==="homebrew-bottle-tar-gzip-v1"?"application/vnd.oci.image.layer.v1.tar+gzip":null;if(o===null||s.mediaType!==o)throw new Error("Lazy tree decoder and media type are inconsistent");let a=qt({sha256:s.sha256,bytes:s.bytes});if(!a)throw new Error("Lazy tree integrity is required");let c=xe(s.transports,"Lazy tree transports",e,le.maxTransportsPerTree).map((y,h)=>he(y,`Lazy tree transport ${h}`,Er));if(new Set(c).size!==c.length)throw new Error("Lazy tree transports contain duplicates");let l=re(s.expandedBytes,"Lazy tree expanded byte count",0,xa),u=re(s.sourceEntryCount,"Lazy tree source entry count",1,gt),d=n?Ya(s.source,s.decoder):void 0,f=i?s.modePolicy:void 0;if(f!==void 0&&(f!=="portable-posix-v1"||s.decoder!=="zip-v1"||n))throw new Error("Lazy tree mode policy is invalid for its decoder");if(d!==void 0&&d.entries.length!==u)throw new Error("Lazy tree source inventory count differs from its content");return{decoder:s.decoder,mediaType:o,sha256:a.sha256,bytes:a.bytes,expandedBytes:l,sourceEntryCount:u,transports:c,...f===void 0?{}:{modePolicy:f},...d===void 0?{}:{source:d}}}function co(r){let e={groups:r.length,archiveBytes:0,expandedBytes:0,payloadBytes:0,entries:0};for(let t of r)t.content===void 0||t.inventory===void 0||(e.archiveBytes+=t.content.bytes,e.expandedBytes+=t.content.expandedBytes,e.payloadBytes+=t.inventory.filter(n=>n.type==="file").reduce((n,i)=>n+i.size,0),e.entries+=t.inventory.length+(t.content.source?.entries.length??0));return e}function vr(r){di(r,"Serialized lazy tree collection")}function qa(r){vr(co(r))}function ja(r){let e=new Map;for(let t of r){let n=t.activation?.atomicGroup;if(n===void 0)continue;if(!pt(n))throw new Error(`Serialized lazy atomic activation group ${n.id} is unsealed`);let i=e.get(n.id);if(i===void 0)i={expectedCount:n.expectedCount,cohortSha256:n.cohortSha256,members:new Set,descriptors:new Set},e.set(n.id,i);else if(i.expectedCount!==n.expectedCount||i.cohortSha256!==n.cohortSha256)throw new Error(`Serialized lazy atomic activation group ${n.id} has inconsistent seals`);if(i.members.has(n.member)||i.descriptors.has(n.descriptorSha256))throw new Error(`Serialized lazy atomic activation group ${n.id} duplicates a member`);i.members.add(n.member),i.descriptors.add(n.descriptorSha256)}for(let[t,n]of e)if(n.members.size!==n.expectedCount)throw new Error(`Serialized lazy atomic activation group ${t} has ${n.members.size} of ${n.expectedCount} members`)}function ji(r){for(let[e,t]of r.entries())if(t.kind===Ht||t.kind===gr||t.kind===Ve)ho(t,t.kind);else if(t.kind===Zt)Sr(t,!1);else throw new Error(`Serialized lazy archive group ${e} has an unsupported kind`);qa(r),ja(r)}function Ya(r,e){if(e!=="homebrew-bottle-tar-gzip-v1")throw new Error("Lazy tree source inventory is valid only for original bottles");let t=Me(r,["schema","kind","entries"],"Lazy tree source inventory");if(t.schema!==1||t.kind!=="homebrew-bottle-tar-gzip-v1")throw new Error("Lazy tree source inventory has an unsupported identity");let n=new Map,i=xe(t.entries,"Lazy tree source entries",1,gt).map((o,a)=>{let c=o,l=typeof c=="object"&&c!==null&&!Array.isArray(c)?c.type:void 0,u=l==="directory"||l==="file"?["sourcePath","type","mode","size"]:l==="symlink"||l==="hardlink"?["sourcePath","type","mode","size","target"]:null;if(u===null)throw new Error(`Lazy tree source entry ${a} has invalid type`);let d=Me(o,u,`Lazy tree source entry ${a}`),f=ye(d.sourcePath,!1,`Lazy tree source entry ${a} path`);if(n.has(f))throw new Error(`Lazy tree source inventory duplicates ${f}`);let y=re(d.mode,`Lazy tree source entry ${f} mode`,0,4095),h=re(d.size,`Lazy tree source entry ${f} size`,0,vn),p;if((l==="directory"||l==="symlink"||l==="hardlink")&&h!==0)throw new Error(`Lazy tree source ${f} has payload for ${String(l)}`);l==="symlink"?p=he(d.target,`Lazy tree source symlink ${f} target`,no):l==="hardlink"&&(p=ye(d.target,!1,`Lazy tree source hardlink ${f} target`));let w={sourcePath:f,type:l,mode:y,size:h,...p===void 0?{}:{target:p}};return n.set(f,w),w}),s=i.map(o=>o.sourcePath);if(s.some((o,a)=>a>0&&s[a-1]>=o))throw new Error("Lazy tree source inventory is not in canonical path order");return{schema:1,kind:"homebrew-bottle-tar-gzip-v1",entries:i}}function lo(r){let e=new Map(r.map(n=>[n.sourcePath,n])),t=new Map;for(let n of r){if(n.type!=="hardlink"||t.has(n.sourcePath))continue;let i=[],s=new Set,o=n,a;for(;o.type==="hardlink"&&(a=t.get(o.sourcePath),a===void 0);){if(s.has(o.sourcePath))throw new Error(`Lazy tree source hardlink cycle includes ${o.sourcePath}`);s.add(o.sourcePath),i.push(o);let c=e.get(o.target);if(c===void 0)throw new Error(`Lazy tree source hardlink ${o.sourcePath} target is absent`);if(c.type!=="file"&&c.type!=="hardlink")throw new Error(`Lazy tree source hardlink ${o.sourcePath} target is not regular`);o=c}a===void 0&&(a=o);for(let c of i)t.set(c.sourcePath,a)}return t}function ye(r,e,t,n=!1){if(typeof r!="string"||r.length===0||new TextEncoder().encode(r).byteLength>An||r.includes("\0")||r.includes("\\")||r.startsWith("/")!==e)throw new Error(`${t} is not a canonical ${e?"absolute":"relative"} path`);if(n&&e&&r==="/")return r;if(r.slice(e?1:0).split("/").some(s=>s===""||s==="."||s===".."))throw new Error(`${t} has an unsafe path segment`);return r}function uo(r){let e=typeof r=="object"&&r!==null&&!Array.isArray(r)?r:null,t=e!==null&&(Object.hasOwn(e,"descriptorSha256")||Object.hasOwn(e,"expectedCount")||Object.hasOwn(e,"cohortSha256")),n=Me(r,t?["id","member","descriptorSha256","expectedCount","cohortSha256"]:["id","member"],"Lazy tree atomic activation membership"),i=he(n.id,"Lazy tree atomic activation group",Wi),s=he(n.member,"Lazy tree atomic activation member",Wi);if(!/^[a-z0-9][a-z0-9:._-]*$/.test(i)||!/^[a-z0-9][a-z0-9:+._/-]*$/.test(s)||s.includes("//")||s.endsWith("/"))throw new Error("Lazy tree atomic activation membership is invalid");if(!t)return{id:i,member:s};let o=he(n.descriptorSha256,"Lazy tree atomic member descriptor digest",64),a=he(n.cohortSha256,"Lazy tree atomic cohort digest",64);if(!mr.test(o)||!mr.test(a))throw new Error("Lazy tree atomic activation digest is invalid");return{id:i,member:s,descriptorSha256:o,expectedCount:re(n.expectedCount,"Lazy tree atomic activation expected member count",1,to),cohortSha256:a}}function pt(r){return r.descriptorSha256!==void 0&&r.expectedCount!==void 0&&r.cohortSha256!==void 0}function Xa(r){let e=Me(r,["uid","gid"],"Lazy tree registration owner");return{uid:re(e.uid,"Lazy tree registration owner uid",0,Zi),gid:re(e.gid,"Lazy tree registration owner gid",0,Zi)}}function fo(r,e,t,n,i=1){let s=En(r,i),o=Vt(t),a=["mode","capabilities","roots",...typeof n=="object"&&n!==null&&!Array.isArray(n)&&Object.hasOwn(n,"atomicGroup")?["atomicGroup"]:[]],c=Me(n,a,"Lazy tree activation");if(c.mode!=="boot-prefetch"&&c.mode!=="first-use")throw new Error("Lazy tree activation mode is invalid");let l=xe(c.capabilities,"Lazy tree activation capabilities",1,Pa).map((v,A)=>{let k=he(v,`Lazy tree activation capability ${A}`,le.maxActivationCapabilityBytes);if(!/^[a-z0-9][a-z0-9:._-]*$/.test(k))throw new Error(`Lazy tree activation capability ${A} is invalid`);return k}),u=xe(c.roots,"Lazy tree activation roots",1,Oa).map((v,A)=>ye(v,!0,`Lazy tree activation root ${A}`,!0));if(new Set(l).size!==l.length||new Set(u).size!==u.length)throw new Error("Lazy tree activation contains duplicates");let d=c.atomicGroup===void 0?void 0:uo(c.atomicGroup);if(d!==void 0&&c.mode!=="first-use")throw new Error("Lazy tree atomic activation group requires a valid first-use identity");let f={mode:c.mode,capabilities:l,roots:u,...d===void 0?{}:{atomicGroup:d}},y=xe(e,"Lazy tree inventory",1,gt),h=[],p=new Map,w=new Map,m=s.source===void 0?void 0:new Map(s.source.entries.map(v=>[v.sourcePath,v])),g=s.source===void 0?void 0:lo(s.source.entries),S=0;for(let[v,A]of y.entries()){if(typeof A!="object"||A===null||Array.isArray(A))throw new Error(`Lazy tree entry ${v} must be an object`);let k=A.type,x=k==="directory"?["vfsPath","sourcePath","type","mode","size"]:k==="file"?["vfsPath","sourcePath","type","mode","size","inodeGroup"]:k==="symlink"?["vfsPath","sourcePath","type","mode","size","target"]:k==="hardlink"?["vfsPath","sourcePath","type","mode","size","target","inodeGroup"]:null;if(!x)throw new Error(`Lazy tree entry ${v} has an invalid type`);let I=Me(A,[...x,...m===void 0?[]:["materialization"]],`Lazy tree entry ${v}`),_=ye(I.vfsPath,!0,`Lazy tree entry ${v} VFS path`),D=ye(I.sourcePath,!1,`Lazy tree entry ${v} source path`),V=m===void 0?void 0:I.materialization;if(m!==void 0&&V!=="archive"&&V!=="archive-homebrew-relocate"&&V!=="archive-copy"&&V!=="archive-copy-mode"&&V!=="descriptor")throw new Error(`Lazy tree entry ${_} has invalid materialization provenance`);if(o!=="/"&&_!==o&&!_.startsWith(`${o}/`))throw new Error(`Lazy tree entry ${_} escapes its mount prefix`);if(p.has(_))throw new Error(`Lazy tree duplicates VFS path ${_}`);let B=re(I.mode,`Lazy tree entry ${_} mode`,0,4095),O=re(I.size,`Lazy tree entry ${_} size`,0,vn),U,ae;if(k==="directory"){if(O!==0)throw new Error(`Lazy tree directory ${_} has nonzero size`)}else if(k==="symlink"){if(U=he(I.target,`Lazy tree symlink ${_} target`,no),new TextEncoder().encode(U).byteLength!==O)throw new Error(`Lazy tree symlink ${_} size differs from its target`)}else ae=he(I.inodeGroup,`Lazy tree entry ${_} inode group`,An),k==="hardlink"&&(U=ye(I.target,!0,`Lazy tree hardlink ${_} target`));if(k!=="hardlink"&&(S+=O,S>vn))throw new Error("Lazy tree inventory exceeds the expansion limit");let N={vfsPath:_,sourcePath:D,...V===void 0?{}:{materialization:V},type:k,mode:B,size:O,...U===void 0?{}:{target:U},...ae===void 0?{}:{inodeGroup:ae}};if(m===void 0){let Z=w.get(D);if(Z){if(s.decoder!=="zip-v1"||N.type!=="hardlink"||Z.inodeGroup!==N.inodeGroup)throw new Error(`Lazy tree duplicates source path ${D}`)}else{if(s.decoder==="zip-v1"&&N.type==="hardlink")throw new Error(`Lazy ZIP hardlink ${_} does not reuse a canonical source path`);w.set(D,N)}}else if(N.materialization==="descriptor"){if(N.type!=="directory"&&N.type!=="symlink")throw new Error(`Lazy tree descriptor entry ${_} is not structural`);if(m.has(D))throw new Error(`Lazy tree descriptor entry ${_} impersonates a source member`)}else{let Z=m.get(D);if(Z===void 0)throw new Error(`Lazy tree entry ${_} names absent source ${D}`);if(N.materialization==="archive-copy"||N.materialization==="archive-copy-mode"){if(N.type!=="file"||Z.type!=="file"||N.materialization==="archive-copy"&&N.mode!==Z.mode)throw new Error(`Lazy tree archive copy ${_} differs from its source`)}else if(N.materialization==="archive-homebrew-relocate"){if(N.type!=="file"&&N.type!=="hardlink"||Z.type!==N.type||N.type==="file"&&Z.mode!==N.mode)throw new Error(`Lazy tree receipt-relocated entry ${_} differs from its source`)}else if(Z.type!==N.type||N.type==="symlink"&&Z.target!==N.target||N.type!=="hardlink"&&Z.mode!==N.mode)throw new Error(`Lazy tree archive entry ${_} differs from its source`)}h.push(N),p.set(_,N)}for(let v of h){let A=v.vfsPath.split("/").filter(Boolean);for(let k=1;k({path:v.vfsPath,type:v.type,mode:v.mode,size:v.size,target:v.target,inodeGroup:v.inodeGroup})),"Lazy tree");if(m!==void 0){let v=new Set;for(let A of h){if(A.materialization!=="archive-homebrew-relocate")continue;let k=m.get(A.sourcePath),x=k.type==="file"?k:g.get(k.sourcePath);if(x?.type!=="file")throw new Error(`Lazy tree receipt-relocated entry ${A.vfsPath} is not regular`);v.add(x.sourcePath)}for(let A of h){if(A.materialization==="descriptor"||A.type!=="file"&&A.type!=="hardlink")continue;let k=m.get(A.sourcePath),x=k.type==="file"?k:g.get(k.sourcePath);if(x?.type!=="file"||!v.has(x.sourcePath)&&A.size!==x.size)throw new Error(`Lazy tree archive entry ${A.vfsPath} differs from its source`)}for(let A of h){if(A.type!=="hardlink"||A.materialization!=="archive"&&A.materialization!=="archive-homebrew-relocate")continue;let k=m.get(A.sourcePath),x=p.get(A.target),I=g.get(k.sourcePath);if(k.target!==x?.sourcePath||I?.type!=="file"||I.mode!==A.mode||x?.mode!==A.mode)throw new Error(`Lazy tree hardlink ${A.vfsPath} differs from its source`)}}if(s.sourceEntryCount!==(m===void 0?w.size:m.size))throw new Error("Lazy tree source entry count differs from its inventory");if(s.source===void 0&&s.expandedBytesA.vfsPath===v||A.vfsPath.startsWith(`${v}/`)))throw new Error(`Lazy tree activation root ${v} is not owned by its inventory`);let E=new Map;for(let v of h)v.type==="file"&&E.set(v.inodeGroup,v);if(E.size!==z.canonicalByGroup.size)throw new Error("Lazy tree regular inode inventory is inconsistent");return{content:s,entries:h,mountPrefix:o,activation:f,canonicalByGroup:E}}function zn(r){return JSON.stringify([r.sourcePath,r.type,r.inodeGroup,r.target])}function Sr(r,e){let t=wr(r,["kind","content","url","mountPrefix","integrity","materialized","entries"],["url","mountPrefix","materialized","entries"],"Serialized legacy lazy archive");if(t.kind===void 0){if(!e)throw new Error("Serialized lazy archive is missing its kind discriminator")}else if(t.kind!==Zt)throw new Error("Serialized legacy lazy archive has an unsupported kind");let n=he(t.url,"Serialized legacy lazy archive URL",Er),i=Vt(t.mountPrefix),s=qt(t.integrity);if(t.content!==void 0){if(!e||t.kind!==void 0)throw new Error("Typed legacy lazy archives cannot carry generic content");let c=En(t.content);if(c.decoder!=="zip-v1"||c.transports.length!==1||c.transports[0]!==n||!s||c.sha256!==s.sha256||c.bytes!==s.bytes)throw new Error("Untagged legacy ZIP content identity is inconsistent")}if(t.materialized!==!1)throw new Error("Serialized legacy lazy archive must describe pending content");let o=new Set,a=xe(t.entries,"Serialized legacy lazy archive entries",1,gt).map((c,l)=>{let u=wr(c,["vfsPath","ino","generation","dataSequence","size","isSymlink","deleted","materialized","archivePath","sourcePath","type","inodeGroup","target"],["vfsPath","ino","size","isSymlink","deleted"],`Serialized legacy lazy archive entry ${l}`),d=ye(u.vfsPath,!0,`Serialized legacy lazy archive entry ${l} VFS path`);if(o.has(d))throw new Error(`Serialized legacy lazy archive duplicates path ${d}`);o.add(d);let f=re(u.ino,`Serialized legacy lazy archive entry ${d} inode`,1,Number.MAX_SAFE_INTEGER),y=u.generation===void 0?void 0:re(u.generation,`Serialized legacy lazy archive entry ${d} generation`,0,Number.MAX_SAFE_INTEGER),h=u.dataSequence===void 0?void 0:re(u.dataSequence,`Serialized legacy lazy archive entry ${d} data sequence`,0,Number.MAX_SAFE_INTEGER),p=re(u.size,`Serialized legacy lazy archive entry ${d} size`,0,vn);if(u.isSymlink!==!1||u.deleted!==!1||u.materialized!==void 0&&u.materialized!==!1)throw new Error(`Serialized legacy lazy archive entry ${d} is not pending`);if(u.type!==void 0&&u.type!=="file")throw new Error(`Serialized legacy lazy archive entry ${d} has an invalid type`);let w=u.archivePath===void 0?void 0:ye(u.archivePath,!1,`Serialized legacy lazy archive entry ${d} archive path`),m=u.sourcePath===void 0?void 0:ye(u.sourcePath,!1,`Serialized legacy lazy archive entry ${d} source path`),g=u.inodeGroup===void 0?void 0:he(u.inodeGroup,`Serialized legacy lazy archive entry ${d} inode group`,An);if(u.target!==void 0)throw new Error(`Serialized legacy lazy archive entry ${d} has a link target`);return{vfsPath:d,ino:f,...y===void 0?{}:{generation:y},...h===void 0?{}:{dataSequence:h},size:p,isSymlink:!1,deleted:!1,materialized:!1,...w===void 0?{}:{archivePath:w},...m===void 0?{}:{sourcePath:m},type:"file",...g===void 0?{}:{inodeGroup:g}}});return{kind:Zt,url:n,mountPrefix:i,...s===void 0?{}:{integrity:s},materialized:!1,entries:a}}function ho(r,e){let t=Me(r,["kind","content","inventory","activation","url","mountPrefix","integrity","materialized","entries"],"Serialized lazy tree");if(t.kind!==e)throw new Error("Serialized lazy tree has an unsupported kind");let n=fo(t.content,t.inventory,t.mountPrefix,t.activation);if(e!==Ve&&e===Ht!=(n.content.source===void 0))throw new Error(e===Ht?"Serialized deferred-tree-v1 cannot contain original-bottle source metadata":"Serialized deferred-tree-v2 requires original-bottle source metadata");let i=n.activation.atomicGroup;if(e===Ve?i===void 0||!pt(i):i!==void 0)throw new Error(e===Ve?"Serialized deferred-tree-v3 requires a sealed atomic activation":"Atomic activation requires serialized deferred-tree-v3");let s=he(t.url,"Serialized lazy tree URL",Er);if(s!==n.content.transports[0])throw new Error("Serialized lazy tree URL differs from its primary transport");let o=qt(t.integrity);if(!o||o.sha256!==n.content.sha256||o.bytes!==n.content.bytes)throw new Error("Serialized lazy tree integrity differs from its content");if(t.materialized!==!1)throw new Error("Serialized lazy tree must describe pending content");let a=new Map(n.entries.map(f=>[f.vfsPath,f])),c=new Map(n.entries.map(f=>[zn(f),f])),l=xe(t.entries,"Serialized lazy tree entries",0,gt),u=new Set,d=l.map((f,y)=>{let h=wr(f,["vfsPath","ino","generation","dataSequence","size","isSymlink","deleted","materialized","archivePath","sourcePath","type","inodeGroup","target"],["vfsPath","ino","generation","dataSequence","size","isSymlink","deleted","materialized","archivePath","sourcePath","type","inodeGroup"],`Serialized lazy tree entry ${y}`),p=ye(h.vfsPath,!0,`Serialized lazy tree entry ${y} VFS path`);if(u.has(p))throw new Error(`Serialized lazy tree duplicates pending path ${p}`);u.add(p);let w=ye(h.sourcePath,!1,`Serialized lazy tree entry ${y} source path`),m=ye(h.archivePath,!1,`Serialized lazy tree entry ${y} archive path`),g=a.get(p),S=c.get(zn({sourcePath:w,type:typeof h.type=="string"?h.type:void 0,inodeGroup:typeof h.inodeGroup=="string"?h.inodeGroup:void 0,target:typeof h.target=="string"?h.target:void 0}))??g;if(!S||S.type!=="file"&&S.type!=="hardlink"||g?.inodeGroup!==void 0&&g.inodeGroup!==S.inodeGroup)throw new Error(`Serialized lazy tree entry ${p} is absent from its inventory`);let z=n.canonicalByGroup.get(S.inodeGroup);if(h.type!==S.type||h.inodeGroup!==S.inodeGroup||h.size!==S.size||m!==z?.sourcePath||h.target!==S.target||h.isSymlink!==!1||h.deleted!==!1||h.materialized!==!1)throw new Error(`Serialized lazy tree entry ${p} disagrees with its inventory`);let E=re(h.ino,`Serialized lazy tree entry ${p} inode`,1,Number.MAX_SAFE_INTEGER),v=re(h.generation,`Serialized lazy tree entry ${p} generation`,0,Number.MAX_SAFE_INTEGER),A=re(h.dataSequence,`Serialized lazy tree entry ${p} data sequence`,0,Number.MAX_SAFE_INTEGER);return{vfsPath:p,ino:E,generation:v,dataSequence:A,size:S.size,isSymlink:!1,deleted:!1,materialized:!1,archivePath:m,sourcePath:w,type:S.type,inodeGroup:S.inodeGroup,...S.target===void 0?{}:{target:S.target}}});for(let f of n.entries)if(n.activation.atomicGroup!==void 0&&(f.type==="file"||f.type==="hardlink")&&!u.has(f.vfsPath))throw new Error(`Serialized lazy tree omits pending path ${f.vfsPath}`);return{kind:e,content:n.content,inventory:n.entries,activation:n.activation,url:s,mountPrefix:n.mountPrefix,integrity:o,materialized:!1,entries:d}}async function Wt(r,e){let t=globalThis.crypto?.subtle;if(!t)throw new Error(`${e} SHA-256 verification is unavailable`);let n=new Uint8Array(r.byteLength);n.set(r);let i=new Uint8Array(await t.digest("SHA-256",n));return Array.from(i,s=>s.toString(16).padStart(2,"0")).join("")}async function hr(r,e,t){if(t===void 0)return;if(r.byteLength!==t.bytes)throw new Error(`Lazy ${e} byte count ${r.byteLength} does not match expected ${t.bytes}`);let n=await Wt(r,`Lazy ${e}`);if(n!==t.sha256)throw new Error(`Lazy ${e} SHA-256 ${n} does not match expected ${t.sha256}`)}function Ja(r,e,t,n){let i=n.atomicGroup;if(i===void 0)throw new Error("Lazy atomic member is missing its typed tree descriptor");let s={schema:1,content:{decoder:r.decoder,mediaType:r.mediaType,sha256:r.sha256,bytes:r.bytes,expandedBytes:r.expandedBytes,sourceEntryCount:r.sourceEntryCount,...r.modePolicy===void 0?{}:{modePolicy:r.modePolicy},...r.source===void 0?{}:{source:r.source}},mountPrefix:t,inventory:[...e].sort((o,a)=>o.vfsPatha.vfsPath?1:0),activation:{mode:n.mode,capabilities:n.capabilities,roots:n.roots,atomicGroup:{id:i.id,member:i.member}}};return new TextEncoder().encode(JSON.stringify(s))}function Yi(r,e){let t=e.map(({member:n,descriptorSha256:i})=>({member:n,descriptorSha256:i}));return new TextEncoder().encode(JSON.stringify({schema:1,id:r,members:t.sort((n,i)=>n.memberi.member?1:0)}))}function Qa(r,e){if(r.byteLength!==e.byteLength)return!1;for(let t=0;tObject.freeze({...i}))};return n!==void 0&&(Object.freeze(n.entries),Object.freeze(n)),Object.freeze({decoder:r.decoder,mediaType:r.mediaType,sha256:r.sha256,bytes:r.bytes,expandedBytes:r.expandedBytes,sourceEntryCount:r.sourceEntryCount,transports:t,...r.modePolicy===void 0?{}:{modePolicy:r.modePolicy},...n===void 0?{}:{source:n}})}function Xi(r){return{decoder:r.decoder,mediaType:r.mediaType,sha256:r.sha256,bytes:r.bytes,expandedBytes:r.expandedBytes,sourceEntryCount:r.sourceEntryCount,transports:[...r.transports],...r.modePolicy===void 0?{}:{modePolicy:r.modePolicy},...r.source===void 0?{}:{source:{schema:1,kind:"homebrew-bottle-tar-gzip-v1",entries:r.source.entries.map(e=>({...e}))}}}}function ec(r){let e=r.map(t=>Object.freeze({...t}));return Object.freeze(e),e}function tc(r,e,t){let n=[...r.capabilities],i=[...r.roots];return Object.freeze(n),Object.freeze(i),Object.freeze({mode:r.mode,capabilities:n,roots:i,atomicGroup:Object.freeze({id:e,member:t})})}function nc(r){return{mode:r.activation.mode,capabilities:[...r.activation.capabilities],roots:[...r.activation.roots],atomicGroup:{id:r.id,member:r.member,descriptorSha256:r.descriptorSha256,expectedCount:r.expectedCount,cohortSha256:r.cohortSha256}}}function rc(r,e){return r.ino===e.ino&&r.generation===e.generation&&r.dataSequence===e.dataSequence&&r.size===e.size&&r.isSymlink===e.isSymlink&&r.deleted===e.deleted&&r.materialized===e.materialized&&r.archivePath===e.archivePath&&r.sourcePath===e.sourcePath&&r.type===e.type&&r.inodeGroup===e.inodeGroup&&r.target===e.target}function pn(r,e,t){let n=r.content,i=r.inventory,s=r.activation,o=r.integrity,a=r.entries,c=r.url,l=r.mountPrefix,u=r.materialized,d=s?.atomicGroup;if(n===void 0||i===void 0||s===void 0||d===void 0||s.mode!=="first-use"||d.id!==e||d.member!==t||u)throw new Error(`Lazy atomic activation member ${t} changed before snapshot`);if(o?.sha256!==n.sha256||o?.bytes!==n.bytes||c!==(n.transports[0]??""))throw new Error(`Lazy atomic activation member ${t} has inconsistent integrity`);let f=yo(n),y=ec(i),h=tc(s,e,t),p=new Map;for(let z of y)z.type==="file"&&p.set(z.inodeGroup,z.sourcePath);let w=y.filter(z=>z.type!=="directory");if(a.size!==w.length)throw new Error(`Lazy atomic activation member ${t} has inconsistent runtime entries`);let m=w.map(z=>{let E=a.get(z.vfsPath),v=z.type==="symlink",A=v?z.sourcePath:p.get(z.inodeGroup),k=E!==void 0&&(E.sourcePath===z.sourcePath&&E.type===z.type&&E.target===z.target||z.type==="hardlink"&&E.sourcePath===A&&E.type==="file"&&E.target===void 0),x=E===void 0?["missing"]:[A===void 0?"archivePath source":void 0,E.generation===void 0?"generation":void 0,E.dataSequence===void 0?"dataSequence":void 0,E.size!==z.size?"size":void 0,E.isSymlink!==v?"symlink kind":void 0,E.deleted?"deletion state":void 0,E.materialized!==v?"materialization state":void 0,E.archivePath!==A?"archivePath":void 0,k?void 0:"descriptor mapping",E.inodeGroup!==z.inodeGroup?"inode group":void 0].filter(_=>_!==void 0);if(x.length>0)throw new Error(`Lazy atomic activation member ${t} has inconsistent mapping at ${z.vfsPath}: ${x.join(", ")}`);let I=E;return Object.freeze({vfsPath:z.vfsPath,ino:I.ino,generation:I.generation,dataSequence:I.dataSequence,size:I.size,isSymlink:I.isSymlink,deleted:!1,materialized:I.materialized,archivePath:A,sourcePath:z.sourcePath,type:z.type,...z.inodeGroup===void 0?{}:{inodeGroup:z.inodeGroup},...z.target===void 0?{}:{target:z.target}})});Object.freeze(m);let g=Object.freeze({sha256:f.sha256,bytes:f.bytes}),S=Ja(f,y,l,h);return Object.freeze({id:e,member:t,descriptorBytes:S,content:f,inventory:y,activation:h,url:f.transports[0]??"",mountPrefix:l,integrity:g,entries:m})}function Ji(r,e,t,n){return Object.freeze({...r,descriptorSha256:e,expectedCount:t,cohortSha256:n})}function Qi(r,e){return r.id!==e.id||r.member!==e.member||r.url!==e.url||r.mountPrefix!==e.mountPrefix||r.integrity.sha256!==e.integrity.sha256||r.integrity.bytes!==e.integrity.bytes||r.content.transports.length!==e.content.transports.length||r.content.transports.some((t,n)=>t!==e.content.transports[n])||!Qa(r.descriptorBytes,e.descriptorBytes)||r.entries.length!==e.entries.length?!1:r.entries.every((t,n)=>{let i=e.entries[n];return i!==void 0&&t.vfsPath===i.vfsPath&&rc(t,i)})}function ic(r,e){let t=yo(r.content,r.content.transports.map(e));return Object.freeze({...r,content:t,url:t.transports[0]??""})}function eo(r){return{paths:Array.from(r.paths),expectedIno:r.ino,expectedGeneration:r.generation,expectedDataSequence:r.dataSequence,data:r.content}}var bn=class r{fs;imageMetadata;lazyFiles=new Map;lazyArchiveGroups=[];deferredTreeMaterializationHandles=new WeakMap;lazyArchiveInodes=new Map;lazyAtomicGroups=new Map;lazyAtomicGroupByTree=new WeakMap;sealedLazyAtomicStates=new WeakMap;lazyDownloadListeners=new Set;lazyPreparations=new Map;lazyTransport={fetcher:(e,t)=>t===void 0?globalThis.fetch(e):globalThis.fetch(e,t)};constructor(e,t=null){this.fs=e,this.imageMetadata=t}static inodeKey(e,t){return`${e}:${t}`}static canAdoptLegacyLazyStub(e){return(e.mode&Le)===Kt&&e.size===0&&e.dataSequence<=1}reconcileLazyIdentityState(e){for(let[t,n]of this.lazyFiles){let i=e.get(t);if(!i||i.dataSequence!==n.dataSequence||i.paths.length===0){this.lazyFiles.delete(t);continue}n.paths=new Set(i.paths),n.paths.has(n.path)||(n.path=i.paths[0])}this.lazyArchiveInodes.clear();for(let t of this.lazyArchiveGroups){let n=this.lazyAtomicGroupByTree.get(t),i=this.sealedLazyAtomicStates.get(t);if(i!==void 0){if(!n?.committed)for(let c of i.snapshot.entries){if(c.isSymlink||c.materialized||c.generation===void 0)continue;let l=r.inodeKey(c.ino,c.generation),u=e.get(l);u!==void 0&&u.dataSequence===c.dataSequence&&u.paths.length>0&&this.lazyArchiveInodes.set(l,t)}continue}let s=t.content!==void 0&&t.inventory!==void 0&&!t.materialized,o=new Map;for(let c of t.entries.values()){if(c.deleted||c.materialized||c.generation===void 0)continue;let l=r.inodeKey(c.ino,c.generation);o.has(l)||o.set(l,c)}let a=new Map(Array.from(t.entries.entries()).filter(([,c])=>c.deleted||c.isSymlink&&!c.deleted));for(let[c,l]of o){let u=e.get(c);if(!(!u||u.dataSequence!==(l.dataSequence??0))){for(let d of u.paths)a.set(d,{...l,ino:u.ino,generation:u.generation,dataSequence:u.dataSequence,deleted:!1,materialized:!1});u.paths.length>0&&this.lazyArchiveInodes.set(c,t)}}t.entries=a,t.materialized=!Array.from(a.values()).some(c=>!c.isSymlink&&!c.materialized)&&!s&&(n===void 0||n.committed)}}validatePendingLazyTreeNamespaceState(e){let t=new Map;for(let n of e.values())for(let i of n.paths){if(t.has(i))throw new Error(`SharedFS namespace identity is ambiguous at ${i}`);t.set(i,n)}for(let n of this.lazyArchiveGroups){let i=this.lazyAtomicGroupByTree.get(n),o=this.sealedLazyAtomicStates.get(n)?.snapshot;if(i?.committed||o===void 0&&n.materialized||o===void 0&&(n.content===void 0||n.inventory===void 0))continue;let a=o?.inventory??n.inventory,c=o===void 0?n.entries:new Map(o.entries.map(y=>[y.vfsPath,y])),l=new Map,u=new Map,d=new Set;for(let y of c.values())y.deleted&&y.inodeGroup!==void 0&&d.add(y.inodeGroup);for(let y of a){if(y.type!=="file"&&y.type!=="hardlink")continue;l.set(y.inodeGroup,(l.get(y.inodeGroup)??0)+1);let h=u.get(y.inodeGroup)??[];h.push(y.vfsPath),u.set(y.inodeGroup,h)}let f=new Set([...d].filter(y=>u.get(y)?.every(h=>!t.has(h))));for(let y of a){let h=t.get(y.vfsPath);if(h===void 0){if(y.inodeGroup!==void 0&&f.has(y.inodeGroup))continue;throw new Error(`Lazy tree namespace entry ${y.vfsPath} is missing from the captured filesystem state`)}let p=y.type==="directory"?He:y.type==="symlink"?hn:Kt;if((h.mode&Le)!==p||(h.mode&4095)!==y.mode)throw new Error(`Lazy tree namespace entry ${y.vfsPath} disagrees with its captured type or mode`);if(y.type==="directory")continue;let w=c.get(y.vfsPath);if(w===void 0||w.ino!==h.ino||w.generation!==h.generation||w.dataSequence!==h.dataSequence)throw new Error(`Lazy tree namespace entry ${y.vfsPath} changed identity before serialization`);if(y.type==="symlink"){let m=new TextEncoder().encode(y.target).byteLength;if(h.linkCount!==1||h.size!==y.size||h.size!==m||h.symlinkTarget!==y.target)throw new Error(`Lazy tree symlink ${y.vfsPath} disagrees with its captured inventory`);continue}if(h.size!==0||h.linkCount!==l.get(y.inodeGroup))throw new Error(`Lazy tree stub ${y.vfsPath} has changed data or undeclared aliases`)}}}lazyFileForStat(e){let t=r.inodeKey(e.ino,e.generation),n=this.lazyFiles.get(t);if(n&&n.dataSequence!==e.dataSequence){this.lazyFiles.delete(t);return}return n}lazyArchiveEntriesForRead(e){let t=this.lazyAtomicGroupByTree.get(e),n=this.sealedLazyAtomicStates.get(e);return n!==void 0&&!t?.committed?n.snapshot.entries:Array.from(e.entries,([i,s])=>({vfsPath:i,...s}))}lazyArchiveForStat(e){let t=r.inodeKey(e.ino,e.generation),n=this.lazyArchiveInodes.get(t);if(!n)return;let i=this.lazyArchiveEntriesForRead(n).filter(o=>o.ino===e.ino&&o.generation===e.generation&&!o.deleted&&!o.materialized);if(i.some(o=>o.dataSequence===e.dataSequence))return n;if(this.lazyArchiveInodes.delete(t),this.sealedLazyAtomicStates.get(n)===void 0)for(let o of i)o.materialized=!0}lazyBackingForStat(e){let t=r.inodeKey(e.ino,e.generation),n=this.lazyFiles.get(t);if(n)return{token:n,path:n.path};let i=this.lazyArchiveInodes.get(t);if(!i)return null;let s=this.lazyArchiveEntriesForRead(i).find(a=>a.ino===e.ino&&a.generation===e.generation&&!a.deleted&&!a.materialized)?.vfsPath;if(s===void 0)return null;let o=this.lazyAtomicGroupByTree.get(i);return o===void 0?{token:i,path:s}:{token:o.token,path:s,atomicGroup:o}}lazyBackingForPath(e){let t=this.lazyArchiveGroups.find(n=>{let i=this.lazyAtomicGroupByTree.get(n),s=this.sealedLazyAtomicStates.get(n)?.snapshot,o=s===void 0?!n.materialized:!i?.committed,a=s?.content??n.content,c=s?.inventory??n.inventory,l=s?.activation??n.activation,u=s?.entries??Array.from(n.entries.values());return o&&a!==void 0&&c!==void 0&&l!==void 0&&u.every(d=>d.deleted||d.materialized||d.isSymlink)&&l.roots.some(d=>d==="/"||e===d||e.startsWith(`${d}/`))});if(t){let n=this.lazyAtomicGroupByTree.get(t);return{token:n?.token??t,path:e,directGroup:t,...n===void 0?{}:{atomicGroup:n}}}try{let n=this.fs.stat(e),i=this.lazyBackingForStat(n);return i?{...i,path:e}:null}catch{return null}}startLazyPreparation(e){let{path:t,token:n}=e,i={status:"pending",promise:Promise.resolve(!1)},s=e.atomicGroup?this.ensureAtomicLazyGroupMaterialized(e.atomicGroup).then(()=>!0):e.directGroup?this.ensureArchiveMaterialized(e.directGroup).then(()=>!0):this.materializePath(t);return i.promise=s.then(o=>(i.status="fulfilled",this.lazyPreparations.get(n)===i&&this.lazyPreparations.delete(n),o),o=>{throw i.status="rejected",i.error=o,o}),i.promise.catch(()=>{}),this.lazyPreparations.set(n,i),i}registerLazyAtomicGroupMembership(e,t=!1){let n=e.activation?.atomicGroup;if(n===void 0)return;let{id:i,member:s}=n;if(e.content===void 0||e.inventory===void 0||e.activation?.mode!=="first-use")throw new Error(`Lazy atomic activation group ${i} accepts only typed first-use trees`);let o=this.lazyAtomicGroups.get(i);if(o===void 0)o={id:i,token:Object.freeze({id:i}),groups:new Map,committed:!1},this.lazyAtomicGroups.set(i,o);else if(o.committed)throw new Error(`Lazy atomic activation group ${i} is already materialized`);if(o.groups.has(s))throw new Error(`Lazy atomic activation group ${i} duplicates member ${s}`);if(pt(n)){if(o.expectedCount!==void 0&&(o.expectedCount!==n.expectedCount||o.cohortSha256!==n.cohortSha256))throw new Error(`Lazy atomic activation group ${i} has inconsistent seals`);o.expectedCount=n.expectedCount,o.cohortSha256=n.cohortSha256;let a=pn(e,i,s);this.sealedLazyAtomicStates.set(e,{snapshot:Ji(a,n.descriptorSha256,n.expectedCount,n.cohortSha256),verified:t})}else if(o.expectedCount!==void 0)throw new Error(`Lazy atomic activation group ${i} mixes sealed and unsealed members`);o.groups.set(s,e),this.lazyAtomicGroupByTree.set(e,o)}async sealLazyAtomicGroup(e,t){let n=t.map(l=>uo({id:e,member:l}).member).sort();if(n.length===0||new Set(n).size!==n.length)throw new Error(`Lazy atomic activation group ${e} expected members are invalid`);let i=this.lazyAtomicGroups.get(e);if(i===void 0||i.committed)throw new Error(`Lazy atomic activation group ${e} is not pending`);let s=[...i.groups.keys()].sort();if(JSON.stringify(s)!==JSON.stringify(n))throw new Error(`Lazy atomic activation group ${e} members differ from its seal`);if(i.expectedCount!==void 0){if(i.expectedCount!==n.length||i.cohortSha256===void 0)throw new Error(`Lazy atomic activation group ${e} has an invalid existing seal`);await this.ensureLazyAtomicGroupSealValidated(i,n.map(l=>i.groups.get(l)),!0);return}let o=n.map(l=>pn(i.groups.get(l),e,l)),a=[];for(let l of o)a.push({member:l.member,descriptorSha256:await Wt(l.descriptorBytes,`Lazy atomic member ${l.member}`),source:l});let c=await Wt(Yi(e,a),`Lazy atomic activation group ${e}`);for(let l of a){let u=i.groups.get(l.member),d=pn(u,e,l.member);if(!Qi(l.source,d))throw new Error(`Lazy atomic activation member ${l.member} changed while sealing`)}for(let l of a){let u=i.groups.get(l.member);u.activation.atomicGroup={id:e,member:l.member,descriptorSha256:l.descriptorSha256,expectedCount:a.length,cohortSha256:c},this.sealedLazyAtomicStates.set(u,{snapshot:Ji(l.source,l.descriptorSha256,a.length,c),verified:!0})}i.expectedCount=a.length,i.cohortSha256=c}async verifyImportedLazyAtomicGroupSeals(){await this.validatePendingLazyAtomicGroupSeals(!0)}guardSynchronousLazyAccess(e){let t=this.lazyBackingForPath(e);if(!t)return;let n=this.lazyPreparations.get(t.token);if(n?.status==="fulfilled"){this.lazyPreparations.delete(t.token);let s=this.lazyBackingForPath(e);if(!s)return;n=this.lazyPreparations.get(s.token)??this.startLazyPreparation(s)}else if(n?.status==="rejected"){this.lazyPreparations.delete(t.token);let s=n.error instanceof Error?n.error.message:String(n.error),o=new Error(`EIO: lazy backing for ${e} failed: ${s}`);throw o.code="EIO",o.cause=n.error,o}else n||(n=this.startLazyPreparation(t));let i=new Error(`EAGAIN: lazy backing for ${e} is being prepared`);throw i.code="EAGAIN",i}invalidateLazyData(e){let t=r.inodeKey(e.ino,e.generation);this.lazyFiles.delete(t);let n=this.lazyArchiveInodes.get(t);if(n){this.lazyArchiveInodes.delete(t);for(let i of n.entries.values())i.ino===e.ino&&i.generation===e.generation&&(i.materialized=!0)}}rewriteLazyNamespacePaths(e,t,n){let i=t.length>1?t.replace(/\/+$/,""):t,s=n.length>1?n.replace(/\/+$/,""):n,o=`${i}/`,a=`${s}/`,c=r.inodeKey(e.ino,e.generation),l=(e.mode&Le)===He,u=d=>d===i?s:l&&d.startsWith(o)?a+d.slice(o.length):d;for(let[d,f]of this.lazyFiles)!l&&d!==c||(f.paths=new Set(Array.from(f.paths,u)),f.path=u(f.path));for(let d of this.lazyArchiveGroups){let f=new Map;for(let[y,h]of d.entries){let p=h.generation===void 0?null:r.inodeKey(h.ino,h.generation);f.set(l||p===c?u(y):y,h)}d.entries=f,d.inventory&&(d.inventory=d.inventory.map(y=>({...y,vfsPath:u(y.vfsPath),...y.type==="hardlink"&&y.target!==void 0?{target:u(y.target)}:{}}))),d.activation&&(d.activation={...d.activation,roots:d.activation.roots.map(u)})}}get sharedBuffer(){return this.fs.buffer}static create(e,t){return new r(be.mkfs(e,t))}static fromExisting(e){return new r(be.mount(e))}rebaseToNewFileSystem(e){if(!Number.isSafeInteger(e)||e<=0)throw new Error(`Invalid MemoryFileSystem maxByteLength: ${e}`);let t=SharedArrayBuffer,{bytes:n,identities:i}=this.fs.snapshotState();this.reconcileLazyIdentityState(i);let s=this.serializeLazyEntries(),o=this.serializeValidatedLazyArchiveEntries(i),a=new t(n.byteLength);new Uint8Array(a).set(n);let c=new r(be.mount(a,{restoreImage:!0}),this.imageMetadata);c.importLazyEntries(s),c.importLazyArchiveEntriesInternal(o,!1,!0,"verified");let l=Math.min(e,Math.max(n.byteLength,La)),u=new t(l,{maxByteLength:e}),d=r.create(u,e);d.setImageMetadata(this.imageMetadata);let f=new Set(s.flatMap(h=>h.paths??[h.path])),y=new Set;for(let h of o)if(!h.materialized)for(let p of h.entries)!p.deleted&&!p.isSymlink&&y.add(p.vfsPath);return c.copyPathToFreshFileSystem("/",d,f,y,new Map),d.importLazyEntries(s.map(h=>{let p=d.fs.lstat(h.path);return{...h,ino:p.ino,generation:p.generation,dataSequence:p.dataSequence}})),d.importLazyArchiveEntriesInternal(o.map(h=>({...h,entries:h.entries.map(p=>{if(p.deleted)return{...p,ino:0,generation:void 0};let w=d.fs.lstat(p.vfsPath);return{...p,ino:w.ino,generation:w.generation,dataSequence:w.dataSequence}})})),!1,!0,"verified"),d}getImageMetadata(){return Ba(this.imageMetadata)}setImageMetadata(e){this.imageMetadata=e===null?null:zr(e)}subscribeLazyDownloads(e){return this.lazyDownloadListeners.add(e),()=>this.lazyDownloadListeners.delete(e)}setLazyFetcher(e,t={}){this.lazyTransport={fetcher:e,...t.signal===void 0?{}:{signal:t.signal}}}emitLazyDownload(e){if(this.lazyDownloadListeners.size===0)return;let t={...e,t:Fa()};for(let n of this.lazyDownloadListeners)try{n(t)}catch{}}async fetchLazyBytes(e,t){let n=0,i=e.integrity?.bytes??e.fallbackTotalBytes,s={id:e.id,kind:e.kind,url:e.url,path:e.path,mountPrefix:e.mountPrefix};for(let o=0;oe.integrity.bytes)throw new Error(`Lazy ${e.kind} exceeded expected byte count ${e.integrity.bytes}`);this.emitLazyDownload({...s,status:"progress",loadedBytes:n,totalBytes:i})}}}catch(d){try{await c.cancel(d)}catch{}throw d}}finally{c.releaseLock()}let u=Va(l,n);return J(t.signal),await hr(u,e.kind,e.integrity),J(t.signal),this.emitLazyDownload({...s,status:"complete",loadedBytes:n,totalBytes:i??n}),u}catch(a){if(t.signal?.aborted){let u=t.signal.reason,d=u instanceof Error?u.message:String(u);throw this.emitLazyDownload({...s,status:"error",loadedBytes:n,totalBytes:i,error:d}),u}let c=o+1({...g})),activation:d,entries:new Map},w=g=>{let S=g.split("/").filter(Boolean),z="";for(let E=0;ES.vfsPath.split("/").length-z.vfsPath.split("/").length))if(g.type==="directory"){w(g.vfsPath);try{this.fs.mkdir(g.vfsPath,g.mode),this.fs.chmod(g.vfsPath,g.mode)}catch{if((this.fs.lstat(g.vfsPath).mode&Le)!==He)throw new Error(`Lazy tree directory collides at ${g.vfsPath}`)}}for(let g of l){if(g.type!=="symlink")continue;w(g.vfsPath),this.fs.symlink(g.target,g.vfsPath);let S=this.fs.lstat(g.vfsPath);p.entries.set(g.vfsPath,{ino:S.ino,generation:S.generation,dataSequence:S.dataSequence,size:g.size,isSymlink:!0,deleted:!1,materialized:!0,archivePath:g.sourcePath,sourcePath:g.sourcePath,type:"symlink",target:g.target})}let m=new Map;for(let g of l){if(g.type!=="file")continue;w(g.vfsPath);let S=this.fs.createLazyStub(g.vfsPath,g.mode);this.invalidateLazyData(S),m.set(g.inodeGroup,S);let z={ino:S.ino,generation:S.generation,dataSequence:S.dataSequence,size:g.size,isSymlink:!1,deleted:!1,materialized:!1,archivePath:g.sourcePath,sourcePath:g.sourcePath,type:"file",inodeGroup:g.inodeGroup};p.entries.set(g.vfsPath,z)}for(let g of l){if(g.type!=="hardlink")continue;let S=f.get(g.inodeGroup);w(g.vfsPath),this.fs.link(S.vfsPath,g.vfsPath);let z=this.fs.lstat(g.vfsPath),E=m.get(g.inodeGroup);if(z.ino!==E.ino||z.generation!==E.generation)throw new Error(`Lazy tree hardlink ${g.vfsPath} did not share its inode`);p.entries.set(g.vfsPath,{ino:z.ino,generation:z.generation,dataSequence:z.dataSequence,size:g.size,isSymlink:!1,deleted:!1,materialized:!1,archivePath:S.sourcePath,sourcePath:g.sourcePath,type:"hardlink",inodeGroup:g.inodeGroup,target:g.target})}if(y!==void 0)for(let g of l)this.lchown(g.vfsPath,y.uid,y.gid);for(let g of p.entries.values())g.isSymlink||g.generation===void 0||this.lazyArchiveInodes.set(r.inodeKey(g.ino,g.generation),p);return this.lazyArchiveGroups.push(p),this.registerLazyAtomicGroupMembership(p),p}registerLazyTreeWithMaterializationHandle(e,t,n="/",i,s){let o=this.registerLazyTreeInternal(e,t,n,i,!0,s),a=Object.freeze({[za]:!0});return this.deferredTreeMaterializationHandles.set(a,o),a}registerLazyArchiveFromEntries(e,t,n,i,s){let o=Vt(n),a=Ca(e,t,o,i);a.some(({entry:l})=>!l.isDirectory&&!l.isSymlink)&&this.assertCanRegisterPendingLazyArchiveGroup();let c={...s?{content:En({decoder:"zip-v1",mediaType:"application/zip",sha256:s.sha256,bytes:s.bytes,expandedBytes:a.reduce((l,u)=>l+u.entry.uncompressedSize,0),sourceEntryCount:a.length,transports:[e]})}:{},url:e,mountPrefix:o,integrity:qt(s),materialized:!1,entries:new Map};for(let{entry:l,vfsPath:u}of a){if(l.isDirectory)continue;let d=u.split("/").filter(Boolean),f="";for(let y=0;yl.deleted||l.materialized),this.lazyArchiveGroups.push(c),c}importLazyArchiveEntries(e){this.importLazyArchiveEntriesInternal(e,!1,!0,"reject")}async importVerifiedLazyArchiveEntries(e){let t=structuredClone(e),n=this.exportLazyArchiveEntries(),i=r.fromExisting(this.sharedBuffer);i.importLazyArchiveEntriesInternal([...n,...t],!1,!0,"pending"),await i.verifyImportedLazyAtomicGroupSeals(),this.importLazyArchiveEntriesInternal(t,!1,!0,"verified")}importLazyArchiveEntriesInternal(e,t,n,i){let s=xe(e,"Serialized lazy archive groups",0,to).map((u,d)=>{if(typeof u!="object"||u===null||Array.isArray(u))throw new Error(`Serialized lazy archive group ${d} must be an object`);let f=u.kind;if(f===Ht||f===gr||f===Ve)return ho(u,f);if(f===Zt)return Sr(u,!1);if(f!==void 0)throw new Error(`Serialized lazy archive group ${d} has an unsupported kind`);if(n)throw new Error(`Serialized lazy archive group ${d} is missing its kind discriminator`);return Sr(u,!0)}),o=this.fs.identityState();this.reconcileLazyIdentityState(o);let a=[...this.serializeValidatedLazyArchiveEntries(o),...s];ji(a);let c=[],l=new Map;for(let u of s){let d=new Map,f=u.mountPrefix.replace(/\/+$/,""),y=u.content!==void 0&&u.inventory!==void 0&&u.activation!==void 0,h=y?new Map(u.inventory.map(E=>[E.vfsPath,E])):null,p=y?new Map(u.inventory.map(E=>[zn(E),E])):null,w=new Map,m=new Map,g=new Map;for(let E of u.entries){let v=null,A=u.materialized||E.materialized===!0||E.isSymlink;if(!E.deleted&&!A){if((E.generation===void 0||E.dataSequence===void 0)&&!t)throw new Error("Live lazy-archive metadata requires inode generation and data sequence");try{v=this.fs.lstat(E.vfsPath)}catch{if(y)throw new Error(`Serialized lazy tree stub ${E.vfsPath} is missing from the filesystem`);continue}if(v.ino!==E.ino){if(y)throw new Error(`Serialized lazy tree stub ${E.vfsPath} has a different inode`);continue}if(E.generation!==void 0&&v.generation!==E.generation){if(y)throw new Error(`Serialized lazy tree stub ${E.vfsPath} has a different generation`);continue}if(E.dataSequence===void 0){if(!r.canAdoptLegacyLazyStub(v)){if(y)throw new Error(`Serialized lazy tree stub ${E.vfsPath} is not pristine`);continue}}else if(v.dataSequence!==E.dataSequence){if(y)throw new Error(`Serialized lazy tree stub ${E.vfsPath} has a different data sequence`);continue}if(y){g.set(E.vfsPath,v);let x=h.get(E.vfsPath),I=p.get(zn(E))??x;if(!I||(v.mode&Le)!==Kt||v.size!==0||(v.mode&4095)!==I.mode||x?.inodeGroup!==void 0&&x.inodeGroup!==I.inodeGroup)throw new Error(`Serialized lazy tree stub ${E.vfsPath} disagrees with its inventory`);let _=r.inodeKey(v.ino,v.generation),D=E.inodeGroup,V=w.get(D),B=m.get(_);if(V!==void 0&&V!==_||B!==void 0&&B!==D)throw new Error(`Serialized lazy tree inode group ${D} disagrees with the filesystem`);w.set(D,_),m.set(_,D)}}d.set(E.vfsPath,{ino:E.ino,generation:v?.generation??E.generation,dataSequence:v?.dataSequence??E.dataSequence,size:E.size,isSymlink:E.isSymlink,deleted:E.deleted,materialized:A,archivePath:E.archivePath??E.vfsPath.slice(f.length+1),sourcePath:E.sourcePath??E.archivePath??E.vfsPath.slice(f.length+1),type:E.type??(E.isSymlink?"symlink":"file"),inodeGroup:E.inodeGroup,target:E.target})}if(y){let E=new Map;for(let v of u.inventory){if(v.type==="file"||v.type==="hardlink"){E.set(v.inodeGroup,(E.get(v.inodeGroup)??0)+1);continue}let A;try{A=this.fs.lstat(v.vfsPath)}catch{throw new Error(`Serialized lazy tree namespace entry ${v.vfsPath} is missing from the filesystem`)}let k=v.type==="directory"?He:hn;if((A.mode&Le)!==k||(A.mode&4095)!==v.mode||v.type==="symlink"&&(A.size!==new TextEncoder().encode(v.target).byteLength||this.fs.readlink(v.vfsPath)!==v.target))throw new Error(`Serialized lazy tree namespace entry ${v.vfsPath} disagrees with its inventory`);v.type==="symlink"&&d.set(v.vfsPath,{ino:A.ino,generation:A.generation,dataSequence:A.dataSequence,size:v.size,isSymlink:!0,deleted:!1,materialized:!0,archivePath:v.sourcePath,sourcePath:v.sourcePath,type:"symlink",target:v.target})}if(u.activation?.atomicGroup!==void 0)for(let v of u.inventory){if(v.type!=="file"&&v.type!=="hardlink")continue;if(g.get(v.vfsPath).linkCount!==E.get(v.inodeGroup))throw new Error(`Serialized lazy atomic tree inode group ${v.inodeGroup} has undeclared aliases`)}}let S=u.content===void 0?void 0:En(u.content),z={content:S,url:S?.transports[0]??u.url,mountPrefix:u.mountPrefix,integrity:S?{sha256:S.sha256,bytes:S.bytes}:qt(u.integrity),materialized:u.materialized||!(S&&u.inventory)&&Array.from(d.values()).every(E=>E.deleted||E.materialized),inventory:u.inventory?.map(E=>({...E})),activation:u.activation?{mode:u.activation.mode,capabilities:[...u.activation.capabilities],roots:[...u.activation.roots],...u.activation.atomicGroup===void 0?{}:{atomicGroup:{...u.activation.atomicGroup}}}:void 0,entries:d};if(c.push(z),!z.materialized){for(let[,E]of d)if(!E.deleted&&!E.materialized&&E.generation!==void 0){let v=r.inodeKey(E.ino,E.generation),A=l.get(v);if(A!==void 0&&A!==z)throw new Error(`Serialized lazy archive groups share pending inode ${v}`);if(this.lazyArchiveInodes.has(v))throw new Error(`Serialized lazy archive group collides with pending inode ${v}`);l.set(v,z)}}}for(let u of c){let d=u.activation?.atomicGroup;if(d!==void 0&&this.lazyAtomicGroups.get(d.id)?.committed)throw new Error(`Lazy atomic activation group ${d.id} is already materialized`)}if(i==="reject"&&c.some(u=>{let d=u.activation?.atomicGroup;return d!==void 0&&pt(d)}))throw new Error("Sealed lazy archive registrations require importVerifiedLazyArchiveEntries()");this.lazyArchiveGroups.push(...c);for(let u of c)this.registerLazyAtomicGroupMembership(u,i==="verified");for(let[u,d]of l)this.lazyArchiveInodes.set(u,d)}rewriteLazyArchiveUrls(e){for(let t of this.lazyArchiveGroups){let n=this.lazyAtomicGroupByTree.get(t),i=this.sealedLazyAtomicStates.get(t);if(i!==void 0&&!n?.committed){this.assertLazyAtomicSnapshotMatchesPublic(t);let s=ic(i.snapshot,e);t.content=Xi(s.content),t.url=s.url,t.integrity={...s.integrity},i.snapshot=s;continue}t.content?(t.content={...t.content,transports:t.content.transports.map(e)},t.url=t.content.transports[0]):t.url=e(t.url)}}serializeLazyArchiveEntries(){let e=[];for(let t of this.lazyArchiveGroups){let n=this.lazyAtomicGroupByTree.get(t),i=this.sealedLazyAtomicStates.get(t);if(i!==void 0){if(n?.committed)continue;let c=i.snapshot;if(c.content.transports.length===0)throw new Error("Direct-materialization tree must be materialized before serialization");e.push({kind:Ve,content:Xi(c.content),inventory:c.inventory.map(l=>({...l})),activation:nc(c),url:c.url,mountPrefix:c.mountPrefix,integrity:{...c.integrity},materialized:!1,entries:c.entries.filter(l=>!l.deleted&&!l.materialized).map(({vfsPath:l,...u})=>({vfsPath:l,...u}))});continue}let s=Array.from(t.entries,([c,l])=>({vfsPath:c,ino:l.ino,generation:l.generation,dataSequence:l.dataSequence,size:l.size,isSymlink:l.isSymlink,deleted:l.deleted,materialized:l.materialized,archivePath:l.archivePath,sourcePath:l.sourcePath,type:l.type,inodeGroup:l.inodeGroup,target:l.target})).filter(c=>!c.deleted&&!c.materialized);if(s.length===0&&!(t.content&&t.inventory&&!t.materialized))continue;let o=t.content!==void 0&&t.inventory!==void 0&&t.activation!==void 0;if(o&&t.content.transports.length===0)throw new Error("Direct-materialization tree must be materialized before serialization");let a=t.activation?.atomicGroup;if(a!==void 0&&!pt(a))throw new Error(`Lazy atomic activation group ${a.id} must be sealed before serialization`);e.push(o?{kind:a!==void 0?Ve:t.content.source===void 0?Ht:gr,content:t.content,inventory:t.inventory,activation:t.activation,url:t.url,mountPrefix:t.mountPrefix,integrity:t.integrity,materialized:!1,entries:s}:{kind:Zt,url:t.url,mountPrefix:t.mountPrefix,integrity:t.integrity,materialized:!1,entries:s})}return e}serializeValidatedLazyArchiveEntries(e){this.assertPendingLazyAtomicSnapshotsReadyForSerialization();let t=this.serializeLazyArchiveEntries();return ji(t),this.validatePendingLazyTreeNamespaceState(e),t}exportLazyArchiveEntries(){let e=this.fs.identityState();return this.reconcileLazyIdentityState(e),this.serializeValidatedLazyArchiveEntries(e)}pendingDeferredTreeUsage(){let e=this.fs.identityState();return this.reconcileLazyIdentityState(e),co(this.serializeValidatedLazyArchiveEntries(e))}assertCanAppendDeferredTreeUsage(e){vr(e);let t=this.pendingDeferredTreeUsage();vr({groups:t.groups+e.groups,archiveBytes:t.archiveBytes+e.archiveBytes,expandedBytes:t.expandedBytes+e.expandedBytes,payloadBytes:t.payloadBytes+e.payloadBytes,entries:t.entries+e.entries})}assertCanRegisterPendingLazyArchiveGroup(){if(this.reconcileLazyIdentityState(this.fs.identityState()),this.lazyArchiveGroups.filter(t=>{let n=this.lazyAtomicGroupByTree.get(t);return this.sealedLazyAtomicStates.get(t)?.snapshot!==void 0?!n?.committed:!t.materialized&&(t.content!==void 0&&t.inventory!==void 0||Array.from(t.entries.values()).some(s=>!s.deleted&&!s.materialized))}).length>=Se.maxGroups)throw new Error(`Cannot register another lazy archive group: ${Se.maxGroups} pending groups already exist`)}async preparePath(e){let t=!1,n=Math.max(3,this.lazyArchiveGroups.length+1);for(let i=0;i!s.materialized&&s.activation?.mode==="boot-prefetch"),t=0,n,i=Array.from({length:Math.min(e.length,Ia)},async()=>{for(;n===void 0;){let s=t;if(t+=1,s>=e.length)return;try{await this.prepareLazyTreeGroup(e[s])}catch(o){n??=o}}});if(await Promise.all(i),n!==void 0)throw n;return e.length}async materializeRegisteredDeferredTree(e,t){let n=this.deferredTreeMaterializationHandles.get(e);if(n===void 0)throw new Error("Deferred-tree handle was not issued by this filesystem");let i=this.lazyAtomicGroupByTree.get(n);if(i!==void 0)throw new Error(`Deferred tree belongs to atomic activation group ${i.id}; materialize the complete group instead`);if(n.materialized)return!1;let s=this.lazyPreparations.get(n);if(s!==void 0)return s.promise;let o=new Uint8Array(t.byteLength);o.set(t);let a={status:"pending",promise:Promise.resolve(!1)};a.promise=Promise.resolve().then(async()=>(await hr(o,"tree",n.integrity),await this.materializeArchiveBytes(n,o),!0)).then(c=>(a.status="fulfilled",c),c=>{throw a.status="rejected",a.error=c,c}),a.promise.catch(()=>{}),this.lazyPreparations.set(n,a);try{return await a.promise}finally{this.lazyPreparations.get(n)===a&&this.lazyPreparations.delete(n)}}async prepareLazyTreeGroup(e){let t=this.lazyAtomicGroupByTree.get(e);if(t?.committed||t===void 0&&e.materialized)return!1;let n=this.sealedLazyAtomicStates.get(e)?.snapshot,i={token:t?.token??e,path:n?.activation.roots[0]??e.activation?.roots[0]??e.mountPrefix,directGroup:e,...t===void 0?{}:{atomicGroup:t}},s=this.lazyPreparations.get(i.token)??this.startLazyPreparation(i);try{return await s.promise}finally{this.lazyPreparations.get(i.token)===s&&this.lazyPreparations.delete(i.token)}}async ensureMaterialized(e){return this.preparePath(e)}async materializePath(e){if(this.lazyFiles.size===0&&this.lazyArchiveInodes.size===0)return!1;let t;try{t=this.fs.stat(e)}catch{return!1}let n=r.inodeKey(t.ino,t.generation),i=this.lazyFiles.get(n);if(i){let o=this.lazyTransport,a=await this.fetchLazyBytes({id:`file:${t.ino}`,kind:"file",url:i.url,path:i.path,fallbackTotalBytes:i.size},o);for(let c=0;c<3;c++){if(this.lazyFiles.get(n)!==i)return!1;for(let l of new Set([e,...i.paths]))if(J(o.signal),this.fs.replaceIfIdentity(l,i.ino,i.generation,i.dataSequence,a))return i.path=l,this.lazyFiles.delete(n),!0;this.reconcileLazyIdentityState(this.fs.identityState())}throw new Error(`Lazy file kept changing names while materializing: ${e}`)}let s=this.lazyArchiveInodes.get(n);return s?(await this.ensureArchiveMaterialized(s,{path:e,ino:t.ino,generation:t.generation}),!this.lazyArchiveInodes.has(n)):!1}async decodeAndValidateLazyTree(e,t,n){let i=n?.content??e.content,s=n?.inventory??e.inventory;if(!i||!s)throw new Error("Lazy tree is missing its decoder or complete inventory");let o=new Map,a=new Map(s.map(f=>[f.vfsPath,f]));if(i.source!==void 0)for(let f of i.source.entries)o.set(f.sourcePath,f);else for(let f of s){if(f.type==="hardlink"){let h=a.get(f.target);if(!h)throw new Error(`Lazy tree hardlink target disappeared: ${f.target}`);if(f.sourcePath===h.sourcePath)continue}if(o.get(f.sourcePath))throw new Error(`Lazy tree inventory duplicates source member ${f.sourcePath}`);o.set(f.sourcePath,{sourcePath:f.sourcePath,type:f.type,mode:f.mode,size:f.size,...f.type==="symlink"?{target:f.target}:{},...f.type==="hardlink"?{target:a.get(f.target)?.sourcePath}:{}})}let c=new Map,l=0;if(i.decoder==="zip-v1"){let{parseZipCentralDirectory:f,extractZipEntryBounded:y}=await Promise.resolve().then(()=>(rr(),nr)),h=f(t);if(h.length!==i.sourceEntryCount||h.length!==o.size)throw new Error("Lazy ZIP tree decoded inventory counts differ from its descriptor");for(let p of h){let w=p.isDirectory?p.fileName.replace(/\/$/,""):p.fileName;if(c.has(w))throw new Error(`Lazy ZIP tree duplicates source member ${w}`);let m=o.get(w);if(!m)throw new Error(`Lazy ZIP tree has undeclared source member ${w}`);if(l+=p.uncompressedSize,l>i.expandedBytes||p.uncompressedSize!==m.size)throw new Error(`Lazy ZIP tree member ${w} exceeds its inventory`);let g=p.isDirectory?"directory":p.isSymlink?"symlink":"file",S=i.modePolicy==="portable-posix-v1"?g==="directory"?493:g==="symlink"?511:(p.mode&73)!==0?493:420:p.mode&4095;if(g!==m.type||S!==m.mode)throw new Error(`Lazy ZIP tree member ${w} differs from inventory`);if(p.isDirectory)c.set(w,{type:"directory",mode:S});else{let z=y(t,p,m.size);if(p.isSymlink){let E;try{E=new TextDecoder("utf-8",{fatal:!0}).decode(z)}catch{throw new Error(`Lazy ZIP tree symlink ${w} is not UTF-8`)}c.set(w,{type:"symlink",mode:S,target:E})}else c.set(w,{type:"file",mode:S,data:z})}}}else{let{parseTarGzip:f}=await Promise.resolve().then(()=>(Gi(),Ui)),y=f(t,{label:`Lazy tree ${i.sha256}`,limits:{maxCompressedBytes:i.bytes,maxUncompressedBytes:i.expandedBytes,maxEntries:i.sourceEntryCount}});l=new DataView(t.buffer,t.byteOffset,t.byteLength).getUint32(t.byteLength-4,!0);for(let h of y){if(c.has(h.path))throw new Error(`Lazy TAR tree duplicates source member ${h.path}`);h.type==="file"?c.set(h.path,{type:"file",mode:h.mode,data:h.data}):h.type==="directory"?c.set(h.path,{type:"directory",mode:h.mode}):c.set(h.path,{type:h.type,mode:h.mode,target:h.linkName})}}if(c.size!==i.sourceEntryCount||c.size!==o.size||l!==i.expandedBytes)throw new Error("Lazy tree decoded inventory counts differ from its descriptor");for(let[f,y]of o){let h=c.get(f);if(!h)throw new Error(`Lazy tree is missing source member ${f}`);let p=y.type;if(h.type!==p)throw new Error(`Lazy tree member ${f} is ${h.type}, expected ${p}`);if((h.mode&4095)!==y.mode)throw new Error(`Lazy tree member ${f} mode differs from inventory`);if(p==="file"&&h.data?.byteLength!==y.size)throw new Error(`Lazy tree member ${f} size differs from inventory`);if(p==="symlink"&&h.target!==y.target)throw new Error(`Lazy tree symlink ${f} target differs from inventory`);if(p==="hardlink"&&h.target!==y.target)throw new Error(`Lazy tree hardlink ${f} target differs from inventory`)}let u=new Set(s.flatMap(f=>f.materialization==="archive-homebrew-relocate"?[f.sourcePath]:[]));if(i.source!==void 0){let f=new Map(i.source.entries.map(p=>[p.sourcePath,p])),y=lo(i.source.entries),h=i.source.entries.filter(p=>p.sourcePath==="INSTALL_RECEIPT.json"||p.sourcePath.endsWith("/INSTALL_RECEIPT.json"));if(h.length>1)throw new Error(`Lazy Homebrew bottle has ${h.length} INSTALL_RECEIPT.json source members, expected at most one`);if(h.length===0){if(u.size>0)throw new Error("Lazy Homebrew bottle marks receipt relocation without INSTALL_RECEIPT.json")}else{let p=h[0],w=p.type==="file"?p:y.get(p.sourcePath),m=w===void 0?void 0:c.get(w.sourcePath);if(w?.type!=="file"||m?.type!=="file"||m.data===void 0)throw new Error("Lazy Homebrew bottle INSTALL_RECEIPT.json is not regular");let g=mi(m.data),S=p.sourcePath.lastIndexOf("/"),z=S<0?"":p.sourcePath.slice(0,S),E=new Set(g.changedFiles.map(A=>z.length===0?A:`${z}/${A}`));if(u.size!==E.size||[...u].some(A=>!E.has(A)))throw new Error("Lazy Homebrew bottle relocation markers differ from INSTALL_RECEIPT.json");let v=new Set;for(let A of E){let k=f.get(A),x=k?.type==="file"?k:k===void 0?void 0:y.get(k.sourcePath),I=x===void 0?void 0:c.get(x.sourcePath);if(x?.type!=="file"||I?.type!=="file"||I.data===void 0)throw new Error(`Lazy Homebrew bottle changed source ${A} is not regular`);v.has(x.sourcePath)||(I.data=gi(I.data,g,A),v.add(x.sourcePath))}}}else if(u.size>0)throw new Error("Lazy tree receipt relocation requires original-bottle source truth");let d=new Map;for(let f of s){if(f.type!=="file"||f.materialization==="descriptor")continue;let y=c.get(f.sourcePath);if(y?.type!=="file"||!y.data)throw new Error(`Lazy tree has no file content for ${f.sourcePath}`);d.set(f.sourcePath,y.data)}return d}async ensureArchiveMaterialized(e,t){let n=this.lazyAtomicGroupByTree.get(e);if(n!==void 0){if(n.committed)return;let o=this.sealedLazyAtomicStates.get(e)?.snapshot,a=this.lazyPreparations.get(n.token)??this.startLazyPreparation({token:n.token,path:o?.activation.roots[0]??e.activation?.roots[0]??e.mountPrefix,atomicGroup:n});try{await a.promise}finally{this.lazyPreparations.get(n.token)===a&&this.lazyPreparations.delete(n.token)}return}if(e.materialized)return;let i=this.lazyTransport,s=await this.fetchLazyArchiveData(e,i);J(i.signal),await this.materializeArchiveBytes(e,s,t,i.signal)}async fetchLazyArchiveData(e,t,n){let i=n?.content??e.content,s=n?.inventory??e.inventory,o=i!==void 0&&s!==void 0,a=n?.mountPrefix??e.mountPrefix,c=n?.integrity??e.integrity,l=o?i.transports:[n?.url??e.url],u=[],d=null;for(let[f,y]of l.entries())try{d=await this.fetchLazyBytes({id:`archive:${a}:${i?.sha256??y}:${f}`,kind:o?"tree":"archive",url:y,mountPrefix:a,integrity:c},t);break}catch(h){if(J(t.signal),ao(h))throw h;u.push(h instanceof Error?h.message:String(h))}if(J(t.signal),d===null)throw new Error(`All ${l.length} lazy ${o?"tree":"archive"} transports failed: ${u.join("; ")}`);return d}async materializeArchiveBytes(e,t,n,i){if(J(i),e.materialized)return;let s=await this.prepareLazyArchiveContents(e,t,i),o=n?r.inodeKey(n.ino,n.generation):null;for(let a=0;a<3;a++){let c=this.collectLazyArchiveReplacements(e,s,n);if(c.size>0&&(J(i),!this.fs.replaceManyIfIdentities(Array.from(c.values(),eo)))){if(this.reconcileLazyIdentityState(this.fs.identityState()),o&&!this.lazyArchiveInodes.has(o))return;continue}if(J(i),this.publishLazyArchiveReplacements(e,c),e.materialized||(this.reconcileLazyIdentityState(this.fs.identityState()),o&&!this.lazyArchiveInodes.has(o)))return}if(o&&this.lazyArchiveInodes.has(o))throw new Error(`Lazy archive member kept changing names while materializing: ${n?.path}`)}async prepareLazyArchiveContents(e,t,n,i){J(n);let s=i?.content??e.content,o=i?.inventory??e.inventory,c=s!==void 0&&o!==void 0?await this.decodeAndValidateLazyTree(e,t,i):null;J(n);let{parseZipCentralDirectory:l,extractZipEntry:u}=await Promise.resolve().then(()=>(rr(),nr));J(n);let d=c?[]:l(t),f=new Map;for(let m of d){if(f.has(m.fileName))throw new Error(`Lazy archive contains duplicate member: ${m.fileName}`);f.set(m.fileName,m)}let h=(i?.mountPrefix??e.mountPrefix).replace(/\/+$/,""),p=new Map,w=i===void 0?Array.from(e.entries):i.entries.map(m=>[m.vfsPath,m]);for(let[m,g]of w){if(g.deleted||g.materialized)continue;let S=g.archivePath??m.slice(h.length+1),z=c?void 0:f.get(S),E=c?.get(S);if(c){if(E===void 0||E.byteLength!==g.size)throw new Error(`Lazy tree member ${S} does not match its registered metadata`)}else if(z===void 0||z.isDirectory||z.isSymlink||z.uncompressedSize!==g.size)throw new Error(`Lazy archive member ${S} does not match its registered metadata`);if(g.generation===void 0)continue;let v=r.inodeKey(g.ino,g.generation),A=p.get(v);if(A&&A.archivePath!==S)throw new Error(`Lazy archive aliases for inode ${v} name different members`);if(!A){let k=E??u(t,z);if(k.byteLength!==g.size)throw new Error(`Lazy archive member ${S} extracted ${k.byteLength} bytes, expected ${g.size}`);p.set(v,{archivePath:S,content:k})}}return p}collectLazyArchiveReplacements(e,t,n,i){let s=new Map,o=i===void 0?Array.from(e.entries):i.entries.map(a=>[a.vfsPath,a]);for(let[a,c]of o){if(c.deleted||c.materialized||c.generation===void 0)continue;let l=r.inodeKey(c.ino,c.generation);if(this.lazyArchiveInodes.get(l)!==e)continue;let u=t.get(l);if(!u)throw new Error(`Lazy archive has no extracted content for inode ${l}`);let d=s.get(l);d||(d={ino:c.ino,generation:c.generation,dataSequence:c.dataSequence??0,paths:new Set,content:u.content},s.set(l,d)),d.paths.add(a),n&&n.ino===c.ino&&n.generation===c.generation&&d.paths.add(n.path)}return s}publishLazyArchiveReplacements(e,t){for(let[n,i]of t){this.lazyArchiveInodes.delete(n);for(let s of e.entries.values())s.ino===i.ino&&s.generation===i.generation&&(s.materialized=!0)}e.materialized=Array.from(e.entries.values()).every(n=>n.deleted||n.materialized)}collectAtomicTreeNamespace(e,t){let n=new Set,i=new Map,s=new Map,o=new Map(t.entries.map(c=>[c.vfsPath,c]));for(let c of t.inventory)(c.type==="file"||c.type==="hardlink")&&s.set(c.inodeGroup,(s.get(c.inodeGroup)??0)+1);let a=[];for(let c of t.inventory){let l;try{l=this.fs.lstat(c.vfsPath)}catch{throw new Error(`Lazy atomic tree changed at ${c.vfsPath}`)}let u=c.type==="directory"?He:c.type==="symlink"?hn:Kt;if((l.mode&Le)!==u||(l.mode&4095)!==c.mode)throw new Error(`Lazy atomic tree changed at ${c.vfsPath}`);if(c.type==="symlink"){let d=o.get(c.vfsPath);if(d===void 0||!d.isSymlink||d.deleted||d.ino!==l.ino||d.generation!==l.generation||d.dataSequence!==l.dataSequence||this.fs.readlink(c.vfsPath)!==c.target)throw new Error(`Lazy atomic tree changed at ${c.vfsPath}`)}else if(c.type==="file"||c.type==="hardlink"){let d=o.get(c.vfsPath);if(d===void 0||d.deleted||d.materialized||d.isSymlink||d.generation===void 0||d.inodeGroup!==c.inodeGroup||d.ino!==l.ino||d.generation!==l.generation||d.dataSequence!==l.dataSequence||l.size!==0||l.linkCount!==s.get(c.inodeGroup))throw new Error(`Lazy atomic tree changed at ${c.vfsPath}`);let f=r.inodeKey(d.ino,d.generation);if(this.lazyArchiveInodes.get(f)!==e)throw new Error(`Lazy atomic tree lost deferred ownership of ${c.vfsPath}`);let y=i.get(c.inodeGroup);if(y!==void 0&&y!==f)throw new Error(`Lazy atomic tree split hard links at ${c.vfsPath}`);i.set(c.inodeGroup,f),n.add(f)}a.push({path:c.vfsPath,expectedIno:l.ino,expectedGeneration:l.generation,expectedDataSequence:l.dataSequence,expectedMode:l.mode,expectedLinkCount:l.linkCount,expectedSize:l.size,expectedUid:l.uid,expectedGid:l.gid})}return{guards:a,pendingIdentities:n.size}}assertLazyAtomicSnapshotMatchesPublic(e){let t=this.sealedLazyAtomicStates.get(e),n=t?.snapshot,i=e.activation?.atomicGroup,s=n?.member??i?.member??"unknown",o;if(n!==void 0)try{o=pn(e,n.id,n.member)}catch{o=void 0}if(t===void 0||n===void 0||i===void 0||!pt(i)||i.id!==n.id||i.member!==n.member||i.descriptorSha256!==n.descriptorSha256||i.expectedCount!==n.expectedCount||i.cohortSha256!==n.cohortSha256||o===void 0||!Qi(n,o))throw new Error(`Lazy atomic activation member ${s} changed after sealing`);return t}assertPendingLazyAtomicSnapshotsReadyForSerialization(){for(let e of this.lazyArchiveGroups){if(!this.sealedLazyAtomicStates.has(e)||this.lazyAtomicGroupByTree.get(e)?.committed)continue;let n=this.assertLazyAtomicSnapshotMatchesPublic(e);if(!n.verified)throw new Error(`Lazy atomic activation group ${n.snapshot.id} has not been cryptographically verified after import`)}}async validatePendingLazyAtomicGroupSeals(e){for(let t of this.lazyAtomicGroups.values()){if(t.committed||t.expectedCount===void 0||t.cohortSha256===void 0)continue;let n=[...t.groups.entries()].sort(([i],[s])=>is?1:0).map(([,i])=>i);await this.ensureLazyAtomicGroupSealValidated(t,n,e)}}async ensureLazyAtomicGroupSealValidated(e,t,n){if(this.assertLazyAtomicGroupSealValidatedAtLinearization(e,t,n))return;let i=e.sealValidationFlight;if(i===void 0&&(i=this.validateLazyAtomicGroupSealOnce(e,t),e.sealValidationFlight=i,i.then(()=>{e.sealValidationFlight===i&&(e.sealValidationFlight=void 0)},()=>{e.sealValidationFlight===i&&(e.sealValidationFlight=void 0)})),await i,!this.assertLazyAtomicGroupSealValidatedAtLinearization(e,t,n))throw new Error(`Lazy atomic activation group ${e.id} seal verification did not authenticate every member`)}assertLazyAtomicGroupSealValidatedAtLinearization(e,t,n){if(e.committed)return!0;if(e.expectedCount===void 0||e.cohortSha256===void 0||t.length!==e.expectedCount)throw new Error(`Lazy atomic activation group ${e.id} is not completely sealed`);let i=!0,s=[];for(let o of t){let a=this.assertLazyAtomicSnapshotMatchesPublic(o),c=a.snapshot;if(c.id!==e.id||c.expectedCount!==e.expectedCount||c.cohortSha256!==e.cohortSha256||e.groups.get(c.member)!==o)throw new Error(`Lazy atomic activation group ${e.id} has an inconsistent member`);i&&=a.verified,s.push(a)}if(i&&n)for(let o=0;ohp?1:0).map(([,h])=>h);if(t.length===0)throw new Error(`Lazy atomic activation group ${e.id} has no trees`);if(await this.ensureLazyAtomicGroupSealValidated(e,t,!0),e.committed)return;let n=t.map(h=>this.sealedLazyAtomicStates.get(h).snapshot),i=t.map((h,p)=>({group:h,...this.collectAtomicTreeNamespace(h,n[p])})),s=this.lazyTransport,o=new Array(t.length),a=0,c=!1,l,u=Array.from({length:Math.min(_a,t.length)},async()=>{for(;!c;){let h=a++;if(h>=t.length)return;let p=t[h],w=n[h];try{let m=await this.fetchLazyArchiveData(p,s,w);J(s.signal),o[h]={group:p,snapshot:w,contents:await this.prepareLazyArchiveContents(p,m,s.signal,w)}}catch(m){c||(c=!0,l=m)}}});if(await Promise.all(u),c)throw o.fill(void 0),l;J(s.signal);for(let h of t)this.assertLazyAtomicSnapshotMatchesPublic(h);let d=[],f=[],y=[];for(let h=0;h{let a=this.lazyAtomicGroupByTree.get(o);return this.sealedLazyAtomicStates.get(o)?.snapshot===void 0?!o.materialized&&o.content!==void 0&&o.inventory!==void 0:!a?.committed});if(this.lazyFiles.size===0&&this.lazyArchiveInodes.size===0&&n.length===0)return;let i=Array.from(this.lazyFiles.values(),o=>o.path);for(let o of i)await this.ensureMaterialized(o);let s=new Set(this.lazyArchiveInodes.values());for(let o of n)s.add(o);for(let o of s)await this.prepareLazyTreeGroup(o)}this.reconcileLazyIdentityState(this.fs.identityState());let e=this.lazyArchiveGroups.some(t=>{let n=this.lazyAtomicGroupByTree.get(t);return this.sealedLazyAtomicStates.get(t)?.snapshot===void 0?!t.materialized&&t.content!==void 0&&t.inventory!==void 0:!n?.committed});if(this.lazyFiles.size!==0||this.lazyArchiveInodes.size!==0||e)throw new Error("Cannot create a self-contained VFS image while lazy entries remain pending")}async saveImage(e){e?.materializeAll&&await this.materializeAllLazyEntries(),await this.validatePendingLazyAtomicGroupSeals(!1);let{bytes:t,identities:n}=this.fs.snapshotState({normalizeTimestampsMs:e?.normalizeTimestampsMs});this.reconcileLazyIdentityState(n);let i=this.serializeLazyEntries(),s=i.length>0,o=s?new TextEncoder().encode(JSON.stringify(i)):new Uint8Array(0);if(o.byteLength>gn)throw new Error(`VFS image lazy metadata exceeds ${gn} bytes`);let a=this.serializeValidatedLazyArchiveEntries(n),c=a.length>0,l=c?new TextEncoder().encode(JSON.stringify(a)):new Uint8Array(0);if(l.byteLength>wn)throw new Error(`VFS image lazy archive metadata exceeds ${wn} bytes`);let u=e?.metadata===void 0?this.imageMetadata:e.metadata,d=Ma(u),f=d.byteLength>0,y=c?4+l.byteLength:0,h=f?4+d.byteLength:0,p=de+t.byteLength+4+o.byteLength+y+h,w=new Uint8Array(p),m=new DataView(w.buffer);m.setUint32(0,yr,!0),m.setUint32(4,pr,!0),m.setUint32(8,(s?lr:0)|(c?mn:0)|(c?ur:0)|(f?dr:0),!0),m.setUint32(12,t.byteLength,!0),w.set(t,de);let g=de+t.byteLength;if(m.setUint32(g,o.byteLength,!0),o.byteLength>0&&w.set(o,g+4),c){let S=g+4+o.byteLength;m.setUint32(S,l.byteLength,!0),w.set(l,S+4)}if(f){let S=g+4+o.byteLength+y;m.setUint32(S,d.byteLength,!0),w.set(d,S+4)}return w}static readImageMetadata(e){let t=yn(e);if(!(t.flags&dr))return null;let{metadataOffset:n}=Vi(t.image,t.view,t.flags,t.sabLen);if(t.image.byteLengthmt)throw new Error(`VFS image metadata exceeds ${mt} bytes`);if(t.image.byteLength0){let w=n.subarray(h+4,h+4+p),m=xe(qi(w,"VFS image lazy metadata"),"VFS image lazy entries",0,gt);y.importLazyEntriesInternal(m,!0)}if(s&mn){let w=a.archiveOffset,m=i.getUint32(w,!0);if(m>0){let g=n.subarray(w+4,w+4+m),S=qi(g,"VFS image lazy archive metadata");y.importLazyArchiveEntriesInternal(S,!0,!!(s&ur),"pending")}}return y}adaptStat(e){return{dev:0,ino:e.ino,mode:e.mode,nlink:e.linkCount,uid:e.uid,gid:e.gid,size:e.size,atimeMs:e.atime,mtimeMs:e.mtime,ctimeMs:e.ctime}}adaptStatWithLazySize(e){let t=this.adaptStat(e),n=this.lazyFileForStat(e);if(n)return t.size=n.size,t;let i=this.lazyArchiveForStat(e);if(i){for(let s of this.lazyArchiveEntriesForRead(i))if(s.ino===e.ino&&s.generation===e.generation&&!s.deleted){t.size=s.size;break}}return t}open(e,t,n){(t&Mt)===0&&!((t&Bt)!==0&&(t&qn)!==0)&&this.guardSynchronousLazyAccess(e);let i=this.fs.open(e,t,n);return(t&Mt)!==0&&this.invalidateLazyData(this.fs.fstat(i)),i}close(e){return this.fs.close(e),0}read(e,t,n,i){if(i>0){let s=this.lazyBackingForStat(this.fs.fstat(e));s&&(this.reconcileLazyIdentityState(this.fs.identityState()),s=this.lazyBackingForStat(this.fs.fstat(e)),s&&this.guardSynchronousLazyAccess(s.path))}return n!==null?this.fs.readAt(e,t.subarray(0,i),n):this.fs.read(e,t.subarray(0,i))}write(e,t,n,i){if(n!==null){let o=this.fs.writeAt(e,t.subarray(0,i),n);return o>0&&this.invalidateLazyData(this.fs.fstat(e)),o}let s=this.fs.write(e,t.subarray(0,i));return s>0&&this.invalidateLazyData(this.fs.fstat(e)),s}seek(e,t,n){return this.fs.lseek(e,t,n)}fstat(e){return this.adaptStatWithLazySize(this.fs.fstat(e))}fpathconf(e,t){let n=this.fstat(e);return $n(n,t,{supportsSymlinks:!0,timestampResolutionNs:1e6})}ftruncate(e,t){this.fs.ftruncate(e,t),this.invalidateLazyData(this.fs.fstat(e))}fsync(e){}fchmod(e,t){this.fs.fchmod(e,t)}fchown(e,t,n){this.fs.fchown(e,t,n)}stat(e){return this.adaptStatWithLazySize(this.fs.stat(e))}lstat(e){return this.adaptStatWithLazySize(this.fs.lstat(e))}statfs(e){this.fs.stat(e);let t=this.fs.statfs();return{type:1397114451,bsize:t.blockSize,blocks:t.totalBlocks,bfree:t.freeBlocks,bavail:t.freeBlocks,files:t.totalInodes,ffree:t.freeInodes,fsid:0,namelen:t.maxName,frsize:t.blockSize,flags:0}}pathconf(e,t){let n=this.stat(e);return $n(n,t,{supportsSymlinks:!0,timestampResolutionNs:1e6})}mkdir(e,t){this.fs.mkdir(e,t)}rmdir(e){this.fs.rmdir(e)}unlink(e){let t=this.fs.unlink(e),n=r.inodeKey(t.ino,t.generation);if(t.linkCount>1&&(this.lazyFiles.has(n)||this.lazyArchiveInodes.has(n))){this.reconcileLazyIdentityState(this.fs.identityState());return}let i=this.lazyFiles.get(n);i&&(i.paths.delete(e),t.linkCount<=1?this.lazyFiles.delete(n):i.path===e&&(i.path=i.paths.values().next().value));let s=this.lazyArchiveInodes.get(n);if(s){let o=s.entries.get(e);if(t.linkCount<=1){for(let a of s.entries.values())a.ino===t.ino&&a.generation===t.generation&&(a.deleted=!0);this.lazyArchiveInodes.delete(n)}else o&&s.entries.delete(e)}}rename(e,t){let{source:n,replaced:i}=this.fs.rename(e,t);if(i&&i.ino===n.ino&&i.generation===n.generation)return;let s=!1;if(i){let o=r.inodeKey(i.ino,i.generation);i.linkCount>1&&(this.lazyFiles.has(o)||this.lazyArchiveInodes.has(o))&&(this.reconcileLazyIdentityState(this.fs.identityState()),s=!0);let a=this.lazyFiles.get(o);!s&&a&&(a.paths.delete(t),i.linkCount<=1?this.lazyFiles.delete(o):a.path===t&&(a.path=a.paths.values().next().value));let c=this.lazyArchiveInodes.get(o);if(!s&&c){let l=c.entries.get(t);i.linkCount<=1?(l&&(l.deleted=!0),this.lazyArchiveInodes.delete(o)):l&&c.entries.delete(t)}}s||this.rewriteLazyNamespacePaths(n,e,t)}link(e,t){let n=this.fs.link(e,t),i=r.inodeKey(n.ino,n.generation),s=this.lazyFiles.get(i);s&&s.paths.add(t);let o=this.lazyArchiveInodes.get(i);if(o){let a=Array.from(o.entries.values()).find(c=>c.ino===n.ino&&c.generation===n.generation);a&&o.entries.set(t,{...a})}}symlink(e,t){this.fs.symlink(e,t)}readlink(e){return this.fs.readlink(e)}chmod(e,t){this.fs.chmod(e,t)}chown(e,t,n){this.fs.chown(e,t,n)}lchown(e,t,n){this.fs.lchown(e,t,n)}createFileWithOwner(e,t,n,i,s){let o=this.open(e,577,t);s.length>0&&this.write(o,s,null,s.length),this.close(o),this.chown(e,n,i),this.chmod(e,t)}mkdirWithOwner(e,t,n,i){this.mkdir(e,t),this.chown(e,n,i),this.chmod(e,t)}symlinkWithOwner(e,t,n,i){this.symlink(e,t),this.lchown(t,n,i)}copyPathToFreshFileSystem(e,t,n,i,s){let o=this.lstat(e),a=o.mode&Le,c=o.mode&4095;if(a===He){e==="/"?(t.chown(e,o.uid,o.gid),t.chmod(e,c)):t.mkdirWithOwner(e,c,o.uid,o.gid);let f=this.opendir(e);try{for(;;){let y=this.readdir(f);if(!y)break;y.name==="."||y.name===".."||this.copyPathToFreshFileSystem(e==="/"?`/${y.name}`:`${e}/${y.name}`,t,n,i,s)}}finally{this.closedir(f)}r.applyTimes(t,e,o);return}let l=o.nlink>1?`${o.dev}:${o.ino}`:null,u=l?s.get(l):void 0;if(u){t.link(u,e);return}if(a===hn){t.symlinkWithOwner(this.readlink(e),e,o.uid,o.gid),l&&s.set(l,e);return}if(a!==Kt)throw new Error(`Unsupported file type while rebasing VFS: ${e}`);if(n.has(e)||i.has(e)){t.createFileWithOwner(e,c,o.uid,o.gid,new Uint8Array(0)),r.applyTimes(t,e,o),l&&s.set(l,e);return}this.copyRegularFileToFreshFileSystem(e,t,o,c),l&&s.set(l,e)}copyRegularFileToFreshFileSystem(e,t,n,i){let s=this.open(e,ba,0),o=null;try{o=t.open(e,Aa,i);let a=new Uint8Array(Math.min(ka,Math.max(1,n.size))),c=n.size;for(;c>0;){let l=Math.min(a.byteLength,c),u=this.read(s,a,null,l);if(u<=0)throw new Error(`Unexpected EOF while rebasing VFS file: ${e}`);let d=0;for(;d!e||e==="."||e===".."))throw new Error(`Binary resolver path must be a normalized portable relative path: ${JSON.stringify(r)}`);return r}var Xe=new Set(["wasm32","wasm64"]);function Fe(r){if(pc(r),!r.startsWith("programs/"))return r;let e=r.slice(9),t=e.split("/",1)[0];return Xe.has(t)?r:`programs/wasm32/${e}`}function mc(r,e=K(_r(),"wasm")){let t=Fe(r),n=[K(e,t)];return r==="kernel.wasm"?n.push(K(e,"kandelo-kernel.wasm")):r==="userspace.wasm"?n.push(K(e,"wasm_posix_userspace.wasm")):r==="rootfs.vfs"&&n.push(K(e,"rootfs.vfs")),n}var xn=class extends Error{constructor(e){super(e),this.name="BinaryNotFoundError"}};function ko(){let r=[],e=!1;try{let n=Ye();e=!0;for(let[i,s]of[["local-binaries",K(n,"local-binaries")],["binaries",K(n,"binaries")]])r.push({label:i,root:s,identity:i==="local-binaries"?"local-generation":"program-cache",allowRegularFileClosure:!1,candidatesFor(o){return[K(s,Fe(o))]}})}catch{}let t=K(_r(),"wasm");return r.push({label:"installed package",root:t,identity:"installed-package",allowRegularFileClosure:!e,candidatesFor(n){return mc(n,t)}}),r}function wt(r,e){return new Error(`Invalid package manifest ${r}: ${e}`)}function se(r){try{return In(r),!0}catch(e){if(e instanceof Error&&"code"in e&&e.code==="ENOENT")return!1;throw e}}function mo(r,e,t){if(r.length===0||r.startsWith("/")||r.includes("\\")||r.includes("\0")||r.split("/").some(n=>!n||n==="."||n===".."))throw wt(e,`${t} must be a normalized portable relative path`);return r}function kn(r,e,t,n=!0){if(r.length===0||r==="."||r===".."||r.includes("/")||r.includes("\\")||r.includes("\0")||!n&&r.includes("@"))throw wt(e,`${t} must be a safe single path component`);return r}var go="kandelo-program-packages-v2",Ie="program-packages.json",wo=null,gc=null,Ln=null,Ar=0;function Pr(){return gc??K(_r(),"wasm",Ie)}function Lo(){if(Object.prototype.hasOwnProperty.call(process.env,"WASM_POSIX_DEPS_REGISTRY")){let e=null;return(process.env.WASM_POSIX_DEPS_REGISTRY??"").split(":").filter(Boolean).map(t=>t.startsWith("~/")&&process.env.HOME!==void 0?K(process.env.HOME,t.slice(2)):_n(t)?we(t):(e??=Ye(),we(e,t)))}let r;try{r=K(Ye(),"packages","registry")}catch{return null}return!se(r)&&xo()===null&&se(Pr())?null:[r]}function xo(){let r;try{r=Ye()}catch{return null}if(!jt(K(r,"tools","xtask","Cargo.toml"))||!jt(K(r,"scripts","dev-shell.sh")))return null;try{let e=ve(Ir()),t=ve(r);return[K(t,"host"),K(t,"scripts")].some(i=>jt(i)&&Br(ve(i),e))?t:null}catch{return null}}function Or(r,e,t){let n=[typeof t.stderr=="string"?t.stderr.trim():"",typeof t.stdout=="string"?t.stdout.trim():"",t.error?.message??""].filter(Boolean).join(` +var Oo=Object.defineProperty;var _n=(r,e,t)=>()=>{if(t)throw t[0];try{return r&&(e=r(r=0)),e}catch(n){throw t=[n],n}};var Cr=(r,e)=>{for(var t in e)Oo(r,t,{get:e[t],enumerable:!0})};import{createRequire as Os}from"module";function bi(r,e){return zi(r,{i:2},e&&e.out,e&&e.dictionary)}var Ts,ht,Rs,Bs,te,ut,Cs,pi,mi,Ns,gi,ht,wi,Ms,vi,$s,Qc,Xn,Ae,$,Mt,$t,$,$,$,$,Si,$,Fs,Ds,jn,ge,Yn,Ei,un,Us,fe,zi,Gs,Ks,ft,Ai,Ws,Zs,Jn=_n(()=>{Ts=Os("/");try{ht=Ts("worker_threads"),Rs=ht.Worker,Bs=ht.isMarkedAsUntransferable}catch{}te=Uint8Array,ut=Uint16Array,Cs=Int32Array,pi=new te([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),mi=new te([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),Ns=new te([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),gi=function(r,e){for(var t=new ut(31),n=0;n<31;++n)t[n]=e+=1<>1|($&21845)<<1,Ae=(Ae&52428)>>2|(Ae&13107)<<2,Ae=(Ae&61680)>>4|(Ae&3855)<<4,Xn[$]=((Ae&65280)>>8|(Ae&255)<<8)>>1;Mt=(function(r,e,t){for(var n=r.length,i=0,s=new ut(e);i>c]=l}else for(a=new ut(n),i=0;i>15-r[i]);return a}),$t=new te(288);for($=0;$<144;++$)$t[$]=8;for($=144;$<256;++$)$t[$]=9;for($=256;$<280;++$)$t[$]=7;for($=280;$<288;++$)$t[$]=8;Si=new te(32);for($=0;$<32;++$)Si[$]=5;Fs=Mt($t,9,1),Ds=Mt(Si,5,1),jn=function(r){for(var e=r[0],t=1;te&&(e=r[t]);return e},ge=function(r,e,t){var n=e/8|0;return(r[n]|r[n+1]<<8)>>(e&7)&t},Yn=function(r,e){var t=e/8|0;return(r[t]|r[t+1]<<8|r[t+2]<<16)>>(e&7)},Ei=function(r){return(r+7)/8|0},un=function(r,e,t){return(e==null||e<0)&&(e=0),(t==null||t>r.length)&&(t=r.length),new te(r.subarray(e,t))},Us=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],fe=function(r,e,t){var n=new Error(e||Us[r]);if(n.code=r,Error.captureStackTrace&&Error.captureStackTrace(n,fe),!t)throw n;return n},zi=function(r,e,t,n){var i=r.length,s=n?n.length:0;if(!i||e.f&&!e.l)return t||new te(0);var o=!t,a=o||e.i!=2,c=e.i;o&&(t=new te(i*3));var l=function(Oe){var Te=t.length;if(Oe>Te){var Jt=new te(Math.max(Te*2,Oe));Jt.set(t),t=Jt}},u=e.f||0,d=e.p||0,f=e.b||0,y=e.l,h=e.d,p=e.m,w=e.n,m=i*8;do{if(!y){u=ge(r,d,1);var g=ge(r,d+1,3);if(d+=3,g)if(g==1)y=Fs,h=Ds,p=9,w=5;else if(g==2){var v=ge(r,d,31)+257,A=ge(r,d+10,15)+4,L=v+ge(r,d+5,31)+1;d+=14;for(var x=new te(L),_=new te(19),I=0;I>4;if(S<16)x[I++]=S;else{var U=0,ae=0;for(S==16?(ae=3+ge(r,d,3),d+=2,U=x[I-1]):S==17?(ae=3+ge(r,d,7),d+=3):S==18&&(ae=11+ge(r,d,127),d+=7);ae--;)x[I++]=U}}var N=x.subarray(0,v),Z=x.subarray(v);p=jn(N),w=jn(Z),y=Mt(N,p,1),h=Mt(Z,w,1)}else fe(1);else{var S=Ei(d)+4,z=r[S-4]|r[S-3]<<8,E=S+z;if(E>i){c&&fe(0);break}a&&l(f+z),t.set(r.subarray(S,E),f),e.b=f+=z,e.p=d=E*8,e.f=u;continue}if(d>m){c&&fe(0);break}}a&&l(f+131072);for(var wt=(1<>4;if(d+=U&15,d>m){c&&fe(0);break}if(U||fe(2),Ee<256)t[f++]=Ee;else if(Ee==256){De=d,y=null;break}else{var vt=Ee-254;if(Ee>264){var I=Ee-257,_e=pi[I];vt=ge(r,d,(1<<_e)-1)+wi[I],d+=_e}var Qe=h[Yn(r,d)&Xt],pe=Qe>>4;Qe||fe(3),d+=Qe&15;var Z=$s[pe];if(pe>3){var _e=mi[pe];Z+=Yn(r,d)&(1<<_e)-1,d+=_e}if(d>m){c&&fe(0);break}a&&l(f+131072);var Pe=f+vt;if(f>3&1)+(e>>4&1);n>0;n-=!r[t++]);return t+(e&2)},ft=(function(){function r(e,t){typeof e=="function"&&(t=e,e={}),this.ondata=t;var n=e&&e.dictionary&&e.dictionary.subarray(-32768);this.s={i:0,b:n?n.length:0},this.o=new te(32768),this.p=new te(0),n&&this.o.set(n)}return r.prototype.e=function(e){if(this.ondata||fe(5),this.d&&fe(4),!this.p.length)this.p=e;else if(e.length){var t=new te(this.p.length+e.length);t.set(this.p),t.set(e,this.p.length),this.p=t}},r.prototype.c=function(e){this.s.i=+(this.d=e||!1);var t=this.s.b,n=zi(this.p,this.s,this.o);this.ondata(un(n,t,this.s.b),this.d),this.o=un(n,this.s.b-32768),this.s.b=this.o.length,this.p=un(this.p,this.s.p/8|0),this.s.p&=7},r.prototype.push=function(e,t){this.e(e),this.c(t)},r})();Ai=(function(){function r(e,t){this.v=1,this.r=0,ft.call(this,e,t)}return r.prototype.push=function(e,t){if(ft.prototype.e.call(this,e),this.r+=e.length,this.v){var n=this.p.subarray(this.v-1),i=n.length>3?Ks(n):4;if(i>n.length){if(!t)return}else this.v>1&&this.onmember&&this.onmember(this.r-n.length);this.p=n.subarray(i),this.v=0}ft.prototype.c.call(this,0),this.s.f&&!this.s.l?(this.v=Ei(this.s.p)+9,this.s={i:0},this.o=new te(0),this.push(new te(0),t)):t&&ft.prototype.c.call(this,t)},r})(),Ws=typeof TextDecoder<"u"&&new TextDecoder,Zs=0;try{Ws.decode(Gs,{stream:!0}),Zs=1}catch{}});var tr={};Cr(tr,{extractZipEntry:()=>Qs,extractZipEntryBounded:()=>ea,fetchZipCentralDirectory:()=>na,parseZipCentralDirectory:()=>Ft});function Pi(r){let e=new DataView(r.buffer,r.byteOffset,r.byteLength),t=Math.max(0,r.length-xi);for(let n=r.length-qs;n>=t;n--)if(e.getUint32(n,!0)===Hs)return n;throw new Error("Zip EOCD record not found")}function Ft(r){let e=new DataView(r.buffer,r.byteOffset,r.byteLength),t=Pi(r),n=e.getUint16(t+10,!0),i=e.getUint32(t+16,!0),s=[],o=i;for(let a=0;a>8,z;S===Li?z=p>>16&65535:g.startsWith("bin/")||g.startsWith("sbin/")||g.includes("/bin/")||g.includes("/sbin/")?z=493:z=420;let E=g.endsWith("/"),v=S===Li&&(z&Ys)===js;s.push({fileName:g,fileNameBytes:m,compressedSize:u,uncompressedSize:d,compressionMethod:l,localHeaderOffset:w,mode:z,isDirectory:E,isSymlink:v,externalAttrs:p,creatorOS:S}),o+=Qn+f+y+h}return s}function Oi(r,e){if(r.byteLength!==e.byteLength)return!1;for(let t=0;t{if(a.byteLength>t-s)throw new Error(`ZIP member ${e.fileName} expands beyond ${t} bytes`);i.set(a,s),s+=a.byteLength}).push(n,!0),s!==t)throw new Error(`ZIP member ${e.fileName} expanded ${s} bytes, expected ${t}`);return i}function ta(r,e){let t=new DataView(r.buffer,r.byteOffset,r.byteLength),n=e.localHeaderOffset;if(n<0||n>r.byteLength-er||t.getUint32(n,!0)!==ki)throw new Error(`Invalid local file header signature at offset ${n}`);let i=t.getUint16(n+8,!0),s=t.getUint16(n+26,!0),o=t.getUint16(n+28,!0),a=n+er,c=a+s+o,l=c+e.compressedSize;if(i!==e.compressionMethod||cr.byteLength||!Oi(r.subarray(a,a+s),e.fileNameBytes))throw new Error(`ZIP member ${e.fileName} has inconsistent local metadata`);return r.subarray(c,l)}async function na(r){let e=await fetch(r,{method:"HEAD"});if(!e.ok)throw new Error(`HEAD request failed: ${e.status} ${e.statusText}`);let t=parseInt(e.headers.get("content-length")||"0",10),n=e.headers.get("accept-ranges");if(!t||n!=="bytes"){let m=await fetch(r);if(!m.ok)throw new Error(`Fetch failed: ${m.status} ${m.statusText}`);let g=new Uint8Array(await m.arrayBuffer());return{entries:Ft(g),totalSize:g.length}}let i=Math.min(t,xi),s=t-i,o=await fetch(r,{headers:{Range:`bytes=${s}-${t-1}`}});if(o.status!==206){let m=await fetch(r);if(!m.ok)throw new Error(`Fetch failed: ${m.status} ${m.statusText}`);let g=new Uint8Array(await m.arrayBuffer());return{entries:Ft(g),totalSize:g.length}}let a=new Uint8Array(await o.arrayBuffer()),c=new DataView(a.buffer,a.byteOffset,a.byteLength),l=Pi(a),u=c.getUint32(l+12,!0),d=c.getUint32(l+16,!0);if(d>=s){let m=t,g=new Uint8Array(m);return g.set(a,s),{entries:Ft(g),totalSize:m}}let f=d+u-1,y=await fetch(r,{headers:{Range:`bytes=${d}-${f}`}});if(y.status!==206)throw new Error(`Range request for CD failed: ${y.status}`);let h=new Uint8Array(await y.arrayBuffer()),p=t,w=new Uint8Array(p);return w.set(h,d),w.set(a,s),{entries:Ft(w),totalSize:p}}var Hs,Vs,ki,xi,qs,Qn,er,Ii,_i,Li,js,Ys,Xs,Js,nr=_n(()=>{"use strict";Jn();Hs=101010256,Vs=33639248,ki=67324752,xi=65557,qs=22,Qn=46,er=30,Ii=0,_i=8,Li=3,js=40960,Ys=61440,Xs=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}),Js=new TextEncoder});var $i={};Cr($i,{DEFAULT_TAR_GZIP_LIMITS:()=>Mi,TarParseError:()=>P,parseTarGzip:()=>sa});function sa(r,e={}){let t=e.label??"TAR gzip archive",n=ca(e.limits,t);if(r.byteLength===0||r.byteLength>n.maxCompressedBytes)throw new P(`${t}: compressed byte count ${r.byteLength} is outside 1..${n.maxCompressedBytes}`);let i=la(r,t);if(i===0||i>n.maxUncompressedBytes)throw new P(`${t}: declared uncompressed byte count ${i} is outside 1..${n.maxUncompressedBytes}`);let s=da(r,t,i);if(s.byteLength!==i)throw new P(`${t}: gzip expanded to ${s.byteLength} bytes, expected ${i}`);let o=new DataView(r.buffer,r.byteOffset,r.byteLength).getUint32(r.byteLength-8,!0);if(ua(s)!==o)throw new P(`${t}: gzip CRC32 mismatch`);return aa(s,t,n)}function aa(r,e,t){if(r.byteLength%Le!==0)throw new P(`${e}: TAR byte count is not block-aligned`);let n=[],i=0,s=0,o=0,a=null,c={},l=!1;for(;i+Le<=r.byteLength;){let u=r.subarray(i,i+Le);if(i+=Le,ir(u)){if(i+Le>r.byteLength)throw new P(`${e}: TAR end marker is truncated`);let E=r.subarray(i,i+Le);if(!ir(E))throw new P(`${e}: TAR has only one zero end block`);if(i+=Le,!ir(r.subarray(i)))throw new P(`${e}: TAR has nonzero data after its end marker`);l=!0;break}pa(u,e);let d=Dt(u,156,1,e)||"0",f=sr(u,124,12,`${e}: TAR entry size`),y=sr(u,100,8,`${e}: TAR entry mode`)&ra,h=ma(u,e,t.maxPathBytes),p=Dt(u,157,100,e);if(d==="x"||d==="g"){if(o+=1,o>t.maxEntries+1)throw new P(`${e}: TAR extension header count exceeds ${t.maxEntries+1}`);let E=Ri(r,i,f,e);i=Bi(i,f,r.byteLength,e);let v=ha(E,e,t);d==="x"?a=v:c={...c,...v};continue}if(s+=1,s>t.maxEntries)throw new P(`${e}: TAR entry count exceeds ${t.maxEntries}`);let w={...c,...a??{}};a=null;let m=w.size===void 0?f:ya(w.size,`${e}: PAX entry size`),g=Ri(r,i,m,e);i=Bi(i,m,r.byteLength,e);let S=or(w.path??h,e,t.maxPathBytes),z=w.linkpath??p;switch(d){case"0":case"\0":n.push({path:S,type:"file",mode:y,data:g});break;case"5":rr(m,e,"directory",S),n.push({path:S,type:"directory",mode:y});break;case"2":rr(m,e,"symlink",S),Ci(z,e,S,t.maxLinkBytes,!1),n.push({path:S,type:"symlink",mode:y,linkName:z});break;case"1":rr(m,e,"hardlink",S),Ci(z,e,S,t.maxLinkBytes,!0),n.push({path:S,type:"hardlink",mode:y,linkName:or(z,`${e}: hardlink target`,t.maxPathBytes)});break;case"3":case"4":case"6":throw new P(`${e}: unsupported TAR device/FIFO entry ${S}`);default:throw new P(`${e}: unsupported TAR entry type ${JSON.stringify(d)} for ${S}`)}}if(!l)throw new P(`${e}: TAR is missing its two-block end marker`);if(a!==null)throw new P(`${e}: local PAX header has no following entry`);return n}function ca(r,e){let t={...Mi,...r};for(let[n,i]of Object.entries(t))if(!Number.isSafeInteger(i)||i<=0)throw new P(`${e}: ${n} must be a positive safe integer`);return t}function la(r,e){if(r.byteLength<18||r[0]!==31||r[1]!==139||r[2]!==8)throw new P(`${e}: invalid gzip header`);return new DataView(r.buffer,r.byteOffset,r.byteLength).getUint32(r.byteLength-4,!0)}function da(r,e,t){let n=new Uint8Array(t),i=0,s=!1,o=new Ai(a=>{if(a.byteLength>t-i)throw new P(`${e}: gzip expansion exceeds its declared ${t} bytes`);n.set(a,i),i+=a.byteLength});o.onmember=()=>{throw s=!0,new P(`${e}: concatenated gzip members are unsupported`)};try{o.push(r,!0)}catch(a){throw a instanceof P?a:new P(`${e}: cannot gunzip archive: ${wa(a)}`)}if(s)throw new P(`${e}: concatenated gzip members are unsupported`);return n.subarray(0,i)}function ua(r){let e=4294967295;for(let t of r)e=oa[(e^t)&255]^e>>>8;return(e^4294967295)>>>0}function fa(){let r=new Uint32Array(256);for(let e=0;e>>1^((t&1)===0?0:3988292384);r[e]=t>>>0}return r}function Ri(r,e,t,n){if(t>r.byteLength-e)throw new P(`${n}: TAR entry is truncated`);return r.subarray(e,e+t)}function Bi(r,e,t,n){let s=Math.ceil(e/Le)*Le;if(!Number.isSafeInteger(s)||s>t-r)throw new P(`${n}: TAR entry padding is truncated`);return r+s}function ha(r,e,t){let n={},i=0;for(;i9)throw new P(`${e}: invalid PAX record length`);if(o=o*10+p,!Number.isSafeInteger(o))throw new P(`${e}: invalid PAX record length`)}let a=i+o;if(o<=s-i+2||a>r.byteLength||r[a-1]!==10)throw new P(`${e}: truncated PAX record`);let c=s+1;for(;c=a-1)throw new P(`${e}: invalid PAX record`);let l=r.subarray(s+1,c);if(l.byteLength>256)throw new P(`${e}: PAX record key is too long`);let u=ar(l,`${e}: PAX record key`),d=r.subarray(c+1,a-1),f=u==="path"?t.maxPathBytes:u==="linkpath"?t.maxLinkBytes:u==="size"?32:0;if(f===0){i=a;continue}if(d.byteLength>f)throw new P(`${e}: PAX ${u} value is too long`);let y=ar(d,`${e}: PAX record value`);n[u]=y,i=a}return n}function ya(r,e){if(!/^(0|[1-9][0-9]*)$/.test(r))throw new P(`${e} is invalid`);let t=Number(r);if(!Number.isSafeInteger(t)||t<0)throw new P(`${e} is invalid`);return t}function pa(r,e){let t=sr(r,148,8,`${e}: TAR checksum`),n=0;for(let i=0;i=148&&i<156?32:r[i];if(t!==n)throw new P(`${e}: TAR checksum mismatch`)}function ma(r,e,t){let n=Dt(r,0,100,e),i=Dt(r,345,155,e);return or(i?`${i}/${n}`:n,e,t)}function or(r,e,t){let n=r;for(;n.startsWith("./");)n=n.slice(2);return n=n.replace(/\/+$/g,""),ga(n,`${e}: TAR path`,t),n}function Dt(r,e,t,n){let i=e,s=e+t;for(;in||r.includes("\0"))throw new P(`${e}: link target for ${t} is invalid`);if(i&&r.includes("\\"))throw new P(`${e}: hardlink target for ${t} is invalid`)}function ga(r,e,t){if(r.length===0||r.startsWith("/")||r.includes("\0")||r.includes("\\")||Ni.encode(r).byteLength>t)throw new P(`${e} ${JSON.stringify(r)} must be a bounded relative POSIX path`);for(let n of r.split("/"))if(n.length===0||n==="."||n==="..")throw new P(`${e} ${JSON.stringify(r)} contains an unsafe path segment`)}function ir(r){for(let e of r)if(e!==0)return!1;return!0}function ar(r,e){try{return ia.decode(r)}catch{throw new P(`${e} contains non-UTF-8 text`)}}function wa(r){return r instanceof Error?r.message:String(r)}var Le,ra,Ti,ia,Ni,oa,Mi,P,Fi=_n(()=>{"use strict";Jn();Le=512,ra=4095,Ti=1024*1024,ia=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}),Ni=new TextEncoder,oa=fa(),Mi=Object.freeze({maxCompressedBytes:256*Ti,maxUncompressedBytes:512*Ti,maxEntries:1e5,maxPathBytes:4096,maxLinkBytes:65536}),P=class extends Error{constructor(e){super(e),this.name="TarParseError"}}});import{existsSync as qt,lstatSync as xn,readdirSync as ic,readFileSync as je,realpathSync as ve,statSync as Ye}from"node:fs";import{createHash as So}from"node:crypto";import{spawnSync as kr}from"node:child_process";import{basename as oc,dirname as Yt,isAbsolute as In,join as K,relative as sc,resolve as we,sep as ac}from"node:path";import{fileURLToPath as cc}from"node:url";var St="kandelo.wpk_fork.linked_frames";var Nr=[75,76,67,70],Et=24,Mr=8,Pn=3,$r=[{bytes:4,chunkHeaderSize:32,nodeHeaderSize:24},{bytes:8,chunkHeaderSize:56,nodeHeaderSize:32}],tt=[{module:"env",name:"__wpk_fork_frame_commit",params:["ptr"],results:[]},{module:"env",name:"__wpk_fork_frame_next",params:["ptr"],results:["ptr"]},{module:"env",name:"__wpk_fork_frame_reserve",params:["ptr"],results:["ptr"]}],zt=[{name:"wpk_fork_abort_begin",params:["ptr"],results:[]},{name:"wpk_fork_abort_end",params:[],results:[]},{name:"wpk_fork_rewind_begin",params:["ptr"],results:[]},{name:"wpk_fork_rewind_end",params:[],results:[]},{name:"wpk_fork_state",params:[],results:["i32"]},{name:"wpk_fork_unwind_begin",params:["ptr"],results:[]},{name:"wpk_fork_unwind_end",params:[],results:[]}];var Fr=["__abi_version","kernel_alloc_scratch","kernel_create_process","kernel_create_process_with_stdio","kernel_dequeue_signal","kernel_exec_prepare","kernel_exec_setup_for_thread","kernel_fork_process","kernel_get_parent_pid","kernel_get_process_exit_signal","kernel_get_process_state","kernel_handle_channel","kernel_has_sa_nocldstop","kernel_host_adapter_manifest_len","kernel_host_adapter_manifest_ptr","kernel_ipc_shmat_for_process","kernel_ipc_shmat_for_task","kernel_ipc_shmdt_for_process","kernel_ipc_shmdt_for_task","kernel_mark_process_signaled","kernel_pipe_has_readers","kernel_posix_timer_fire","kernel_prepare_write_operation","kernel_reap_exited_child","kernel_remove_process","kernel_set_current_tid","kernel_spawn_process","kernel_thread_exit","kernel_validate_task","kernel_wait_child_poll"];var H={LINK_MAX:0,MAX_CANON:1,MAX_INPUT:2,NAME_MAX:3,PATH_MAX:4,PIPE_BUF:5,CHOWN_RESTRICTED:6,NO_TRUNC:7,VDISABLE:8,SYNC_IO:9,ASYNC_IO:10,PRIO_IO:11,SOCK_MAXBUF:12,FILESIZEBITS:13,REC_INCR_XFER_SIZE:14,REC_MAX_XFER_SIZE:15,REC_MIN_XFER_SIZE:16,REC_XFER_ALIGN:17,ALLOC_SIZE_MIN:18,SYMLINK_MAX:19,POSIX2_SYMLINKS:20,FALLOC:21,TEXTDOMAIN_MAX:22,TIMESTAMP_RESOLUTION:23};function k(r,e){let t=0,n=0,i=e;for(;;){let s=r[i++];if(t|=(s&127)<=21&&n<=34?bt(e,t):n===84||n>=92&&n<=99||n>=112&&n<=123||n>=124&&n<=131||n>=156&&n<=159?t+1:t:r===254?n===0||n===1||n===2?bt(e,t):n===3?t:n>=16&&n<=79?bt(e,t):null:null}function No(r,e,t){let[n,i]=k(r,e);e+=i+n;let[s,o]=k(r,e);e+=o+s;let a=r[e++];if(a===0){t.funcImports++;let[,c]=k(r,e);e+=c}else if(a===1){e++;let c=r[e++],[,l]=k(r,e);if(e+=l,c&1){let[,u]=k(r,e);e+=u}}else if(a===2){let c=r[e++],[,l]=k(r,e);if(e+=l,c&1){let[,u]=k(r,e);e+=u}}else a===3&&(t.globalImports++,e+=2);return e}function Qt(r){return r.length>=8&&r[0]===0&&r[1]===97&&r[2]===115&&r[3]===109}function Re(r,e){let[t,n]=k(r,e);return e+=n,[new TextDecoder().decode(r.subarray(e,e+t)),e+t]}function Mo(r,e){if(e.length===0)return!0;let t=new TextEncoder().encode(e);e:for(let n=0;n<=r.length-t.length;n++){for(let i=0;ir);function Dr(r,e,t){if(!t)throw new Error(`function ${e} refers to an unknown type`);let n=r.get(e)??[];n.push(t),r.set(e,n)}function On(r,e){let[t,n]=k(r,e);e+=n;let[,i]=k(r,e);if(e+=i,(t&1)!==0){let[,s]=k(r,e);e+=s}return{flags:t,next:e}}function Fo(r){let e=new Uint8Array(r);if(!Qt(e))throw new Error("not a wasm binary");let t=[],n=[],i=[],s={functionImports:new Map,functionExports:new Map,memoryPointerWidths:[],linkedFrameDescriptors:[],importsKernelFork:!1},o=8;for(;oe.length)throw new Error("wasm section exceeds file size");let f=u,y=!1;if(a===0){let[h,p]=Re(e,f);h===St&&s.linkedFrameDescriptors.push(e.slice(p,d))}else if(a===1){y=!0;let[h,p]=k(e,f);f+=p;for(let w=0;wr[c]===a))throw new Error("linked-frame descriptor has invalid magic");let e=new DataView(r.buffer,r.byteOffset,r.byteLength),t=e.getUint16(4,!0);if(t!==1)throw new Error(`linked-frame descriptor version ${t} is unsupported`);let n=e.getUint16(6,!0);if(n!==Et)throw new Error(`linked-frame descriptor declares size ${n}, expected ${Et}`);let i=e.getUint8(8),s=$r.find(({bytes:a})=>a===i);if(!s)throw new Error(`linked-frame descriptor pointer width ${i} is unsupported`);if(e.getUint8(9)!==Mr)throw new Error(`linked-frame descriptor alignment ${e.getUint8(9)} is unsupported`);let o=e.getUint16(10,!0);if(o!==Pn)throw new Error(`linked-frame descriptor flags 0x${o.toString(16)} do not equal required flags 0x${Pn.toString(16)}`);if(e.getUint32(12,!0)!==s.chunkHeaderSize||e.getUint32(16,!0)!==s.nodeHeaderSize)throw new Error(`linked-frame descriptor header sizes do not match its ${i}-byte pointer width`);return s.bytes}function Ur(r,e){return r==="i32"?127:e===8?126:127}function Gr(r,e,t,n){return r.params.length===e.length&&r.results.length===t.length&&r.params.every((i,s)=>i===Ur(e[s],n))&&r.results.every((i,s)=>i===Ur(t[s],n))}function Kr(r,e,t){let n=i=>i==="ptr"&&t===8?"i64":"i32";return`(${r.map(n).join(", ")}) -> (${e.map(n).join(", ")})`}function Uo(r){let e=[];for(let o of zt){let a=r.functionExports.get(o.name);a&&a.length!==1&&e.push(`duplicate ABI 42 wasm-fork-instrument export ${o.name}`)}let t=zt.filter(({name:o})=>!r.functionExports.has(o)).map(({name:o})=>o);t.length>0&&e.push(`incomplete wasm-fork-instrument exports; missing ${t.join(", ")}`);let n=null;if(r.linkedFrameDescriptors.length===0)e.push(`missing required ${St} descriptor`);else if(r.linkedFrameDescriptors.length!==1)e.push(`has ${r.linkedFrameDescriptors.length} ${St} descriptors, expected exactly one`);else try{n=Do(r.linkedFrameDescriptors[0])}catch(o){e.push(o instanceof Error?o.message:String(o))}let i=tt.filter(({module:o,name:a})=>r.functionImports.has(`${o}.${a}`)),s=r.importsKernelFork||i.length>0;if(s){let o=tt.filter(({module:a,name:c})=>!r.functionImports.has(`${a}.${c}`)).map(({module:a,name:c})=>`${a}.${c}`);o.length>0&&e.push(`incomplete ABI 42 linked-frame imports; missing ${o.join(", ")}`);for(let a of tt){let c=`${a.module}.${a.name}`,l=r.functionImports.get(c);l&&l.length!==1&&e.push(`duplicate ABI 42 linked-frame import ${c}`)}}if(n!==null){if(r.memoryPointerWidths.length!==1)e.push(`ABI 42 fork instrumentation requires exactly one module memory, found ${r.memoryPointerWidths.length}`);else if(r.memoryPointerWidths[0]!==n){let o=n===8?"an":"a";e.push(`ABI 42 linked-frame descriptor declares ${o} ${n}-byte pointer but the module memory uses ${r.memoryPointerWidths[0]}-byte addresses`)}for(let o of zt){let a=r.functionExports.get(o.name);a?.length===1&&!Gr(a[0],o.params,o.results,n)&&e.push(`ABI 42 wasm-fork-instrument export ${o.name} has the wrong signature; expected ${Kr(o.params,o.results,n)}`)}if(s)for(let o of tt){let a=`${o.module}.${o.name}`,c=r.functionImports.get(a);c?.length===1&&!Gr(c[0],o.params,o.results,n)&&e.push(`ABI 42 linked-frame import ${a} has the wrong signature; expected ${Kr(o.params,o.results,n)}`)}}return e}function Go(r){let e=new Uint8Array(r);if(!Qt(e))return[];let t=[],n=8;for(;nt.startsWith("reloc."))}function Zr(r,e={}){let t=[];if(Wo(r)&&t.push("contains asyncify_"),e.expectedAbi!==void 0&&e.expectedAbi!==null){let d=Vo(r);d!==null&&d!==e.expectedAbi&&t.push(`ABI ${d}, expected ${e.expectedAbi}`)}let n=new Set(Ko(r));if(e.requiredExports){let d=e.requiredExports.filter(f=>!n.has(f));d.length>0&&t.push(`missing required exports: ${d.join(", ")}`)}let i=$o.filter(d=>n.has(d)),s=Go(r),o=Wr(r),a=tt.filter(({module:d,name:f})=>s.includes(`${d}.${f}`)),c=o.filter(d=>d===St).length,l=i.length>0||a.length>0||c>0;if(e.forbidForkInstrumentation&&l&&t.push("contains ABI 42 wasm-fork-instrument metadata, imports, or exports"),(e.requireForkInstrumentation??!Zo(r))&&(l||s.includes("kernel.kernel_fork")))try{t.push(...Uo(Fo(r)))}catch(d){t.push(`cannot validate ABI 42 fork-artifact contract: ${d instanceof Error?d.message:String(d)}`)}return t}function Ho(r,e){let t=new Uint8Array(r);if(t.length<8)return null;let n=0,i=null,s=null,o=8;for(;o=c)return null;let p=a;for(let g=0;g=h)return null;let[p,w]=k(t,y);y+=w;for(let m=0;mh)return null}return y}function f(y,h=0){if(h>4)return null;let p=u(y);if(!p)return null;let w=d(p.start,p.end);if(w===null)return null;let m=w,g=p.end;for(;m=32&&S<=38||S===208){let[,z]=k(t,m);m+=z}else if(S>=40&&S<=62)m=bt(t,m);else if(S===63||S===64)m++;else if(S===66){let[,z]=Ro(t,m);m+=z}else if(S===67)m+=4;else if(S===68)m+=8;else if(S===252||S===253||S===254){let z=Co(S,t,m);if(z===null)return null;m=z}}return null}return f(i)}function Vo(r){return Ho(r,"__abi_version")}var qo=ArrayBuffer,j=Uint8Array,en=Uint16Array,jo=Int16Array;var tn=Int32Array,Rn=function(r,e,t){if(j.prototype.slice)return j.prototype.slice.call(r,e,t);(e==null||e<0)&&(e=0),(t==null||t>r.length)&&(t=r.length);var n=new j(t-e);return n.set(r.subarray(e,t)),n},Lt=function(r,e,t,n){if(j.prototype.fill)return j.prototype.fill.call(r,e,t,n);for((t==null||t<0)&&(t=0),(n==null||n>r.length)&&(n=r.length);tr.length)&&(n=r.length);t2046MB)","invalid block type","FSE accuracy too high","match distance too far back","unexpected EOF"],Y=function(r,e,t){var n=new Error(e||Xo[r]);if(n.code=r,Error.captureStackTrace&&Error.captureStackTrace(n,Y),!t)throw n;return n},Hr=function(r,e,t){for(var n=0,i=0;n>>0},Qo=function(r,e){var t=r[0]|r[1]<<8|r[2]<<16;if(t==3126568&&r[3]==253){var n=r[4],i=n>>5&1,s=n>>2&1,o=n&3,a=n>>6;n&8&&Y(0);var c=6-i,l=o==3?4:o,u=Hr(r,c,l);c+=l;var d=a?1<>3);y=h+(h>>3)*(r[5]&7)}y>2145386496&&Y(1);var p=new j((e==1?f||y:e?0:y)+12);return p[0]=1,p[4]=4,p[8]=8,{b:c+d,y:0,l:0,d:u,w:e&&e!=1?e:p.subarray(12),e:y,o:new tn(p.buffer,0,3),u:f,c:s,m:Math.min(131072,y)}}else if((t>>4|r[3]<<20)==25481893)return Jo(r,4)+8;Y(0)},Ge=function(r){for(var e=0;1<t&&Y(3);for(var s=1<0;){var g=Ge(o+1),S=n>>3,z=(1<>(n&7)&z,v=(1<v&&(E-=A)),f[++a]=--E,E==-1?(o+=E,w[--u]=a):o-=E,!E)do{var x=n>>3;c=(r[x]|r[x+1]<<8)>>(n&7)&3,n+=2,a+=c}while(c==3)}(a>255||o)&&Y(0);for(var _=0,I=(s>>1)+(s>>3)+3,M=s-1,V=0;V<=a;++V){var C=f[V];if(C<1){y[V]=-C;continue}for(l=0;l=u)}}for(_&&Y(0),l=0;l>3,{b:i,s:w,n:m,t:h}]},es=function(r,e){var t=0,n=-1,i=new j(292),s=r[e],o=i.subarray(0,256),a=i.subarray(256,268),c=new en(i.buffer,268);if(s<128){var l=kt(r,e+1,6),u=l[0],d=l[1];e+=s;var f=u<<3,y=r[e];y||Y(0);for(var h=0,p=0,w=d.b,m=w,g=(++e<<3)-8+Ge(y);g-=w,!(g>3;if(h+=(r[S]|r[S+1]<<8)>>(g&7)&(1<>3,p+=(r[S]|r[S+1]<<8)>>(g&7)&(1<255&&Y(0)}else{for(n=s-127;t>4,o[t+1]=z&15}++e}var E=0;for(t=0;t11&&Y(0),E+=v&&1<0;--t){var V=c[t];Lt(M,t,V,c[t-1]=V+a[t]*(1<a&&d>3,y=(r[f]|r[f+1]<<8|r[f+2]<<16)>>(u&7);c=(c<>2,o=s<<1,a=s+o;At(r.subarray(n,n+=r[0]|r[1]<<8),e.subarray(0,s),t),At(r.subarray(n,n+=r[2]|r[3]<<8),e.subarray(s,o),t),At(r.subarray(n,n+=r[4]|r[5]<<8),e.subarray(o,a),t),At(r.subarray(n),e.subarray(a),t)},as=function(r,e,t){var n,i=e.b,s=r[i],o=s>>1&3;e.l=s&1;var a=s>>3|r[i+1]<<5|r[i+2]<<13,c=(i+=3)+a;if(o==1)return i>=r.length?void 0:(e.b=i+1,t?(Lt(t,r[i],e.y,e.y+=a),t):Lt(new j(a),r[i]));if(!(c>r.length)){if(o==0)return e.b=c,t?(t.set(r.subarray(i,c),e.y),e.y+=a,t):Rn(r,i,c);if(o==2){var l=r[i],u=l&3,d=l>>2&3,f=l>>4,y=0,h=0;u<2?d&1?f|=r[++i]<<4|(d&2&&r[++i]<<12):f=l>>3:(h=d,d<2?(f|=(r[++i]&63)<<4,y=r[i]>>6|r[++i]<<2):d==2?(f|=r[++i]<<4|(r[++i]&3)<<12,y=r[i]>>2|r[++i]<<6):(f|=r[++i]<<4|(r[++i]&63)<<12,y=r[i]>>6|r[++i]<<2|r[++i]<<10)),++i;var p=t?t.subarray(e.y,e.y+e.m):new j(e.m),w=p.length-f;if(u==0)p.set(r.subarray(i,i+=f),w);else if(u==1)Lt(p,r[i++],w);else{var m=e.h;if(u==2){var g=es(r,i);y+=i-(i=g[0]),e.h=m=g[1]}else m||Y(0);(h?ss:At)(r.subarray(i,i+=y),p.subarray(w),m)}var S=r[i++];if(S){S==255?S=(r[i++]|r[i++]<<8)+32512:S>127&&(S=S-128<<8|r[i++]);var z=r[i++];z&3&&Y(0);for(var E=[ns,rs,ts],v=2;v>-1;--v){var A=z>>(v<<1)+2&3;if(A==1){var L=new j([0,0,r[i++]]);E[v]={s:L.subarray(2,3),n:L.subarray(0,1),t:new en(L.buffer,0,1),b:0}}else A==2?(n=kt(r,i,9-(v&1)),i=n[0],E[v]=n[1]):A==3&&(e.t||Y(0),E[v]=e.t[v])}var x=e.t=E,_=x[0],I=x[1],M=x[2],V=r[c-1];V||Y(0);var C=(c<<3)-8+Ge(V)-M.b,O=C>>3,U=0,ae=(r[O]|r[O+1]<<8)>>(C&7)&(1<>3;var N=(r[O]|r[O+1]<<8)>>(C&7)&(1<>3;var Z=(r[O]|r[O+1]<<8)>>(C&7)&(1<<_.b)-1;for(++S;--S;){var wt=M.s[ae],Xt=M.n[ae],De=_.s[Z],Ee=_.n[Z],vt=I.s[N],_e=I.n[N];O=(C-=vt)>>3;var Qe=1<>>(C&7)&Qe-1);O=(C-=Cn[De])>>3;var Pe=os[De]+((r[O]|r[O+1]<<8|r[O+2]<<16)>>(C&7)&(1<>3;var Ue=is[wt]+((r[O]|r[O+1]<<8|r[O+2]<<16)>>(C&7)&(1<>3,ae=M.t[ae]+((r[O]|r[O+1]<<8)>>(C&7)&(1<>3,Z=_.t[Z]+((r[O]|r[O+1]<<8)>>(C&7)&(1<>3,N=I.t[N]+((r[O]|r[O+1]<<8)>>(C&7)&(1<<_e)-1),pe>3)e.o[2]=e.o[1],e.o[1]=e.o[0],e.o[0]=pe-=3;else{var et=pe-(Ue!=0);et?(pe=et==3?e.o[0]-1:e.o[et],et>1&&(e.o[2]=e.o[1]),e.o[1]=e.o[0],e.o[0]=pe):pe=e.o[0]}for(var v=0;vPe&&(Te=Pe);for(var v=0;v=i){let L=(y+1)*4096;try{e.grow(L)}catch{throw new b(ee)}if(i=Math.floor(e.byteLength/4096),y>=i)throw new b(ee)}new Uint8Array(e).fill(0);let h=new r(e);h.w32(Dn,$n),h.w32(Un,Fn),h.w32(on,4096),h.w32(it,i),h.w32(Be,o),h.w32(Ke,u),h.w32(an,d),h.w32(Xr,f),h.w32(cn,y),h.w32(ws,a),h.w32(vs,c),h.w32(Ss,l),h.w32(It,s),h.w32(Jr,256);let p=d*4096;for(let L=0;L>2)+(L>>5);h.i32[x]|=1<<(L&31)}let w=i-y;Atomics.store(h.i32,ot>>2,w),h.blockAllocHint=y;let m=u*4096;h.i32[m>>2]|=3,Atomics.store(h.i32,sn>>2,o-2),h.inodeAllocHint=2;let g=h.inodeOffset(1);h.w32(g+B,W|493),h.w32(g+F,2),h.w64(g+ie,1);let S=h.blockAlloc();if(S<0)throw new b(ee);h.w32(g+Q,S);let z=S*4096,E=Ne(R+1),v=Ne(R+2);h.w32(z,1),h.view.setUint16(z+4,E,!0),h.view.setUint16(z+6,1,!0),h.u8[z+R]=46;let A=z+E;return h.w32(A,1),h.view.setUint16(A+4,v,!0),h.view.setUint16(A+6,2,!0),h.u8[A+R]=46,h.u8[A+R+1]=46,h.w64(g+T,E+v),Atomics.store(h.i32,Gn>>2,1),h}static inspectImageCapacity(e){if(e.byteLengththis.snapshotBytesUnlocked(e))}snapshotState(e){return this.withNamespaceLock(()=>({bytes:this.snapshotBytesUnlocked(e),identities:this.collectIdentityStateUnlocked()}))}identityState(){return this.withNamespaceLock(()=>this.collectIdentityStateUnlocked())}snapshotBytesUnlocked(e){let t=e?.normalizeTimestampsMs;if(t!==void 0&&(!Number.isSafeInteger(t)||t<0))throw new b(q,"Snapshot timestamp must be a non-negative safe integer in milliseconds");let n=t===void 0?void 0:BigInt(t);for(let a=0;a>2)!==0)throw new b(Wn,"Cannot save a VFS image with open descriptors")}let i=this.r32(Be);for(let a=0;a=1&&this.inodeIsAllocated(a)?n:0n;o.setBigUint64(c+_t,l,!0),o.setBigUint64(c+ce,l,!0),o.setBigUint64(c+X,l,!0)}}return s}collectIdentityStateUnlocked(){let e=new Map,t=this.inodeOffset(1),n=this.r64(t+ie);e.set(`1:${n}`,{ino:1,generation:n,dataSequence:Atomics.load(this.i32,t+oe>>2)>>>0,mode:this.r32(t+B),linkCount:this.r32(t+F),size:this.r64(t+T),uid:this.r32(t+Ot),gid:this.r32(t+Tt),paths:["/"]});let i=[{ino:1,path:"/"}],s=new Set;for(;i.length>0;){let o=i.pop();if(s.has(o.ino))throw new b(D);s.add(o.ino);let a=this.inodeOffset(o.ino);if((this.r32(a+B)&G)!==W)throw new b(D);let c=this.r64(a+T),l=0;for(;l>2)>>>0,mode:_,linkCount:this.r32(v+F),size:this.r64(v+T),uid:this.r32(v+Ot),gid:this.r32(v+Tt),...(_&G)===nt?{symlinkTarget:this.readSymlinkInodeUnlocked(m)}:{},paths:[]},e.set(L,x)}x.paths.push(E),(this.r32(v+B)&G)===W&&i.push({ino:m,path:E})}}p+=g}l+=h}}return e}statfs(){let e=this.r32(on),t=this.r32(it),n=this.r32(It),i=typeof this.buffer.maxByteLength=="number"?this.buffer.maxByteLength:this.buffer.byteLength,s=Math.floor(i/e),o=Math.max(t,Math.min(n,s)),a=Atomics.load(this.i32,ot>>2),c=Math.max(0,o-t);return{blockSize:e,totalBlocks:o,freeBlocks:a+c,totalInodes:this.r32(Be),freeInodes:Atomics.load(this.i32,sn>>2),maxName:255}}r32(e){return this.view.getUint32(e,!0)}w32(e,t){this.view.setUint32(e,t,!0)}r64(e){return Number(this.view.getBigUint64(e,!0))}w64(e,t){this.view.setBigUint64(e,BigInt(t),!0)}waitForAtomicChange(e,t){if(this.atomicsWaitAllowed!==!1)try{Atomics.wait(this.i32,e,t),this.atomicsWaitAllowed=!0;return}catch(n){if(!(n instanceof TypeError))throw n;this.atomicsWaitAllowed=!1}for(;Atomics.load(this.i32,e)===t;);}resetAllocationHints(){this.blockAllocHint=this.findNextFreeBlockHint(),this.inodeAllocHint=this.findNextFreeInodeHint()}findNextFreeBlockHint(){let e=this.r32(it),t=this.r32(cn),n=this.r32(an)*4096;for(let i=t;i>2)+(i>>5),o=i&31;if((Atomics.load(this.i32,s)&1<>2)+(n>>5),s=n&31;if((Atomics.load(this.i32,i)&1<>2;for(;;){if(Atomics.compareExchange(this.i32,e,0,1)===0)return;this.waitForAtomicChange(e,1)}}sbUnlock(){let e=ln>>2;Atomics.store(this.i32,e,0),Atomics.notify(this.i32,e,1/0)}namespaceLock(){let e=dn>>2;for(;;){if(Atomics.compareExchange(this.i32,e,0,1)===0)return;this.waitForAtomicChange(e,1)}}namespaceUnlock(){let e=dn>>2;Atomics.store(this.i32,e,0),Atomics.notify(this.i32,e,1/0)}withNamespaceLock(e){this.namespaceLock();try{return e()}finally{this.namespaceUnlock()}}resetRestoredRuntimeState(){Atomics.store(this.i32,ln>>2,0),Atomics.store(this.i32,dn>>2,0),this.u8.fill(0,256,4096);let e=this.r32(Be),t=this.r32(Ke)*4096;for(let n=0;n>5)*4)&1<<(n&31))===0||this.r32(i+F)!==0)continue;let o=this.r32(i+B),a=this.r64(i+T);(o&G)===nt&&a<=40?(this.u8.fill(0,i+Q,i+Q+40),this.w64(i+T,0)):this.inodeTruncate(n,0),this.inodeFree(n)}}blockAlloc(){let e=this.r32(it),t=this.r32(an)*4096,n=this.r32(cn),i=this.blockAllocHint>=n&&this.blockAllocHint>2)+(a>>5),l=a&31,u=Atomics.load(this.i32,c);if(u&1<>2,1),this.blockAllocHint=a+1>2)+(e>>5),i=e&31;for(;;){let s=Atomics.load(this.i32,n),o=s&~(1<>2,1),e>=this.r32(cn)&&e>2)>0)return 0;let e=this.r32(it),t=this.r32(It),n=this.r32(Jr),i=e+n;if(i>t&&(i=t,n=i-e,n===0))return ee;let s=i*4096;if(this.buffer.byteLength>2,n),Atomics.add(this.i32,Gn>>2,1),this.blockAllocHint=e,0}finally{this.sbUnlock()}}inodeOffset(e){let n=this.r32(Xr)+Math.floor(e/32),i=e%32*128;return n*4096+i}inodeAlloc(){let e=this.r32(Be),t=this.r32(Ke)*4096,n=this.inodeAllocHint>=2&&this.inodeAllocHint>2)+(o>>5),c=o&31,l=Atomics.load(this.i32,a);if(l&1<>2,1),this.inodeAllocHint=o+1>2,1)+1}inodeFree(e){let n=(this.r32(Ke)*4096>>2)+(e>>5),i=e&31;for(;;){let s=Atomics.load(this.i32,n);if((s&1<>2,1),e>=2&&e0&&this.w32(n+Ce,i-1),i<=1&&this.r32(n+F)===0&&(this.inodeTruncate(e,0),t=!0)}finally{this.inodeWriteUnlock(e)}t&&this.inodeFree(e)}inodeDropLinkRefLocked(e){let t=this.inodeOffset(e),n=this.r32(t+F);return n>1?(this.w32(t+F,n-1),this.w64(t+X,Date.now()),!1):this.inodeOrphanLocked(e)}inodeOrphanLocked(e){let t=this.inodeOffset(e);if(this.w32(t+F,0),this.w64(t+X,Date.now()),this.r32(t+Ce)>0)return!1;let n=this.r32(t+B),i=this.r64(t+T);return(n&G)===nt&&i<=40?(this.u8.fill(0,t+Q,t+Q+40),this.w64(t+T,0)):this.inodeTruncate(e,0),!0}inodeReadLock(e){let t=this.inodeOffset(e)+st>>2;for(;;){let n=Atomics.load(this.i32,t);if(n&ri){this.waitForAtomicChange(t,n);continue}if(Atomics.compareExchange(this.i32,t,n,n+1)===n)return}}inodeReadUnlock(e){let t=this.inodeOffset(e)+st>>2;(Atomics.sub(this.i32,t,1)&Es)===1&&Atomics.notify(this.i32,t,1)}inodeWriteLock(e){let t=this.inodeOffset(e)+st>>2;for(;;){let n=Atomics.load(this.i32,t);if(n!==0){this.waitForAtomicChange(t,n);continue}if(Atomics.compareExchange(this.i32,t,0,ri)===0)return}}inodeWriteUnlock(e){let t=this.inodeOffset(e)+st>>2;Atomics.store(this.i32,t,0),Atomics.notify(this.i32,t,1/0)}inodeBlockMap(e,t,n){let i=this.inodeOffset(e);if(t<10){let s=this.r32(i+Q+t*4);if(s!==0)return s;if(!n)return 0;let o=this.blockAllocWithGrow();return o<0||this.w32(i+Q+t*4,o),o}if(t-=10,t<1024){let s=this.r32(i+Pt),o=!1;if(s===0){if(!n)return 0;if(s=this.blockAllocWithGrow(),s<0)return s;this.w32(i+Pt,s),o=!0}let a=s*4096+t*4,c=this.r32(a);if(c!==0)return c;if(!n)return 0;let l=this.blockAllocWithGrow();return l<0?(o&&(this.w32(i+Pt,0),this.blockFree(s)),l):(this.w32(a,l),l)}if(t-=1024,t<1024*1024){let s=Math.floor(t/1024),o=t%1024,a=this.r32(i+at),c=!1;if(a===0){if(!n)return 0;if(a=this.blockAllocWithGrow(),a<0)return a;this.w32(i+at,a),c=!0}let l=a*4096+s*4,u=this.r32(l),d=!1;if(u===0){if(!n)return 0;if(u=this.blockAllocWithGrow(),u<0)return c&&(this.w32(i+at,0),this.blockFree(a)),u;this.w32(l,u),d=!0}let f=u*4096+o*4,y=this.r32(f);if(y!==0)return y;if(!n)return 0;let h=this.blockAllocWithGrow();return h<0?(d&&(this.w32(l,0),this.blockFree(u)),c&&(this.w32(i+at,0),this.blockFree(a)),h):(this.w32(f,h),h)}return q}inodeReadData(e,t,n,i){let s=this.inodeOffset(e),o=this.r64(s+T);if(t>=o)return 0;t+i>o&&(i=o-t);let a=0,c=0;for(;i>0;){let l=Math.floor(t/4096),u=t%4096,d=4096-u;d>i&&(d=i);let f=this.inodeBlockMap(e,l,!1);if(f<=0)n.fill(0,c,c+d);else{let y=f*4096+u;n.set(this.u8.subarray(y,y+d),c)}c+=d,t+=d,i-=d,a+=d}return a}inodeWriteData(e,t,n,i){let s=this.inodeOffset(e),o=this.r64(s+T);t>o&&this.zeroOldEofTail(e,o);let a=0,c=0;for(;i>0;){let l=Math.floor(t/4096),u=t%4096,d=4096-u;d>i&&(d=i);let f=this.inodeBlockMap(e,l,!0);if(f<0){if(a===0)return f;break}let y=f*4096+u;this.u8.set(n.subarray(c,c+d),y),c+=d,t+=d,i-=d,a+=d}if(a>0&&t>this.r64(s+T)&&this.w64(s+T,t),a>0){let l=Date.now();this.w64(s+ce,l),this.w64(s+X,l),Atomics.add(this.i32,s+oe>>2,1)}return a}zeroInodeRange(e,t,n){for(;t0){let c=a*4096+s;this.u8.fill(0,c,c+o)}t+=o}}zeroOldEofTail(e,t){let n=t%4096;if(n===0)return;let i=Math.floor(t/4096),s=this.inodeBlockMap(e,i,!1);if(s<=0)return;let o=s*4096+n;this.u8.fill(0,o,s*4096+4096)}freeBlocksFrom(e,t){let n=this.inodeOffset(e);for(let o=t;o<10;o++){let a=this.r32(n+Q+o*4);a&&(this.blockFree(a),this.w32(n+Q+o*4,0))}let i=this.r32(n+Pt);if(i){let o=t>10?t-10:0;for(let a=o;a<1024;a++){let c=i*4096+a*4,l=this.r32(c);l&&(this.blockFree(l),this.w32(c,0))}o===0&&(this.blockFree(i),this.w32(n+Pt,0))}let s=this.r32(n+at);if(s){let o=t>1034?t-10-1024:0,a=Math.floor(o/1024);for(let c=a;c<1024;c++){let l=s*4096+c*4,u=this.r32(l);if(!u)continue;let d=c===a?o%1024:0;for(let f=d;f<1024;f++){let y=u*4096+f*4,h=this.r32(y);h&&(this.blockFree(h),this.w32(y,0))}d===0&&(this.blockFree(u),this.w32(l,0))}a===0&&(this.blockFree(s),this.w32(n+at,0))}}inodeTruncate(e,t,n=!1){let i=this.inodeOffset(e),s=this.r64(i+T),o=t!==s;if(t>=s){if(t>s&&this.zeroOldEofTail(e,s),this.w64(i+T,t),o||n){let c=Date.now();this.w64(i+ce,c),this.w64(i+X,c),Atomics.add(this.i32,i+oe>>2,1)}return}t%4096!==0&&this.zeroInodeRange(e,t,Math.ceil(t/4096)*4096);let a=Math.ceil(t/4096);if(this.freeBlocksFrom(e,a),this.w64(i+T,t),o||n){let c=Date.now();this.w64(i+ce,c),this.w64(i+X,c),Atomics.add(this.i32,i+oe>>2,1)}}validateFileSize(e){if(!Number.isSafeInteger(e)||e<0)throw new b(q);if(e>ct)throw new b(Rt)}validateSeekPosition(e){if(!Number.isSafeInteger(e))throw new b(ai);if(e<0)throw new b(q);if(e>ct)throw new b(Rt)}touchDirectoryMutation(e){let t=this.inodeOffset(e),n=Date.now();this.w64(t+ce,n),this.w64(t+X,n);let i=Atomics.add(this.i32,t+Qr>>2,1)+1>>>0,s=this.dirIndexes.get(e);s&&(s.mutationSequence=i,s.size=this.r64(t+T))}dirNameKey(e){return lt(e)}dirEntryNameMatches(e,t){if(this.view.getUint16(e+6,!0)!==t.length)return!1;for(let i=0;i=R&&n%4===0&&e+n<=t&&i<=n-R}inodeIsAllocated(e){let t=this.r32(Be);if(e<=0||e>=t)return!1;let n=this.r32(Ke)*4096;return(Atomics.load(this.i32,(n>>2)+(e>>5))&1<<(e&31))!==0}rebuildDirIndex(e,t,n,i){let s=new Map,o=[],a=0;for(;a4096-u&&(y=4096-u);let h=u;for(;h=R&&o.push({abs:p,recLen:m});h+=m}a+=y}let c={generation:t,mutationSequence:n,size:i,entries:s,free:o};return this.dirIndexes.set(e,c),c}getDirIndex(e){let t=this.inodeOffset(e),n=this.r64(t+T),i=this.r64(t+ie),s=Atomics.load(this.i32,t+Qr>>2)>>>0,o=this.dirIndexes.get(e);return o&&o.generation===i&&o.mutationSequence===s&&o.size===n?o:(o&&this.dirIndexes.delete(e),n=0;o--){let a=e.free[o];if(!(a.recLen4096-c&&(d=4096-c);let f=c;for(;fn)return-1;a=c,o+=l}return o===n?a:-1}dirAppendEntry(e,t,n,i=-1){let s=this.inodeOffset(e),o=this.r64(s+T),a=Ne(R+t.length),c=o,l=Math.floor(c/4096),u=c%4096,d=0;if(u!==0&&u+a>4096){let h=4096-u,p=0;if(h>=R){if(p=this.inodeBlockMap(e,l,!1),p<=0)return D}else if(i<0&&(i=this.findLastDirEntryInBlock(e,l,u)),i<0)return D;if(d=this.inodeBlockMap(e,l+1,!0),d<0)return d;if(h>=R){let w=p*4096+u;this.w32(w,0),this.view.setUint16(w+4,h,!0),this.view.setUint16(w+6,0,!0)}else{let m=this.view.getUint16(i+4,!0)+h;this.view.setUint16(i+4,m,!0),this.updateDirIndexRecLen(e,i,m)}c=(l+1)*4096,l++,u=0}let f;if(u===0){if(f=d||this.inodeBlockMap(e,l,!0),f<0)return f}else if(f=this.inodeBlockMap(e,l,!1),f<=0)return D;let y=f*4096+u;return this.w32(y,n),this.view.setUint16(y+4,a,!0),this.view.setUint16(y+6,t.length,!0),this.u8.set(t,y+R),this.w64(s+T,c+a),this.touchDirectoryMutation(e),this.updateDirIndexAdd(e,t,n,y,a),0}dirAddEntry(e,t,n){let i=this.getDirIndex(e);if(typeof i=="number")return i;if(i)return this.useDirIndexFreeSlot(i,e,t,n)?0:this.dirAppendEntry(e,t,n);let s=this.inodeOffset(e),o=this.r64(s+T),a=Ne(R+t.length),c=-1,l=0;for(;l4096-d&&(h=4096-d);let p=d;for(;pd+h||S>g-R)return D;if(m===0&&g>=a)return this.w32(w,n),this.view.setUint16(w+6,t.length,!0),this.u8.set(t,w+R),this.touchDirectoryMutation(e),this.updateDirIndexAdd(e,t,n,w,g),0;let z=Ne(R+S),E=g-z;if(m!==0&&E>=a){this.view.setUint16(w+4,z,!0);let v=w+z;return this.w32(v,n),this.view.setUint16(v+4,E,!0),this.view.setUint16(v+6,t.length,!0),this.u8.set(t,v+R),this.touchDirectoryMutation(e),this.updateDirIndexAdd(e,t,n,v,E),0}c=w,p+=g}l+=h}return this.dirAppendEntry(e,t,n,c)}dirRemoveEntry(e,t){let n=this.getDirIndex(e);if(typeof n=="number")return n;if(n){let a=this.dirNameKey(t),c=n.entries.get(a);if(!c)return me;if(this.r32(c.abs)===c.ino&&this.view.getUint16(c.abs+4,!0)===c.recLen&&this.view.getUint16(c.abs+6,!0)===c.nameLen&&this.dirEntryNameMatches(c.abs,t))return this.w32(c.abs,0),n.entries.delete(a),n.free.push({abs:c.abs,recLen:c.recLen}),this.touchDirectoryMutation(e),0;n.entries.delete(a)}let i=this.inodeOffset(e),s=this.r64(i+T),o=0;for(;o4096-c&&(d=4096-c);let f=c;for(;f4096-l&&(f=4096-l);let y=l;for(;y4096-o&&(l=4096-o);let u=o;for(;uo+l||h>y-R)throw new b(D);if(f!==0){if(h===1&&this.u8[d+R]===46){u+=y;continue}if(h===2&&this.u8[d+R]===46&&this.u8[d+R+1]===46){u+=y;continue}return!1}u+=y}i+=l}return!0}dirIsAncestor(e,t){let n=t;for(let i=0;i<8*1024;i++){if(n===e)return!0;if(n===1)return!1;let s=this.dirLookup(n,ii);if(s<0||s===n)throw new b(D);n=s}throw new b(D)}pathResolve(e,t){if(!e.startsWith("/"))return me;let n=1,i=e.split("/").filter(o=>o.length>0),s=0;for(let o=0;o255)return Zn;let c=ue.encode(a),l;this.inodeReadLock(n);try{let f=this.inodeOffset(n);if((this.r32(f+B)&G)!==W)return ze;l=this.dirLookup(n,c)}finally{this.inodeReadUnlock(n)}if(l<0)return l;let u=this.inodeOffset(l);if((this.r32(u+B)&G)===nt&&(!(o===i.length-1)||t)){if(++s>8)return si;let y=this.r64(u+T),h;if(y<=40)h=lt(this.u8.subarray(u+Q,u+Q+y));else{let p=new Uint8Array(y);this.inodeReadData(l,0,p,y),h=Ct.decode(p)}if(h.startsWith("/")){n=1;let p=h.split("/").filter(m=>m.length>0),w=i.slice(o+1);i.length=0,i.push(...p,...w),o=-1}else{let p=h.split("/").filter(m=>m.length>0),w=i.slice(o+1);i.length=o,i.push(...p,...w),o--}continue}n=l}return n}pathResolveParent(e){if(!e.startsWith("/"))throw new b(q,"Path must be absolute");let t=e.split("/").filter(c=>c.length>0);if(t.length===0)throw new b(q,"Cannot operate on /");let n=t.pop();if(n.length>255)throw new b(Zn);let i="/"+t.join("/"),s=this.pathResolve(i,!0);if(s<0)throw new b(s);let o=this.inodeOffset(s);if((this.r32(o+B)&G)!==W)throw new b(ze);return{parentIno:s,name:n}}fdAlloc(e,t,n){for(let i=0;i>2;if(Atomics.compareExchange(this.i32,o,0,1)===0)return this.w32(s+ei,e),this.w64(s+We,0),this.w32(s+ti,t),this.w32(s+ni,n?1:0),this.inodeAddOpenRef(e)?i:(Atomics.store(this.i32,o,0),me)}return oi}fdGet(e){if(e<0||e>=nn)return null;let t=256+e*24;return Atomics.load(this.i32,t>>2)?{base:t,ino:this.r32(t+ei),offset:this.r64(t+We),flags:this.r32(t+ti),isDir:this.r32(t+ni)!==0}:null}fdFree(e){if(e>=0&&e>2,0)}}buildStat(e){let t=this.inodeOffset(e);return{ino:e,generation:this.r64(t+ie),dataSequence:this.r32(t+oe),mode:this.r32(t+B),linkCount:this.r32(t+F),size:this.r64(t+T),mtime:this.r64(t+ce),ctime:this.r64(t+X),atime:this.r64(t+_t),uid:this.r32(t+Ot),gid:this.r32(t+Tt)}}namespaceEntryIdentity(e){let t=this.inodeOffset(e);return{ino:e,generation:this.r64(t+ie),linkCount:this.r32(t+F),mode:this.r32(t+B)}}open(e,t,n=420){return this.withNamespaceLock(()=>this.openUnlocked(e,t,n))}createLazyStub(e,t){return this.withNamespaceLock(()=>{let n=this.openUnlocked(e,Yr|Bt,t);try{let i=this.fdGet(n);if(!i)throw new b(ne);this.inodeWriteLock(i.ino);try{return this.inodeTruncate(i.ino,0,!0),this.buildStat(i.ino)}finally{this.inodeWriteUnlock(i.ino)}}finally{this.closeUnlocked(n)}})}replaceIfIdentity(e,t,n,i,s){return this.withNamespaceLock(()=>{let o=this.pathResolve(e,!0);if(o<0||o!==t)return!1;let a=this.inodeOffset(o);if(this.r64(a+ie)!==n||this.r32(a+oe)!==i||(this.r32(a+B)&G)!==xt)return!1;this.validateFileSize(s.byteLength),this.inodeWriteLock(o);try{if(this.r64(a+ie)!==n||this.r32(a+oe)!==i||this.r64(a+T)!==0)return!1;let c=this.r64(a+ce),l=this.r64(a+X);this.inodeTruncate(o,0,!0);let u=s.byteLength>0?this.inodeWriteData(o,0,s,s.byteLength):0;if(u!==s.byteLength)throw this.inodeTruncate(o,0,!0),Atomics.store(this.i32,a+oe>>2,i),this.w64(a+ce,c),this.w64(a+X,l),new b(u<0?u:ee);return!0}finally{this.inodeWriteUnlock(o)}})}replaceManyIfIdentities(e,t=[]){return e.length===0&&t.length===0?!0:this.withNamespaceLock(()=>{let n=[],i=new Set,s=a=>{let c=this.pathResolve(a.path,!1);if(c<0||c!==a.expectedIno)return!1;let l=this.inodeOffset(c);return this.r64(l+ie)===a.expectedGeneration&&this.r32(l+oe)===a.expectedDataSequence&&this.r32(l+B)===a.expectedMode&&this.r32(l+F)===a.expectedLinkCount&&this.r64(l+T)===a.expectedSize&&this.r32(l+Ot)===a.expectedUid&&this.r32(l+Tt)===a.expectedGid};for(let a of t)if(!s(a))return!1;for(let a of e){this.validateFileSize(a.data.byteLength);let c=-1;for(let l of a.paths){let u=this.pathResolve(l,!0);if(u!==a.expectedIno)continue;let d=this.inodeOffset(u);if(this.r64(d+ie)===a.expectedGeneration&&this.r32(d+oe)===a.expectedDataSequence&&(this.r32(d+B)&G)===xt&&this.r64(d+T)===0){c=u;break}}if(c<0)return!1;if(i.has(c))throw new b(q,"duplicate conditional replacement inode");i.add(c),n.push({...a,ino:c})}let o=[...i].sort((a,c)=>a-c);for(let a of o)this.inodeWriteLock(a);try{for(let l of n){let u=this.inodeOffset(l.ino);if(this.r64(u+ie)!==l.expectedGeneration||this.r32(u+oe)!==l.expectedDataSequence||(this.r32(u+B)&G)!==xt||this.r64(u+T)!==0)return!1}for(let l of t)if(!s(l))return!1;let a=n.map(l=>{let u=this.inodeOffset(l.ino);return{ino:l.ino,dataSequence:this.r32(u+oe),mtime:this.r64(u+ce),ctime:this.r64(u+X)}}),c=0;try{for(let l of n){c++,this.inodeTruncate(l.ino,0,!0);let u=l.data.byteLength>0?this.inodeWriteData(l.ino,0,l.data,l.data.byteLength):0;if(u!==l.data.byteLength)throw new b(u<0?u:ee)}}catch(l){for(let u=c-1;u>=0;u--){let d=a[u],f=this.inodeOffset(d.ino);this.inodeTruncate(d.ino,0,!0),Atomics.store(this.i32,f+oe>>2,d.dataSequence),this.w64(f+ce,d.mtime),this.w64(f+X,d.ctime)}throw l}return!0}finally{for(let a=o.length-1;a>=0;a--)this.inodeWriteUnlock(o[a])}})}openUnlocked(e,t,n=420){let i=t&rn,s=(t&Bt)!==0,o=(t&Vn)!==0;if(s&&o){let d=this.pathResolve(e,!1);if(d>=0)throw new b(dt);if(d!==me)throw new b(d)}let a=this.pathResolve(e,!0);if(a<0&&a===me&&s){let{parentIno:d,name:f}=this.pathResolveParent(e);this.inodeWriteLock(d);try{let y=ue.encode(f),h=this.dirLookup(d,y);if(h>=0){if(o)throw new b(dt);a=h}else{let p=this.inodeAlloc();if(p<0)throw new b(ee);let w=this.inodeOffset(p);this.w32(w+B,xt|n&4095),this.w32(w+F,1),this.w64(w+T,0);let m=Date.now();this.w64(w+_t,m),this.w64(w+ce,m),this.w64(w+X,m);let g=this.dirAddEntry(d,y,p);if(g<0)throw this.inodeFree(p),new b(g);a=p}}finally{this.inodeWriteUnlock(d)}}if(a<0)throw new b(a);let c=this.inodeOffset(a),l=this.r32(c+B);if((l&G)===W&&i!==rt)throw new b(Ze);if(t&hs&&(l&G)!==W)throw new b(ze);if(t&Nt){if((l&G)===W)throw new b(Ze);this.inodeWriteLock(a),this.inodeTruncate(a,0,!0),this.inodeWriteUnlock(a)}let u=this.fdAlloc(a,t,!1);if(u<0)throw new b(u);return u}close(e){this.withNamespaceLock(()=>this.closeUnlocked(e))}closeUnlocked(e){let t=this.fdGet(e);if(!t)throw new b(ne);this.fdFree(e),this.inodeDropOpenRef(t.ino)}read(e,t){let n=this.fdGet(e);if(!n)throw new b(ne);let i=this.inodeOffset(n.ino);if((this.r32(i+B)&G)===W)throw new b(Ze);this.inodeReadLock(n.ino);try{let o=this.inodeReadData(n.ino,n.offset,t,t.length),a=256+e*24;return this.w64(a+We,n.offset+o),o}finally{this.inodeReadUnlock(n.ino)}}readAt(e,t,n){let i=this.fdGet(e);if(!i)throw new b(ne);let s=this.inodeOffset(i.ino);if((this.r32(s+B)&G)===W)throw new b(Ze);this.validateSeekPosition(n),this.inodeReadLock(i.ino);try{return this.inodeReadData(i.ino,n,t,t.length)}finally{this.inodeReadUnlock(i.ino)}}write(e,t){let n=this.fdGet(e);if(!n)throw new b(ne);if((n.flags&rn)===rt)throw new b(ne);this.inodeWriteLock(n.ino);try{let s=n.offset;if(n.flags&fs){let c=this.inodeOffset(n.ino);s=this.r64(c+T)}if(!Number.isSafeInteger(s)||s<0)throw new b(q);if(s>ct||t.length>ct-s)throw new b(Rt);let o=this.inodeWriteData(n.ino,s,t,t.length);if(o<0)return o;let a=256+e*24;return this.w64(a+We,s+o),o}finally{this.inodeWriteUnlock(n.ino)}}writeAt(e,t,n){let i=this.fdGet(e);if(!i)throw new b(ne);if((i.flags&rn)===rt)throw new b(ne);this.validateSeekPosition(n),this.inodeWriteLock(i.ino);try{if(n>ct||t.length>ct-n)throw new b(Rt);return this.inodeWriteData(i.ino,n,t,t.length)}finally{this.inodeWriteUnlock(i.ino)}}lseek(e,t,n){let i=this.fdGet(e);if(!i)throw new b(ne);let s;if(n===ys)s=t;else if(n===ps)s=i.offset+t;else if(n===ms){let a=this.inodeOffset(i.ino);s=this.r64(a+T)+t}else throw new b(q);this.validateSeekPosition(s);let o=256+e*24;return this.w64(o+We,s),s}ftruncate(e,t){let n=this.fdGet(e);if(!n)throw new b(ne);if((n.flags&rn)===rt)throw new b(ne);this.validateFileSize(t),this.inodeWriteLock(n.ino);try{this.inodeTruncate(n.ino,t,!0)}finally{this.inodeWriteUnlock(n.ino)}}fstat(e){let t=this.fdGet(e);if(!t)throw new b(ne);this.inodeReadLock(t.ino);try{return this.buildStat(t.ino)}finally{this.inodeReadUnlock(t.ino)}}stat(e){return this.withNamespaceLock(()=>this.statUnlocked(e))}statUnlocked(e){let t=this.pathResolve(e,!0);if(t<0)throw new b(t);this.inodeReadLock(t);try{return this.buildStat(t)}finally{this.inodeReadUnlock(t)}}lstat(e){return this.withNamespaceLock(()=>this.lstatUnlocked(e))}lstatUnlocked(e){let t=this.pathResolve(e,!1);if(t<0)throw new b(t);this.inodeReadLock(t);try{return this.buildStat(t)}finally{this.inodeReadUnlock(t)}}unlink(e){return this.withNamespaceLock(()=>this.unlinkUnlocked(e))}unlinkUnlocked(e){let{parentIno:t,name:n}=this.pathResolveParent(e),i=ue.encode(n),s=e.length>1&&e.endsWith("/");this.inodeWriteLock(t);try{let o=this.dirLookup(t,i);if(o<0)throw new b(o);let a=this.inodeOffset(o),c=this.r32(a+B);if(s&&(c&G)!==W)throw new b(ze);if((c&G)===W)throw new b(Ze);let l=this.namespaceEntryIdentity(o),u=this.dirRemoveEntry(t,i);if(u<0)throw new b(u);let d=!1;this.inodeWriteLock(o);try{d=this.inodeDropLinkRefLocked(o)}finally{this.inodeWriteUnlock(o)}return d&&this.inodeFree(o),l}finally{this.inodeWriteUnlock(t)}}rename(e,t){return this.withNamespaceLock(()=>this.renameUnlocked(e,t))}renameUnlocked(e,t){let{parentIno:n,name:i}=this.pathResolveParent(e),{parentIno:s,name:o}=this.pathResolveParent(t);if(Kn(i)||Kn(o))throw new b(q);let a=ue.encode(i),c=ue.encode(o),l=e.length>1&&e.endsWith("/"),u=t.length>1&&t.endsWith("/"),d=Math.min(n,s),f=Math.max(n,s);this.inodeWriteLock(d),d!==f&&this.inodeWriteLock(f);try{let y=this.dirLookup(n,a);if(y<0)throw new b(y);let h=this.inodeOffset(y),w=this.r32(h+B)&G,m=this.namespaceEntryIdentity(y);if((l||u)&&w!==W)throw new b(ze);if(w===W&&this.dirIsAncestor(y,s))throw new b(q);let g=this.dirLookup(s,c),S=!1,z;if(g>=0){if(g===y)return{source:m,replaced:m};z=this.namespaceEntryIdentity(g);let v=this.inodeOffset(g),L=this.r32(v+B)&G;if(w===W&&L!==W)throw new b(ze);if(w!==W&&L===W)throw new b(Ze);let x=!1,_=g===n||g===s;_||this.inodeWriteLock(g);try{if(L===W&&!this.dirIsEmpty(g))throw new b(Hn);let I=this.dirReplaceEntryIno(s,c,y);if(I<0)throw new b(I);x=L===W?this.inodeOrphanLocked(g):this.inodeDropLinkRefLocked(g)}finally{_||this.inodeWriteUnlock(g)}x&&this.inodeFree(g),S=L===W}else{let v=this.dirAddEntry(s,c,y);if(v<0)throw new b(v)}let E=this.dirRemoveEntry(n,a);if(E<0)throw new b(E);if(w===W){if(n!==s){let v=this.inodeOffset(n);this.w32(v+F,this.r32(v+F)-1);let A=this.inodeOffset(s);this.w32(A+F,this.r32(A+F)+1),this.inodeWriteLock(y);try{let L=this.dirReplaceEntryIno(y,ii,s);if(L<0)throw new b(L);this.w64(h+X,Date.now())}finally{this.inodeWriteUnlock(y)}}if(S){let v=this.inodeOffset(s);this.w32(v+F,this.r32(v+F)-1)}}else if(S){let v=this.inodeOffset(s);this.w32(v+F,this.r32(v+F)-1)}return{source:m,replaced:z}}finally{d!==f&&this.inodeWriteUnlock(f),this.inodeWriteUnlock(d)}}mkdir(e,t=493){this.withNamespaceLock(()=>this.mkdirUnlocked(e,t))}mkdirUnlocked(e,t=493){let{parentIno:n,name:i}=this.pathResolveParent(e),s=ue.encode(i);this.inodeWriteLock(n);try{if(this.dirLookup(n,s)>=0)throw new b(dt);let a=this.inodeAlloc();if(a<0)throw new b(ee);let c=this.inodeOffset(a);this.w32(c+B,W|t),this.w32(c+F,2),this.w64(c+T,0);let l=Date.now();this.w64(c+_t,l),this.w64(c+ce,l),this.w64(c+X,l);let u=this.blockAllocWithGrow();if(u<0)throw this.inodeFree(a),new b(ee);this.w32(c+Q,u);let d=u*4096,f=Ne(R+1),y=Ne(R+2);this.w32(d,a),this.view.setUint16(d+4,f,!0),this.view.setUint16(d+6,1,!0),this.u8[d+R]=46;let h=d+f;this.w32(h,n),this.view.setUint16(h+4,y,!0),this.view.setUint16(h+6,2,!0),this.u8[h+R]=46,this.u8[h+R+1]=46,this.w64(c+T,f+y);let p=this.dirAddEntry(n,s,a);if(p<0)throw this.blockFree(u),this.inodeFree(a),new b(p);let w=this.inodeOffset(n);this.w32(w+F,this.r32(w+F)+1)}finally{this.inodeWriteUnlock(n)}}rmdir(e){this.withNamespaceLock(()=>this.rmdirUnlocked(e))}rmdirUnlocked(e){let{parentIno:t,name:n}=this.pathResolveParent(e);if(Kn(n))throw new b(q);let i=ue.encode(n);this.inodeWriteLock(t);try{let s=this.dirLookup(t,i);if(s<0)throw new b(s);let o=this.inodeOffset(s);if((this.r32(o+B)&G)!==W)throw new b(ze);let c=!1;this.inodeWriteLock(s);try{if(!this.dirIsEmpty(s))throw new b(Hn);let u=this.dirRemoveEntry(t,i);if(u<0)throw new b(u);c=this.inodeOrphanLocked(s)}finally{this.inodeWriteUnlock(s)}c&&this.inodeFree(s);let l=this.inodeOffset(t);this.w32(l+F,this.r32(l+F)-1)}finally{this.inodeWriteUnlock(t)}}symlink(e,t){this.withNamespaceLock(()=>this.symlinkUnlocked(e,t))}symlinkUnlocked(e,t){let{parentIno:n,name:i}=this.pathResolveParent(t),s=ue.encode(i),o=ue.encode(e);this.inodeWriteLock(n);try{if(this.dirLookup(n,s)>=0)throw new b(dt);let c=this.inodeAlloc();if(c<0)throw new b(ee);let l=this.inodeOffset(c);if(this.w32(l+B,nt|511),this.w32(l+F,1),o.length<=40)this.u8.set(o,l+Q),this.w64(l+T,o.length);else{this.w64(l+T,0);let d=this.inodeWriteData(c,0,o,o.length);if(d!==o.length)throw d>0&&this.inodeTruncate(c,0),this.inodeFree(c),new b(d<0?d:ee)}let u=this.dirAddEntry(n,s,c);if(u<0)throw o.length<=40?(this.u8.fill(0,l+Q,l+Q+40),this.w64(l+T,0)):this.inodeTruncate(c,0),this.inodeFree(c),new b(u)}finally{this.inodeWriteUnlock(n)}}chmod(e,t){this.withNamespaceLock(()=>this.chmodUnlocked(e,t))}chmodUnlocked(e,t){let n=this.pathResolve(e,!0);if(n<0)throw new b(n);this.inodeWriteLock(n);try{let i=this.inodeOffset(n),s=this.r32(i+B);this.w32(i+B,s&G|t&4095),this.w64(i+X,Date.now())}finally{this.inodeWriteUnlock(n)}}fchmod(e,t){let n=this.fdGet(e);if(!n)throw new b(ne);this.inodeWriteLock(n.ino);try{let i=this.inodeOffset(n.ino),s=this.r32(i+B);this.w32(i+B,s&G|t&4095),this.w64(i+X,Date.now())}finally{this.inodeWriteUnlock(n.ino)}}chown(e,t,n){this.withNamespaceLock(()=>this.chownUnlocked(e,t,n))}chownUnlocked(e,t,n){let i=this.pathResolve(e,!0);if(i<0)throw new b(i);this.inodeWriteLock(i);try{this.chownInodeUnlocked(i,t,n)}finally{this.inodeWriteUnlock(i)}}fchown(e,t,n){let i=this.fdGet(e);if(!i)throw new b(ne);this.inodeWriteLock(i.ino);try{this.chownInodeUnlocked(i.ino,t,n)}finally{this.inodeWriteUnlock(i.ino)}}lchown(e,t,n){this.withNamespaceLock(()=>this.lchownUnlocked(e,t,n))}lchownUnlocked(e,t,n){let i=this.pathResolve(e,!1);if(i<0)throw new b(i);this.inodeWriteLock(i);try{this.chownInodeUnlocked(i,t,n)}finally{this.inodeWriteUnlock(i)}}chownInodeUnlocked(e,t,n){let i=this.inodeOffset(e);t!==jr&&this.w32(i+Ot,t),n!==jr&&this.w32(i+Tt,n);let s=this.r32(i+B);(s&G)===xt&&(s&us)!==0&&this.w32(i+B,s&~(ls|ds)),this.w64(i+X,Date.now())}utimens(e,t,n,i,s){this.withNamespaceLock(()=>this.utimensUnlocked(e,t,n,i,s))}utimensUnlocked(e,t,n,i,s){let o=this.pathResolve(e,!0);if(o<0)throw new b(o);this.inodeWriteLock(o);try{let a=this.inodeOffset(o),c=1073741823,l=1073741822,u=Date.now();if(n!==l){let d=n===c?u:t*1e3+Math.floor(n/1e6);this.w64(a+_t,d)}if(s!==l){let d=s===c?u:i*1e3+Math.floor(s/1e6);this.w64(a+ce,d)}this.w64(a+X,u)}finally{this.inodeWriteUnlock(o)}}link(e,t){return this.withNamespaceLock(()=>this.linkUnlocked(e,t))}linkUnlocked(e,t){let n=this.pathResolve(e,!1);if(n<0)throw new b(n);let i=this.inodeOffset(n);if((this.r32(i+B)&G)===W)throw new b(gs);let{parentIno:o,name:a}=this.pathResolveParent(t),c=ue.encode(a);this.inodeWriteLock(o);try{if(this.dirLookup(o,c)>=0)throw new b(dt);let u=this.dirAddEntry(o,c,n);if(u<0)throw new b(u);this.inodeWriteLock(n);try{let d=this.r32(i+F);this.w32(i+F,d+1),this.w64(i+X,Date.now())}finally{this.inodeWriteUnlock(n)}return{...this.namespaceEntryIdentity(n),linkCount:this.r32(i+F)}}finally{this.inodeWriteUnlock(o)}}readlink(e){return this.withNamespaceLock(()=>this.readlinkUnlocked(e))}readlinkUnlocked(e){let t=this.pathResolve(e,!1);if(t<0)throw new b(t);return this.readSymlinkInodeUnlocked(t)}readSymlinkInodeUnlocked(e){let t=this.inodeOffset(e);if((this.r32(t+B)&G)!==nt)throw new b(q);let i=this.r64(t+T);if(i<=40)return lt(this.u8.subarray(t+Q,t+Q+i));this.inodeReadLock(e);try{let s=new Uint8Array(i);return this.inodeReadData(e,0,s,i),Ct.decode(s)}finally{this.inodeReadUnlock(e)}}opendir(e){return this.withNamespaceLock(()=>this.opendirUnlocked(e))}opendirUnlocked(e){let t=this.pathResolve(e,!0);if(t<0)throw new b(t);let n=this.inodeOffset(t);if((this.r32(n+B)&G)!==W)throw new b(ze);let s=this.fdAlloc(t,rt,!0);if(s<0)throw new b(s);return s}readdirEntry(e){return this.withNamespaceLock(()=>this.readdirEntryUnlocked(e))}readdirEntryUnlocked(e){let t=this.fdGet(e);if(!t||!t.isDir)throw new b(ne);let n=this.inodeOffset(t.ino),i=this.r64(n+T);for(;t.offset=this.r32(Be))throw new b(D);let p=this.r32(Ke)*4096;if((this.r32(p+(u>>5)*4)&1<<(u&31))===0)throw new b(D);let m=lt(this.u8.subarray(l+R,l+R+f)),g=this.buildStat(u);return this.w64(h+We,y),t.offset=y,{name:m,stat:g}}return null}closedir(e){this.close(e)}readdir(e){let t=this.opendir(e),n=[];try{let i;for(;(i=this.readdirEntry(t))!==null;)i.name!=="."&&i.name!==".."&&n.push(i.name)}finally{this.closedir(t)}return n}writeFile(e,t){let n=typeof t=="string"?ue.encode(t):t,i=this.open(e,Yr|Bt|Nt);try{this.write(i,n)}finally{this.close(i)}}readFile(e){let t=this.open(e,rt);try{let n=this.fstat(t),i=new Uint8Array(n.size);return this.read(t,i),i}finally{this.close(t)}}readFileText(e){return Ct.decode(this.readFile(e))}};function ci(r,e){let t=new Map,n=new Map;for(let o of r){if(t.has(o.path))throw new Error(`${e} duplicates path ${o.path}`);if(t.set(o.path,o),o.type==="file"){if(!o.inodeGroup)throw new Error(`${e} file ${o.path} has no inode group`);if(n.has(o.inodeGroup))throw new Error(`${e} inode group ${o.inodeGroup} has multiple files`);n.set(o.inodeGroup,o)}}let i=new Set,s=new Map;for(let o of r){if(o.type!=="hardlink"||s.has(o.path))continue;let a=[],c=o,l;for(;c.type==="hardlink";){let d=s.get(c.path);if(d){l=d;break}if(i.has(c.path))throw new Error(`${e} hardlink cycle reaches ${c.path}`);if(i.add(c.path),a.push(c),!c.target)throw new Error(`${e} hardlink ${c.path} has no target`);let f=t.get(c.target);if(!f)throw new Error(`${e} hardlink ${c.path} target ${c.target} is missing`);if(f.type!=="file"&&f.type!=="hardlink"||!c.inodeGroup||f.inodeGroup!==c.inodeGroup||f.size!==c.size||f.mode!==c.mode)throw new Error(`${e} hardlink ${c.path} has an invalid target`);c=f}l??=c.type==="file"?c:void 0;let u=n.get(o.inodeGroup??"");if(!l||l!==u)throw new Error(`${e} hardlink ${o.path} does not resolve to its inode`);for(let d=a.length-1;d>=0;d-=1){let f=a[d];if(n.get(f.inodeGroup??"")!==l)throw new Error(`${e} hardlink ${f.path} does not resolve to its inode`);i.delete(f.path),s.set(f.path,l)}}return{canonicalByGroup:n,canonicalTargetByPath:s}}var le={maxArchiveBytes:268435456,maxExpandedBytes:268435456,maxPayloadBytes:268435456,maxEntries:1e5,maxPathBytes:4096,maxSymlinkTargetBytes:65536,maxStringBytes:8192,maxTransportsPerTree:8,maxActivationCapabilities:32,maxActivationRoots:64,maxActivationCapabilityBytes:255},Se={maxArchiveBytes:512*1024*1024,maxExpandedBytes:512*1024*1024,maxPayloadBytes:512*1024*1024,maxEntries:1e5,maxGroups:512};function li(r,e="Deferred tree collection"){for(let[t,n]of Object.entries(r))if(!Number.isSafeInteger(n)||n<0)throw new Error(`${e} ${t} usage is invalid`);if(r.groups>Se.maxGroups)throw new Error(`${e} exceeds the ${Se.maxGroups}-group cap`);if(r.archiveBytes>Se.maxArchiveBytes)throw new Error(`${e} exceeds the archive-byte cap`);if(r.expandedBytes>Se.maxExpandedBytes)throw new Error(`${e} exceeds the expansion cap`);if(r.payloadBytes>Se.maxPayloadBytes)throw new Error(`${e} exceeds the payload-byte cap`);if(r.entries>Se.maxEntries)throw new Error(`${e} exceeds the entry-count cap`)}var As=["@@HOMEBREW_PREFIX@@","@@HOMEBREW_CELLAR@@","@@HOMEBREW_REPOSITORY@@","@@HOMEBREW_LIBRARY@@","@@HOMEBREW_PERL@@"],qn="@@HOMEBREW_JAVA@@",Ls=/^openjdk(?:@\d+(?:\.\d+)*)?/,He=new TextEncoder,ks=[...As,qn].map(r=>({placeholder:r,bytes:He.encode(r)}));function fi(r){let e;try{e=JSON.parse(new TextDecoder("utf-8",{fatal:!0}).decode(r))}catch(a){throw new Error("INSTALL_RECEIPT.json is not valid UTF-8 JSON: "+Ps(a))}if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("INSTALL_RECEIPT.json must contain an object");let t=e,n=t.changed_files;if(n!=null&&!Array.isArray(n))throw new Error("INSTALL_RECEIPT.json changed_files must be an array or null when present");let i=Array.isArray(n)?n:[];if(i.length>1e5)throw new Error(`INSTALL_RECEIPT.json declares ${i.length} changed files, limit 100000`);let s=[],o=new Set;for(let[a,c]of i.entries()){if(typeof c!="string")throw new Error(`INSTALL_RECEIPT.json changed_files[${a}] is not a string`);if(_s(c,"Homebrew changed file"),o.has(c))throw new Error(`INSTALL_RECEIPT.json repeats changed file ${c}`);o.add(c),s.push(c)}return{changedFiles:s,runtimeDependencies:t.runtime_dependencies}}function hi(r,e,t,n){Is(n);let i=[["@@HOMEBREW_PREFIX@@",n],["@@HOMEBREW_CELLAR@@",`${n}/Cellar`],["@@HOMEBREW_REPOSITORY@@",n],["@@HOMEBREW_LIBRARY@@",`${n}/Library`],["@@HOMEBREW_PERL@@",`${n}/opt/perl/bin/perl`]],s=r;for(let[c,l]of i)s=ui(s,He.encode(c),He.encode(l));let o=He.encode(qn);if(di(s,o)){let c=xs(e.runtimeDependencies,n);if(c===void 0)throw new Error(`Homebrew changed file ${t} uses ${qn} without exactly one OpenJDK runtime dependency`);s=ui(s,o,He.encode(c))}let a=ks.find(({bytes:c})=>di(s,c));if(a!==void 0)throw new Error(`Homebrew changed file ${t} retains ${a.placeholder}`);return s}function xs(r,e){if(!Array.isArray(r))return;let t=[];for(let i of r){if(typeof i!="object"||i===null||Array.isArray(i))continue;let s=i,o=typeof s.full_name=="string"?s.full_name.split("/").at(-1):typeof s.name=="string"?s.name.split("/").at(-1):void 0,a=o===void 0?null:Ls.exec(o);o!==void 0&&a?.[0]===o&&t.push(o)}let n=[...new Set(t)];return n.length===1?`${e}/opt/${n[0]}/libexec`:void 0}function Is(r){if(typeof r!="string"||r==="/"||!r.startsWith("/")||r.endsWith("/")||r.includes("\\")||r.includes("\0")||yi(r)||He.encode(r).byteLength>4096||r.slice(1).split("/").some(e=>e===""||e==="."||e===".."))throw new Error(`Homebrew relocation prefix is not canonical: ${r}`)}function _s(r,e){if(r.length===0||r.startsWith("/")||r.includes("\\")||r.includes("\0")||yi(r)||He.encode(r).byteLength>4096||r.split("/").some(t=>t===""||t==="."||t===".."))throw new Error(`${e} has an unsafe path segment: ${r}`)}function yi(r){for(let e=0;e57343)){if(t<=56319&&e+1=56320&&r.charCodeAt(e+1)<=57343){e+=1;continue}return!0}}return!1}function di(r,e){if(e.byteLength===0||e.byteLength>r.byteLength)return!1;e:for(let t=0;t<=r.byteLength-e.byteLength;t+=1){for(let n=0;nbn||r.includes("\0")||r.includes("\\"))throw new Error(`Lazy archive mount prefix must be an absolute POSIX path: ${JSON.stringify(r)}`);let e=r.replace(/\/+$/,"");if(e==="")return"/";if(e.slice(1).split("/").some(n=>n===""||n==="."||n===".."))throw new Error(`Lazy archive mount prefix is not canonical: ${JSON.stringify(r)}`);return e}function Oa(r,e,t,n){let i=Ht(t),s=new Map,o=e.map(a=>{let c=a.fileName,l=`Lazy archive ${JSON.stringify(r)} member ${JSON.stringify(c)}`;if(c.length===0)throw new Error(`${l} has an empty path`);if(c.includes("\0"))throw new Error(`${l} contains a NUL byte`);if(c.includes("\\"))throw new Error(`${l} contains a backslash`);if(c.startsWith("/")||/^[A-Za-z]:\//.test(c))throw new Error(`${l} must be relative, not absolute`);if(a.isDirectory&&a.isSymlink)throw new Error(`${l} has conflicting directory and symlink types`);if(a.isDirectory!==c.endsWith("/"))throw new Error(`${l} has inconsistent directory metadata`);let u=a.isDirectory?c.slice(0,-1):c,d=u.split("/");if(u.length===0||d.some(f=>f===""||f==="."||f===".."))throw new Error(`${l} is not a canonical relative POSIX path`);if(s.has(u))throw new Error(`${l} collides with another member at ${JSON.stringify(u)}`);if(a.isSymlink&&!n?.has(c))throw new Error(`Lazy archive symlink target was not provided: ${c}`);return s.set(u,a),{entry:a,archivePath:u,vfsPath:i==="/"?`/${u}`:`${i}/${u}`}});for(let{archivePath:a}of o){let c=a.split("/");for(let l=1;lpt)throw new Error(`VFS image metadata exceeds ${pt} bytes`);let e;try{e=JSON.parse(new TextDecoder().decode(r))}catch(t){let n=t instanceof Error?t.message:String(t);throw new Error(`Invalid VFS image metadata JSON: ${n}`)}return Er(e)}function Ba(r){if(r===null)return new Uint8Array(0);let e=Er(r),t=new TextEncoder().encode(JSON.stringify(e));if(t.byteLength>pt)throw new Error(`VFS image metadata exceeds ${pt} bytes`);return t}function Ca(r){return r.byteLength>=Ut.length&&r[0]===Ut[0]&&r[1]===Ut[1]&&r[2]===Ut[2]&&r[3]===Ut[3]?rc(r):r}function hn(r){let e=Ca(r);if(e.byteLengthmn)throw new Error(`VFS image lazy metadata exceeds ${mn} bytes`);if(r.byteLengthgn)throw new Error(`VFS image lazy archive metadata exceeds ${gn} bytes`);if(r.byteLength=0?n:void 0}function $a(r){return r===408||r===429||r>=500&&r<=599}function Fa(r,e=Date.now()){let t=r?.get("retry-after")?.trim();if(!t)return;let n;if(/^\d+$/.test(t))n=Number(t)*1e3;else{let i=Date.parse(t);if(!Number.isFinite(i))return;n=Math.max(0,i-e)}if(!(!Number.isSafeInteger(n)||n<0))return Math.min(n,eo)}function Da(r){if(!(typeof r!="object"||r===null||!("cause"in r)))return r.cause}function to(r){if(!(typeof r!="object"||r===null||!("name"in r)))return typeof r.name=="string"?r.name:void 0}function no(r){if(!(typeof r!="object"||r===null||!("code"in r)))return typeof r.code=="string"?r.code:void 0}function ro(r,e){let t=new Set,n=r;for(let i=0;n!==void 0&&i<8;i+=1){if(t.has(n))return!1;if(t.add(n),e(n))return!0;n=Da(n)}return!1}function io(r){return ro(r,e=>to(e)==="AbortError"||no(e)==="ABORT_ERR")}function Ua(r){return io(r)?!1:ro(r,e=>{let t=to(e),n=no(e);return e instanceof TypeError||t==="NetworkError"||t==="TimeoutError"||n!==void 0&&Pa.has(n)})}function Ga(r,e){if(r instanceof vn){if(!$a(r.status))return null;if(r.retryAfterMs!==void 0)return r.retryAfterMs}else if(!Ua(r))return null;return Math.min(_a*2**e,eo)}function J(r){if(r?.aborted)throw r.reason}function Ka(r,e){return J(e),r===0?Promise.resolve():new Promise((t,n)=>{let i=setTimeout(()=>a(!1),r),s=()=>a(!0,e.reason),o=!1;function a(c,l){o||(o=!0,clearTimeout(i),e?.removeEventListener("abort",s),c?n(l):t())}e?.addEventListener("abort",s,{once:!0}),e?.aborted&&s()})}async function ur(r,e){try{await r.body?.cancel(e)}catch{}}function Wa(r,e){if(r.length===1)return r[0];let t=new Uint8Array(e),n=0;for(let i of r)t.set(i,n),n+=i.byteLength;return t}function Vt(r){if(r===void 0)return;if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("Lazy archive integrity must be an object");let e=r;if(Object.keys(e).length!==2||!("sha256"in e)||!("bytes"in e))throw new Error("Lazy archive integrity has unexpected fields");if(typeof e.sha256!="string"||!pr.test(e.sha256))throw new Error("Lazy archive integrity has an invalid SHA-256 digest");if(!Number.isSafeInteger(e.bytes)||Number(e.bytes)<=0||Number(e.bytes)>Di)throw new Error(`Lazy archive integrity byte count must be between 1 and ${Di}`);return{sha256:e.sha256,bytes:Number(e.bytes)}}function Me(r,e,t){if(typeof r!="object"||r===null||Array.isArray(r))throw new Error(`${t} must be an object`);let n=r;if(Object.keys(n).length!==e.length||e.some(s=>!Object.prototype.hasOwnProperty.call(n,s)))throw new Error(`${t} has unexpected or missing fields`);return n}function gr(r,e,t,n){if(typeof r!="object"||r===null||Array.isArray(r))throw new Error(`${n} must be an object`);let i=r,s=new Set(e);if(Object.keys(i).some(o=>!s.has(o))||t.some(o=>!Object.prototype.hasOwnProperty.call(i,o)))throw new Error(`${n} has unexpected or missing fields`);return i}function xe(r,e,t,n){if(!Array.isArray(r)||r.lengthn)throw new Error(`${e} must contain ${t} to ${n} items`);return r}function he(r,e,t){if(typeof r!="string"||r.length===0||r.includes("\0")||new TextEncoder().encode(r).byteLength>t)throw new Error(`${e} is invalid or exceeds ${t} bytes`);return r}function re(r,e,t,n){if(!Number.isSafeInteger(r)||Number(r)n)throw new Error(`${e} must be an integer between ${t} and ${n}`);return Number(r)}function Sn(r,e=1){let t=r,n=typeof t=="object"&&t!==null&&!Array.isArray(t)&&t.source!==void 0,i=typeof t=="object"&&t!==null&&!Array.isArray(t)&&t.modePolicy!==void 0,s=Me(r,["decoder","mediaType","sha256","bytes","expandedBytes","sourceEntryCount","transports",...i?["modePolicy"]:[],...n?["source"]:[]],"Lazy tree content"),o=s.decoder==="zip-v1"?"application/zip":s.decoder==="homebrew-bottle-tar-gzip-v1"?"application/vnd.oci.image.layer.v1.tar+gzip":null;if(o===null||s.mediaType!==o)throw new Error("Lazy tree decoder and media type are inconsistent");let a=Vt({sha256:s.sha256,bytes:s.bytes});if(!a)throw new Error("Lazy tree integrity is required");let c=xe(s.transports,"Lazy tree transports",e,le.maxTransportsPerTree).map((y,h)=>he(y,`Lazy tree transport ${h}`,Sr));if(new Set(c).size!==c.length)throw new Error("Lazy tree transports contain duplicates");let l=re(s.expandedBytes,"Lazy tree expanded byte count",0,Aa),u=re(s.sourceEntryCount,"Lazy tree source entry count",1,mt),d=n?Va(s.source,s.decoder):void 0,f=i?s.modePolicy:void 0;if(f!==void 0&&(f!=="portable-posix-v1"||s.decoder!=="zip-v1"||n))throw new Error("Lazy tree mode policy is invalid for its decoder");if(d!==void 0&&d.entries.length!==u)throw new Error("Lazy tree source inventory count differs from its content");return{decoder:s.decoder,mediaType:o,sha256:a.sha256,bytes:a.bytes,expandedBytes:l,sourceEntryCount:u,transports:c,...f===void 0?{}:{modePolicy:f},...d===void 0?{}:{source:d}}}function oo(r){let e={groups:r.length,archiveBytes:0,expandedBytes:0,payloadBytes:0,entries:0};for(let t of r)t.content===void 0||t.inventory===void 0||(e.archiveBytes+=t.content.bytes,e.expandedBytes+=t.content.expandedBytes,e.payloadBytes+=t.inventory.filter(n=>n.type==="file").reduce((n,i)=>n+i.size,0),e.entries+=t.inventory.length+(t.content.source?.entries.length??0));return e}function wr(r){li(r,"Serialized lazy tree collection")}function Za(r){wr(oo(r))}function Ha(r){let e=new Map;for(let t of r){let n=t.activation?.atomicGroup;if(n===void 0)continue;if(!yt(n))throw new Error(`Serialized lazy atomic activation group ${n.id} is unsealed`);let i=e.get(n.id);if(i===void 0)i={expectedCount:n.expectedCount,cohortSha256:n.cohortSha256,members:new Set,descriptors:new Set},e.set(n.id,i);else if(i.expectedCount!==n.expectedCount||i.cohortSha256!==n.cohortSha256)throw new Error(`Serialized lazy atomic activation group ${n.id} has inconsistent seals`);if(i.members.has(n.member)||i.descriptors.has(n.descriptorSha256))throw new Error(`Serialized lazy atomic activation group ${n.id} duplicates a member`);i.members.add(n.member),i.descriptors.add(n.descriptorSha256)}for(let[t,n]of e)if(n.members.size!==n.expectedCount)throw new Error(`Serialized lazy atomic activation group ${t} has ${n.members.size} of ${n.expectedCount} members`)}function Hi(r){for(let[e,t]of r.entries())if(t.kind===Zt||t.kind===mr||t.kind===qe)lo(t,t.kind);else if(t.kind===Wt)vr(t,!1);else throw new Error(`Serialized lazy archive group ${e} has an unsupported kind`);Za(r),Ha(r)}function Va(r,e){if(e!=="homebrew-bottle-tar-gzip-v1")throw new Error("Lazy tree source inventory is valid only for original bottles");let t=Me(r,["schema","kind","entries"],"Lazy tree source inventory");if(t.schema!==1||t.kind!=="homebrew-bottle-tar-gzip-v1")throw new Error("Lazy tree source inventory has an unsupported identity");let n=new Map,i=xe(t.entries,"Lazy tree source entries",1,mt).map((o,a)=>{let c=o,l=typeof c=="object"&&c!==null&&!Array.isArray(c)?c.type:void 0,u=l==="directory"||l==="file"?["sourcePath","type","mode","size"]:l==="symlink"||l==="hardlink"?["sourcePath","type","mode","size","target"]:null;if(u===null)throw new Error(`Lazy tree source entry ${a} has invalid type`);let d=Me(o,u,`Lazy tree source entry ${a}`),f=ye(d.sourcePath,!1,`Lazy tree source entry ${a} path`);if(n.has(f))throw new Error(`Lazy tree source inventory duplicates ${f}`);let y=re(d.mode,`Lazy tree source entry ${f} mode`,0,4095),h=re(d.size,`Lazy tree source entry ${f} size`,0,wn),p;if((l==="directory"||l==="symlink"||l==="hardlink")&&h!==0)throw new Error(`Lazy tree source ${f} has payload for ${String(l)}`);l==="symlink"?p=he(d.target,`Lazy tree source symlink ${f} target`,Qi):l==="hardlink"&&(p=ye(d.target,!1,`Lazy tree source hardlink ${f} target`));let w={sourcePath:f,type:l,mode:y,size:h,...p===void 0?{}:{target:p}};return n.set(f,w),w}),s=i.map(o=>o.sourcePath);if(s.some((o,a)=>a>0&&s[a-1]>=o))throw new Error("Lazy tree source inventory is not in canonical path order");return{schema:1,kind:"homebrew-bottle-tar-gzip-v1",entries:i}}function so(r){let e=new Map(r.map(n=>[n.sourcePath,n])),t=new Map;for(let n of r){if(n.type!=="hardlink"||t.has(n.sourcePath))continue;let i=[],s=new Set,o=n,a;for(;o.type==="hardlink"&&(a=t.get(o.sourcePath),a===void 0);){if(s.has(o.sourcePath))throw new Error(`Lazy tree source hardlink cycle includes ${o.sourcePath}`);s.add(o.sourcePath),i.push(o);let c=e.get(o.target);if(c===void 0)throw new Error(`Lazy tree source hardlink ${o.sourcePath} target is absent`);if(c.type!=="file"&&c.type!=="hardlink")throw new Error(`Lazy tree source hardlink ${o.sourcePath} target is not regular`);o=c}a===void 0&&(a=o);for(let c of i)t.set(c.sourcePath,a)}return t}function qa(r,e,t,n){let i=new Set,s=`/Cellar/${t}`;for(let o of r)o.sourcePath===t&&o.vfsPath.endsWith(s)&&i.add(o.vfsPath.slice(0,-s.length));if(n!==""){let o=`/Cellar/${n}`;for(let a of e?.roots??[])a.endsWith(o)&&i.add(a.slice(0,-o.length))}if(i.size!==1)throw new Error("Lazy Homebrew bottle cannot derive one relocation prefix from its authenticated guest projection");return i.values().next().value}function ye(r,e,t,n=!1){if(typeof r!="string"||r.length===0||new TextEncoder().encode(r).byteLength>bn||r.includes("\0")||r.includes("\\")||r.startsWith("/")!==e)throw new Error(`${t} is not a canonical ${e?"absolute":"relative"} path`);if(n&&e&&r==="/")return r;if(r.slice(e?1:0).split("/").some(s=>s===""||s==="."||s===".."))throw new Error(`${t} has an unsafe path segment`);return r}function ao(r){let e=typeof r=="object"&&r!==null&&!Array.isArray(r)?r:null,t=e!==null&&(Object.hasOwn(e,"descriptorSha256")||Object.hasOwn(e,"expectedCount")||Object.hasOwn(e,"cohortSha256")),n=Me(r,t?["id","member","descriptorSha256","expectedCount","cohortSha256"]:["id","member"],"Lazy tree atomic activation membership"),i=he(n.id,"Lazy tree atomic activation group",Ui),s=he(n.member,"Lazy tree atomic activation member",Ui);if(!/^[a-z0-9][a-z0-9:._-]*$/.test(i)||!/^[a-z0-9][a-z0-9:+._/-]*$/.test(s)||s.includes("//")||s.endsWith("/"))throw new Error("Lazy tree atomic activation membership is invalid");if(!t)return{id:i,member:s};let o=he(n.descriptorSha256,"Lazy tree atomic member descriptor digest",64),a=he(n.cohortSha256,"Lazy tree atomic cohort digest",64);if(!pr.test(o)||!pr.test(a))throw new Error("Lazy tree atomic activation digest is invalid");return{id:i,member:s,descriptorSha256:o,expectedCount:re(n.expectedCount,"Lazy tree atomic activation expected member count",1,Ji),cohortSha256:a}}function yt(r){return r.descriptorSha256!==void 0&&r.expectedCount!==void 0&&r.cohortSha256!==void 0}function ja(r){let e=Me(r,["uid","gid"],"Lazy tree registration owner");return{uid:re(e.uid,"Lazy tree registration owner uid",0,Gi),gid:re(e.gid,"Lazy tree registration owner gid",0,Gi)}}function co(r,e,t,n,i=1){let s=Sn(r,i),o=Ht(t),a=["mode","capabilities","roots",...typeof n=="object"&&n!==null&&!Array.isArray(n)&&Object.hasOwn(n,"atomicGroup")?["atomicGroup"]:[]],c=Me(n,a,"Lazy tree activation");if(c.mode!=="boot-prefetch"&&c.mode!=="first-use")throw new Error("Lazy tree activation mode is invalid");let l=xe(c.capabilities,"Lazy tree activation capabilities",1,xa).map((v,A)=>{let L=he(v,`Lazy tree activation capability ${A}`,le.maxActivationCapabilityBytes);if(!/^[a-z0-9][a-z0-9:._-]*$/.test(L))throw new Error(`Lazy tree activation capability ${A} is invalid`);return L}),u=xe(c.roots,"Lazy tree activation roots",1,Ia).map((v,A)=>ye(v,!0,`Lazy tree activation root ${A}`,!0));if(new Set(l).size!==l.length||new Set(u).size!==u.length)throw new Error("Lazy tree activation contains duplicates");let d=c.atomicGroup===void 0?void 0:ao(c.atomicGroup);if(d!==void 0&&c.mode!=="first-use")throw new Error("Lazy tree atomic activation group requires a valid first-use identity");let f={mode:c.mode,capabilities:l,roots:u,...d===void 0?{}:{atomicGroup:d}},y=xe(e,"Lazy tree inventory",1,mt),h=[],p=new Map,w=new Map,m=s.source===void 0?void 0:new Map(s.source.entries.map(v=>[v.sourcePath,v])),g=s.source===void 0?void 0:so(s.source.entries),S=0;for(let[v,A]of y.entries()){if(typeof A!="object"||A===null||Array.isArray(A))throw new Error(`Lazy tree entry ${v} must be an object`);let L=A.type,x=L==="directory"?["vfsPath","sourcePath","type","mode","size"]:L==="file"?["vfsPath","sourcePath","type","mode","size","inodeGroup"]:L==="symlink"?["vfsPath","sourcePath","type","mode","size","target"]:L==="hardlink"?["vfsPath","sourcePath","type","mode","size","target","inodeGroup"]:null;if(!x)throw new Error(`Lazy tree entry ${v} has an invalid type`);let _=Me(A,[...x,...m===void 0?[]:["materialization"]],`Lazy tree entry ${v}`),I=ye(_.vfsPath,!0,`Lazy tree entry ${v} VFS path`),M=ye(_.sourcePath,!1,`Lazy tree entry ${v} source path`),V=m===void 0?void 0:_.materialization;if(m!==void 0&&V!=="archive"&&V!=="archive-homebrew-relocate"&&V!=="archive-copy"&&V!=="archive-copy-mode"&&V!=="descriptor")throw new Error(`Lazy tree entry ${I} has invalid materialization provenance`);if(o!=="/"&&I!==o&&!I.startsWith(`${o}/`))throw new Error(`Lazy tree entry ${I} escapes its mount prefix`);if(p.has(I))throw new Error(`Lazy tree duplicates VFS path ${I}`);let C=re(_.mode,`Lazy tree entry ${I} mode`,0,4095),O=re(_.size,`Lazy tree entry ${I} size`,0,wn),U,ae;if(L==="directory"){if(O!==0)throw new Error(`Lazy tree directory ${I} has nonzero size`)}else if(L==="symlink"){if(U=he(_.target,`Lazy tree symlink ${I} target`,Qi),new TextEncoder().encode(U).byteLength!==O)throw new Error(`Lazy tree symlink ${I} size differs from its target`)}else ae=he(_.inodeGroup,`Lazy tree entry ${I} inode group`,bn),L==="hardlink"&&(U=ye(_.target,!0,`Lazy tree hardlink ${I} target`));if(L!=="hardlink"&&(S+=O,S>wn))throw new Error("Lazy tree inventory exceeds the expansion limit");let N={vfsPath:I,sourcePath:M,...V===void 0?{}:{materialization:V},type:L,mode:C,size:O,...U===void 0?{}:{target:U},...ae===void 0?{}:{inodeGroup:ae}};if(m===void 0){let Z=w.get(M);if(Z){if(s.decoder!=="zip-v1"||N.type!=="hardlink"||Z.inodeGroup!==N.inodeGroup)throw new Error(`Lazy tree duplicates source path ${M}`)}else{if(s.decoder==="zip-v1"&&N.type==="hardlink")throw new Error(`Lazy ZIP hardlink ${I} does not reuse a canonical source path`);w.set(M,N)}}else if(N.materialization==="descriptor"){if(N.type!=="directory"&&N.type!=="symlink")throw new Error(`Lazy tree descriptor entry ${I} is not structural`);if(m.has(M))throw new Error(`Lazy tree descriptor entry ${I} impersonates a source member`)}else{let Z=m.get(M);if(Z===void 0)throw new Error(`Lazy tree entry ${I} names absent source ${M}`);if(N.materialization==="archive-copy"||N.materialization==="archive-copy-mode"){if(N.type!=="file"||Z.type!=="file"||N.materialization==="archive-copy"&&N.mode!==Z.mode)throw new Error(`Lazy tree archive copy ${I} differs from its source`)}else if(N.materialization==="archive-homebrew-relocate"){if(N.type!=="file"&&N.type!=="hardlink"||Z.type!==N.type||N.type==="file"&&Z.mode!==N.mode)throw new Error(`Lazy tree receipt-relocated entry ${I} differs from its source`)}else if(Z.type!==N.type||N.type==="symlink"&&Z.target!==N.target||N.type!=="hardlink"&&Z.mode!==N.mode)throw new Error(`Lazy tree archive entry ${I} differs from its source`)}h.push(N),p.set(I,N)}for(let v of h){let A=v.vfsPath.split("/").filter(Boolean);for(let L=1;L({path:v.vfsPath,type:v.type,mode:v.mode,size:v.size,target:v.target,inodeGroup:v.inodeGroup})),"Lazy tree");if(m!==void 0){let v=new Set;for(let A of h){if(A.materialization!=="archive-homebrew-relocate")continue;let L=m.get(A.sourcePath),x=L.type==="file"?L:g.get(L.sourcePath);if(x?.type!=="file")throw new Error(`Lazy tree receipt-relocated entry ${A.vfsPath} is not regular`);v.add(x.sourcePath)}for(let A of h){if(A.materialization==="descriptor"||A.type!=="file"&&A.type!=="hardlink")continue;let L=m.get(A.sourcePath),x=L.type==="file"?L:g.get(L.sourcePath);if(x?.type!=="file"||!v.has(x.sourcePath)&&A.size!==x.size)throw new Error(`Lazy tree archive entry ${A.vfsPath} differs from its source`)}for(let A of h){if(A.type!=="hardlink"||A.materialization!=="archive"&&A.materialization!=="archive-homebrew-relocate")continue;let L=m.get(A.sourcePath),x=p.get(A.target),_=g.get(L.sourcePath);if(L.target!==x?.sourcePath||_?.type!=="file"||_.mode!==A.mode||x?.mode!==A.mode)throw new Error(`Lazy tree hardlink ${A.vfsPath} differs from its source`)}}if(s.sourceEntryCount!==(m===void 0?w.size:m.size))throw new Error("Lazy tree source entry count differs from its inventory");if(s.source===void 0&&s.expandedBytesA.vfsPath===v||A.vfsPath.startsWith(`${v}/`)))throw new Error(`Lazy tree activation root ${v} is not owned by its inventory`);let E=new Map;for(let v of h)v.type==="file"&&E.set(v.inodeGroup,v);if(E.size!==z.canonicalByGroup.size)throw new Error("Lazy tree regular inode inventory is inconsistent");return{content:s,entries:h,mountPrefix:o,activation:f,canonicalByGroup:E}}function En(r){return JSON.stringify([r.sourcePath,r.type,r.inodeGroup,r.target])}function vr(r,e){let t=gr(r,["kind","content","url","mountPrefix","integrity","materialized","entries"],["url","mountPrefix","materialized","entries"],"Serialized legacy lazy archive");if(t.kind===void 0){if(!e)throw new Error("Serialized lazy archive is missing its kind discriminator")}else if(t.kind!==Wt)throw new Error("Serialized legacy lazy archive has an unsupported kind");let n=he(t.url,"Serialized legacy lazy archive URL",Sr),i=Ht(t.mountPrefix),s=Vt(t.integrity);if(t.content!==void 0){if(!e||t.kind!==void 0)throw new Error("Typed legacy lazy archives cannot carry generic content");let c=Sn(t.content);if(c.decoder!=="zip-v1"||c.transports.length!==1||c.transports[0]!==n||!s||c.sha256!==s.sha256||c.bytes!==s.bytes)throw new Error("Untagged legacy ZIP content identity is inconsistent")}if(t.materialized!==!1)throw new Error("Serialized legacy lazy archive must describe pending content");let o=new Set,a=xe(t.entries,"Serialized legacy lazy archive entries",1,mt).map((c,l)=>{let u=gr(c,["vfsPath","ino","generation","dataSequence","size","isSymlink","deleted","materialized","archivePath","sourcePath","type","inodeGroup","target"],["vfsPath","ino","size","isSymlink","deleted"],`Serialized legacy lazy archive entry ${l}`),d=ye(u.vfsPath,!0,`Serialized legacy lazy archive entry ${l} VFS path`);if(o.has(d))throw new Error(`Serialized legacy lazy archive duplicates path ${d}`);o.add(d);let f=re(u.ino,`Serialized legacy lazy archive entry ${d} inode`,1,Number.MAX_SAFE_INTEGER),y=u.generation===void 0?void 0:re(u.generation,`Serialized legacy lazy archive entry ${d} generation`,0,Number.MAX_SAFE_INTEGER),h=u.dataSequence===void 0?void 0:re(u.dataSequence,`Serialized legacy lazy archive entry ${d} data sequence`,0,Number.MAX_SAFE_INTEGER),p=re(u.size,`Serialized legacy lazy archive entry ${d} size`,0,wn);if(u.isSymlink!==!1||u.deleted!==!1||u.materialized!==void 0&&u.materialized!==!1)throw new Error(`Serialized legacy lazy archive entry ${d} is not pending`);if(u.type!==void 0&&u.type!=="file")throw new Error(`Serialized legacy lazy archive entry ${d} has an invalid type`);let w=u.archivePath===void 0?void 0:ye(u.archivePath,!1,`Serialized legacy lazy archive entry ${d} archive path`),m=u.sourcePath===void 0?void 0:ye(u.sourcePath,!1,`Serialized legacy lazy archive entry ${d} source path`),g=u.inodeGroup===void 0?void 0:he(u.inodeGroup,`Serialized legacy lazy archive entry ${d} inode group`,bn);if(u.target!==void 0)throw new Error(`Serialized legacy lazy archive entry ${d} has a link target`);return{vfsPath:d,ino:f,...y===void 0?{}:{generation:y},...h===void 0?{}:{dataSequence:h},size:p,isSymlink:!1,deleted:!1,materialized:!1,...w===void 0?{}:{archivePath:w},...m===void 0?{}:{sourcePath:m},type:"file",...g===void 0?{}:{inodeGroup:g}}});return{kind:Wt,url:n,mountPrefix:i,...s===void 0?{}:{integrity:s},materialized:!1,entries:a}}function lo(r,e){let t=Me(r,["kind","content","inventory","activation","url","mountPrefix","integrity","materialized","entries"],"Serialized lazy tree");if(t.kind!==e)throw new Error("Serialized lazy tree has an unsupported kind");let n=co(t.content,t.inventory,t.mountPrefix,t.activation);if(e!==qe&&e===Zt!=(n.content.source===void 0))throw new Error(e===Zt?"Serialized deferred-tree-v1 cannot contain original-bottle source metadata":"Serialized deferred-tree-v2 requires original-bottle source metadata");let i=n.activation.atomicGroup;if(e===qe?i===void 0||!yt(i):i!==void 0)throw new Error(e===qe?"Serialized deferred-tree-v3 requires a sealed atomic activation":"Atomic activation requires serialized deferred-tree-v3");let s=he(t.url,"Serialized lazy tree URL",Sr);if(s!==n.content.transports[0])throw new Error("Serialized lazy tree URL differs from its primary transport");let o=Vt(t.integrity);if(!o||o.sha256!==n.content.sha256||o.bytes!==n.content.bytes)throw new Error("Serialized lazy tree integrity differs from its content");if(t.materialized!==!1)throw new Error("Serialized lazy tree must describe pending content");let a=new Map(n.entries.map(f=>[f.vfsPath,f])),c=new Map(n.entries.map(f=>[En(f),f])),l=xe(t.entries,"Serialized lazy tree entries",0,mt),u=new Set,d=l.map((f,y)=>{let h=gr(f,["vfsPath","ino","generation","dataSequence","size","isSymlink","deleted","materialized","archivePath","sourcePath","type","inodeGroup","target"],["vfsPath","ino","generation","dataSequence","size","isSymlink","deleted","materialized","archivePath","sourcePath","type","inodeGroup"],`Serialized lazy tree entry ${y}`),p=ye(h.vfsPath,!0,`Serialized lazy tree entry ${y} VFS path`);if(u.has(p))throw new Error(`Serialized lazy tree duplicates pending path ${p}`);u.add(p);let w=ye(h.sourcePath,!1,`Serialized lazy tree entry ${y} source path`),m=ye(h.archivePath,!1,`Serialized lazy tree entry ${y} archive path`),g=a.get(p),S=c.get(En({sourcePath:w,type:typeof h.type=="string"?h.type:void 0,inodeGroup:typeof h.inodeGroup=="string"?h.inodeGroup:void 0,target:typeof h.target=="string"?h.target:void 0}))??g;if(!S||S.type!=="file"&&S.type!=="hardlink"||g?.inodeGroup!==void 0&&g.inodeGroup!==S.inodeGroup)throw new Error(`Serialized lazy tree entry ${p} is absent from its inventory`);let z=n.canonicalByGroup.get(S.inodeGroup);if(h.type!==S.type||h.inodeGroup!==S.inodeGroup||h.size!==S.size||m!==z?.sourcePath||h.target!==S.target||h.isSymlink!==!1||h.deleted!==!1||h.materialized!==!1)throw new Error(`Serialized lazy tree entry ${p} disagrees with its inventory`);let E=re(h.ino,`Serialized lazy tree entry ${p} inode`,1,Number.MAX_SAFE_INTEGER),v=re(h.generation,`Serialized lazy tree entry ${p} generation`,0,Number.MAX_SAFE_INTEGER),A=re(h.dataSequence,`Serialized lazy tree entry ${p} data sequence`,0,Number.MAX_SAFE_INTEGER);return{vfsPath:p,ino:E,generation:v,dataSequence:A,size:S.size,isSymlink:!1,deleted:!1,materialized:!1,archivePath:m,sourcePath:w,type:S.type,inodeGroup:S.inodeGroup,...S.target===void 0?{}:{target:S.target}}});for(let f of n.entries)if(n.activation.atomicGroup!==void 0&&(f.type==="file"||f.type==="hardlink")&&!u.has(f.vfsPath))throw new Error(`Serialized lazy tree omits pending path ${f.vfsPath}`);return{kind:e,content:n.content,inventory:n.entries,activation:n.activation,url:s,mountPrefix:n.mountPrefix,integrity:o,materialized:!1,entries:d}}async function Kt(r,e){let t=globalThis.crypto?.subtle;if(!t)throw new Error(`${e} SHA-256 verification is unavailable`);let n=new Uint8Array(r.byteLength);n.set(r);let i=new Uint8Array(await t.digest("SHA-256",n));return Array.from(i,s=>s.toString(16).padStart(2,"0")).join("")}async function fr(r,e,t){if(t===void 0)return;if(r.byteLength!==t.bytes)throw new Error(`Lazy ${e} byte count ${r.byteLength} does not match expected ${t.bytes}`);let n=await Kt(r,`Lazy ${e}`);if(n!==t.sha256)throw new Error(`Lazy ${e} SHA-256 ${n} does not match expected ${t.sha256}`)}function Ya(r,e,t,n){let i=n.atomicGroup;if(i===void 0)throw new Error("Lazy atomic member is missing its typed tree descriptor");let s={schema:1,content:{decoder:r.decoder,mediaType:r.mediaType,sha256:r.sha256,bytes:r.bytes,expandedBytes:r.expandedBytes,sourceEntryCount:r.sourceEntryCount,...r.modePolicy===void 0?{}:{modePolicy:r.modePolicy},...r.source===void 0?{}:{source:r.source}},mountPrefix:t,inventory:[...e].sort((o,a)=>o.vfsPatha.vfsPath?1:0),activation:{mode:n.mode,capabilities:n.capabilities,roots:n.roots,atomicGroup:{id:i.id,member:i.member}}};return new TextEncoder().encode(JSON.stringify(s))}function Vi(r,e){let t=e.map(({member:n,descriptorSha256:i})=>({member:n,descriptorSha256:i}));return new TextEncoder().encode(JSON.stringify({schema:1,id:r,members:t.sort((n,i)=>n.memberi.member?1:0)}))}function Xa(r,e){if(r.byteLength!==e.byteLength)return!1;for(let t=0;tObject.freeze({...i}))};return n!==void 0&&(Object.freeze(n.entries),Object.freeze(n)),Object.freeze({decoder:r.decoder,mediaType:r.mediaType,sha256:r.sha256,bytes:r.bytes,expandedBytes:r.expandedBytes,sourceEntryCount:r.sourceEntryCount,transports:t,...r.modePolicy===void 0?{}:{modePolicy:r.modePolicy},...n===void 0?{}:{source:n}})}function qi(r){return{decoder:r.decoder,mediaType:r.mediaType,sha256:r.sha256,bytes:r.bytes,expandedBytes:r.expandedBytes,sourceEntryCount:r.sourceEntryCount,transports:[...r.transports],...r.modePolicy===void 0?{}:{modePolicy:r.modePolicy},...r.source===void 0?{}:{source:{schema:1,kind:"homebrew-bottle-tar-gzip-v1",entries:r.source.entries.map(e=>({...e}))}}}}function Ja(r){let e=r.map(t=>Object.freeze({...t}));return Object.freeze(e),e}function Qa(r,e,t){let n=[...r.capabilities],i=[...r.roots];return Object.freeze(n),Object.freeze(i),Object.freeze({mode:r.mode,capabilities:n,roots:i,atomicGroup:Object.freeze({id:e,member:t})})}function ec(r){return{mode:r.activation.mode,capabilities:[...r.activation.capabilities],roots:[...r.activation.roots],atomicGroup:{id:r.id,member:r.member,descriptorSha256:r.descriptorSha256,expectedCount:r.expectedCount,cohortSha256:r.cohortSha256}}}function tc(r,e){return r.ino===e.ino&&r.generation===e.generation&&r.dataSequence===e.dataSequence&&r.size===e.size&&r.isSymlink===e.isSymlink&&r.deleted===e.deleted&&r.materialized===e.materialized&&r.archivePath===e.archivePath&&r.sourcePath===e.sourcePath&&r.type===e.type&&r.inodeGroup===e.inodeGroup&&r.target===e.target}function yn(r,e,t){let n=r.content,i=r.inventory,s=r.activation,o=r.integrity,a=r.entries,c=r.url,l=r.mountPrefix,u=r.materialized,d=s?.atomicGroup;if(n===void 0||i===void 0||s===void 0||d===void 0||s.mode!=="first-use"||d.id!==e||d.member!==t||u)throw new Error(`Lazy atomic activation member ${t} changed before snapshot`);if(o?.sha256!==n.sha256||o?.bytes!==n.bytes||c!==(n.transports[0]??""))throw new Error(`Lazy atomic activation member ${t} has inconsistent integrity`);let f=uo(n),y=Ja(i),h=Qa(s,e,t),p=new Map;for(let z of y)z.type==="file"&&p.set(z.inodeGroup,z.sourcePath);let w=y.filter(z=>z.type!=="directory");if(a.size!==w.length)throw new Error(`Lazy atomic activation member ${t} has inconsistent runtime entries`);let m=w.map(z=>{let E=a.get(z.vfsPath),v=z.type==="symlink",A=v?z.sourcePath:p.get(z.inodeGroup),L=E!==void 0&&(E.sourcePath===z.sourcePath&&E.type===z.type&&E.target===z.target||z.type==="hardlink"&&E.sourcePath===A&&E.type==="file"&&E.target===void 0),x=E===void 0?["missing"]:[A===void 0?"archivePath source":void 0,E.generation===void 0?"generation":void 0,E.dataSequence===void 0?"dataSequence":void 0,E.size!==z.size?"size":void 0,E.isSymlink!==v?"symlink kind":void 0,E.deleted?"deletion state":void 0,E.materialized!==v?"materialization state":void 0,E.archivePath!==A?"archivePath":void 0,L?void 0:"descriptor mapping",E.inodeGroup!==z.inodeGroup?"inode group":void 0].filter(I=>I!==void 0);if(x.length>0)throw new Error(`Lazy atomic activation member ${t} has inconsistent mapping at ${z.vfsPath}: ${x.join(", ")}`);let _=E;return Object.freeze({vfsPath:z.vfsPath,ino:_.ino,generation:_.generation,dataSequence:_.dataSequence,size:_.size,isSymlink:_.isSymlink,deleted:!1,materialized:_.materialized,archivePath:A,sourcePath:z.sourcePath,type:z.type,...z.inodeGroup===void 0?{}:{inodeGroup:z.inodeGroup},...z.target===void 0?{}:{target:z.target}})});Object.freeze(m);let g=Object.freeze({sha256:f.sha256,bytes:f.bytes}),S=Ya(f,y,l,h);return Object.freeze({id:e,member:t,descriptorBytes:S,content:f,inventory:y,activation:h,url:f.transports[0]??"",mountPrefix:l,integrity:g,entries:m})}function ji(r,e,t,n){return Object.freeze({...r,descriptorSha256:e,expectedCount:t,cohortSha256:n})}function Yi(r,e){return r.id!==e.id||r.member!==e.member||r.url!==e.url||r.mountPrefix!==e.mountPrefix||r.integrity.sha256!==e.integrity.sha256||r.integrity.bytes!==e.integrity.bytes||r.content.transports.length!==e.content.transports.length||r.content.transports.some((t,n)=>t!==e.content.transports[n])||!Xa(r.descriptorBytes,e.descriptorBytes)||r.entries.length!==e.entries.length?!1:r.entries.every((t,n)=>{let i=e.entries[n];return i!==void 0&&t.vfsPath===i.vfsPath&&tc(t,i)})}function nc(r,e){let t=uo(r.content,r.content.transports.map(e));return Object.freeze({...r,content:t,url:t.transports[0]??""})}function Xi(r){return{paths:Array.from(r.paths),expectedIno:r.ino,expectedGeneration:r.generation,expectedDataSequence:r.dataSequence,data:r.content}}var zn=class r{fs;imageMetadata;lazyFiles=new Map;lazyArchiveGroups=[];deferredTreeMaterializationHandles=new WeakMap;lazyArchiveInodes=new Map;lazyAtomicGroups=new Map;lazyAtomicGroupByTree=new WeakMap;sealedLazyAtomicStates=new WeakMap;lazyDownloadListeners=new Set;lazyPreparations=new Map;lazyTransport={fetcher:(e,t)=>t===void 0?globalThis.fetch(e):globalThis.fetch(e,t)};constructor(e,t=null){this.fs=e,this.imageMetadata=t}static inodeKey(e,t){return`${e}:${t}`}static canAdoptLegacyLazyStub(e){return(e.mode&ke)===Gt&&e.size===0&&e.dataSequence<=1}reconcileLazyIdentityState(e){for(let[t,n]of this.lazyFiles){let i=e.get(t);if(!i||i.dataSequence!==n.dataSequence||i.paths.length===0){this.lazyFiles.delete(t);continue}n.paths=new Set(i.paths),n.paths.has(n.path)||(n.path=i.paths[0])}this.lazyArchiveInodes.clear();for(let t of this.lazyArchiveGroups){let n=this.lazyAtomicGroupByTree.get(t),i=this.sealedLazyAtomicStates.get(t);if(i!==void 0){if(!n?.committed)for(let c of i.snapshot.entries){if(c.isSymlink||c.materialized||c.generation===void 0)continue;let l=r.inodeKey(c.ino,c.generation),u=e.get(l);u!==void 0&&u.dataSequence===c.dataSequence&&u.paths.length>0&&this.lazyArchiveInodes.set(l,t)}continue}let s=t.content!==void 0&&t.inventory!==void 0&&!t.materialized,o=new Map;for(let c of t.entries.values()){if(c.deleted||c.materialized||c.generation===void 0)continue;let l=r.inodeKey(c.ino,c.generation);o.has(l)||o.set(l,c)}let a=new Map(Array.from(t.entries.entries()).filter(([,c])=>c.deleted||c.isSymlink&&!c.deleted));for(let[c,l]of o){let u=e.get(c);if(!(!u||u.dataSequence!==(l.dataSequence??0))){for(let d of u.paths)a.set(d,{...l,ino:u.ino,generation:u.generation,dataSequence:u.dataSequence,deleted:!1,materialized:!1});u.paths.length>0&&this.lazyArchiveInodes.set(c,t)}}t.entries=a,t.materialized=!Array.from(a.values()).some(c=>!c.isSymlink&&!c.materialized)&&!s&&(n===void 0||n.committed)}}validatePendingLazyTreeNamespaceState(e){let t=new Map;for(let n of e.values())for(let i of n.paths){if(t.has(i))throw new Error(`SharedFS namespace identity is ambiguous at ${i}`);t.set(i,n)}for(let n of this.lazyArchiveGroups){let i=this.lazyAtomicGroupByTree.get(n),o=this.sealedLazyAtomicStates.get(n)?.snapshot;if(i?.committed||o===void 0&&n.materialized||o===void 0&&(n.content===void 0||n.inventory===void 0))continue;let a=o?.inventory??n.inventory,c=o===void 0?n.entries:new Map(o.entries.map(y=>[y.vfsPath,y])),l=new Map,u=new Map,d=new Set;for(let y of c.values())y.deleted&&y.inodeGroup!==void 0&&d.add(y.inodeGroup);for(let y of a){if(y.type!=="file"&&y.type!=="hardlink")continue;l.set(y.inodeGroup,(l.get(y.inodeGroup)??0)+1);let h=u.get(y.inodeGroup)??[];h.push(y.vfsPath),u.set(y.inodeGroup,h)}let f=new Set([...d].filter(y=>u.get(y)?.every(h=>!t.has(h))));for(let y of a){let h=t.get(y.vfsPath);if(h===void 0){if(y.inodeGroup!==void 0&&f.has(y.inodeGroup))continue;throw new Error(`Lazy tree namespace entry ${y.vfsPath} is missing from the captured filesystem state`)}let p=y.type==="directory"?Ve:y.type==="symlink"?fn:Gt;if((h.mode&ke)!==p||(h.mode&4095)!==y.mode)throw new Error(`Lazy tree namespace entry ${y.vfsPath} disagrees with its captured type or mode`);if(y.type==="directory")continue;let w=c.get(y.vfsPath);if(w===void 0||w.ino!==h.ino||w.generation!==h.generation||w.dataSequence!==h.dataSequence)throw new Error(`Lazy tree namespace entry ${y.vfsPath} changed identity before serialization`);if(y.type==="symlink"){let m=new TextEncoder().encode(y.target).byteLength;if(h.linkCount!==1||h.size!==y.size||h.size!==m||h.symlinkTarget!==y.target)throw new Error(`Lazy tree symlink ${y.vfsPath} disagrees with its captured inventory`);continue}if(h.size!==0||h.linkCount!==l.get(y.inodeGroup))throw new Error(`Lazy tree stub ${y.vfsPath} has changed data or undeclared aliases`)}}}lazyFileForStat(e){let t=r.inodeKey(e.ino,e.generation),n=this.lazyFiles.get(t);if(n&&n.dataSequence!==e.dataSequence){this.lazyFiles.delete(t);return}return n}lazyArchiveEntriesForRead(e){let t=this.lazyAtomicGroupByTree.get(e),n=this.sealedLazyAtomicStates.get(e);return n!==void 0&&!t?.committed?n.snapshot.entries:Array.from(e.entries,([i,s])=>({vfsPath:i,...s}))}lazyArchiveForStat(e){let t=r.inodeKey(e.ino,e.generation),n=this.lazyArchiveInodes.get(t);if(!n)return;let i=this.lazyArchiveEntriesForRead(n).filter(o=>o.ino===e.ino&&o.generation===e.generation&&!o.deleted&&!o.materialized);if(i.some(o=>o.dataSequence===e.dataSequence))return n;if(this.lazyArchiveInodes.delete(t),this.sealedLazyAtomicStates.get(n)===void 0)for(let o of i)o.materialized=!0}lazyBackingForStat(e){let t=r.inodeKey(e.ino,e.generation),n=this.lazyFiles.get(t);if(n)return{token:n,path:n.path};let i=this.lazyArchiveInodes.get(t);if(!i)return null;let s=this.lazyArchiveEntriesForRead(i).find(a=>a.ino===e.ino&&a.generation===e.generation&&!a.deleted&&!a.materialized)?.vfsPath;if(s===void 0)return null;let o=this.lazyAtomicGroupByTree.get(i);return o===void 0?{token:i,path:s}:{token:o.token,path:s,atomicGroup:o}}lazyBackingForPath(e){let t=this.lazyArchiveGroups.find(n=>{let i=this.lazyAtomicGroupByTree.get(n),s=this.sealedLazyAtomicStates.get(n)?.snapshot,o=s===void 0?!n.materialized:!i?.committed,a=s?.content??n.content,c=s?.inventory??n.inventory,l=s?.activation??n.activation,u=s?.entries??Array.from(n.entries.values());return o&&a!==void 0&&c!==void 0&&l!==void 0&&u.every(d=>d.deleted||d.materialized||d.isSymlink)&&l.roots.some(d=>d==="/"||e===d||e.startsWith(`${d}/`))});if(t){let n=this.lazyAtomicGroupByTree.get(t);return{token:n?.token??t,path:e,directGroup:t,...n===void 0?{}:{atomicGroup:n}}}try{let n=this.fs.stat(e),i=this.lazyBackingForStat(n);return i?{...i,path:e}:null}catch{return null}}startLazyPreparation(e){let{path:t,token:n}=e,i={status:"pending",promise:Promise.resolve(!1)},s=e.atomicGroup?this.ensureAtomicLazyGroupMaterialized(e.atomicGroup).then(()=>!0):e.directGroup?this.ensureArchiveMaterialized(e.directGroup).then(()=>!0):this.materializePath(t);return i.promise=s.then(o=>(i.status="fulfilled",this.lazyPreparations.get(n)===i&&this.lazyPreparations.delete(n),o),o=>{throw i.status="rejected",i.error=o,o}),i.promise.catch(()=>{}),this.lazyPreparations.set(n,i),i}registerLazyAtomicGroupMembership(e,t=!1){let n=e.activation?.atomicGroup;if(n===void 0)return;let{id:i,member:s}=n;if(e.content===void 0||e.inventory===void 0||e.activation?.mode!=="first-use")throw new Error(`Lazy atomic activation group ${i} accepts only typed first-use trees`);let o=this.lazyAtomicGroups.get(i);if(o===void 0)o={id:i,token:Object.freeze({id:i}),groups:new Map,committed:!1},this.lazyAtomicGroups.set(i,o);else if(o.committed)throw new Error(`Lazy atomic activation group ${i} is already materialized`);if(o.groups.has(s))throw new Error(`Lazy atomic activation group ${i} duplicates member ${s}`);if(yt(n)){if(o.expectedCount!==void 0&&(o.expectedCount!==n.expectedCount||o.cohortSha256!==n.cohortSha256))throw new Error(`Lazy atomic activation group ${i} has inconsistent seals`);o.expectedCount=n.expectedCount,o.cohortSha256=n.cohortSha256;let a=yn(e,i,s);this.sealedLazyAtomicStates.set(e,{snapshot:ji(a,n.descriptorSha256,n.expectedCount,n.cohortSha256),verified:t})}else if(o.expectedCount!==void 0)throw new Error(`Lazy atomic activation group ${i} mixes sealed and unsealed members`);o.groups.set(s,e),this.lazyAtomicGroupByTree.set(e,o)}async sealLazyAtomicGroup(e,t){let n=t.map(l=>ao({id:e,member:l}).member).sort();if(n.length===0||new Set(n).size!==n.length)throw new Error(`Lazy atomic activation group ${e} expected members are invalid`);let i=this.lazyAtomicGroups.get(e);if(i===void 0||i.committed)throw new Error(`Lazy atomic activation group ${e} is not pending`);let s=[...i.groups.keys()].sort();if(JSON.stringify(s)!==JSON.stringify(n))throw new Error(`Lazy atomic activation group ${e} members differ from its seal`);if(i.expectedCount!==void 0){if(i.expectedCount!==n.length||i.cohortSha256===void 0)throw new Error(`Lazy atomic activation group ${e} has an invalid existing seal`);await this.ensureLazyAtomicGroupSealValidated(i,n.map(l=>i.groups.get(l)),!0);return}let o=n.map(l=>yn(i.groups.get(l),e,l)),a=[];for(let l of o)a.push({member:l.member,descriptorSha256:await Kt(l.descriptorBytes,`Lazy atomic member ${l.member}`),source:l});let c=await Kt(Vi(e,a),`Lazy atomic activation group ${e}`);for(let l of a){let u=i.groups.get(l.member),d=yn(u,e,l.member);if(!Yi(l.source,d))throw new Error(`Lazy atomic activation member ${l.member} changed while sealing`)}for(let l of a){let u=i.groups.get(l.member);u.activation.atomicGroup={id:e,member:l.member,descriptorSha256:l.descriptorSha256,expectedCount:a.length,cohortSha256:c},this.sealedLazyAtomicStates.set(u,{snapshot:ji(l.source,l.descriptorSha256,a.length,c),verified:!0})}i.expectedCount=a.length,i.cohortSha256=c}async verifyImportedLazyAtomicGroupSeals(){await this.validatePendingLazyAtomicGroupSeals(!0)}guardSynchronousLazyAccess(e){let t=this.lazyBackingForPath(e);if(!t)return;let n=this.lazyPreparations.get(t.token);if(n?.status==="fulfilled"){this.lazyPreparations.delete(t.token);let s=this.lazyBackingForPath(e);if(!s)return;n=this.lazyPreparations.get(s.token)??this.startLazyPreparation(s)}else if(n?.status==="rejected"){this.lazyPreparations.delete(t.token);let s=n.error instanceof Error?n.error.message:String(n.error),o=new Error(`EIO: lazy backing for ${e} failed: ${s}`);throw o.code="EIO",o.cause=n.error,o}else n||(n=this.startLazyPreparation(t));let i=new Error(`EAGAIN: lazy backing for ${e} is being prepared`);throw i.code="EAGAIN",i}invalidateLazyData(e){let t=r.inodeKey(e.ino,e.generation);this.lazyFiles.delete(t);let n=this.lazyArchiveInodes.get(t);if(n){this.lazyArchiveInodes.delete(t);for(let i of n.entries.values())i.ino===e.ino&&i.generation===e.generation&&(i.materialized=!0)}}rewriteLazyNamespacePaths(e,t,n){let i=t.length>1?t.replace(/\/+$/,""):t,s=n.length>1?n.replace(/\/+$/,""):n,o=`${i}/`,a=`${s}/`,c=r.inodeKey(e.ino,e.generation),l=(e.mode&ke)===Ve,u=d=>d===i?s:l&&d.startsWith(o)?a+d.slice(o.length):d;for(let[d,f]of this.lazyFiles)!l&&d!==c||(f.paths=new Set(Array.from(f.paths,u)),f.path=u(f.path));for(let d of this.lazyArchiveGroups){let f=new Map;for(let[y,h]of d.entries){let p=h.generation===void 0?null:r.inodeKey(h.ino,h.generation);f.set(l||p===c?u(y):y,h)}d.entries=f,d.inventory&&(d.inventory=d.inventory.map(y=>({...y,vfsPath:u(y.vfsPath),...y.type==="hardlink"&&y.target!==void 0?{target:u(y.target)}:{}}))),d.activation&&(d.activation={...d.activation,roots:d.activation.roots.map(u)})}}get sharedBuffer(){return this.fs.buffer}static create(e,t){return new r(be.mkfs(e,t))}static fromExisting(e){return new r(be.mount(e))}rebaseToNewFileSystem(e){if(!Number.isSafeInteger(e)||e<=0)throw new Error(`Invalid MemoryFileSystem maxByteLength: ${e}`);let t=SharedArrayBuffer,{bytes:n,identities:i}=this.fs.snapshotState();this.reconcileLazyIdentityState(i);let s=this.serializeLazyEntries(),o=this.serializeValidatedLazyArchiveEntries(i),a=new t(n.byteLength);new Uint8Array(a).set(n);let c=new r(be.mount(a,{restoreImage:!0}),this.imageMetadata);c.importLazyEntries(s),c.importLazyArchiveEntriesInternal(o,!1,!0,"verified");let l=Math.min(e,Math.max(n.byteLength,ba)),u=new t(l,{maxByteLength:e}),d=r.create(u,e);d.setImageMetadata(this.imageMetadata);let f=new Set(s.flatMap(h=>h.paths??[h.path])),y=new Set;for(let h of o)if(!h.materialized)for(let p of h.entries)!p.deleted&&!p.isSymlink&&y.add(p.vfsPath);return c.copyPathToFreshFileSystem("/",d,f,y,new Map),d.importLazyEntries(s.map(h=>{let p=d.fs.lstat(h.path);return{...h,ino:p.ino,generation:p.generation,dataSequence:p.dataSequence}})),d.importLazyArchiveEntriesInternal(o.map(h=>({...h,entries:h.entries.map(p=>{if(p.deleted)return{...p,ino:0,generation:void 0};let w=d.fs.lstat(p.vfsPath);return{...p,ino:w.ino,generation:w.generation,dataSequence:w.dataSequence}})})),!1,!0,"verified"),d}getImageMetadata(){return Ta(this.imageMetadata)}setImageMetadata(e){this.imageMetadata=e===null?null:Er(e)}subscribeLazyDownloads(e){return this.lazyDownloadListeners.add(e),()=>this.lazyDownloadListeners.delete(e)}setLazyFetcher(e,t={}){this.lazyTransport={fetcher:e,...t.signal===void 0?{}:{signal:t.signal}}}emitLazyDownload(e){if(this.lazyDownloadListeners.size===0)return;let t={...e,t:Na()};for(let n of this.lazyDownloadListeners)try{n(t)}catch{}}async fetchLazyBytes(e,t){let n=0,i=e.integrity?.bytes??e.fallbackTotalBytes,s={id:e.id,kind:e.kind,url:e.url,path:e.path,mountPrefix:e.mountPrefix};for(let o=0;oe.integrity.bytes)throw new Error(`Lazy ${e.kind} exceeded expected byte count ${e.integrity.bytes}`);this.emitLazyDownload({...s,status:"progress",loadedBytes:n,totalBytes:i})}}}catch(d){try{await c.cancel(d)}catch{}throw d}}finally{c.releaseLock()}let u=Wa(l,n);return J(t.signal),await fr(u,e.kind,e.integrity),J(t.signal),this.emitLazyDownload({...s,status:"complete",loadedBytes:n,totalBytes:i??n}),u}catch(a){if(t.signal?.aborted){let u=t.signal.reason,d=u instanceof Error?u.message:String(u);throw this.emitLazyDownload({...s,status:"error",loadedBytes:n,totalBytes:i,error:d}),u}let c=o+1({...g})),activation:d,entries:new Map},w=g=>{let S=g.split("/").filter(Boolean),z="";for(let E=0;ES.vfsPath.split("/").length-z.vfsPath.split("/").length))if(g.type==="directory"){w(g.vfsPath);try{this.fs.mkdir(g.vfsPath,g.mode),this.fs.chmod(g.vfsPath,g.mode)}catch{if((this.fs.lstat(g.vfsPath).mode&ke)!==Ve)throw new Error(`Lazy tree directory collides at ${g.vfsPath}`)}}for(let g of l){if(g.type!=="symlink")continue;w(g.vfsPath),this.fs.symlink(g.target,g.vfsPath);let S=this.fs.lstat(g.vfsPath);p.entries.set(g.vfsPath,{ino:S.ino,generation:S.generation,dataSequence:S.dataSequence,size:g.size,isSymlink:!0,deleted:!1,materialized:!0,archivePath:g.sourcePath,sourcePath:g.sourcePath,type:"symlink",target:g.target})}let m=new Map;for(let g of l){if(g.type!=="file")continue;w(g.vfsPath);let S=this.fs.createLazyStub(g.vfsPath,g.mode);this.invalidateLazyData(S),m.set(g.inodeGroup,S);let z={ino:S.ino,generation:S.generation,dataSequence:S.dataSequence,size:g.size,isSymlink:!1,deleted:!1,materialized:!1,archivePath:g.sourcePath,sourcePath:g.sourcePath,type:"file",inodeGroup:g.inodeGroup};p.entries.set(g.vfsPath,z)}for(let g of l){if(g.type!=="hardlink")continue;let S=f.get(g.inodeGroup);w(g.vfsPath),this.fs.link(S.vfsPath,g.vfsPath);let z=this.fs.lstat(g.vfsPath),E=m.get(g.inodeGroup);if(z.ino!==E.ino||z.generation!==E.generation)throw new Error(`Lazy tree hardlink ${g.vfsPath} did not share its inode`);p.entries.set(g.vfsPath,{ino:z.ino,generation:z.generation,dataSequence:z.dataSequence,size:g.size,isSymlink:!1,deleted:!1,materialized:!1,archivePath:S.sourcePath,sourcePath:g.sourcePath,type:"hardlink",inodeGroup:g.inodeGroup,target:g.target})}if(y!==void 0){u!=="/"&&this.lchown(u,y.uid,y.gid);for(let g of l)this.lchown(g.vfsPath,y.uid,y.gid)}for(let g of p.entries.values())g.isSymlink||g.generation===void 0||this.lazyArchiveInodes.set(r.inodeKey(g.ino,g.generation),p);return this.lazyArchiveGroups.push(p),this.registerLazyAtomicGroupMembership(p),p}registerLazyTreeWithMaterializationHandle(e,t,n="/",i,s){let o=this.registerLazyTreeInternal(e,t,n,i,!0,s),a=Object.freeze({[va]:!0});return this.deferredTreeMaterializationHandles.set(a,o),a}registerLazyArchiveFromEntries(e,t,n,i,s){let o=Ht(n),a=Oa(e,t,o,i);a.some(({entry:l})=>!l.isDirectory&&!l.isSymlink)&&this.assertCanRegisterPendingLazyArchiveGroup();let c={...s?{content:Sn({decoder:"zip-v1",mediaType:"application/zip",sha256:s.sha256,bytes:s.bytes,expandedBytes:a.reduce((l,u)=>l+u.entry.uncompressedSize,0),sourceEntryCount:a.length,transports:[e]})}:{},url:e,mountPrefix:o,integrity:Vt(s),materialized:!1,entries:new Map};for(let{entry:l,vfsPath:u}of a){if(l.isDirectory)continue;let d=u.split("/").filter(Boolean),f="";for(let y=0;yl.deleted||l.materialized),this.lazyArchiveGroups.push(c),c}importLazyArchiveEntries(e){this.importLazyArchiveEntriesInternal(e,!1,!0,"reject")}async importVerifiedLazyArchiveEntries(e){let t=structuredClone(e),n=this.exportLazyArchiveEntries(),i=r.fromExisting(this.sharedBuffer);i.importLazyArchiveEntriesInternal([...n,...t],!1,!0,"pending"),await i.verifyImportedLazyAtomicGroupSeals(),this.importLazyArchiveEntriesInternal(t,!1,!0,"verified")}importLazyArchiveEntriesInternal(e,t,n,i){let s=xe(e,"Serialized lazy archive groups",0,Ji).map((u,d)=>{if(typeof u!="object"||u===null||Array.isArray(u))throw new Error(`Serialized lazy archive group ${d} must be an object`);let f=u.kind;if(f===Zt||f===mr||f===qe)return lo(u,f);if(f===Wt)return vr(u,!1);if(f!==void 0)throw new Error(`Serialized lazy archive group ${d} has an unsupported kind`);if(n)throw new Error(`Serialized lazy archive group ${d} is missing its kind discriminator`);return vr(u,!0)}),o=this.fs.identityState();this.reconcileLazyIdentityState(o);let a=[...this.serializeValidatedLazyArchiveEntries(o),...s];Hi(a);let c=[],l=new Map;for(let u of s){let d=new Map,f=u.mountPrefix.replace(/\/+$/,""),y=u.content!==void 0&&u.inventory!==void 0&&u.activation!==void 0,h=y?new Map(u.inventory.map(E=>[E.vfsPath,E])):null,p=y?new Map(u.inventory.map(E=>[En(E),E])):null,w=new Map,m=new Map,g=new Map;for(let E of u.entries){let v=null,A=u.materialized||E.materialized===!0||E.isSymlink;if(!E.deleted&&!A){if((E.generation===void 0||E.dataSequence===void 0)&&!t)throw new Error("Live lazy-archive metadata requires inode generation and data sequence");try{v=this.fs.lstat(E.vfsPath)}catch{if(y)throw new Error(`Serialized lazy tree stub ${E.vfsPath} is missing from the filesystem`);continue}if(v.ino!==E.ino){if(y)throw new Error(`Serialized lazy tree stub ${E.vfsPath} has a different inode`);continue}if(E.generation!==void 0&&v.generation!==E.generation){if(y)throw new Error(`Serialized lazy tree stub ${E.vfsPath} has a different generation`);continue}if(E.dataSequence===void 0){if(!r.canAdoptLegacyLazyStub(v)){if(y)throw new Error(`Serialized lazy tree stub ${E.vfsPath} is not pristine`);continue}}else if(v.dataSequence!==E.dataSequence){if(y)throw new Error(`Serialized lazy tree stub ${E.vfsPath} has a different data sequence`);continue}if(y){g.set(E.vfsPath,v);let x=h.get(E.vfsPath),_=p.get(En(E))??x;if(!_||(v.mode&ke)!==Gt||v.size!==0||(v.mode&4095)!==_.mode||x?.inodeGroup!==void 0&&x.inodeGroup!==_.inodeGroup)throw new Error(`Serialized lazy tree stub ${E.vfsPath} disagrees with its inventory`);let I=r.inodeKey(v.ino,v.generation),M=E.inodeGroup,V=w.get(M),C=m.get(I);if(V!==void 0&&V!==I||C!==void 0&&C!==M)throw new Error(`Serialized lazy tree inode group ${M} disagrees with the filesystem`);w.set(M,I),m.set(I,M)}}d.set(E.vfsPath,{ino:E.ino,generation:v?.generation??E.generation,dataSequence:v?.dataSequence??E.dataSequence,size:E.size,isSymlink:E.isSymlink,deleted:E.deleted,materialized:A,archivePath:E.archivePath??E.vfsPath.slice(f.length+1),sourcePath:E.sourcePath??E.archivePath??E.vfsPath.slice(f.length+1),type:E.type??(E.isSymlink?"symlink":"file"),inodeGroup:E.inodeGroup,target:E.target})}if(y){let E=new Map;for(let v of u.inventory){if(v.type==="file"||v.type==="hardlink"){E.set(v.inodeGroup,(E.get(v.inodeGroup)??0)+1);continue}let A;try{A=this.fs.lstat(v.vfsPath)}catch{throw new Error(`Serialized lazy tree namespace entry ${v.vfsPath} is missing from the filesystem`)}let L=v.type==="directory"?Ve:fn;if((A.mode&ke)!==L||(A.mode&4095)!==v.mode||v.type==="symlink"&&(A.size!==new TextEncoder().encode(v.target).byteLength||this.fs.readlink(v.vfsPath)!==v.target))throw new Error(`Serialized lazy tree namespace entry ${v.vfsPath} disagrees with its inventory`);v.type==="symlink"&&d.set(v.vfsPath,{ino:A.ino,generation:A.generation,dataSequence:A.dataSequence,size:v.size,isSymlink:!0,deleted:!1,materialized:!0,archivePath:v.sourcePath,sourcePath:v.sourcePath,type:"symlink",target:v.target})}if(u.activation?.atomicGroup!==void 0)for(let v of u.inventory){if(v.type!=="file"&&v.type!=="hardlink")continue;if(g.get(v.vfsPath).linkCount!==E.get(v.inodeGroup))throw new Error(`Serialized lazy atomic tree inode group ${v.inodeGroup} has undeclared aliases`)}}let S=u.content===void 0?void 0:Sn(u.content),z={content:S,url:S?.transports[0]??u.url,mountPrefix:u.mountPrefix,integrity:S?{sha256:S.sha256,bytes:S.bytes}:Vt(u.integrity),materialized:u.materialized||!(S&&u.inventory)&&Array.from(d.values()).every(E=>E.deleted||E.materialized),inventory:u.inventory?.map(E=>({...E})),activation:u.activation?{mode:u.activation.mode,capabilities:[...u.activation.capabilities],roots:[...u.activation.roots],...u.activation.atomicGroup===void 0?{}:{atomicGroup:{...u.activation.atomicGroup}}}:void 0,entries:d};if(c.push(z),!z.materialized){for(let[,E]of d)if(!E.deleted&&!E.materialized&&E.generation!==void 0){let v=r.inodeKey(E.ino,E.generation),A=l.get(v);if(A!==void 0&&A!==z)throw new Error(`Serialized lazy archive groups share pending inode ${v}`);if(this.lazyArchiveInodes.has(v))throw new Error(`Serialized lazy archive group collides with pending inode ${v}`);l.set(v,z)}}}for(let u of c){let d=u.activation?.atomicGroup;if(d!==void 0&&this.lazyAtomicGroups.get(d.id)?.committed)throw new Error(`Lazy atomic activation group ${d.id} is already materialized`)}if(i==="reject"&&c.some(u=>{let d=u.activation?.atomicGroup;return d!==void 0&&yt(d)}))throw new Error("Sealed lazy archive registrations require importVerifiedLazyArchiveEntries()");this.lazyArchiveGroups.push(...c);for(let u of c)this.registerLazyAtomicGroupMembership(u,i==="verified");for(let[u,d]of l)this.lazyArchiveInodes.set(u,d)}rewriteLazyArchiveUrls(e){for(let t of this.lazyArchiveGroups){let n=this.lazyAtomicGroupByTree.get(t),i=this.sealedLazyAtomicStates.get(t);if(i!==void 0&&!n?.committed){this.assertLazyAtomicSnapshotMatchesPublic(t);let s=nc(i.snapshot,e);t.content=qi(s.content),t.url=s.url,t.integrity={...s.integrity},i.snapshot=s;continue}t.content?(t.content={...t.content,transports:t.content.transports.map(e)},t.url=t.content.transports[0]):t.url=e(t.url)}}serializeLazyArchiveEntries(){let e=[];for(let t of this.lazyArchiveGroups){let n=this.lazyAtomicGroupByTree.get(t),i=this.sealedLazyAtomicStates.get(t);if(i!==void 0){if(n?.committed)continue;let c=i.snapshot;if(c.content.transports.length===0)throw new Error("Direct-materialization tree must be materialized before serialization");e.push({kind:qe,content:qi(c.content),inventory:c.inventory.map(l=>({...l})),activation:ec(c),url:c.url,mountPrefix:c.mountPrefix,integrity:{...c.integrity},materialized:!1,entries:c.entries.filter(l=>!l.deleted&&!l.materialized).map(({vfsPath:l,...u})=>({vfsPath:l,...u}))});continue}let s=Array.from(t.entries,([c,l])=>({vfsPath:c,ino:l.ino,generation:l.generation,dataSequence:l.dataSequence,size:l.size,isSymlink:l.isSymlink,deleted:l.deleted,materialized:l.materialized,archivePath:l.archivePath,sourcePath:l.sourcePath,type:l.type,inodeGroup:l.inodeGroup,target:l.target})).filter(c=>!c.deleted&&!c.materialized);if(s.length===0&&!(t.content&&t.inventory&&!t.materialized))continue;let o=t.content!==void 0&&t.inventory!==void 0&&t.activation!==void 0;if(o&&t.content.transports.length===0)throw new Error("Direct-materialization tree must be materialized before serialization");let a=t.activation?.atomicGroup;if(a!==void 0&&!yt(a))throw new Error(`Lazy atomic activation group ${a.id} must be sealed before serialization`);e.push(o?{kind:a!==void 0?qe:t.content.source===void 0?Zt:mr,content:t.content,inventory:t.inventory,activation:t.activation,url:t.url,mountPrefix:t.mountPrefix,integrity:t.integrity,materialized:!1,entries:s}:{kind:Wt,url:t.url,mountPrefix:t.mountPrefix,integrity:t.integrity,materialized:!1,entries:s})}return e}serializeValidatedLazyArchiveEntries(e){this.assertPendingLazyAtomicSnapshotsReadyForSerialization();let t=this.serializeLazyArchiveEntries();return Hi(t),this.validatePendingLazyTreeNamespaceState(e),t}exportLazyArchiveEntries(){let e=this.fs.identityState();return this.reconcileLazyIdentityState(e),this.serializeValidatedLazyArchiveEntries(e)}pendingDeferredTreeUsage(){let e=this.fs.identityState();return this.reconcileLazyIdentityState(e),oo(this.serializeValidatedLazyArchiveEntries(e))}assertCanAppendDeferredTreeUsage(e){wr(e);let t=this.pendingDeferredTreeUsage();wr({groups:t.groups+e.groups,archiveBytes:t.archiveBytes+e.archiveBytes,expandedBytes:t.expandedBytes+e.expandedBytes,payloadBytes:t.payloadBytes+e.payloadBytes,entries:t.entries+e.entries})}assertCanRegisterPendingLazyArchiveGroup(){if(this.reconcileLazyIdentityState(this.fs.identityState()),this.lazyArchiveGroups.filter(t=>{let n=this.lazyAtomicGroupByTree.get(t);return this.sealedLazyAtomicStates.get(t)?.snapshot!==void 0?!n?.committed:!t.materialized&&(t.content!==void 0&&t.inventory!==void 0||Array.from(t.entries.values()).some(s=>!s.deleted&&!s.materialized))}).length>=Se.maxGroups)throw new Error(`Cannot register another lazy archive group: ${Se.maxGroups} pending groups already exist`)}async preparePath(e){let t=!1,n=Math.max(3,this.lazyArchiveGroups.length+1);for(let i=0;i!s.materialized&&s.activation?.mode==="boot-prefetch"),t=0,n,i=Array.from({length:Math.min(e.length,La)},async()=>{for(;n===void 0;){let s=t;if(t+=1,s>=e.length)return;try{await this.prepareLazyTreeGroup(e[s])}catch(o){n??=o}}});if(await Promise.all(i),n!==void 0)throw n;return e.length}async materializeRegisteredDeferredTree(e,t){let n=this.deferredTreeMaterializationHandles.get(e);if(n===void 0)throw new Error("Deferred-tree handle was not issued by this filesystem");let i=this.lazyAtomicGroupByTree.get(n);if(i!==void 0)throw new Error(`Deferred tree belongs to atomic activation group ${i.id}; materialize the complete group instead`);if(n.materialized)return!1;let s=this.lazyPreparations.get(n);if(s!==void 0)return s.promise;let o=new Uint8Array(t.byteLength);o.set(t);let a={status:"pending",promise:Promise.resolve(!1)};a.promise=Promise.resolve().then(async()=>(await fr(o,"tree",n.integrity),await this.materializeArchiveBytes(n,o),!0)).then(c=>(a.status="fulfilled",c),c=>{throw a.status="rejected",a.error=c,c}),a.promise.catch(()=>{}),this.lazyPreparations.set(n,a);try{return await a.promise}finally{this.lazyPreparations.get(n)===a&&this.lazyPreparations.delete(n)}}async prepareLazyTreeGroup(e){let t=this.lazyAtomicGroupByTree.get(e);if(t?.committed||t===void 0&&e.materialized)return!1;let n=this.sealedLazyAtomicStates.get(e)?.snapshot,i={token:t?.token??e,path:n?.activation.roots[0]??e.activation?.roots[0]??e.mountPrefix,directGroup:e,...t===void 0?{}:{atomicGroup:t}},s=this.lazyPreparations.get(i.token)??this.startLazyPreparation(i);try{return await s.promise}finally{this.lazyPreparations.get(i.token)===s&&this.lazyPreparations.delete(i.token)}}async ensureMaterialized(e){return this.preparePath(e)}async materializePath(e){if(this.lazyFiles.size===0&&this.lazyArchiveInodes.size===0)return!1;let t;try{t=this.fs.stat(e)}catch{return!1}let n=r.inodeKey(t.ino,t.generation),i=this.lazyFiles.get(n);if(i){let o=this.lazyTransport,a=await this.fetchLazyBytes({id:`file:${t.ino}`,kind:"file",url:i.url,path:i.path,fallbackTotalBytes:i.size},o);for(let c=0;c<3;c++){if(this.lazyFiles.get(n)!==i)return!1;for(let l of new Set([e,...i.paths]))if(J(o.signal),this.fs.replaceIfIdentity(l,i.ino,i.generation,i.dataSequence,a))return i.path=l,this.lazyFiles.delete(n),!0;this.reconcileLazyIdentityState(this.fs.identityState())}throw new Error(`Lazy file kept changing names while materializing: ${e}`)}let s=this.lazyArchiveInodes.get(n);return s?(await this.ensureArchiveMaterialized(s,{path:e,ino:t.ino,generation:t.generation}),!this.lazyArchiveInodes.has(n)):!1}async decodeAndValidateLazyTree(e,t,n){let i=n?.content??e.content,s=n?.inventory??e.inventory;if(!i||!s)throw new Error("Lazy tree is missing its decoder or complete inventory");let o=new Map,a=new Map(s.map(f=>[f.vfsPath,f]));if(i.source!==void 0)for(let f of i.source.entries)o.set(f.sourcePath,f);else for(let f of s){if(f.type==="hardlink"){let h=a.get(f.target);if(!h)throw new Error(`Lazy tree hardlink target disappeared: ${f.target}`);if(f.sourcePath===h.sourcePath)continue}if(o.get(f.sourcePath))throw new Error(`Lazy tree inventory duplicates source member ${f.sourcePath}`);o.set(f.sourcePath,{sourcePath:f.sourcePath,type:f.type,mode:f.mode,size:f.size,...f.type==="symlink"?{target:f.target}:{},...f.type==="hardlink"?{target:a.get(f.target)?.sourcePath}:{}})}let c=new Map,l=0;if(i.decoder==="zip-v1"){let{parseZipCentralDirectory:f,extractZipEntryBounded:y}=await Promise.resolve().then(()=>(nr(),tr)),h=f(t);if(h.length!==i.sourceEntryCount||h.length!==o.size)throw new Error("Lazy ZIP tree decoded inventory counts differ from its descriptor");for(let p of h){let w=p.isDirectory?p.fileName.replace(/\/$/,""):p.fileName;if(c.has(w))throw new Error(`Lazy ZIP tree duplicates source member ${w}`);let m=o.get(w);if(!m)throw new Error(`Lazy ZIP tree has undeclared source member ${w}`);if(l+=p.uncompressedSize,l>i.expandedBytes||p.uncompressedSize!==m.size)throw new Error(`Lazy ZIP tree member ${w} exceeds its inventory`);let g=p.isDirectory?"directory":p.isSymlink?"symlink":"file",S=i.modePolicy==="portable-posix-v1"?g==="directory"?493:g==="symlink"?511:(p.mode&73)!==0?493:420:p.mode&4095;if(g!==m.type||S!==m.mode)throw new Error(`Lazy ZIP tree member ${w} differs from inventory`);if(p.isDirectory)c.set(w,{type:"directory",mode:S});else{let z=y(t,p,m.size);if(p.isSymlink){let E;try{E=new TextDecoder("utf-8",{fatal:!0}).decode(z)}catch{throw new Error(`Lazy ZIP tree symlink ${w} is not UTF-8`)}c.set(w,{type:"symlink",mode:S,target:E})}else c.set(w,{type:"file",mode:S,data:z})}}}else{let{parseTarGzip:f}=await Promise.resolve().then(()=>(Fi(),$i)),y=f(t,{label:`Lazy tree ${i.sha256}`,limits:{maxCompressedBytes:i.bytes,maxUncompressedBytes:i.expandedBytes,maxEntries:i.sourceEntryCount}});l=new DataView(t.buffer,t.byteOffset,t.byteLength).getUint32(t.byteLength-4,!0);for(let h of y){if(c.has(h.path))throw new Error(`Lazy TAR tree duplicates source member ${h.path}`);h.type==="file"?c.set(h.path,{type:"file",mode:h.mode,data:h.data}):h.type==="directory"?c.set(h.path,{type:"directory",mode:h.mode}):c.set(h.path,{type:h.type,mode:h.mode,target:h.linkName})}}if(c.size!==i.sourceEntryCount||c.size!==o.size||l!==i.expandedBytes)throw new Error("Lazy tree decoded inventory counts differ from its descriptor");for(let[f,y]of o){let h=c.get(f);if(!h)throw new Error(`Lazy tree is missing source member ${f}`);let p=y.type;if(h.type!==p)throw new Error(`Lazy tree member ${f} is ${h.type}, expected ${p}`);if((h.mode&4095)!==y.mode)throw new Error(`Lazy tree member ${f} mode differs from inventory`);if(p==="file"&&h.data?.byteLength!==y.size)throw new Error(`Lazy tree member ${f} size differs from inventory`);if(p==="symlink"&&h.target!==y.target)throw new Error(`Lazy tree symlink ${f} target differs from inventory`);if(p==="hardlink"&&h.target!==y.target)throw new Error(`Lazy tree hardlink ${f} target differs from inventory`)}let u=new Set(s.flatMap(f=>f.materialization==="archive-homebrew-relocate"?[f.sourcePath]:[]));if(i.source!==void 0){let f=new Map(i.source.entries.map(p=>[p.sourcePath,p])),y=so(i.source.entries),h=i.source.entries.filter(p=>p.sourcePath==="INSTALL_RECEIPT.json"||p.sourcePath.endsWith("/INSTALL_RECEIPT.json"));if(h.length>1)throw new Error(`Lazy Homebrew bottle has ${h.length} INSTALL_RECEIPT.json source members, expected at most one`);if(h.length===0){if(u.size>0)throw new Error("Lazy Homebrew bottle marks receipt relocation without INSTALL_RECEIPT.json")}else{let p=h[0],w=p.type==="file"?p:y.get(p.sourcePath),m=w===void 0?void 0:c.get(w.sourcePath);if(w?.type!=="file"||m?.type!=="file"||m.data===void 0)throw new Error("Lazy Homebrew bottle INSTALL_RECEIPT.json is not regular");let g=fi(m.data),S=p.sourcePath.lastIndexOf("/"),z=S<0?"":p.sourcePath.slice(0,S),E=qa(s,n?.activation??e.activation,p.sourcePath,z),v=new Set(g.changedFiles.map(x=>z.length===0?x:`${z}/${x}`)),A=new Set(s.flatMap(x=>x.materialization!=="descriptor"&&v.has(x.sourcePath)?[x.sourcePath]:[]));if(u.size!==A.size||[...u].some(x=>!A.has(x)))throw new Error("Lazy Homebrew bottle relocation markers differ from INSTALL_RECEIPT.json");let L=new Set;for(let x of v){let _=f.get(x),I=_?.type==="file"?_:_===void 0?void 0:y.get(_.sourcePath),M=I===void 0?void 0:c.get(I.sourcePath);if(I?.type!=="file"||M?.type!=="file"||M.data===void 0)throw new Error(`Lazy Homebrew bottle changed source ${x} is not regular`);L.has(I.sourcePath)||(M.data=hi(M.data,g,x,E),L.add(I.sourcePath))}}}else if(u.size>0)throw new Error("Lazy tree receipt relocation requires original-bottle source truth");let d=new Map;for(let f of s){if(f.type!=="file"||f.materialization==="descriptor")continue;let y=c.get(f.sourcePath);if(y?.type!=="file"||!y.data)throw new Error(`Lazy tree has no file content for ${f.sourcePath}`);d.set(f.sourcePath,y.data)}return d}async ensureArchiveMaterialized(e,t){let n=this.lazyAtomicGroupByTree.get(e);if(n!==void 0){if(n.committed)return;let o=this.sealedLazyAtomicStates.get(e)?.snapshot,a=this.lazyPreparations.get(n.token)??this.startLazyPreparation({token:n.token,path:o?.activation.roots[0]??e.activation?.roots[0]??e.mountPrefix,atomicGroup:n});try{await a.promise}finally{this.lazyPreparations.get(n.token)===a&&this.lazyPreparations.delete(n.token)}return}if(e.materialized)return;let i=this.lazyTransport,s=await this.fetchLazyArchiveData(e,i);J(i.signal),await this.materializeArchiveBytes(e,s,t,i.signal)}async fetchLazyArchiveData(e,t,n){let i=n?.content??e.content,s=n?.inventory??e.inventory,o=i!==void 0&&s!==void 0,a=n?.mountPrefix??e.mountPrefix,c=n?.integrity??e.integrity,l=o?i.transports:[n?.url??e.url],u=[],d=null;for(let[f,y]of l.entries())try{d=await this.fetchLazyBytes({id:`archive:${a}:${i?.sha256??y}:${f}`,kind:o?"tree":"archive",url:y,mountPrefix:a,integrity:c},t);break}catch(h){if(J(t.signal),io(h))throw h;u.push(h instanceof Error?h.message:String(h))}if(J(t.signal),d===null)throw new Error(`All ${l.length} lazy ${o?"tree":"archive"} transports failed: ${u.join("; ")}`);return d}async materializeArchiveBytes(e,t,n,i){if(J(i),e.materialized)return;let s=await this.prepareLazyArchiveContents(e,t,i),o=n?r.inodeKey(n.ino,n.generation):null;for(let a=0;a<3;a++){let c=this.collectLazyArchiveReplacements(e,s,n);if(c.size>0&&(J(i),!this.fs.replaceManyIfIdentities(Array.from(c.values(),Xi)))){if(this.reconcileLazyIdentityState(this.fs.identityState()),o&&!this.lazyArchiveInodes.has(o))return;continue}if(J(i),this.publishLazyArchiveReplacements(e,c),e.materialized||(this.reconcileLazyIdentityState(this.fs.identityState()),o&&!this.lazyArchiveInodes.has(o)))return}if(o&&this.lazyArchiveInodes.has(o))throw new Error(`Lazy archive member kept changing names while materializing: ${n?.path}`)}async prepareLazyArchiveContents(e,t,n,i){J(n);let s=i?.content??e.content,o=i?.inventory??e.inventory,c=s!==void 0&&o!==void 0?await this.decodeAndValidateLazyTree(e,t,i):null;J(n);let{parseZipCentralDirectory:l,extractZipEntry:u}=await Promise.resolve().then(()=>(nr(),tr));J(n);let d=c?[]:l(t),f=new Map;for(let m of d){if(f.has(m.fileName))throw new Error(`Lazy archive contains duplicate member: ${m.fileName}`);f.set(m.fileName,m)}let h=(i?.mountPrefix??e.mountPrefix).replace(/\/+$/,""),p=new Map,w=i===void 0?Array.from(e.entries):i.entries.map(m=>[m.vfsPath,m]);for(let[m,g]of w){if(g.deleted||g.materialized)continue;let S=g.archivePath??m.slice(h.length+1),z=c?void 0:f.get(S),E=c?.get(S);if(c){if(E===void 0||E.byteLength!==g.size)throw new Error(`Lazy tree member ${S} does not match its registered metadata`)}else if(z===void 0||z.isDirectory||z.isSymlink||z.uncompressedSize!==g.size)throw new Error(`Lazy archive member ${S} does not match its registered metadata`);if(g.generation===void 0)continue;let v=r.inodeKey(g.ino,g.generation),A=p.get(v);if(A&&A.archivePath!==S)throw new Error(`Lazy archive aliases for inode ${v} name different members`);if(!A){let L=E??u(t,z);if(L.byteLength!==g.size)throw new Error(`Lazy archive member ${S} extracted ${L.byteLength} bytes, expected ${g.size}`);p.set(v,{archivePath:S,content:L})}}return p}collectLazyArchiveReplacements(e,t,n,i){let s=new Map,o=i===void 0?Array.from(e.entries):i.entries.map(a=>[a.vfsPath,a]);for(let[a,c]of o){if(c.deleted||c.materialized||c.generation===void 0)continue;let l=r.inodeKey(c.ino,c.generation);if(this.lazyArchiveInodes.get(l)!==e)continue;let u=t.get(l);if(!u)throw new Error(`Lazy archive has no extracted content for inode ${l}`);let d=s.get(l);d||(d={ino:c.ino,generation:c.generation,dataSequence:c.dataSequence??0,paths:new Set,content:u.content},s.set(l,d)),d.paths.add(a),n&&n.ino===c.ino&&n.generation===c.generation&&d.paths.add(n.path)}return s}publishLazyArchiveReplacements(e,t){for(let[n,i]of t){this.lazyArchiveInodes.delete(n);for(let s of e.entries.values())s.ino===i.ino&&s.generation===i.generation&&(s.materialized=!0)}e.materialized=Array.from(e.entries.values()).every(n=>n.deleted||n.materialized)}collectAtomicTreeNamespace(e,t){let n=new Set,i=new Map,s=new Map,o=new Map(t.entries.map(c=>[c.vfsPath,c]));for(let c of t.inventory)(c.type==="file"||c.type==="hardlink")&&s.set(c.inodeGroup,(s.get(c.inodeGroup)??0)+1);let a=[];for(let c of t.inventory){let l;try{l=this.fs.lstat(c.vfsPath)}catch{throw new Error(`Lazy atomic tree changed at ${c.vfsPath}`)}let u=c.type==="directory"?Ve:c.type==="symlink"?fn:Gt;if((l.mode&ke)!==u||(l.mode&4095)!==c.mode)throw new Error(`Lazy atomic tree changed at ${c.vfsPath}`);if(c.type==="symlink"){let d=o.get(c.vfsPath);if(d===void 0||!d.isSymlink||d.deleted||d.ino!==l.ino||d.generation!==l.generation||d.dataSequence!==l.dataSequence||this.fs.readlink(c.vfsPath)!==c.target)throw new Error(`Lazy atomic tree changed at ${c.vfsPath}`)}else if(c.type==="file"||c.type==="hardlink"){let d=o.get(c.vfsPath);if(d===void 0||d.deleted||d.materialized||d.isSymlink||d.generation===void 0||d.inodeGroup!==c.inodeGroup||d.ino!==l.ino||d.generation!==l.generation||d.dataSequence!==l.dataSequence||l.size!==0||l.linkCount!==s.get(c.inodeGroup))throw new Error(`Lazy atomic tree changed at ${c.vfsPath}`);let f=r.inodeKey(d.ino,d.generation);if(this.lazyArchiveInodes.get(f)!==e)throw new Error(`Lazy atomic tree lost deferred ownership of ${c.vfsPath}`);let y=i.get(c.inodeGroup);if(y!==void 0&&y!==f)throw new Error(`Lazy atomic tree split hard links at ${c.vfsPath}`);i.set(c.inodeGroup,f),n.add(f)}a.push({path:c.vfsPath,expectedIno:l.ino,expectedGeneration:l.generation,expectedDataSequence:l.dataSequence,expectedMode:l.mode,expectedLinkCount:l.linkCount,expectedSize:l.size,expectedUid:l.uid,expectedGid:l.gid})}return{guards:a,pendingIdentities:n.size}}assertLazyAtomicSnapshotMatchesPublic(e){let t=this.sealedLazyAtomicStates.get(e),n=t?.snapshot,i=e.activation?.atomicGroup,s=n?.member??i?.member??"unknown",o;if(n!==void 0)try{o=yn(e,n.id,n.member)}catch{o=void 0}if(t===void 0||n===void 0||i===void 0||!yt(i)||i.id!==n.id||i.member!==n.member||i.descriptorSha256!==n.descriptorSha256||i.expectedCount!==n.expectedCount||i.cohortSha256!==n.cohortSha256||o===void 0||!Yi(n,o))throw new Error(`Lazy atomic activation member ${s} changed after sealing`);return t}assertPendingLazyAtomicSnapshotsReadyForSerialization(){for(let e of this.lazyArchiveGroups){if(!this.sealedLazyAtomicStates.has(e)||this.lazyAtomicGroupByTree.get(e)?.committed)continue;let n=this.assertLazyAtomicSnapshotMatchesPublic(e);if(!n.verified)throw new Error(`Lazy atomic activation group ${n.snapshot.id} has not been cryptographically verified after import`)}}async validatePendingLazyAtomicGroupSeals(e){for(let t of this.lazyAtomicGroups.values()){if(t.committed||t.expectedCount===void 0||t.cohortSha256===void 0)continue;let n=[...t.groups.entries()].sort(([i],[s])=>is?1:0).map(([,i])=>i);await this.ensureLazyAtomicGroupSealValidated(t,n,e)}}async ensureLazyAtomicGroupSealValidated(e,t,n){if(this.assertLazyAtomicGroupSealValidatedAtLinearization(e,t,n))return;let i=e.sealValidationFlight;if(i===void 0&&(i=this.validateLazyAtomicGroupSealOnce(e,t),e.sealValidationFlight=i,i.then(()=>{e.sealValidationFlight===i&&(e.sealValidationFlight=void 0)},()=>{e.sealValidationFlight===i&&(e.sealValidationFlight=void 0)})),await i,!this.assertLazyAtomicGroupSealValidatedAtLinearization(e,t,n))throw new Error(`Lazy atomic activation group ${e.id} seal verification did not authenticate every member`)}assertLazyAtomicGroupSealValidatedAtLinearization(e,t,n){if(e.committed)return!0;if(e.expectedCount===void 0||e.cohortSha256===void 0||t.length!==e.expectedCount)throw new Error(`Lazy atomic activation group ${e.id} is not completely sealed`);let i=!0,s=[];for(let o of t){let a=this.assertLazyAtomicSnapshotMatchesPublic(o),c=a.snapshot;if(c.id!==e.id||c.expectedCount!==e.expectedCount||c.cohortSha256!==e.cohortSha256||e.groups.get(c.member)!==o)throw new Error(`Lazy atomic activation group ${e.id} has an inconsistent member`);i&&=a.verified,s.push(a)}if(i&&n)for(let o=0;ohp?1:0).map(([,h])=>h);if(t.length===0)throw new Error(`Lazy atomic activation group ${e.id} has no trees`);if(await this.ensureLazyAtomicGroupSealValidated(e,t,!0),e.committed)return;let n=t.map(h=>this.sealedLazyAtomicStates.get(h).snapshot),i=t.map((h,p)=>({group:h,...this.collectAtomicTreeNamespace(h,n[p])})),s=this.lazyTransport,o=new Array(t.length),a=0,c=!1,l,u=Array.from({length:Math.min(ka,t.length)},async()=>{for(;!c;){let h=a++;if(h>=t.length)return;let p=t[h],w=n[h];try{let m=await this.fetchLazyArchiveData(p,s,w);J(s.signal),o[h]={group:p,snapshot:w,contents:await this.prepareLazyArchiveContents(p,m,s.signal,w)}}catch(m){c||(c=!0,l=m)}}});if(await Promise.all(u),c)throw o.fill(void 0),l;J(s.signal);for(let h of t)this.assertLazyAtomicSnapshotMatchesPublic(h);let d=[],f=[],y=[];for(let h=0;h{let a=this.lazyAtomicGroupByTree.get(o);return this.sealedLazyAtomicStates.get(o)?.snapshot===void 0?!o.materialized&&o.content!==void 0&&o.inventory!==void 0:!a?.committed});if(this.lazyFiles.size===0&&this.lazyArchiveInodes.size===0&&n.length===0)return;let i=Array.from(this.lazyFiles.values(),o=>o.path);for(let o of i)await this.ensureMaterialized(o);let s=new Set(this.lazyArchiveInodes.values());for(let o of n)s.add(o);for(let o of s)await this.prepareLazyTreeGroup(o)}this.reconcileLazyIdentityState(this.fs.identityState());let e=this.lazyArchiveGroups.some(t=>{let n=this.lazyAtomicGroupByTree.get(t);return this.sealedLazyAtomicStates.get(t)?.snapshot===void 0?!t.materialized&&t.content!==void 0&&t.inventory!==void 0:!n?.committed});if(this.lazyFiles.size!==0||this.lazyArchiveInodes.size!==0||e)throw new Error("Cannot create a self-contained VFS image while lazy entries remain pending")}async saveImage(e){e?.materializeAll&&await this.materializeAllLazyEntries(),await this.validatePendingLazyAtomicGroupSeals(!1);let{bytes:t,identities:n}=this.fs.snapshotState({normalizeTimestampsMs:e?.normalizeTimestampsMs});this.reconcileLazyIdentityState(n);let i=this.serializeLazyEntries(),s=i.length>0,o=s?new TextEncoder().encode(JSON.stringify(i)):new Uint8Array(0);if(o.byteLength>mn)throw new Error(`VFS image lazy metadata exceeds ${mn} bytes`);let a=this.serializeValidatedLazyArchiveEntries(n),c=a.length>0,l=c?new TextEncoder().encode(JSON.stringify(a)):new Uint8Array(0);if(l.byteLength>gn)throw new Error(`VFS image lazy archive metadata exceeds ${gn} bytes`);let u=e?.metadata===void 0?this.imageMetadata:e.metadata,d=Ba(u),f=d.byteLength>0,y=c?4+l.byteLength:0,h=f?4+d.byteLength:0,p=de+t.byteLength+4+o.byteLength+y+h,w=new Uint8Array(p),m=new DataView(w.buffer);m.setUint32(0,hr,!0),m.setUint32(4,yr,!0),m.setUint32(8,(s?cr:0)|(c?pn:0)|(c?dr:0)|(f?lr:0),!0),m.setUint32(12,t.byteLength,!0),w.set(t,de);let g=de+t.byteLength;if(m.setUint32(g,o.byteLength,!0),o.byteLength>0&&w.set(o,g+4),c){let S=g+4+o.byteLength;m.setUint32(S,l.byteLength,!0),w.set(l,S+4)}if(f){let S=g+4+o.byteLength+y;m.setUint32(S,d.byteLength,!0),w.set(d,S+4)}return w}static readImageMetadata(e){let t=hn(e);if(!(t.flags&lr))return null;let{metadataOffset:n}=Wi(t.image,t.view,t.flags,t.sabLen);if(t.image.byteLengthpt)throw new Error(`VFS image metadata exceeds ${pt} bytes`);if(t.image.byteLength0){let w=n.subarray(h+4,h+4+p),m=xe(Zi(w,"VFS image lazy metadata"),"VFS image lazy entries",0,mt);y.importLazyEntriesInternal(m,!0)}if(s&pn){let w=a.archiveOffset,m=i.getUint32(w,!0);if(m>0){let g=n.subarray(w+4,w+4+m),S=Zi(g,"VFS image lazy archive metadata");y.importLazyArchiveEntriesInternal(S,!0,!!(s&dr),"pending")}}return y}adaptStat(e){return{dev:0,ino:e.ino,mode:e.mode,nlink:e.linkCount,uid:e.uid,gid:e.gid,size:e.size,atimeMs:e.atime,mtimeMs:e.mtime,ctimeMs:e.ctime}}adaptStatWithLazySize(e){let t=this.adaptStat(e),n=this.lazyFileForStat(e);if(n)return t.size=n.size,t;let i=this.lazyArchiveForStat(e);if(i){for(let s of this.lazyArchiveEntriesForRead(i))if(s.ino===e.ino&&s.generation===e.generation&&!s.deleted){t.size=s.size;break}}return t}open(e,t,n){(t&Nt)===0&&!((t&Bt)!==0&&(t&Vn)!==0)&&this.guardSynchronousLazyAccess(e);let i=this.fs.open(e,t,n);return(t&Nt)!==0&&this.invalidateLazyData(this.fs.fstat(i)),i}close(e){return this.fs.close(e),0}read(e,t,n,i){if(i>0){let s=this.lazyBackingForStat(this.fs.fstat(e));s&&(this.reconcileLazyIdentityState(this.fs.identityState()),s=this.lazyBackingForStat(this.fs.fstat(e)),s&&this.guardSynchronousLazyAccess(s.path))}return n!==null?this.fs.readAt(e,t.subarray(0,i),n):this.fs.read(e,t.subarray(0,i))}write(e,t,n,i){if(n!==null){let o=this.fs.writeAt(e,t.subarray(0,i),n);return o>0&&this.invalidateLazyData(this.fs.fstat(e)),o}let s=this.fs.write(e,t.subarray(0,i));return s>0&&this.invalidateLazyData(this.fs.fstat(e)),s}seek(e,t,n){return this.fs.lseek(e,t,n)}fstat(e){return this.adaptStatWithLazySize(this.fs.fstat(e))}fpathconf(e,t){let n=this.fstat(e);return Mn(n,t,{supportsSymlinks:!0,timestampResolutionNs:1e6})}ftruncate(e,t){this.fs.ftruncate(e,t),this.invalidateLazyData(this.fs.fstat(e))}fsync(e){}fchmod(e,t){this.fs.fchmod(e,t)}fchown(e,t,n){this.fs.fchown(e,t,n)}stat(e){return this.adaptStatWithLazySize(this.fs.stat(e))}lstat(e){return this.adaptStatWithLazySize(this.fs.lstat(e))}statfs(e){this.fs.stat(e);let t=this.fs.statfs();return{type:1397114451,bsize:t.blockSize,blocks:t.totalBlocks,bfree:t.freeBlocks,bavail:t.freeBlocks,files:t.totalInodes,ffree:t.freeInodes,fsid:0,namelen:t.maxName,frsize:t.blockSize,flags:0}}pathconf(e,t){let n=this.stat(e);return Mn(n,t,{supportsSymlinks:!0,timestampResolutionNs:1e6})}mkdir(e,t){this.fs.mkdir(e,t)}rmdir(e){this.fs.rmdir(e)}unlink(e){let t=this.fs.unlink(e),n=r.inodeKey(t.ino,t.generation);if(t.linkCount>1&&(this.lazyFiles.has(n)||this.lazyArchiveInodes.has(n))){this.reconcileLazyIdentityState(this.fs.identityState());return}let i=this.lazyFiles.get(n);i&&(i.paths.delete(e),t.linkCount<=1?this.lazyFiles.delete(n):i.path===e&&(i.path=i.paths.values().next().value));let s=this.lazyArchiveInodes.get(n);if(s){let o=s.entries.get(e);if(t.linkCount<=1){for(let a of s.entries.values())a.ino===t.ino&&a.generation===t.generation&&(a.deleted=!0);this.lazyArchiveInodes.delete(n)}else o&&s.entries.delete(e)}}rename(e,t){let{source:n,replaced:i}=this.fs.rename(e,t);if(i&&i.ino===n.ino&&i.generation===n.generation)return;let s=!1;if(i){let o=r.inodeKey(i.ino,i.generation);i.linkCount>1&&(this.lazyFiles.has(o)||this.lazyArchiveInodes.has(o))&&(this.reconcileLazyIdentityState(this.fs.identityState()),s=!0);let a=this.lazyFiles.get(o);!s&&a&&(a.paths.delete(t),i.linkCount<=1?this.lazyFiles.delete(o):a.path===t&&(a.path=a.paths.values().next().value));let c=this.lazyArchiveInodes.get(o);if(!s&&c){let l=c.entries.get(t);i.linkCount<=1?(l&&(l.deleted=!0),this.lazyArchiveInodes.delete(o)):l&&c.entries.delete(t)}}s||this.rewriteLazyNamespacePaths(n,e,t)}link(e,t){let n=this.fs.link(e,t),i=r.inodeKey(n.ino,n.generation),s=this.lazyFiles.get(i);s&&s.paths.add(t);let o=this.lazyArchiveInodes.get(i);if(o){let a=Array.from(o.entries.values()).find(c=>c.ino===n.ino&&c.generation===n.generation);a&&o.entries.set(t,{...a})}}symlink(e,t){this.fs.symlink(e,t)}readlink(e){return this.fs.readlink(e)}chmod(e,t){this.fs.chmod(e,t)}chown(e,t,n){this.fs.chown(e,t,n)}lchown(e,t,n){this.fs.lchown(e,t,n)}createFileWithOwner(e,t,n,i,s){let o=this.open(e,577,t);s.length>0&&this.write(o,s,null,s.length),this.close(o),this.chown(e,n,i),this.chmod(e,t)}mkdirWithOwner(e,t,n,i){this.mkdir(e,t),this.chown(e,n,i),this.chmod(e,t)}symlinkWithOwner(e,t,n,i){this.symlink(e,t),this.lchown(t,n,i)}copyPathToFreshFileSystem(e,t,n,i,s){let o=this.lstat(e),a=o.mode&ke,c=o.mode&4095;if(a===Ve){e==="/"?(t.chown(e,o.uid,o.gid),t.chmod(e,c)):t.mkdirWithOwner(e,c,o.uid,o.gid);let f=this.opendir(e);try{for(;;){let y=this.readdir(f);if(!y)break;y.name==="."||y.name===".."||this.copyPathToFreshFileSystem(e==="/"?`/${y.name}`:`${e}/${y.name}`,t,n,i,s)}}finally{this.closedir(f)}r.applyTimes(t,e,o);return}let l=o.nlink>1?`${o.dev}:${o.ino}`:null,u=l?s.get(l):void 0;if(u){t.link(u,e);return}if(a===fn){t.symlinkWithOwner(this.readlink(e),e,o.uid,o.gid),l&&s.set(l,e);return}if(a!==Gt)throw new Error(`Unsupported file type while rebasing VFS: ${e}`);if(n.has(e)||i.has(e)){t.createFileWithOwner(e,c,o.uid,o.gid,new Uint8Array(0)),r.applyTimes(t,e,o),l&&s.set(l,e);return}this.copyRegularFileToFreshFileSystem(e,t,o,c),l&&s.set(l,e)}copyRegularFileToFreshFileSystem(e,t,n,i){let s=this.open(e,Sa,0),o=null;try{o=t.open(e,Ea,i);let a=new Uint8Array(Math.min(za,Math.max(1,n.size))),c=n.size;for(;c>0;){let l=Math.min(a.byteLength,c),u=this.read(s,a,null,l);if(u<=0)throw new Error(`Unexpected EOF while rebasing VFS file: ${e}`);let d=0;for(;d!e||e==="."||e===".."))throw new Error(`Binary resolver path must be a normalized portable relative path: ${JSON.stringify(r)}`);return r}var Je=new Set(["wasm32","wasm64"]);function Fe(r){if(hc(r),!r.startsWith("programs/"))return r;let e=r.slice(9),t=e.split("/",1)[0];return Je.has(t)?r:`programs/wasm32/${e}`}function yc(r,e=K(Ir(),"wasm")){let t=Fe(r),n=[K(e,t)];return r==="kernel.wasm"?n.push(K(e,"kandelo-kernel.wasm")):r==="userspace.wasm"?n.push(K(e,"wasm_posix_userspace.wasm")):r==="rootfs.vfs"&&n.push(K(e,"rootfs.vfs")),n}var kn=class extends Error{constructor(e){super(e),this.name="BinaryNotFoundError"}};function zo(){let r=[],e=!1;try{let n=Xe();e=!0;for(let[i,s]of[["local-binaries",K(n,"local-binaries")],["binaries",K(n,"binaries")]])r.push({label:i,root:s,identity:i==="local-binaries"?"local-generation":"program-cache",allowRegularFileClosure:!1,candidatesFor(o){return[K(s,Fe(o))]}})}catch{}let t=K(Ir(),"wasm");return r.push({label:"installed package",root:t,identity:"installed-package",allowRegularFileClosure:!e,candidatesFor(n){return yc(n,t)}}),r}function gt(r,e){return new Error(`Invalid package manifest ${r}: ${e}`)}function se(r){try{return xn(r),!0}catch(e){if(e instanceof Error&&"code"in e&&e.code==="ENOENT")return!1;throw e}}function ho(r,e,t){if(r.length===0||r.startsWith("/")||r.includes("\\")||r.includes("\0")||r.split("/").some(n=>!n||n==="."||n===".."))throw gt(e,`${t} must be a normalized portable relative path`);return r}function An(r,e,t,n=!0){if(r.length===0||r==="."||r===".."||r.includes("/")||r.includes("\\")||r.includes("\0")||!n&&r.includes("@"))throw gt(e,`${t} must be a safe single path component`);return r}var yo="kandelo-program-packages-v2",Ie="program-packages.json",po=null,pc=null,Ln=null,br=0;function _r(){return pc??K(Ir(),"wasm",Ie)}function bo(){if(Object.prototype.hasOwnProperty.call(process.env,"WASM_POSIX_DEPS_REGISTRY")){let e=null;return(process.env.WASM_POSIX_DEPS_REGISTRY??"").split(":").filter(Boolean).map(t=>t.startsWith("~/")&&process.env.HOME!==void 0?K(process.env.HOME,t.slice(2)):In(t)?we(t):(e??=Xe(),we(e,t)))}let r;try{r=K(Xe(),"packages","registry")}catch{return null}return!se(r)&&Ao()===null&&se(_r())?null:[r]}function Ao(){let r;try{r=Xe()}catch{return null}if(!qt(K(r,"tools","xtask","Cargo.toml"))||!qt(K(r,"scripts","dev-shell.sh")))return null;try{let e=ve(xr()),t=ve(r);return[K(t,"host"),K(t,"scripts")].some(i=>qt(i)&&Br(ve(i),e))?t:null}catch{return null}}function Pr(r,e,t){let n=[typeof t.stderr=="string"?t.stderr.trim():"",typeof t.stdout=="string"?t.stdout.trim():"",t.error?.message??""].filter(Boolean).join(` `);return`${r} ${e.join(" ")} failed${t.status===null?"":` with status ${t.status}`}${n?`: -${n}`:""}`}function wc(r){let e=process.env.KANDELO_DEV_SHELL_TOOL_PATH!==void 0,t=e?"rustc":"bash",n=e?["-vV"]:[K(r,"scripts","dev-shell.sh"),"rustc","-vV"],i=xr(t,n,{cwd:r,encoding:"utf8"});if(i.status!==0)throw new Error(Or(t,n,i));let s=i.stdout.split(/\r?\n/).find(o=>o.startsWith("host: "))?.slice(6).trim();if(!s)throw new Error(`Could not determine the Rust host target for ${r}`);return s}function kr(r){try{if(In(r).isFile())return ve(r)}catch{}throw new Error(`Prepared xtask is not a regular file: ${r}`)}function vc(r){let e=process.env.WASM_POSIX_XTASK_BIN;if(e!==void 0){let l=_n(e)?we(e):we(r,e);return kr(l)}if(Ln?.sourceRepoRoot===r)return kr(Ln.xtaskPath);let t=wc(r),n=K(r,"target",t,"release",process.platform==="win32"?"xtask.exe":"xtask"),i=["build","--release","-p","xtask","--target",t,"--quiet"],s=process.env.KANDELO_DEV_SHELL_TOOL_PATH!==void 0,o=s?"cargo":"bash",a=s?i:[K(r,"scripts","dev-shell.sh"),"cargo",...i],c=xr(o,a,{cwd:r,encoding:"utf8"});if(c.status!==0)throw new Error(Or(o,a,c));return Ln={sourceRepoRoot:r,xtaskPath:kr(n)},Ln.xtaskPath}function Sc(){let r=xo();if(r===null)return;let e=Lo();if(e===null)return;if(wo){wo(r,e);return}let t=vc(r),n=["build-deps","program-index-context-check","--source-repo-root",r],i=xr(t,n,{cwd:r,encoding:"utf8",env:{...process.env,WASM_POSIX_DEPS_REGISTRY:e.join(":")}});if(i.status!==0)throw new Error(`Program package source projection is not current: -${Or(t,n,i)}`)}function Ec(r,e){if(Ar>0||!r.some(t=>t.startsWith("programs/")))return e();Ar+=1;try{return Sc(),e()}finally{Ar-=1}}function $e(r,e){let t=Object.keys(r).sort(),n=[...e].sort();return t.length===n.length&&t.every((i,s)=>i===n[s])}function Lr(r){let e;try{e=JSON.parse(qe(r,"utf8"))}catch(o){throw new Error(`Invalid program package index ${r}: ${o instanceof Error?o.message:String(o)}`)}if(typeof e!="object"||e===null||!$e(e,["format","identities","packages"])||e.format!==go||typeof e.identities!="object"||e.identities===null||Array.isArray(e.identities)||typeof e.packages!="object"||e.packages===null||Array.isArray(e.packages))throw new Error(`Invalid program package index ${r}: expected ${go}`);let t=new Map,n=e.identities;for(let[o,a]of Object.entries(n)){if(kn(o,r,"identity package name",!1),typeof a!="object"||a===null||!$e(a,["manifestSha256","cacheKeys"])||typeof a.manifestSha256!="string"||!/^[a-f0-9]{64}$/.test(a.manifestSha256)||typeof a.cacheKeys!="object"||a.cacheKeys===null||Array.isArray(a.cacheKeys))throw new Error(`Invalid program package index ${r}: malformed identity ${JSON.stringify(o)}`);let c=a.cacheKeys;if(!$e(c,["wasm32","wasm64"])||Object.values(c).some(l=>typeof l!="string"||!/^[a-f0-9]{64}$/.test(l)))throw new Error(`Invalid program package index ${r}: identity ${JSON.stringify(o)} has invalid contextual cache keys`);t.set(o,{manifestSha256:a.manifestSha256,cacheKeys:c})}let i=new Map,s=e.packages;for(let[o,a]of Object.entries(s)){if(kn(o,r,"package name",!1),typeof a!="object"||a===null||!$e(a,["manifestSha256","arches","cacheKeys","dependencyClosures","members"])||!Array.isArray(a.arches)||typeof a.cacheKeys!="object"||a.cacheKeys===null||Array.isArray(a.cacheKeys)||typeof a.dependencyClosures!="object"||a.dependencyClosures===null||Array.isArray(a.dependencyClosures)||!Array.isArray(a.members)||typeof a.manifestSha256!="string"||!/^[a-f0-9]{64}$/.test(a.manifestSha256))throw new Error(`Invalid program package index ${r}: malformed package ${JSON.stringify(o)}`);let c=a.arches;if(c.length===0||new Set(c).size!==c.length||c.some(p=>typeof p!="string"||!Xe.has(p)))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} has invalid arches`);let l=a.cacheKeys;if(!$e(l,c)||Object.values(l).some(p=>typeof p!="string"||!/^[a-f0-9]{64}$/.test(p)))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} has invalid cache keys`);let u=a.dependencyClosures;if(!$e(u,c))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} has invalid dependency closure arches`);let d={};for(let p of c){let w=u[p];if(!Array.isArray(w))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} has a malformed dependency closure for ${p}`);let m=new Set;d[p]=w.map((g,S)=>{if(typeof g!="object"||g===null||!$e(g,["packageName","manifestSha256","cacheKey"])||typeof g.packageName!="string"||typeof g.manifestSha256!="string"||!/^[a-f0-9]{64}$/.test(g.manifestSha256)||typeof g.cacheKey!="string"||!/^[a-f0-9]{64}$/.test(g.cacheKey))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} dependency ${S+1} for ${p} is malformed`);let z=g;if(kn(z.packageName,r,`${o} dependency packageName`,!1),z.packageName===o||m.has(z.packageName))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} dependency closure for ${p} must contain unique dependencies other than itself`);m.add(z.packageName);let E=t.get(z.packageName);if(!E||E.manifestSha256!==z.manifestSha256||E.cacheKeys[p]!==z.cacheKey)throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} dependency ${JSON.stringify(z.packageName)} for ${p} does not match the index's authoritative contextual identity`);return z})}let f=a.members.map((p,w)=>{if(typeof p!="object"||p===null||p.kind!=="output"&&p.kind!=="runtime-file"||typeof p.sourceArtifact!="string"||typeof p.mirrorPath!="string")throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} member ${w+1} is malformed`);let m=p,g=m.kind==="output"?["kind","sourceArtifact","mirrorPath","outputName","forkInstrumentation"]:["kind","sourceArtifact","mirrorPath","guestPath","mode"];if(!$e(m,g))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} member ${w+1} has unknown or missing fields`);if(mo(m.sourceArtifact,r,`${o} sourceArtifact`),mo(m.mirrorPath,r,`${o} mirrorPath`),m.kind==="output"){if(typeof m.outputName!="string"||m.forkInstrumentation!=="auto"&&m.forkInstrumentation!=="disabled")throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} output member lacks outputName or forkInstrumentation`);kn(m.outputName,r,`${o} outputName`)}else if(typeof m.guestPath!="string"||!m.guestPath.startsWith("/")||!Number.isInteger(m.mode)||m.mode<0||m.mode>511)throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} runtime member lacks valid guestPath or mode`);return m});if(f.length===0||new Set(f.map(p=>p.sourceArtifact)).size!==f.length||new Set(f.map(p=>p.mirrorPath)).size!==f.length||f.length===1&&f[0].mirrorPath.includes("/")||f.length>1&&f.some(p=>!p.mirrorPath.startsWith(`${o}/`)))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} members are empty, collide, or violate scalar/package-directory layout`);let y=a.manifestSha256,h=t.get(o);if(!h||h.manifestSha256!==y||c.some(p=>h.cacheKeys[p]!==l[p]))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} does not match its contextual package identity`);i.set(o,{manifestSha256:y,arches:c,cacheKeys:l,dependencyClosures:d,members:f})}return{identities:t,packages:i,indexPath:r}}function Io(r){return JSON.stringify({manifestSha256:r.manifestSha256,arches:r.arches,cacheKeys:Object.fromEntries(r.arches.map(e=>[e,r.cacheKeys[e]])),dependencyClosures:Object.fromEntries(r.arches.map(e=>[e,[...r.dependencyClosures[e]].sort((t,n)=>t.packageNamen.packageName?1:0)])),members:r.members.map(e=>e.kind==="output"?{kind:e.kind,sourceArtifact:e.sourceArtifact,mirrorPath:e.mirrorPath,outputName:e.outputName,forkInstrumentation:e.forkInstrumentation}:{kind:e.kind,sourceArtifact:e.sourceArtifact,mirrorPath:e.mirrorPath,guestPath:e.guestPath,mode:e.mode})})}function Tr(){let r=Pr();return se(r)?Lr(r):null}function zc(r){let e=Tr();if(!e)return null;let t=r.split("/");if(t[0]!=="programs"||!Xe.has(t[1]))return null;let n=t[1];if(t.length>=4){let s=t[2];return e.packages.get(s)?.arches.includes(n)?s:null}if(t.length!==3)return null;let i=t[2];for(let[s,o]of e.packages)if(o.arches.includes(n)&&o.members.some(a=>a.kind==="output"&&a.mirrorPath.split("/").at(-1)===i))return s;return null}function vo(r){let e=zc(r);if(e)throw new Error(`Installed package resolver path ${JSON.stringify(r)} is owned by ${JSON.stringify(e)}, but that package is not selected by the configured program registry`)}function _o(){let r=Lo(),e=new Map,t=new Map,n=new Map,i=new Map,s=[];if(r===null){let l=Pr();if(!se(l))return{identities:e,unidentifiedPackages:t,packages:n,unprojectedPackages:i,physicalProgramClaims:s};let u=Lr(l);for(let[d,f]of u.identities)e.set(d,{...f,packageName:d,policyPath:`${u.indexPath}#identities.${d}`});for(let[d,f]of u.packages)s.push({packageName:d,projection:f,selected:!0}),n.set(d,{...f,packageName:d,policyPath:`${u.indexPath}#${d}`});return{identities:e,unidentifiedPackages:t,packages:n,unprojectedPackages:i,physicalProgramClaims:s}}let o=new Set,a=null,c=null;for(let l of r){if(!se(l))continue;if(!je(l).isDirectory())throw new Error(`Program registry root is not a directory: ${l}`);let u=K(l,Ie);if(!se(u))throw new Error(`Program registry ${l} is missing ${Ie}; generate it with xtask build-deps program-index`);let d=Lr(u);a??=d.identities,c??=d.packages;let f=sc(l,{withFileTypes:!0}).filter(y=>y.isDirectory()||y.isSymbolicLink()).sort((y,h)=>y.name.localeCompare(h.name));for(let y of f){let h=y.name,p=K(l,h,"package.toml");if(!se(p))continue;let w=!1;try{w=je(p).isFile()}catch{w=!1}if(!w)continue;let m=d.packages.get(h),g=!o.has(h);if(m&&s.push({packageName:h,projection:m,selected:g}),!g)continue;o.add(h);let S=a.get(h);S?e.set(h,{...S,packageName:h,manifestPath:p,policyPath:p}):t.set(h,p);let z=c.get(h);if(!z){i.set(h,p);continue}n.set(h,{...z,packageName:h,manifestPath:p,policyPath:p})}}return{identities:e,unidentifiedPackages:t,packages:n,unprojectedPackages:i,physicalProgramClaims:s}}function So(r){if(!r.manifestPath)return;let e;try{e=qe(r.manifestPath)}catch(n){throw new Error(`Program package identity cannot verify ${r.manifestPath}: ${n instanceof Error?n.message:String(n)}`)}if(bo("sha256").update(e).digest("hex")!==r.manifestSha256)throw new Error(`Program package identity is stale for ${r.manifestPath}; regenerate ${Ie}`)}function bc(r){if(!r.manifestPath)return;let e;try{e=qe(r.manifestPath)}catch(n){throw new Error(`Program package projection cannot verify ${r.manifestPath}: ${n instanceof Error?n.message:String(n)}`)}if(bo("sha256").update(e).digest("hex")!==r.manifestSha256)throw new Error(`Program package projection is stale for ${r.manifestPath}; regenerate ${Ie}`)}function Yt(r){let e=Rr(),t=e.packages.get(r);if(t)return bc(t),t;let n=e.unprojectedPackages.get(r);if(n)throw new Error(`Package ${JSON.stringify(r)} is selected at ${n} but is absent from ${Ie}; regenerate the registry projection`);return null}function Ac(r,e){let t=r.dependencyClosures[e];if(!t)throw wt(r.policyPath,`package ${JSON.stringify(r.packageName)} lacks a dependency identity closure for ${e}`);let n=_o(),i=n.identities.get(r.packageName);if(!i){let o=n.unidentifiedPackages.get(r.packageName);throw new Error(`Program package ${JSON.stringify(r.packageName)} has no authoritative contextual identity for ${e}${o?` at ${o}`:""}; regenerate ${Ie} with the exact ordered registry roots`)}So(i);let s=i.cacheKeys[e];if(i.manifestSha256!==r.manifestSha256||s!==r.cacheKeys[e])throw new Error(`Program package ${JSON.stringify(r.packageName)} was projected with manifest ${r.manifestSha256} and cache key ${r.cacheKeys[e]} for ${e}, but the authoritative first-hit registry context at ${i.policyPath} requires manifest ${i.manifestSha256} and cache key ${s??""}. Regenerate this program projection with the exact ordered registry roots; the highest-priority index must carry the complete combined-context projection rather than relying on a lower suffix-context build identity.`);for(let o of t){let a=n.identities.get(o.packageName);if(!a){let l=n.unidentifiedPackages.get(o.packageName);throw l?new Error(`Program package ${JSON.stringify(r.packageName)} was generated against dependency ${JSON.stringify(o.packageName)}, but the first-hit package at ${l} has no contextual identity in ${Ie}`):new Error(`Program package ${JSON.stringify(r.packageName)} was generated against dependency ${JSON.stringify(o.packageName)}, but that dependency is absent from the configured first-hit registry roots`)}So(a);let c=a.cacheKeys[e];if(a.manifestSha256!==o.manifestSha256||c!==o.cacheKey)throw new Error(`Program package ${JSON.stringify(r.packageName)} has a contextual cache identity mismatch for ${e}: its projection expects dependency ${JSON.stringify(o.packageName)} manifest ${o.manifestSha256} and cache key ${o.cacheKey}, but first-hit selection at ${a.policyPath} provides manifest ${a.manifestSha256} and cache key ${c??""}. Regenerate the program projection with the exact ordered registry roots; the complete highest-priority projection must bind every selected program to the same combined dependency context.`)}}function Rr(){let r=_o(),{physicalProgramClaims:e,...t}=r,n={...t,legacyFlatOutputs:new Map,forkInstrumentationDisabledOutputs:new Map},i=[];for(let s of r.packages.values()){let o=s.members.length>1;for(let a of s.arches)for(let c of s.members){let l=i.find(y=>y.arch===a&&(y.path===c.mirrorPath||y.path.startsWith(`${c.mirrorPath}/`)||c.mirrorPath.startsWith(`${y.path}/`)));if(l)throw new Error(`Program resolver paths programs/${a}/${l.path} and programs/${a}/${c.mirrorPath} conflict between selected packages ${JSON.stringify(l.packageName)} and ${JSON.stringify(s.packageName)}`);if(i.push({arch:a,path:c.mirrorPath,packageName:s.packageName}),c.kind!=="output")continue;let u=c.mirrorPath.split("/").at(-1),d=`${a}/${u}`,f=n.legacyFlatOutputs.get(d);f||(f={scalarOwners:new Set,packagePaths:new Map,shadowedOwners:new Set},n.legacyFlatOutputs.set(d,f)),o?f.packagePaths.set(`programs/${a}/${c.mirrorPath}`,s.packageName):f.scalarOwners.add(s.packageName),c.forkInstrumentation==="disabled"&&n.forkInstrumentationDisabledOutputs.set(`${a}/${c.mirrorPath}`,s.packageName)}}for(let{packageName:s,projection:o,selected:a}of e)if(!(a&&r.packages.has(s)))for(let c of o.arches)for(let l of o.members){if(l.kind!=="output")continue;let u=l.mirrorPath.split("/").at(-1),d=`${c}/${u}`,f=n.legacyFlatOutputs.get(d);f||(f={scalarOwners:new Set,packagePaths:new Map,shadowedOwners:new Set},n.legacyFlatOutputs.set(d,f)),f.shadowedOwners.add(s)}return n}function kc(r){let e=r.split("/");if(e.length!==3||e[0]!=="programs"||!Xe.has(e[1]))return null;let t=Rr().legacyFlatOutputs.get(`${e[1]}/${e[2]}`);if(!t)return null;for(let n of t.scalarOwners){let i=Yt(n);if(i)return i}for(let n of t.packagePaths.values())Yt(n);if(t.packagePaths.size>0)throw new Error(`Legacy flat resolver path ${JSON.stringify(r)} belongs to a multi-member package; use ${[...t.packagePaths.keys()].sort().map(n=>JSON.stringify(n)).join(" or ")}`);for(let n of t.shadowedOwners){let i=Yt(n);if(i)return i;throw new Error(`Legacy flat resolver path ${JSON.stringify(r)} is claimed by a lower-root program package ${JSON.stringify(n)}, but its first-hit selected package does not project that program; stale scalar mirror fallback is forbidden`)}return null}function Eo(r,e,t){if(!r.arches.includes(e))throw wt(r.policyPath,`package ${JSON.stringify(r.packageName)} does not declare resolver artifacts for ${e}`);let n=r.cacheKeys[e];if(!n)throw wt(r.policyPath,`package ${JSON.stringify(r.packageName)} lacks a cache identity for ${e}`);Ac(r,e);let i=Io(r),s=r.members.map(o=>({packageName:r.packageName,relPath:`programs/${e}/${o.mirrorPath}`,sourceArtifact:o.sourceArtifact,cacheKey:n,forkInstrumentation:o.kind==="output"?o.forkInstrumentation??null:null,projectionIdentity:i}));if(!s.some(o=>o.relPath===t))throw wt(r.policyPath,`resolver path ${JSON.stringify(t)} is not a declared member of package ${JSON.stringify(r.packageName)}`);return{manifestPath:r.policyPath,packageName:r.packageName,members:s}}function Lc(r){let e=Fe(r),t=e.split("/");if(t[0]==="programs"&&!hc()&&Tr()===null)throw new Error(`Installed host package is missing wasm/${Ie}; program artifacts cannot be resolved without packaged policy`);if(t.length===3){let o=kc(e);return o?Eo(o,t[1],e):(vo(e),null)}if(t.length<4||t[0]!=="programs"||!Xe.has(t[1]))return null;let n=t[1],i=t[2],s=Yt(i);return s?Eo(s,n,e):(vo(e),null)}function xc(r){let e=Fe(r);for(let t of["programs/wasm32/","programs/wasm64/"])if(e.startsWith(t))return e.slice(t.length);return null}function Ic(r){let e=Fe(r);for(let t of Xe){let n=`programs/${t}/`;if(e.startsWith(n)){let i=Rr().forkInstrumentationDisabledOutputs.get(`${t}/${e.slice(n.length)}`);return i?Yt(i)!==null:!1}}return!1}function _c(r){let e=Fe(r);if(e==="kernel.wasm")return Dr;let t=xc(e);if(t&&t.endsWith(".wasm"))return uc}function Pc(r,e,t){if(!r.endsWith(".wasm"))return!1;try{let n=qe(r),i=n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength),s=t===void 0?Ic(e):t==="disabled";return Hr(i,{expectedAbi:42,requiredExports:_c(e),requireForkInstrumentation:s?!1:void 0,forbidForkInstrumentation:s}).length>0}catch{return!0}}function Oc(r){if(!r.endsWith(".vfs")&&!r.endsWith(".vfs.zst"))return!1;try{let t=bn.readImageMetadata(qe(r))?.kernelAbi;return t!==void 0&&t!==42}catch{return!0}}function Cr(r,e,t){return Pc(r,e,t)||Oc(r)}function Po(r,e,t){let n=r.filter(se);return n.length===0?null:n.find(i=>{try{return je(i).isFile()&&!Cr(i,e,t)}catch{return!1}})??null}function Oo(r,e,t){try{if(!In(r).isSymbolicLink())return r;let i=ve(r);if(!je(i).isFile()||Cr(i,e,t))throw new Error("canonical target is not an accepted regular file");if(Fe(e).startsWith("programs/")&&Tc(i))throw new Error("resolver-owned program generation has no matching selected package projection");return i}catch(n){throw new Error(`Binary changed or became invalid while pinning ${e}: ${n instanceof Error?n.message:String(n)}`)}}function Tc(r){let e=[Ao()];try{e.push(K(Ye(),"local-binaries",".kandelo-local-generations"))}catch{}return e.some(t=>{try{return se(t)&&Br(ve(t),r)}catch{return!1}})}function Br(r,e){let t=cc(r,e);return t===""||t!==".."&&!t.startsWith(`..${lc}`)&&!_n(t)}function Rc(r,e){let t=e.split("/"),n=r;for(let i=0;ia.packageName!==s))return"declared package members do not share a valid program namespace";if(!je(e).isDirectory())return"shared package generation root is not a directory";let o=t[0].cacheKey;if(!/^[a-f0-9]{64}$/.test(o)||t.some(a=>a.cacheKey!==o))return"declared package members do not share one valid cache identity";if(r.identity==="local-generation"){let a=K(r.root,".kandelo-local-generations",i,s,o);if(!se(a))return"local mirror targets are not one direct immutable local generation";let c=ve(a);return Xt(e)===c?null:"local mirror targets are not one direct immutable local generation"}if(r.identity==="program-cache"){let a=Ao();if(!se(a))return"fetched mirror targets are not one canonical program-cache generation";let c=ve(a),l=ac(e),u=l.startsWith(`${s}-`)&&new RegExp(`-rev[0-9]+-${i}-${o}$`).test(l);return Xt(e)===c&&u?null:"fetched mirror targets are not one canonical program-cache generation"}return"installed-package symlink closures are not an immutable installed identity"}function Bc(r,e,t){if(e.length!==t.length)return{failure:"internal member/path count mismatch"};try{let n=e.map(l=>{let u=In(l);return u.isSymbolicLink()?"symlink":u.isFile()?"file":"other"});if(n.includes("other"))return{failure:"a selected mirror member is neither a regular file nor a symlink"};let i=n.every(l=>l==="symlink"),s=n.every(l=>l==="file");if(!i&&!s)return{failure:"regular files and symlinks cannot share one package identity"};if(s){if(!r.allowRegularFileClosure)return{failure:"a mutable source-checkout wasm tree is not an installed package identity"};let l=t[0].packageName,u=t[0].projectionIdentity;if(t.some(p=>p.packageName!==l||p.projectionIdentity!==u))return{failure:"declared members do not share one selected package projection"};let f=Tr()?.packages.get(l);if(!f||Io(f)!==u)return{failure:"installed bytes do not match the selected package projection"};let y=ve(r.root),h=[];for(let p of e){let w=ve(p);if(!Br(y,w)||!je(w).isFile())return{failure:"an installed-package member escapes its immutable wasm tree"};h.push(w)}return{paths:h}}let o=null,a=[];for(let l=0;lNc(r))}function Nc(r){let e=Fe(r),t=Lc(e);if(t){let o=Mc(t.members.map(a=>a.relPath),t.members);if(o)return o[t.members.findIndex(a=>a.relPath===e)];throw new xn(`Package artifacts not found for ${t.packageName}: ${e}`)}let n=[],i=[];for(let o of ko())for(let a of o.candidatesFor(r))n.push(a),i.push(a);let s=Po(i,r);if(s)return Oo(s,r);throw i.some(se)?new Error(`Binary exists but was rejected by artifact policy: ${r} +${n}`:""}`}function mc(r){let e=process.env.KANDELO_DEV_SHELL_TOOL_PATH!==void 0,t=e?"rustc":"bash",n=e?["-vV"]:[K(r,"scripts","dev-shell.sh"),"rustc","-vV"],i=kr(t,n,{cwd:r,encoding:"utf8"});if(i.status!==0)throw new Error(Pr(t,n,i));let s=i.stdout.split(/\r?\n/).find(o=>o.startsWith("host: "))?.slice(6).trim();if(!s)throw new Error(`Could not determine the Rust host target for ${r}`);return s}function Ar(r){try{if(xn(r).isFile())return ve(r)}catch{}throw new Error(`Prepared xtask is not a regular file: ${r}`)}function gc(r){let e=process.env.WASM_POSIX_XTASK_BIN;if(e!==void 0){let l=In(e)?we(e):we(r,e);return Ar(l)}if(Ln?.sourceRepoRoot===r)return Ar(Ln.xtaskPath);let t=mc(r),n=K(r,"target",t,"release",process.platform==="win32"?"xtask.exe":"xtask"),i=["build","--release","-p","xtask","--target",t,"--quiet"],s=process.env.KANDELO_DEV_SHELL_TOOL_PATH!==void 0,o=s?"cargo":"bash",a=s?i:[K(r,"scripts","dev-shell.sh"),"cargo",...i],c=kr(o,a,{cwd:r,encoding:"utf8"});if(c.status!==0)throw new Error(Pr(o,a,c));return Ln={sourceRepoRoot:r,xtaskPath:Ar(n)},Ln.xtaskPath}function wc(){let r=Ao();if(r===null)return;let e=bo();if(e===null)return;if(po){po(r,e);return}let t=gc(r),n=["build-deps","program-index-context-check","--source-repo-root",r],i=kr(t,n,{cwd:r,encoding:"utf8",env:{...process.env,WASM_POSIX_DEPS_REGISTRY:e.join(":")}});if(i.status!==0)throw new Error(`Program package source projection is not current: +${Pr(t,n,i)}`)}function vc(r,e){if(br>0||!r.some(t=>t.startsWith("programs/")))return e();br+=1;try{return wc(),e()}finally{br-=1}}function $e(r,e){let t=Object.keys(r).sort(),n=[...e].sort();return t.length===n.length&&t.every((i,s)=>i===n[s])}function Lr(r){let e;try{e=JSON.parse(je(r,"utf8"))}catch(o){throw new Error(`Invalid program package index ${r}: ${o instanceof Error?o.message:String(o)}`)}if(typeof e!="object"||e===null||!$e(e,["format","identities","packages"])||e.format!==yo||typeof e.identities!="object"||e.identities===null||Array.isArray(e.identities)||typeof e.packages!="object"||e.packages===null||Array.isArray(e.packages))throw new Error(`Invalid program package index ${r}: expected ${yo}`);let t=new Map,n=e.identities;for(let[o,a]of Object.entries(n)){if(An(o,r,"identity package name",!1),typeof a!="object"||a===null||!$e(a,["manifestSha256","cacheKeys"])||typeof a.manifestSha256!="string"||!/^[a-f0-9]{64}$/.test(a.manifestSha256)||typeof a.cacheKeys!="object"||a.cacheKeys===null||Array.isArray(a.cacheKeys))throw new Error(`Invalid program package index ${r}: malformed identity ${JSON.stringify(o)}`);let c=a.cacheKeys;if(!$e(c,["wasm32","wasm64"])||Object.values(c).some(l=>typeof l!="string"||!/^[a-f0-9]{64}$/.test(l)))throw new Error(`Invalid program package index ${r}: identity ${JSON.stringify(o)} has invalid contextual cache keys`);t.set(o,{manifestSha256:a.manifestSha256,cacheKeys:c})}let i=new Map,s=e.packages;for(let[o,a]of Object.entries(s)){if(An(o,r,"package name",!1),typeof a!="object"||a===null||!$e(a,["manifestSha256","arches","cacheKeys","dependencyClosures","members"])||!Array.isArray(a.arches)||typeof a.cacheKeys!="object"||a.cacheKeys===null||Array.isArray(a.cacheKeys)||typeof a.dependencyClosures!="object"||a.dependencyClosures===null||Array.isArray(a.dependencyClosures)||!Array.isArray(a.members)||typeof a.manifestSha256!="string"||!/^[a-f0-9]{64}$/.test(a.manifestSha256))throw new Error(`Invalid program package index ${r}: malformed package ${JSON.stringify(o)}`);let c=a.arches;if(c.length===0||new Set(c).size!==c.length||c.some(p=>typeof p!="string"||!Je.has(p)))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} has invalid arches`);let l=a.cacheKeys;if(!$e(l,c)||Object.values(l).some(p=>typeof p!="string"||!/^[a-f0-9]{64}$/.test(p)))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} has invalid cache keys`);let u=a.dependencyClosures;if(!$e(u,c))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} has invalid dependency closure arches`);let d={};for(let p of c){let w=u[p];if(!Array.isArray(w))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} has a malformed dependency closure for ${p}`);let m=new Set;d[p]=w.map((g,S)=>{if(typeof g!="object"||g===null||!$e(g,["packageName","manifestSha256","cacheKey"])||typeof g.packageName!="string"||typeof g.manifestSha256!="string"||!/^[a-f0-9]{64}$/.test(g.manifestSha256)||typeof g.cacheKey!="string"||!/^[a-f0-9]{64}$/.test(g.cacheKey))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} dependency ${S+1} for ${p} is malformed`);let z=g;if(An(z.packageName,r,`${o} dependency packageName`,!1),z.packageName===o||m.has(z.packageName))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} dependency closure for ${p} must contain unique dependencies other than itself`);m.add(z.packageName);let E=t.get(z.packageName);if(!E||E.manifestSha256!==z.manifestSha256||E.cacheKeys[p]!==z.cacheKey)throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} dependency ${JSON.stringify(z.packageName)} for ${p} does not match the index's authoritative contextual identity`);return z})}let f=a.members.map((p,w)=>{if(typeof p!="object"||p===null||p.kind!=="output"&&p.kind!=="runtime-file"||typeof p.sourceArtifact!="string"||typeof p.mirrorPath!="string")throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} member ${w+1} is malformed`);let m=p,g=m.kind==="output"?["kind","sourceArtifact","mirrorPath","outputName","forkInstrumentation"]:["kind","sourceArtifact","mirrorPath","guestPath","mode"];if(!$e(m,g))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} member ${w+1} has unknown or missing fields`);if(ho(m.sourceArtifact,r,`${o} sourceArtifact`),ho(m.mirrorPath,r,`${o} mirrorPath`),m.kind==="output"){if(typeof m.outputName!="string"||m.forkInstrumentation!=="auto"&&m.forkInstrumentation!=="disabled")throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} output member lacks outputName or forkInstrumentation`);An(m.outputName,r,`${o} outputName`)}else if(typeof m.guestPath!="string"||!m.guestPath.startsWith("/")||!Number.isInteger(m.mode)||m.mode<0||m.mode>511)throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} runtime member lacks valid guestPath or mode`);return m});if(f.length===0||new Set(f.map(p=>p.sourceArtifact)).size!==f.length||new Set(f.map(p=>p.mirrorPath)).size!==f.length||f.length===1&&f[0].mirrorPath.includes("/")||f.length>1&&f.some(p=>!p.mirrorPath.startsWith(`${o}/`)))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} members are empty, collide, or violate scalar/package-directory layout`);let y=a.manifestSha256,h=t.get(o);if(!h||h.manifestSha256!==y||c.some(p=>h.cacheKeys[p]!==l[p]))throw new Error(`Invalid program package index ${r}: package ${JSON.stringify(o)} does not match its contextual package identity`);i.set(o,{manifestSha256:y,arches:c,cacheKeys:l,dependencyClosures:d,members:f})}return{identities:t,packages:i,indexPath:r}}function Lo(r){return JSON.stringify({manifestSha256:r.manifestSha256,arches:r.arches,cacheKeys:Object.fromEntries(r.arches.map(e=>[e,r.cacheKeys[e]])),dependencyClosures:Object.fromEntries(r.arches.map(e=>[e,[...r.dependencyClosures[e]].sort((t,n)=>t.packageNamen.packageName?1:0)])),members:r.members.map(e=>e.kind==="output"?{kind:e.kind,sourceArtifact:e.sourceArtifact,mirrorPath:e.mirrorPath,outputName:e.outputName,forkInstrumentation:e.forkInstrumentation}:{kind:e.kind,sourceArtifact:e.sourceArtifact,mirrorPath:e.mirrorPath,guestPath:e.guestPath,mode:e.mode})})}function Or(){let r=_r();return se(r)?Lr(r):null}function Sc(r){let e=Or();if(!e)return null;let t=r.split("/");if(t[0]!=="programs"||!Je.has(t[1]))return null;let n=t[1];if(t.length>=4){let s=t[2];return e.packages.get(s)?.arches.includes(n)?s:null}if(t.length!==3)return null;let i=t[2];for(let[s,o]of e.packages)if(o.arches.includes(n)&&o.members.some(a=>a.kind==="output"&&a.mirrorPath.split("/").at(-1)===i))return s;return null}function mo(r){let e=Sc(r);if(e)throw new Error(`Installed package resolver path ${JSON.stringify(r)} is owned by ${JSON.stringify(e)}, but that package is not selected by the configured program registry`)}function ko(){let r=bo(),e=new Map,t=new Map,n=new Map,i=new Map,s=[];if(r===null){let l=_r();if(!se(l))return{identities:e,unidentifiedPackages:t,packages:n,unprojectedPackages:i,physicalProgramClaims:s};let u=Lr(l);for(let[d,f]of u.identities)e.set(d,{...f,packageName:d,policyPath:`${u.indexPath}#identities.${d}`});for(let[d,f]of u.packages)s.push({packageName:d,projection:f,selected:!0}),n.set(d,{...f,packageName:d,policyPath:`${u.indexPath}#${d}`});return{identities:e,unidentifiedPackages:t,packages:n,unprojectedPackages:i,physicalProgramClaims:s}}let o=new Set,a=null,c=null;for(let l of r){if(!se(l))continue;if(!Ye(l).isDirectory())throw new Error(`Program registry root is not a directory: ${l}`);let u=K(l,Ie);if(!se(u))throw new Error(`Program registry ${l} is missing ${Ie}; generate it with xtask build-deps program-index`);let d=Lr(u);a??=d.identities,c??=d.packages;let f=ic(l,{withFileTypes:!0}).filter(y=>y.isDirectory()||y.isSymbolicLink()).sort((y,h)=>y.name.localeCompare(h.name));for(let y of f){let h=y.name,p=K(l,h,"package.toml");if(!se(p))continue;let w=!1;try{w=Ye(p).isFile()}catch{w=!1}if(!w)continue;let m=d.packages.get(h),g=!o.has(h);if(m&&s.push({packageName:h,projection:m,selected:g}),!g)continue;o.add(h);let S=a.get(h);S?e.set(h,{...S,packageName:h,manifestPath:p,policyPath:p}):t.set(h,p);let z=c.get(h);if(!z){i.set(h,p);continue}n.set(h,{...z,packageName:h,manifestPath:p,policyPath:p})}}return{identities:e,unidentifiedPackages:t,packages:n,unprojectedPackages:i,physicalProgramClaims:s}}function go(r){if(!r.manifestPath)return;let e;try{e=je(r.manifestPath)}catch(n){throw new Error(`Program package identity cannot verify ${r.manifestPath}: ${n instanceof Error?n.message:String(n)}`)}if(So("sha256").update(e).digest("hex")!==r.manifestSha256)throw new Error(`Program package identity is stale for ${r.manifestPath}; regenerate ${Ie}`)}function Ec(r){if(!r.manifestPath)return;let e;try{e=je(r.manifestPath)}catch(n){throw new Error(`Program package projection cannot verify ${r.manifestPath}: ${n instanceof Error?n.message:String(n)}`)}if(So("sha256").update(e).digest("hex")!==r.manifestSha256)throw new Error(`Program package projection is stale for ${r.manifestPath}; regenerate ${Ie}`)}function jt(r){let e=Tr(),t=e.packages.get(r);if(t)return Ec(t),t;let n=e.unprojectedPackages.get(r);if(n)throw new Error(`Package ${JSON.stringify(r)} is selected at ${n} but is absent from ${Ie}; regenerate the registry projection`);return null}function zc(r,e){let t=r.dependencyClosures[e];if(!t)throw gt(r.policyPath,`package ${JSON.stringify(r.packageName)} lacks a dependency identity closure for ${e}`);let n=ko(),i=n.identities.get(r.packageName);if(!i){let o=n.unidentifiedPackages.get(r.packageName);throw new Error(`Program package ${JSON.stringify(r.packageName)} has no authoritative contextual identity for ${e}${o?` at ${o}`:""}; regenerate ${Ie} with the exact ordered registry roots`)}go(i);let s=i.cacheKeys[e];if(i.manifestSha256!==r.manifestSha256||s!==r.cacheKeys[e])throw new Error(`Program package ${JSON.stringify(r.packageName)} was projected with manifest ${r.manifestSha256} and cache key ${r.cacheKeys[e]} for ${e}, but the authoritative first-hit registry context at ${i.policyPath} requires manifest ${i.manifestSha256} and cache key ${s??""}. Regenerate this program projection with the exact ordered registry roots; the highest-priority index must carry the complete combined-context projection rather than relying on a lower suffix-context build identity.`);for(let o of t){let a=n.identities.get(o.packageName);if(!a){let l=n.unidentifiedPackages.get(o.packageName);throw l?new Error(`Program package ${JSON.stringify(r.packageName)} was generated against dependency ${JSON.stringify(o.packageName)}, but the first-hit package at ${l} has no contextual identity in ${Ie}`):new Error(`Program package ${JSON.stringify(r.packageName)} was generated against dependency ${JSON.stringify(o.packageName)}, but that dependency is absent from the configured first-hit registry roots`)}go(a);let c=a.cacheKeys[e];if(a.manifestSha256!==o.manifestSha256||c!==o.cacheKey)throw new Error(`Program package ${JSON.stringify(r.packageName)} has a contextual cache identity mismatch for ${e}: its projection expects dependency ${JSON.stringify(o.packageName)} manifest ${o.manifestSha256} and cache key ${o.cacheKey}, but first-hit selection at ${a.policyPath} provides manifest ${a.manifestSha256} and cache key ${c??""}. Regenerate the program projection with the exact ordered registry roots; the complete highest-priority projection must bind every selected program to the same combined dependency context.`)}}function Tr(){let r=ko(),{physicalProgramClaims:e,...t}=r,n={...t,legacyFlatOutputs:new Map,forkInstrumentationDisabledOutputs:new Map},i=[];for(let s of r.packages.values()){let o=s.members.length>1;for(let a of s.arches)for(let c of s.members){let l=i.find(y=>y.arch===a&&(y.path===c.mirrorPath||y.path.startsWith(`${c.mirrorPath}/`)||c.mirrorPath.startsWith(`${y.path}/`)));if(l)throw new Error(`Program resolver paths programs/${a}/${l.path} and programs/${a}/${c.mirrorPath} conflict between selected packages ${JSON.stringify(l.packageName)} and ${JSON.stringify(s.packageName)}`);if(i.push({arch:a,path:c.mirrorPath,packageName:s.packageName}),c.kind!=="output")continue;let u=c.mirrorPath.split("/").at(-1),d=`${a}/${u}`,f=n.legacyFlatOutputs.get(d);f||(f={scalarOwners:new Set,packagePaths:new Map,shadowedOwners:new Set},n.legacyFlatOutputs.set(d,f)),o?f.packagePaths.set(`programs/${a}/${c.mirrorPath}`,s.packageName):f.scalarOwners.add(s.packageName),c.forkInstrumentation==="disabled"&&n.forkInstrumentationDisabledOutputs.set(`${a}/${c.mirrorPath}`,s.packageName)}}for(let{packageName:s,projection:o,selected:a}of e)if(!(a&&r.packages.has(s)))for(let c of o.arches)for(let l of o.members){if(l.kind!=="output")continue;let u=l.mirrorPath.split("/").at(-1),d=`${c}/${u}`,f=n.legacyFlatOutputs.get(d);f||(f={scalarOwners:new Set,packagePaths:new Map,shadowedOwners:new Set},n.legacyFlatOutputs.set(d,f)),f.shadowedOwners.add(s)}return n}function bc(r){let e=r.split("/");if(e.length!==3||e[0]!=="programs"||!Je.has(e[1]))return null;let t=Tr().legacyFlatOutputs.get(`${e[1]}/${e[2]}`);if(!t)return null;for(let n of t.scalarOwners){let i=jt(n);if(i)return i}for(let n of t.packagePaths.values())jt(n);if(t.packagePaths.size>0)throw new Error(`Legacy flat resolver path ${JSON.stringify(r)} belongs to a multi-member package; use ${[...t.packagePaths.keys()].sort().map(n=>JSON.stringify(n)).join(" or ")}`);for(let n of t.shadowedOwners){let i=jt(n);if(i)return i;throw new Error(`Legacy flat resolver path ${JSON.stringify(r)} is claimed by a lower-root program package ${JSON.stringify(n)}, but its first-hit selected package does not project that program; stale scalar mirror fallback is forbidden`)}return null}function wo(r,e,t){if(!r.arches.includes(e))throw gt(r.policyPath,`package ${JSON.stringify(r.packageName)} does not declare resolver artifacts for ${e}`);let n=r.cacheKeys[e];if(!n)throw gt(r.policyPath,`package ${JSON.stringify(r.packageName)} lacks a cache identity for ${e}`);zc(r,e);let i=Lo(r),s=r.members.map(o=>({packageName:r.packageName,relPath:`programs/${e}/${o.mirrorPath}`,sourceArtifact:o.sourceArtifact,cacheKey:n,forkInstrumentation:o.kind==="output"?o.forkInstrumentation??null:null,projectionIdentity:i}));if(!s.some(o=>o.relPath===t))throw gt(r.policyPath,`resolver path ${JSON.stringify(t)} is not a declared member of package ${JSON.stringify(r.packageName)}`);return{manifestPath:r.policyPath,packageName:r.packageName,members:s}}function Ac(r){let e=Fe(r),t=e.split("/");if(t[0]==="programs"&&!uc()&&Or()===null)throw new Error(`Installed host package is missing wasm/${Ie}; program artifacts cannot be resolved without packaged policy`);if(t.length===3){let o=bc(e);return o?wo(o,t[1],e):(mo(e),null)}if(t.length<4||t[0]!=="programs"||!Je.has(t[1]))return null;let n=t[1],i=t[2],s=jt(i);return s?wo(s,n,e):(mo(e),null)}function Lc(r){let e=Fe(r);for(let t of["programs/wasm32/","programs/wasm64/"])if(e.startsWith(t))return e.slice(t.length);return null}function kc(r){let e=Fe(r);for(let t of Je){let n=`programs/${t}/`;if(e.startsWith(n)){let i=Tr().forkInstrumentationDisabledOutputs.get(`${t}/${e.slice(n.length)}`);return i?jt(i)!==null:!1}}return!1}function xc(r){let e=Fe(r);if(e==="kernel.wasm")return Fr;let t=Lc(e);if(t&&t.endsWith(".wasm"))return lc}function Ic(r,e,t){if(!r.endsWith(".wasm"))return!1;try{let n=je(r),i=n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength),s=t===void 0?kc(e):t==="disabled";return Zr(i,{expectedAbi:42,requiredExports:xc(e),requireForkInstrumentation:s?!1:void 0,forbidForkInstrumentation:s}).length>0}catch{return!0}}function _c(r){if(!r.endsWith(".vfs")&&!r.endsWith(".vfs.zst"))return!1;try{let t=zn.readImageMetadata(je(r))?.kernelAbi;return t!==void 0&&t!==42}catch{return!0}}function Rr(r,e,t){return Ic(r,e,t)||_c(r)}function xo(r,e,t){let n=r.filter(se);return n.length===0?null:n.find(i=>{try{return Ye(i).isFile()&&!Rr(i,e,t)}catch{return!1}})??null}function Io(r,e,t){try{if(!xn(r).isSymbolicLink())return r;let i=ve(r);if(!Ye(i).isFile()||Rr(i,e,t))throw new Error("canonical target is not an accepted regular file");if(Fe(e).startsWith("programs/")&&Pc(i))throw new Error("resolver-owned program generation has no matching selected package projection");return i}catch(n){throw new Error(`Binary changed or became invalid while pinning ${e}: ${n instanceof Error?n.message:String(n)}`)}}function Pc(r){let e=[Eo()];try{e.push(K(Xe(),"local-binaries",".kandelo-local-generations"))}catch{}return e.some(t=>{try{return se(t)&&Br(ve(t),r)}catch{return!1}})}function Br(r,e){let t=sc(r,e);return t===""||t!==".."&&!t.startsWith(`..${ac}`)&&!In(t)}function Oc(r,e){let t=e.split("/"),n=r;for(let i=0;ia.packageName!==s))return"declared package members do not share a valid program namespace";if(!Ye(e).isDirectory())return"shared package generation root is not a directory";let o=t[0].cacheKey;if(!/^[a-f0-9]{64}$/.test(o)||t.some(a=>a.cacheKey!==o))return"declared package members do not share one valid cache identity";if(r.identity==="local-generation"){let a=K(r.root,".kandelo-local-generations",i,s,o);if(!se(a))return"local mirror targets are not one direct immutable local generation";let c=ve(a);return Yt(e)===c?null:"local mirror targets are not one direct immutable local generation"}if(r.identity==="program-cache"){let a=Eo();if(!se(a))return"fetched mirror targets are not one canonical program-cache generation";let c=ve(a),l=oc(e),u=l.startsWith(`${s}-`)&&new RegExp(`-rev[0-9]+-${i}-${o}$`).test(l);return Yt(e)===c&&u?null:"fetched mirror targets are not one canonical program-cache generation"}return"installed-package symlink closures are not an immutable installed identity"}function Rc(r,e,t){if(e.length!==t.length)return{failure:"internal member/path count mismatch"};try{let n=e.map(l=>{let u=xn(l);return u.isSymbolicLink()?"symlink":u.isFile()?"file":"other"});if(n.includes("other"))return{failure:"a selected mirror member is neither a regular file nor a symlink"};let i=n.every(l=>l==="symlink"),s=n.every(l=>l==="file");if(!i&&!s)return{failure:"regular files and symlinks cannot share one package identity"};if(s){if(!r.allowRegularFileClosure)return{failure:"a mutable source-checkout wasm tree is not an installed package identity"};let l=t[0].packageName,u=t[0].projectionIdentity;if(t.some(p=>p.packageName!==l||p.projectionIdentity!==u))return{failure:"declared members do not share one selected package projection"};let f=Or()?.packages.get(l);if(!f||Lo(f)!==u)return{failure:"installed bytes do not match the selected package projection"};let y=ve(r.root),h=[];for(let p of e){let w=ve(p);if(!Br(y,w)||!Ye(w).isFile())return{failure:"an installed-package member escapes its immutable wasm tree"};h.push(w)}return{paths:h}}let o=null,a=[];for(let l=0;lBc(r))}function Bc(r){let e=Fe(r),t=Ac(e);if(t){let o=Cc(t.members.map(a=>a.relPath),t.members);if(o)return o[t.members.findIndex(a=>a.relPath===e)];throw new kn(`Package artifacts not found for ${t.packageName}: ${e}`)}let n=[],i=[];for(let o of zo())for(let a of o.candidatesFor(r))n.push(a),i.push(a);let s=xo(i,r);if(s)return Io(s,r);throw i.some(se)?new Error(`Binary exists but was rejected by artifact policy: ${r} `+n.map(o=>` checked: ${o}`).join(` -`)):new xn(`Binary not found: ${r} +`)):new kn(`Binary not found: ${r} `+n.map(o=>` checked: ${o}`).join(` `)+` - Run scripts/fetch-binaries.sh, place a file at local-binaries/${e}, or install a package that includes wasm/${r}.`)}function Mc(r,e){if(r.length===0)return[];let t=!1,n=[];for(let i of ko()){let s=[],o=[];if(e){let[a,c,l]=e[0].relPath.split("/");a==="programs"&&c&&l&&(t||=se(K(i.root,a,c,l)))}for(let[a,c]of r.entries()){let l=i.candidatesFor(c),u=l.filter(se);t||=u.length>0;let d=Po(l,c,e?.[a]?.forkInstrumentation);d?s.push(d):u.length>0?o.push(`${c} (rejected by artifact policy)`):o.push(`${c} (missing)`)}if(o.length===0&&e){let a=Bc(i,s,e);if("failure"in a)o.push(`shared package identity rejected: ${a.failure}`);else{let c=a.paths.flatMap((l,u)=>Cr(l,r[u],e[u].forkInstrumentation)?[r[u]]:[]);if(c.length>0)o.push(`pinned package generation rejected by artifact policy: ${c.join(", ")}`);else return a.paths}}if(o.length===0)return s.map((a,c)=>Oo(a,r[c],e?.[c]?.forkInstrumentation));n.push(` ${i.label} (${i.root}): ${o.join(", ")}`)}if(!t)return null;throw new Error(`Package artifact closure is incomplete: no single provenance tier contains every accepted artifact, and tiers will not be mixed. + Run scripts/fetch-binaries.sh, place a file at local-binaries/${e}, or install a package that includes wasm/${r}.`)}function Cc(r,e){if(r.length===0)return[];let t=!1,n=[];for(let i of zo()){let s=[],o=[];if(e){let[a,c,l]=e[0].relPath.split("/");a==="programs"&&c&&l&&(t||=se(K(i.root,a,c,l)))}for(let[a,c]of r.entries()){let l=i.candidatesFor(c),u=l.filter(se);t||=u.length>0;let d=xo(l,c,e?.[a]?.forkInstrumentation);d?s.push(d):u.length>0?o.push(`${c} (rejected by artifact policy)`):o.push(`${c} (missing)`)}if(o.length===0&&e){let a=Rc(i,s,e);if("failure"in a)o.push(`shared package identity rejected: ${a.failure}`);else{let c=a.paths.flatMap((l,u)=>Rr(l,r[u],e[u].forkInstrumentation)?[r[u]]:[]);if(c.length>0)o.push(`pinned package generation rejected by artifact policy: ${c.join(", ")}`);else return a.paths}}if(o.length===0)return s.map((a,c)=>Io(a,r[c],e?.[c]?.forkInstrumentation));n.push(` ${i.label} (${i.root}): ${o.join(", ")}`)}if(!t)return null;throw new Error(`Package artifact closure is incomplete: no single provenance tier contains every accepted artifact, and tiers will not be mixed. `+n.join(` -`))}var[Ro,...$c]=process.argv.slice(2);(!Ro||$c.length>0)&&(console.error("usage: scripts/resolve-binary.sh "),process.exit(2));try{process.stdout.write(`${To(Ro)} +`))}var[Po,...Nc]=process.argv.slice(2);(!Po||Nc.length>0)&&(console.error("usage: scripts/resolve-binary.sh "),process.exit(2));try{process.stdout.write(`${_o(Po)} `)}catch(r){console.error(r instanceof Error?r.message:String(r)),process.exit(1)} diff --git a/scripts/test-finalize-homebrew-main-shell-release.py b/scripts/test-finalize-homebrew-main-shell-release.py index d7bd391fdd..415ffbdcd3 100755 --- a/scripts/test-finalize-homebrew-main-shell-release.py +++ b/scripts/test-finalize-homebrew-main-shell-release.py @@ -281,7 +281,10 @@ def create_closed_selection( entry["formula"]["name"]: entry["formula"] for entry in migration["packages"] } - dependencies = {**DEPENDENCIES, "ruby": ["zlib", "libyaml"]} + dependencies = { + **DEPENDENCIES, + "git": [*DEPENDENCIES["git"], "bzip2"], + } names = { identity.split("/")[-1] for identity in migration["formula_closure"] @@ -289,7 +292,7 @@ def create_closed_selection( identity.split("/")[-1] for identity in support["availability"]["reusable_public_abi42"] } - names.update({"homebrew-bootstrap", "libyaml"}) + names.add("homebrew-bootstrap") packages = { name: package_record(name, formulae.get(name), dependencies) for name in names @@ -423,16 +426,13 @@ def set_shell_revision(source: pathlib.Path, literal: str) -> None: path.write_text(updated) -def add_future_libyaml_shape(source: pathlib.Path) -> None: +def add_future_bzip2_shape(source: pathlib.Path) -> None: support_path = source / "homebrew/main-shell-homebrew-runtime-support.json" support = json.loads(support_path.read_text()) - libyaml = f"{TAP_NAME}/libyaml" - ruby = f"{TAP_NAME}/ruby" + bzip2 = f"{TAP_NAME}/bzip2" + git = f"{TAP_NAME}/git" formula_order = support["formula_order"] - formula_order.insert(formula_order.index(ruby), libyaml) - support["additional_formula_order"].insert(0, libyaml) - reusable = support["availability"]["reusable_public_abi42"] - reusable.insert(reusable.index(ruby), libyaml) + formula_order.insert(formula_order.index(git), bzip2) write_json(support_path, support) @@ -515,10 +515,10 @@ def misorder_embedded_formulae(policy: dict) -> None: assert applied_json["base_formulae"] == 38 assert applied_json["embedded"] == 3 assert applied_json["lazy"] == 35 - assert applied_json["runtime_formulae"] == 21 - assert applied_json["audited_formulae"] == 25 - assert applied_json["runtime_extra"] == 1 - assert applied_json["total"] == 39 + assert applied_json["runtime_formulae"] == 20 + assert applied_json["audited_formulae"] == 24 + assert applied_json["runtime_extra"] == 0 + assert applied_json["total"] == 38 head = subprocess.run( ["git", "-C", str(tap), "rev-parse", "HEAD"], check=True, @@ -589,8 +589,8 @@ def misorder_embedded_formulae(policy: dict) -> None: assert_product_state(source, "awaiting-selection") checker = run_checker(source, tap) assert ( - "38 base Formulae, 21 runtime Formulae, and 25 audited Formulae; " - "the runtime adds 1 beyond the base, yielding 39 total Formulae" + "38 base Formulae, 20 runtime Formulae, and 24 audited Formulae; " + "the runtime adds 0 beyond the base, yielding 38 total Formulae" in checker.stdout ) @@ -642,12 +642,14 @@ def misorder_embedded_formulae(policy: dict) -> None: ) as temporary: root = pathlib.Path(temporary) source = copy_source(root) - add_future_libyaml_shape(source) + add_future_bzip2_shape(source) set_shell_revision(source, "23") tap = create_tap( root, source, - dependency_overrides={"ruby": ["zlib", "libyaml"]}, + dependency_overrides={ + "git": [*DEPENDENCIES["git"], "bzip2"], + }, ) applied = run( "--source-root", str(source), "--tap-root", str(tap), "--apply" @@ -657,10 +659,10 @@ def misorder_embedded_formulae(policy: dict) -> None: assert summary["base_formulae"] == 38 assert summary["embedded"] == 3 assert summary["lazy"] == 35 - assert summary["runtime_formulae"] == 22 - assert summary["audited_formulae"] == 26 - assert summary["runtime_extra"] == 2 - assert summary["total"] == 40 + assert summary["runtime_formulae"] == 21 + assert summary["audited_formulae"] == 24 + assert summary["runtime_extra"] == 0 + assert summary["total"] == 38 assert re.search( r"^revision\s*=\s*23$", (source / "packages/registry/shell/build.toml").read_text(), @@ -668,20 +670,19 @@ def misorder_embedded_formulae(policy: dict) -> None: ) checker = run_checker(source, tap) assert ( - "38 base Formulae, 22 runtime Formulae, and 26 audited Formulae; " - "the runtime adds 2 beyond the base, yielding 40 total Formulae" + "38 base Formulae, 21 runtime Formulae, and 24 audited Formulae; " + "the runtime adds 0 beyond the base, yielding 38 total Formulae" in checker.stdout ) support_path = source / "homebrew/main-shell-homebrew-runtime-support.json" baseline = json.loads(support_path.read_text()) - libyaml = f"{TAP_NAME}/libyaml" - ruby = f"{TAP_NAME}/ruby" + bzip2 = f"{TAP_NAME}/bzip2" + git = f"{TAP_NAME}/git" missing_runtime = json.loads(json.dumps(baseline)) - missing_runtime["formula_order"].remove(libyaml) - missing_runtime["additional_formula_order"].remove(libyaml) - missing_runtime["availability"]["reusable_public_abi42"].remove(libyaml) + missing_runtime["formula_order"].remove(bzip2) + missing_runtime["availability"]["reusable_public_abi42"].remove(bzip2) write_json(support_path, missing_runtime) assert_failure( run_checker(source, tap, success=False), @@ -691,18 +692,18 @@ def misorder_embedded_formulae(policy: dict) -> None: missing_availability = json.loads(json.dumps(baseline)) missing_availability["availability"]["reusable_public_abi42"].remove( - libyaml + bzip2 ) write_json(support_path, missing_availability) assert_failure( run_checker(source, tap, success=False), "Homebrew runtime-support activation includes Formulae without " - f"admitted public ABI-42 bottles: {libyaml}", + f"admitted public ABI-42 bottles: {bzip2}", ) - missing_additional = json.loads(json.dumps(baseline)) - missing_additional["additional_formula_order"].remove(libyaml) - write_json(support_path, missing_additional) + unexpected_additional = json.loads(json.dumps(baseline)) + unexpected_additional["additional_formula_order"].append(bzip2) + write_json(support_path, unexpected_additional) assert_failure( run_checker(source, tap, success=False), "Homebrew runtime-support additional closure is not its exact " @@ -710,49 +711,49 @@ def misorder_embedded_formulae(policy: dict) -> None: ) duplicate_runtime = json.loads(json.dumps(baseline)) - duplicate_runtime["formula_order"].append(libyaml) + duplicate_runtime["formula_order"].append(bzip2) write_json(support_path, duplicate_runtime) assert_failure( run_checker(source, tap, success=False), - f"Homebrew runtime-support formula_order contains duplicate {libyaml}", + f"Homebrew runtime-support formula_order contains duplicate {bzip2}", ) duplicate_additional = json.loads(json.dumps(baseline)) - duplicate_additional["additional_formula_order"].append(libyaml) + duplicate_additional["additional_formula_order"].extend([bzip2, bzip2]) write_json(support_path, duplicate_additional) assert_failure( run_checker(source, tap, success=False), "Homebrew runtime-support additional_formula_order contains " - f"duplicate {libyaml}", + f"duplicate {bzip2}", ) duplicate_availability = json.loads(json.dumps(baseline)) duplicate_availability["availability"]["reusable_public_abi42"].append( - libyaml + bzip2 ) write_json(support_path, duplicate_availability) assert_failure( run_checker(source, tap, success=False), "Homebrew runtime-support availability.reusable_public_abi42 " - f"contains duplicate {libyaml}", + f"contains duplicate {bzip2}", ) duplicate_across_partitions = json.loads(json.dumps(baseline)) duplicate_across_partitions["availability"]["requires_rebuild"] = [ - libyaml + bzip2 ] write_json(support_path, duplicate_across_partitions) assert_failure( run_checker(source, tap, success=False), "Homebrew runtime-support availability partition contains duplicate " - f"{libyaml}", + f"{bzip2}", ) mismatched_availability = json.loads(json.dumps(baseline)) reusable = mismatched_availability["availability"][ "reusable_public_abi42" ] - reusable[reusable.index(libyaml)] = f"{TAP_NAME}/unknown" + reusable[reusable.index(bzip2)] = f"{TAP_NAME}/unknown" write_json(support_path, mismatched_availability) assert_failure( run_checker(source, tap, success=False), @@ -762,13 +763,13 @@ def misorder_embedded_formulae(policy: dict) -> None: mismatched_runtime_order = json.loads(json.dumps(baseline)) formula_order = mismatched_runtime_order["formula_order"] - libyaml_index = formula_order.index(libyaml) - ruby_index = formula_order.index(ruby) - formula_order[libyaml_index], formula_order[ruby_index] = ( - formula_order[ruby_index], - formula_order[libyaml_index], + bzip2_index = formula_order.index(bzip2) + git_index = formula_order.index(git) + formula_order[bzip2_index], formula_order[git_index] = ( + formula_order[git_index], + formula_order[bzip2_index], ) - mismatched_runtime_order["additional_formula_order"] = [ruby, libyaml] + mismatched_runtime_order["additional_formula_order"] = [] write_json(support_path, mismatched_runtime_order) assert_failure( run_checker(source, tap, success=False), @@ -777,13 +778,9 @@ def misorder_embedded_formulae(policy: dict) -> None: ) substituted_runtime = json.loads(json.dumps(baseline)) - bzip2 = f"{TAP_NAME}/bzip2" + xz = f"{TAP_NAME}/xz" formula_order = substituted_runtime["formula_order"] - formula_order[formula_order.index(libyaml)] = bzip2 - substituted_runtime["additional_formula_order"].remove(libyaml) - substituted_runtime["availability"]["reusable_public_abi42"].remove( - libyaml - ) + formula_order[formula_order.index(bzip2)] = xz write_json(support_path, substituted_runtime) assert_failure( run_checker(source, tap, success=False), @@ -835,11 +832,11 @@ def misorder_embedded_formulae(policy: dict) -> None: ) preview_summary = json.loads(preview.stdout) assert preview_summary["final_tap_commit"] == source_commit - assert preview_summary["runtime_formulae"] == 22 - assert preview_summary["audited_formulae"] == 26 - assert preview_summary["runtime_extra"] == 2 - assert preview_summary["total"] == 40 - assert preview_summary["selection"]["formula_count"] == 41 + assert preview_summary["runtime_formulae"] == 21 + assert preview_summary["audited_formulae"] == 24 + assert preview_summary["runtime_extra"] == 0 + assert preview_summary["total"] == 38 + assert preview_summary["selection"]["formula_count"] == 39 assert before == {path: digest(path) for path in paths} applied = run( @@ -867,18 +864,18 @@ def misorder_embedded_formulae(policy: dict) -> None: artifact_lock = json.loads( (source / "homebrew/main-shell-lazy-artifact-lock.json").read_text() ) - libyaml = f"{TAP_NAME}/libyaml" - ruby = f"{TAP_NAME}/ruby" + bzip2 = f"{TAP_NAME}/bzip2" + git = f"{TAP_NAME}/git" assert migration["catalog"]["tap_commit"] == source_commit assert support["catalog"]["tap_commit"] == source_commit assert support["availability"]["audited_catalog"][ "checkout_commit" ] == source_commit - assert support["formula_order"].index(libyaml) < support[ + assert support["formula_order"].index(bzip2) < support[ "formula_order" - ].index(ruby) - assert support["additional_formula_order"] == [libyaml, ruby] - assert libyaml in support["availability"]["reusable_public_abi42"] + ].index(git) + assert support["additional_formula_order"] == [] + assert bzip2 in support["availability"]["reusable_public_abi42"] assert selection_lock["state"] == "sealed" assert selection_lock["release"]["target_commitish"] == source_commit assert artifact_lock["state"] == "pending" diff --git a/scripts/test-homebrew-main-shell-closure.sh b/scripts/test-homebrew-main-shell-closure.sh index dcc8102294..d8b36497f4 100755 --- a/scripts/test-homebrew-main-shell-closure.sh +++ b/scripts/test-homebrew-main-shell-closure.sh @@ -2827,6 +2827,8 @@ if [[ "$composer" == */scripts/prepare-homebrew-main-shell-inputs.sh ]]; then printf '{}\n' >"$prepared/selection-receipt.json" printf 'Formula-owned Homebrew source\n' \ >"$prepared/bootstrap/homebrew-bootstrap.zip" + printf 'Formula-owned portable Ruby\n' \ + >"$prepared/bootstrap/homebrew-portable-ruby.zip" printf 'HOMEBREW_NO_ANALYTICS=1\n' \ >"$prepared/bootstrap/homebrew-brew.env" printf '{}\n' >"$prepared/bootstrap/report.json" @@ -2854,6 +2856,7 @@ done [ -n "$work" ] && [ -n "$report" ] && [ -n "$cache" ] && [ -n "$out" ] && [ "$prepared" = "$WASM_POSIX_DEP_OUT_DIR/.homebrew-shell-build/prepared-inputs" ] && [ -f "$prepared/bootstrap/homebrew-bootstrap.zip" ] && + [ -f "$prepared/bootstrap/homebrew-portable-ruby.zip" ] && [ "$review" = "${FAKE_EXPECT_REVIEW:-true}" ] [ ! -e "$work" ] && [ ! -L "$work" ] mkdir "$work" @@ -3028,6 +3031,8 @@ printf '%s\n' "untrusted Homebrew source" \ >"$bootstrap_dir/homebrew-bootstrap.zip" printf '%s\n' "HOMEBREW_NO_ANALYTICS=1" \ >"$bootstrap_dir/homebrew-brew.env" +printf '%s\n' "untrusted portable Ruby" \ + >"$bootstrap_dir/homebrew-portable-ruby.zip" # A manual composer invocation must not be able to seal a locally generated or # stale ZIP under the trusted homebrew-bootstrap package name. The package's @@ -3038,6 +3043,8 @@ expect_failure "homebrew-bootstrap source lock: output archive has" \ --migration-lock "$lock" \ --package-tree-spec "$PACKAGE_TREE_SPEC" \ --package-tree-archive "$bootstrap_dir/homebrew-bootstrap.zip" \ + --homebrew-portable-ruby-archive \ + "$bootstrap_dir/homebrew-portable-ruby.zip" \ --homebrew-bootstrap-env "$bootstrap_dir/homebrew-brew.env" wrong_epoch_lock="$TMP_ROOT/main-shell-wrong-epoch-lock.json" diff --git a/scripts/test-homebrew-main-shell-mirror-handoff.sh b/scripts/test-homebrew-main-shell-mirror-handoff.sh index 4b4da40dee..951cc60ea1 100755 --- a/scripts/test-homebrew-main-shell-mirror-handoff.sh +++ b/scripts/test-homebrew-main-shell-mirror-handoff.sh @@ -35,6 +35,7 @@ make_fixture() { mkdir -p "$root/mirror" printf 'shell\n' >"$root/main-shell.vfs.zst" printf 'bootstrap\n' >"$root/homebrew-bootstrap.zip" + printf 'portable ruby\n' >"$root/homebrew-portable-ruby.zip" printf 'env\n' >"$root/homebrew-brew.env" printf 'bottle\n' >"$root/mirror/hello.bottle.tar.gz" bottle_sha="$(sha256_file "$root/mirror/hello.bottle.tar.gz")" @@ -115,6 +116,8 @@ make_fixture() { --argjson image_bytes "$(bytes "$root/main-shell.vfs.zst")" \ --arg bootstrap_sha "$(sha256_file "$root/homebrew-bootstrap.zip")" \ --argjson bootstrap_bytes "$(bytes "$root/homebrew-bootstrap.zip")" \ + --arg portable_ruby_sha "$(sha256_file "$root/homebrew-portable-ruby.zip")" \ + --argjson portable_ruby_bytes "$(bytes "$root/homebrew-portable-ruby.zip")" \ --arg env_sha "$(sha256_file "$root/homebrew-brew.env")" \ --argjson env_bytes "$(bytes "$root/homebrew-brew.env")" \ --arg publish_sha "$(sha256_file "$root/publish.json")" \ @@ -135,6 +138,9 @@ make_fixture() { "homebrew-bootstrap.zip": { sha256: $bootstrap_sha, bytes: $bootstrap_bytes }, + "homebrew-portable-ruby.zip": { + sha256: $portable_ruby_sha, bytes: $portable_ruby_bytes + }, "homebrew-brew.env": { sha256: $env_sha, bytes: $env_bytes }, "publish.json": { sha256: $publish_sha, bytes: $publish_bytes } }, diff --git a/scripts/test-homebrew-main-shell-mirror-workflow.sh b/scripts/test-homebrew-main-shell-mirror-workflow.sh index ac4fc72821..0f5275d86a 100755 --- a/scripts/test-homebrew-main-shell-mirror-workflow.sh +++ b/scripts/test-homebrew-main-shell-mirror-workflow.sh @@ -61,7 +61,7 @@ sed \ 's#verify-existing-immutable-github-release.sh#publish-immutable-github-release.sh#' \ "$WORKFLOW" >"$TMP_ROOT/republished-mirror.yml" expect_rejected "$TMP_ROOT/republished-mirror.yml" -sed 's/(.assets | length) == 4/(.assets | length) == 3/' \ +sed 's/(.assets | length) == 5/(.assets | length) == 4/' \ "$WORKFLOW" >"$TMP_ROOT/wrong-lifecycle-asset-count.yml" expect_rejected "$TMP_ROOT/wrong-lifecycle-asset-count.yml" sed 's|cmp "$lifecycle/$name" "$RUNNER_TEMP/public-$name"|true|' \ diff --git a/scripts/verify-homebrew-guest-lifecycle-publication.sh b/scripts/verify-homebrew-guest-lifecycle-publication.sh index a96b1d18d2..cd4a1fc4a5 100644 --- a/scripts/verify-homebrew-guest-lifecycle-publication.sh +++ b/scripts/verify-homebrew-guest-lifecycle-publication.sh @@ -55,6 +55,7 @@ expected_names="$( handoff.json \ homebrew-bootstrap.zip \ homebrew-brew.env \ + homebrew-portable-ruby.zip \ main-shell-brew-package-tree.json \ main-shell.vfs.zst \ publish.json | @@ -134,6 +135,7 @@ jq -e \ .release.tag + "/") and (.files | keys | sort) == [ "homebrew-bootstrap.zip", "homebrew-brew.env", + "homebrew-portable-ruby.zip", "main-shell-brew-package-tree.json", "main-shell.vfs.zst", "publish.json" ] and @@ -151,6 +153,7 @@ for name in \ main-shell.vfs.zst \ main-shell-brew-package-tree.json \ homebrew-bootstrap.zip \ + homebrew-portable-ruby.zip \ homebrew-brew.env \ publish.json do @@ -175,6 +178,7 @@ jq -e \ "main-shell.vfs.zst", "main-shell-brew-package-tree.json", "homebrew-bootstrap.zip", + "homebrew-portable-ruby.zip", "homebrew-brew.env" ] | map(. as $name | {name: $name} + $handoff[0].files[$name] diff --git a/scripts/verify-homebrew-main-shell-mirror-handoff.sh b/scripts/verify-homebrew-main-shell-mirror-handoff.sh index 600a06bd6c..0224ee089e 100755 --- a/scripts/verify-homebrew-main-shell-mirror-handoff.sh +++ b/scripts/verify-homebrew-main-shell-mirror-handoff.sh @@ -91,6 +91,7 @@ for path in \ "$PUBLISH" \ "$ROOT/main-shell.vfs.zst" \ "$ROOT/homebrew-bootstrap.zip" \ + "$ROOT/homebrew-portable-ruby.zip" \ "$ROOT/homebrew-brew.env" do [ -f "$path" ] && [ ! -L "$path" ] || { @@ -112,6 +113,7 @@ expected_root_entries="$( handoff.json \ homebrew-bootstrap.zip \ homebrew-brew.env \ + homebrew-portable-ruby.zip \ main-shell.vfs.zst \ mirror \ publish.json | @@ -150,6 +152,7 @@ jq -e \ .canary_ref == $canary and (.files | keys | sort) == [ "homebrew-bootstrap.zip", "homebrew-brew.env", + "homebrew-portable-ruby.zip", "main-shell.vfs.zst", "publish.json" ] and ([.files[] | @@ -179,6 +182,7 @@ file_bytes() { for name in \ main-shell.vfs.zst \ homebrew-bootstrap.zip \ + homebrew-portable-ruby.zip \ homebrew-brew.env \ publish.json do diff --git a/sdk/kandelo/bin/wasm32posix-cc b/sdk/kandelo/bin/wasm32posix-cc index b8a8aa7563..db305fa727 100755 --- a/sdk/kandelo/bin/wasm32posix-cc +++ b/sdk/kandelo/bin/wasm32posix-cc @@ -276,7 +276,9 @@ while [[ "$#" -gt 0 ]]; do raw_threads_or_dynamic=1 filtered+=("$arg") ;; - -lpthread|-rdynamic) + # The SDK places libc after its executable glue. Shared modules import the + # same process-wide libc state instead of embedding a second copy. + -lc|-lpthread|-rdynamic) raw_threads_or_dynamic=1 ;; -fPIE|-pie|-lrt|-lresolv|-lm|-lcrypt|-lutil|-Wl,-Bsymbolic) @@ -679,9 +681,31 @@ if (( lld_major >= 22 )); then # stack-after-data layout; LLD 21 does not accept this negative option. exe_link_flags+=(-Wl,--no-stack-first) fi +libc_prefix_inputs=() +libc_inputs=("${SYSROOT}/lib/libc.a") +if [[ "$link_dl" -eq 1 ]]; then + dynamic_libc="${SYSROOT}/lib/libc-dynamic.o" + dynamic_libc_anchor="${SYSROOT}/lib/libc-dynamic-anchor.o" + [[ -f "$dynamic_libc" ]] || die "missing $dynamic_libc; rebuild the Kandelo sysroot" + [[ -f "$dynamic_libc_anchor" ]] || \ + die "missing $dynamic_libc_anchor; rebuild the Kandelo sysroot" + # WHY: place the anchor's undefined relocations before caller archives so + # intentional executable interpositions win. Keep the fallback dynamic + # libc after caller inputs so the archive resolver selects one definition. + exe_link_flags+=( + -Wl,--export-all + -Wl,--undefined=__kandelo_dynamic_libc_functions + -Wl,--undefined=__kandelo_dynamic_libc_data + ) + libc_prefix_inputs=("$dynamic_libc_anchor") + # The resolved object contains one coherent libc selected by the ordinary + # archive resolver, with weak definitions for executable interposition. + libc_inputs=("$dynamic_libc") +fi append_ld_flags "${exe_link_flags[@]}" "$WASM_LD" -m wasm32 -L"${SYSROOT}/lib" \ - "${object_inputs[@]}" "${compiled_objects[@]}" "${glue_inputs[@]}" \ - "${SYSROOT}/lib/crt1.o" "${user_link_args[@]}" "${stdlib_inputs[@]}" "${SYSROOT}/lib/libc.a" \ + "${libc_prefix_inputs[@]}" "${object_inputs[@]}" "${compiled_objects[@]}" \ + "${glue_inputs[@]}" \ + "${SYSROOT}/lib/crt1.o" "${user_link_args[@]}" "${stdlib_inputs[@]}" "${libc_inputs[@]}" \ "${ld_flags[@]}" -o "$output" exit $? diff --git a/sdk/src/bin/cc.ts b/sdk/src/bin/cc.ts index 6db0f1af5d..e3047ce2bd 100755 --- a/sdk/src/bin/cc.ts +++ b/sdk/src/bin/cc.ts @@ -239,6 +239,13 @@ function buildClangArgsInternal( if (parsed.preprocessOnly) args.push('-E'); if (parsed.assemblyOnly) args.push('-S'); if (parsed.outputFile) args.push('-o', parsed.outputFile); + const dynamicLibcAnchor = join(toolchain.sysroot, 'lib', 'libc-dynamic-anchor.o'); + if (linking && !classifyLink && !parsed.shared && parsed.linkDl) { + // WHY: the anchor's undefined relocations must exist before caller + // archives are scanned. That lets an executable's intentional libc + // interpositions win before the fallback dynamic-libc archive is seen. + args.push(dynamicLibcAnchor); + } // Static link semantics depend on the caller's exact ordering of objects, // archives, -l flags, and linker group controls. Parsed classifications are // for SDK decisions only; forwarding must never rebuild the command in @@ -315,9 +322,25 @@ function buildClangArgsInternal( if (parsed.linkDl) { args.push(join(toolchain.glueDir, 'dlopen.c')); } + const libcArchive = join(toolchain.sysroot, 'lib', 'libc.a'); + const dynamicLibcObject = join(toolchain.sysroot, 'lib', 'libc-dynamic.o'); args.push( join(toolchain.sysroot, 'lib', 'crt1.o'), - join(toolchain.sysroot, 'lib', 'libc.a'), + // A side module shares the process's one libc instead of linking a + // second copy. The rooted anchor retains the process-libc scope using + // ordinary relocations, so executable/glue definitions interpose and + // musl's archive resolver still chooses one coherent implementation. + ...(parsed.linkDl + ? [ + '-Wl,--export-all', + '-Wl,--undefined=__kandelo_dynamic_libc_functions', + '-Wl,--undefined=__kandelo_dynamic_libc_data', + // This resolved object contains one coherent libc selected by + // the ordinary archive resolver, with weak definitions so + // caller-owned strong symbols can interpose. + dynamicLibcObject, + ] + : [libcArchive]), // LLD 22 made --stack-first the default; LLD 21 neither defaults to // it nor accepts --no-stack-first. Preserve Kandelo's established // stack-after-data layout explicitly only where the option exists. diff --git a/sdk/src/bin/pkg-config.ts b/sdk/src/bin/pkg-config.ts index 9576f834b2..fc8659e55f 100755 --- a/sdk/src/bin/pkg-config.ts +++ b/sdk/src/bin/pkg-config.ts @@ -1,4 +1,5 @@ #!/usr/bin/env -S node --experimental-strip-types +import { isAbsolute } from 'node:path'; import { findSysroot } from '../lib/toolchain.ts'; import { runPassthrough } from '../lib/exec.ts'; import { isMain } from '../lib/is-main.ts'; @@ -35,13 +36,16 @@ export function buildPkgConfigEnv( }; } -/** True if the path is part of the kandelo cache or sysroot. +/** True if the path is caller-local or part of the Kandelo cache/sysroot. * Anything under `kandelo/...` (the dep cache root used by * `~/.cache/kandelo/libs/...`) or under `sysroot`/ - * `sysroot64` is wasm32/64 targeted; everything else is host-targeted - * (Nix-store host pc files, /usr/lib/pkgconfig, etc.) and gets dropped. */ + * `sysroot64` is wasm32/64 targeted. Relative entries are also explicit + * caller-owned build paths; projects conventionally use `PKG_CONFIG_PATH=.` + * for metadata they just generated. Other absolute paths are host-targeted + * (Nix-store host pc files, /usr/lib/pkgconfig, etc.) and get dropped. */ function isWasmPosixPath(p: string): boolean { return ( + !isAbsolute(p) || p.includes('kandelo/') || p.includes('/sysroot/') || p.includes('/sysroot64/') || diff --git a/sdk/src/lib/flags.ts b/sdk/src/lib/flags.ts index 7778c5fa26..2ab4858d5b 100644 --- a/sdk/src/lib/flags.ts +++ b/sdk/src/lib/flags.ts @@ -305,7 +305,9 @@ export const SHARED_LINK_FLAGS: string[] = [ ]; const IGNORED_EXACT = new Set([ - '-lpthread', + // Executables get the authoritative libc archive after SDK glue; side + // modules must import that same process-wide libc state from their loader. + '-lc', '-lpthread', '-fPIE', '-pie', '-lrt', '-lresolv', '-lm', '-lcrypt', '-lutil', '-rdynamic', '-Wl,-Bsymbolic', diff --git a/sdk/test/cc.test.ts b/sdk/test/cc.test.ts index 6882c046af..af4632caab 100644 --- a/sdk/test/cc.test.ts +++ b/sdk/test/cc.test.ts @@ -50,10 +50,33 @@ describe('buildClangArgs', () => { expect(args.join(' ')).toContain('libc.a'); }); - it('-ldl selects the functional dynamic-loading glue', () => { - const args = build(['foo.c', '-ldl', '-o', 'foo.wasm']); + it('owns libc placement for executables and imports libc from shared modules', () => { + const executableArgs = build(['foo.o', '-lc', '-o', 'foo.wasm']); + expect(executableArgs).not.toContain('-lc'); + expect(executableArgs.filter((arg) => arg === '/tmp/sysroot/lib/libc.a')).toHaveLength(1); + + const sharedArgs = build(['-shared', 'foo.o', '-lc', '-o', 'foo.so']); + expect(sharedArgs).not.toContain('-lc'); + expect(sharedArgs).not.toContain('/tmp/sysroot/lib/libc.a'); + expect(sharedArgs.join(' ')).not.toContain('channel_syscall.c'); + expect(sharedArgs).toContain('-Wl,--shared'); + expect(sharedArgs).toContain('-Wl,--allow-undefined'); + }); + + it('-ldl provides the complete process symbol scope for side modules', () => { + const args = build(['foo.c', '/tmp/libinterpose.a', '-ldl', '-o', 'foo.wasm']); expect(args).not.toContain('-ldl'); expect(args).toContain('/tmp/glue/dlopen.c'); + expect(args).toContain('-Wl,--export-all'); + expect(args).toContain('/tmp/sysroot/lib/libc-dynamic-anchor.o'); + expect(args).toContain('-Wl,--undefined=__kandelo_dynamic_libc_functions'); + expect(args).toContain('-Wl,--undefined=__kandelo_dynamic_libc_data'); + expect(args).toContain('/tmp/sysroot/lib/libc-dynamic.o'); + expect(args).not.toContain('/tmp/sysroot/lib/libc.a'); + expect(args.indexOf('/tmp/sysroot/lib/libc-dynamic-anchor.o')) + .toBeLessThan(args.indexOf('/tmp/libinterpose.a')); + expect(args.indexOf('/tmp/libinterpose.a')) + .toBeLessThan(args.indexOf('/tmp/sysroot/lib/libc-dynamic.o')); }); it('uses the 8 MiB stack floor for default and smaller requests', () => { diff --git a/sdk/test/native-cc.test.ts b/sdk/test/native-cc.test.ts index 459acadb07..1d18492724 100644 --- a/sdk/test/native-cc.test.ts +++ b/sdk/test/native-cc.test.ts @@ -79,11 +79,14 @@ done writeFileSync(source, 'int main(void) { return 0; }\n'); for (const path of [ join(sysroot, 'lib/libc.a'), + join(sysroot, 'lib/libc-dynamic.o'), + join(sysroot, 'lib/libc-dynamic-anchor.o'), join(sysroot, 'lib/crt1.o'), join(glue, 'channel_syscall.c'), join(glueObjects, 'channel_syscall.o'), join(glueObjects, 'compiler_rt.o'), join(glueObjects, 'cxxrt.o'), + join(glueObjects, 'dlopen.o'), ]) writeFileSync(path, ''); const env = { @@ -264,6 +267,23 @@ done for (const arg of preserved ?? []) expect(emitted).toContain(arg); } + execFileSync( + 'bash', + [nativeCc, source, join(root, 'libinterpose.a'), '-ldl', '-o', join(root, 'dlopen.wasm')], + { cwd: root, env }, + ); + const dynamicLink = readFileSync(capture, 'utf8').trim().split('\n'); + expect(dynamicLink).toContain('--export-all'); + expect(dynamicLink).toContain('--undefined=__kandelo_dynamic_libc_functions'); + expect(dynamicLink).toContain('--undefined=__kandelo_dynamic_libc_data'); + expect(dynamicLink).toContain(join(sysroot, 'lib/libc-dynamic-anchor.o')); + expect(dynamicLink).toContain(join(sysroot, 'lib/libc-dynamic.o')); + expect(dynamicLink).not.toContain(join(sysroot, 'lib/libc.a')); + expect(dynamicLink.indexOf(join(sysroot, 'lib/libc-dynamic-anchor.o'))) + .toBeLessThan(dynamicLink.indexOf(join(root, 'libinterpose.a'))); + expect(dynamicLink.indexOf(join(root, 'libinterpose.a'))) + .toBeLessThan(dynamicLink.indexOf(join(sysroot, 'lib/libc-dynamic.o'))); + for (const [name, value] of Object.entries(responseValues)) { expect(readFileSync(responses[name], 'utf8')).toBe(`-z\nstack-size=${value}\n`); } diff --git a/sdk/test/pkg-config.test.ts b/sdk/test/pkg-config.test.ts index 58cfb75196..7bde871328 100644 --- a/sdk/test/pkg-config.test.ts +++ b/sdk/test/pkg-config.test.ts @@ -17,6 +17,12 @@ describe('buildPkgConfigEnv', () => { expect(env.PKG_CONFIG_PATH).toBe(cachePath); }); + it('honors caller-local metadata paths', () => { + const relativePaths = ['.', './pkgconfig', '../target/pkgconfig'].join(':'); + const env = buildPkgConfigEnv({ PKG_CONFIG_PATH: relativePaths }, SYSROOT); + expect(env.PKG_CONFIG_PATH).toBe(relativePaths); + }); + it('keeps exact-main dependency metadata only inside the target-cache namespace', () => { const oldRunnerPath = '/home/runner/work/_temp/exact-main-package-cache.abc/libs/icu/lib/pkgconfig'; diff --git a/tests/package-system/homebrew-bootstrap-package.test.ts b/tests/package-system/homebrew-bootstrap-package.test.ts index 781af5fc64..94e0ea9dcf 100644 --- a/tests/package-system/homebrew-bootstrap-package.test.ts +++ b/tests/package-system/homebrew-bootstrap-package.test.ts @@ -62,6 +62,9 @@ describe("homebrew-bootstrap package contract", () => { expect(manifest).toContain(`spdx = "${lock.license.expression}"`); expect(manifest).toContain('name = "homebrew-bootstrap"\nwasm = "homebrew-bootstrap.zip"'); expect(manifest).toContain('name = "homebrew-brew"\nwasm = "homebrew-brew.env"'); + expect(manifest).toContain( + 'name = "homebrew-portable-ruby"\nwasm = "homebrew-portable-ruby.zip"', + ); expect(manifest).toContain('fork_instrumentation = "disabled"'); expect(build).toContain('name = "homebrew_brew"'); @@ -84,6 +87,7 @@ describe("homebrew-bootstrap package contract", () => { expect(parseMultilineStringArray(build, "inputs")).toEqual([ "packages/registry/homebrew-bootstrap/build-homebrew-bootstrap.sh", "scripts/package-build-roots.sh", + "images/vfs/scripts/create-deterministic-zip.sh", "scripts/prepare-homebrew-bootstrap-source.sh", "scripts/verify-homebrew-bootstrap-source-lock.mjs", "homebrew/homebrew-bootstrap-source-lock.json", @@ -96,30 +100,40 @@ describe("homebrew-bootstrap package contract", () => { ); }); - it("projects the non-Wasm package output through the ordinary resolver policy", () => { + it("projects the complete package closure through the ordinary resolver policy", () => { const projection = JSON.parse(readFileSync(projectionPath, "utf8")); expect(projection.format).toBe("kandelo-program-packages-v2"); expect(projection.identities["homebrew-bootstrap"]).toBeDefined(); - expect(projection.packages["homebrew-bootstrap"]).toMatchObject({ - arches: ["wasm32"], - dependencyClosures: { wasm32: [] }, - members: [ - { - kind: "output", - sourceArtifact: "homebrew-bootstrap.zip", - mirrorPath: "homebrew-bootstrap/homebrew-bootstrap.zip", - outputName: "homebrew-bootstrap", - forkInstrumentation: "disabled", - }, - { - kind: "output", - sourceArtifact: "homebrew-brew.env", - mirrorPath: "homebrew-bootstrap/homebrew-brew.env", - outputName: "homebrew-brew", - forkInstrumentation: "disabled", - }, - ], - }); + const packageProjection = projection.packages["homebrew-bootstrap"]; + expect(packageProjection.arches).toEqual(["wasm32"]); + expect( + packageProjection.dependencyClosures.wasm32.map( + (dependency: { packageName: string }) => dependency.packageName, + ), + ).toEqual(["ruby", "zlib"]); + expect(packageProjection.members).toEqual([ + { + kind: "output", + sourceArtifact: "homebrew-bootstrap.zip", + mirrorPath: "homebrew-bootstrap/homebrew-bootstrap.zip", + outputName: "homebrew-bootstrap", + forkInstrumentation: "disabled", + }, + { + kind: "output", + sourceArtifact: "homebrew-brew.env", + mirrorPath: "homebrew-bootstrap/homebrew-brew.env", + outputName: "homebrew-brew", + forkInstrumentation: "disabled", + }, + { + kind: "output", + sourceArtifact: "homebrew-portable-ruby.zip", + mirrorPath: "homebrew-bootstrap/homebrew-portable-ruby.zip", + outputName: "homebrew-portable-ruby", + forkInstrumentation: "disabled", + }, + ]); }); it("rejects source, patch, prepared-tree, and output lock drift", () => { diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs index 8388e6c160..441ef8d485 100644 --- a/tools/xtask/src/main.rs +++ b/tools/xtask/src/main.rs @@ -55,6 +55,8 @@ //! Bind one statically-scanned Formula bridge to its //! authoritative in-tree registry recipe. //! homebrew-validate Validate Kandelo/Homebrew tap sidecar metadata. +//! weaken-wasm-archive Make archive definitions weak process-libc fallbacks. +//! weaken-wasm-object Make object definitions weak process-libc fallbacks. use std::cell::RefCell; use std::collections::BTreeMap; @@ -98,6 +100,7 @@ mod source_extract; mod staging_reuse; mod update_pkg_manifest; mod util; +mod weaken_wasm_archive; fn main() -> ExitCode { let mut args = std::env::args().skip(1); @@ -106,7 +109,7 @@ fn main() -> ExitCode { None => { eprintln!("usage: xtask [args...]"); eprintln!( - "subcommands: dump-abi, bundle-program, build-deps, compute-cache-key-sha, sort-package-matrix, partition-package-matrix, package-dependency-artifacts, materialize-package-output, staging-reuse, archive-stage, archive-extract-member, build-index, set-build-commit, set-package-binary, index-update, index-candidate, homebrew-sidecars, homebrew-tier2-preflight, homebrew-validate" + "subcommands: dump-abi, bundle-program, build-deps, compute-cache-key-sha, sort-package-matrix, partition-package-matrix, package-dependency-artifacts, materialize-package-output, staging-reuse, archive-stage, archive-extract-member, build-index, set-build-commit, set-package-binary, index-update, index-candidate, homebrew-sidecars, homebrew-tier2-preflight, homebrew-validate, weaken-wasm-archive, weaken-wasm-object" ); return ExitCode::from(2); } @@ -132,6 +135,8 @@ fn main() -> ExitCode { "homebrew-sidecars" => homebrew_sidecars::run(rest), "homebrew-tier2-preflight" => homebrew_tier2_preflight::run(rest), "homebrew-validate" => homebrew_validate::run(rest), + "weaken-wasm-archive" => weaken_wasm_archive::run(rest), + "weaken-wasm-object" => weaken_wasm_archive::run_object(rest), other => { eprintln!("xtask: unknown subcommand {other:?}"); return ExitCode::from(2); diff --git a/tools/xtask/src/weaken_wasm_archive.rs b/tools/xtask/src/weaken_wasm_archive.rs new file mode 100644 index 0000000000..abf4ed61f5 --- /dev/null +++ b/tools/xtask/src/weaken_wasm_archive.rs @@ -0,0 +1,362 @@ +//! Mark definitions in a relocatable WebAssembly archive as weak fallbacks. +//! +//! LLVM's object model understands weak Wasm symbols, but llvm-objcopy does +//! not currently implement symbol rewriting for Wasm. This transformer edits +//! only the binding bit in each member's linking symbol table. Because the +//! encoded value keeps the same length, archive offsets and symbol indexes +//! remain authoritative. + +use std::fs; +use std::path::PathBuf; + +use wasmparser::{KnownCustom, Linking, Parser, Payload}; + +const ARCHIVE_MAGIC: &[u8; 8] = b"!\n"; +const ARCHIVE_HEADER_LEN: usize = 60; +const WASM_MAGIC: &[u8; 4] = b"\0asm"; +const BINDING_WEAK: u32 = 1 << 0; +const BINDING_LOCAL: u32 = 1 << 1; +const UNDEFINED: u32 = 1 << 4; +const SYMBOL_KIND_SECTION: u8 = 3; + +#[derive(Debug, Default, PartialEq, Eq)] +struct WeakenedArchive { + bytes: Vec, + object_members: usize, + weakened_symbols: usize, +} + +fn read_u32_leb(bytes: &[u8], start: usize) -> Result<(u32, usize), String> { + let mut value = 0u32; + for index in 0..5 { + let offset = start + .checked_add(index) + .ok_or_else(|| "LEB128 offset overflow".to_string())?; + let byte = *bytes + .get(offset) + .ok_or_else(|| "truncated symbol flags".to_string())?; + if index == 4 && byte & 0xf0 != 0 { + return Err("symbol flags overflow u32".to_string()); + } + value |= u32::from(byte & 0x7f) << (index * 7); + if byte & 0x80 == 0 { + return Ok((value, index + 1)); + } + } + Err("symbol flags exceed five-byte u32 LEB128".to_string()) +} + +fn weaken_object_symbols(bytes: &mut [u8]) -> Result { + let mut flag_offsets = Vec::new(); + let mut linking_sections = 0usize; + let mut symbol_tables = 0usize; + + for payload in Parser::new(0).parse_all(bytes) { + let payload = payload.map_err(|error| format!("parse Wasm object: {error}"))?; + let Payload::CustomSection(section) = payload else { + continue; + }; + if section.name() != "linking" { + continue; + } + linking_sections += 1; + let KnownCustom::Linking(linking) = section.as_known() else { + return Err("malformed or unsupported linking custom section".to_string()); + }; + for subsection in linking { + let subsection = + subsection.map_err(|error| format!("parse linking section: {error}"))?; + let Linking::SymbolTable(symbols) = subsection else { + continue; + }; + symbol_tables += 1; + for symbol in symbols.into_iter_with_offsets() { + let (offset, _) = + symbol.map_err(|error| format!("parse linking symbol: {error}"))?; + let kind = *bytes + .get(offset) + .ok_or_else(|| "symbol kind lies outside the Wasm object".to_string())?; + let flags_offset = offset + .checked_add(1) + .ok_or_else(|| "symbol flags offset overflow".to_string())?; + let (flags, _) = read_u32_leb(bytes, flags_offset)?; + if kind != SYMBOL_KIND_SECTION + && flags & (BINDING_WEAK | BINDING_LOCAL | UNDEFINED) == 0 + { + flag_offsets.push(flags_offset); + } + } + } + } + + if linking_sections != 1 { + return Err(format!( + "expected one linking custom section, found {linking_sections}" + )); + } + // LLVM omits the symbol-table subsection for legitimate relocatable + // objects with no symbols (musl has several architecture placeholders). + if symbol_tables > 1 { + return Err(format!( + "expected at most one linking symbol table, found {symbol_tables}" + )); + } + + for offset in &flag_offsets { + // WHY: BINDING_WEAK is the low bit of the flags ULEB. Setting it in + // place cannot change the encoded width, so every relocation and ar + // member offset remains byte-for-byte stable. + bytes[*offset] |= BINDING_WEAK as u8; + } + Ok(flag_offsets.len()) +} + +fn archive_member_size(header: &[u8], header_offset: usize) -> Result { + let raw = std::str::from_utf8(&header[48..58]) + .map_err(|_| format!("archive member at {header_offset} has a non-UTF-8 size"))?; + raw.trim() + .parse::() + .map_err(|_| format!("archive member at {header_offset} has invalid size {raw:?}")) +} + +fn archive_object_start( + header: &[u8], + data_start: usize, + data_end: usize, + header_offset: usize, +) -> Result { + let name = std::str::from_utf8(&header[..16]) + .map_err(|_| format!("archive member at {header_offset} has a non-UTF-8 name"))? + .trim(); + let Some(length) = name.strip_prefix("#1/") else { + return Ok(data_start); + }; + let length = length + .trim() + .parse::() + .map_err(|_| format!("archive member at {header_offset} has invalid BSD name length"))?; + let object_start = data_start + .checked_add(length) + .ok_or_else(|| "archive extended-name offset overflow".to_string())?; + if object_start > data_end { + return Err(format!( + "archive member at {header_offset} has an out-of-bounds BSD name" + )); + } + Ok(object_start) +} + +fn weaken_archive(input: &[u8]) -> Result { + if !input.starts_with(ARCHIVE_MAGIC) { + return Err("input is not a regular Unix archive".to_string()); + } + + let mut output = input.to_vec(); + let mut offset = ARCHIVE_MAGIC.len(); + let mut object_members = 0usize; + let mut weakened_symbols = 0usize; + + while offset < input.len() { + let header_end = offset + .checked_add(ARCHIVE_HEADER_LEN) + .ok_or_else(|| "archive header offset overflow".to_string())?; + let header = input + .get(offset..header_end) + .ok_or_else(|| format!("truncated archive header at {offset}"))?; + if &header[58..60] != b"`\n" { + return Err(format!("invalid archive header terminator at {offset}")); + } + let size = archive_member_size(header, offset)?; + let data_start = header_end; + let data_end = data_start + .checked_add(size) + .ok_or_else(|| "archive member size overflow".to_string())?; + if data_end > input.len() { + return Err(format!( + "archive member at {offset} extends past end of file" + )); + } + let object_start = archive_object_start(header, data_start, data_end, offset)?; + if input + .get(object_start..object_start + WASM_MAGIC.len()) + .is_some_and(|magic| magic == WASM_MAGIC) + { + object_members += 1; + weakened_symbols += weaken_object_symbols(&mut output[object_start..data_end])?; + } + offset = data_end + .checked_add(size & 1) + .ok_or_else(|| "archive padding offset overflow".to_string())?; + if offset > input.len() { + return Err("archive is missing its final padding byte".to_string()); + } + } + + if object_members == 0 { + return Err("archive contains no relocatable WebAssembly members".to_string()); + } + Ok(WeakenedArchive { + bytes: output, + object_members, + weakened_symbols, + }) +} + +fn parse_args(args: Vec) -> Result<(PathBuf, PathBuf), String> { + if args.len() != 2 { + return Err("usage: xtask weaken-wasm-archive ".to_string()); + } + let input = PathBuf::from(&args[0]); + let output = PathBuf::from(&args[1]); + if input == output { + return Err("input and output archive paths must differ".to_string()); + } + Ok((input, output)) +} + +pub fn run(args: Vec) -> Result<(), String> { + let (input_path, output_path) = parse_args(args)?; + let input = + fs::read(&input_path).map_err(|error| format!("read {}: {error}", input_path.display()))?; + let weakened = weaken_archive(&input)?; + fs::write(&output_path, &weakened.bytes) + .map_err(|error| format!("write {}: {error}", output_path.display()))?; + eprintln!( + "weaken-wasm-archive: weakened {} definitions across {} Wasm members", + weakened.weakened_symbols, weakened.object_members + ); + Ok(()) +} + +pub fn run_object(args: Vec) -> Result<(), String> { + if args.len() != 2 { + return Err("usage: xtask weaken-wasm-object ".to_string()); + } + let input_path = PathBuf::from(&args[0]); + let output_path = PathBuf::from(&args[1]); + if input_path == output_path { + return Err("input and output object paths must differ".to_string()); + } + let mut bytes = + fs::read(&input_path).map_err(|error| format!("read {}: {error}", input_path.display()))?; + if !bytes.starts_with(WASM_MAGIC) { + return Err("input is not a WebAssembly object".to_string()); + } + let weakened_symbols = weaken_object_symbols(&mut bytes)?; + fs::write(&output_path, bytes) + .map_err(|error| format!("write {}: {error}", output_path.display()))?; + eprintln!("weaken-wasm-object: weakened {weakened_symbols} definitions"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn u32_leb(mut value: u32) -> Vec { + let mut bytes = Vec::new(); + loop { + let mut byte = (value & 0x7f) as u8; + value >>= 7; + if value != 0 { + byte |= 0x80; + } + bytes.push(byte); + if value == 0 { + return bytes; + } + } + } + + fn push_name(bytes: &mut Vec, name: &str) { + bytes.extend(u32_leb(name.len() as u32)); + bytes.extend(name.as_bytes()); + } + + fn fixture_object() -> Vec { + let mut symbols = Vec::new(); + symbols.extend(u32_leb(4)); + // Defined hidden function: becomes weak+hidden. + symbols.extend([0, 4, 0]); + push_name(&mut symbols, "fallback"); + // Undefined function: remains undefined, not weak. + symbols.extend([0, UNDEFINED as u8, 1]); + // Local data: remains local, not weak. + symbols.extend([1, BINDING_LOCAL as u8]); + push_name(&mut symbols, "local_data"); + symbols.extend([0, 0, 1]); + // Section symbol: is never a linkable fallback definition. + symbols.extend([SYMBOL_KIND_SECTION, 0, 0]); + + let mut linking = vec![2, 8]; + linking.extend(u32_leb(symbols.len() as u32)); + linking.extend(symbols); + + let mut custom = Vec::new(); + push_name(&mut custom, "linking"); + custom.extend(linking); + + let mut module = b"\0asm\x01\0\0\0".to_vec(); + module.push(0); + module.extend(u32_leb(custom.len() as u32)); + module.extend(custom); + module + } + + fn fixture_archive_member(name: &str, bytes: &[u8]) -> Vec { + let mut header = [b' '; ARCHIVE_HEADER_LEN]; + let name = format!("{name}/"); + header[..name.len()].copy_from_slice(name.as_bytes()); + header[16] = b'0'; + header[28] = b'0'; + header[34] = b'0'; + header[40..46].copy_from_slice(b"100644"); + let size = bytes.len().to_string(); + header[48..48 + size.len()].copy_from_slice(size.as_bytes()); + header[58..60].copy_from_slice(b"`\n"); + let mut member = header.to_vec(); + member.extend(bytes); + if bytes.len() & 1 != 0 { + member.push(b'\n'); + } + member + } + + #[test] + fn weakens_only_defined_global_symbols_without_changing_size() { + let mut object = fixture_object(); + let original_len = object.len(); + assert_eq!(weaken_object_symbols(&mut object).unwrap(), 1); + assert_eq!(object.len(), original_len); + assert_eq!(weaken_object_symbols(&mut object).unwrap(), 0); + } + + #[test] + fn preserves_archive_layout_and_non_wasm_members() { + let object = fixture_object(); + let text = b"symbol index"; + let mut archive = ARCHIVE_MAGIC.to_vec(); + archive.extend(fixture_archive_member("/", text)); + archive.extend(fixture_archive_member("fallback.o", &object)); + + let weakened = weaken_archive(&archive).unwrap(); + assert_eq!(weakened.object_members, 1); + assert_eq!(weakened.weakened_symbols, 1); + assert_eq!(weakened.bytes.len(), archive.len()); + assert_eq!( + weakened + .bytes + .iter() + .zip(&archive) + .filter(|(left, right)| left != right) + .count(), + 1 + ); + } + + #[test] + fn rejects_in_place_cli_writes_and_malformed_archives() { + assert!(parse_args(vec!["same.a".into(), "same.a".into()]).is_err()); + assert!(weaken_archive(b"not an archive").is_err()); + } +}