Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions .github/scripts/test-merge-candidate-workflows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ ARCHIVE_SOURCE_SCRIPT="$SCRIPT_DIR/select-package-archive-source.sh"
ARCHIVE_DOWNLOAD_SCRIPT="$SCRIPT_DIR/download-verified-release-asset.sh"
STAGING_REUSE_SCRIPT="$SCRIPT_DIR/validate-staging-release.sh"
STAGING_COMPOSE_SCRIPT="$SCRIPT_DIR/compose-staging-release-snapshots.sh"
PACK_WORKSPACE_SCRIPT="$REPO_ROOT/scripts/pack-ci-test-workspace.sh"

fail() {
echo "merge-candidate workflow contract: $*" >&2
Expand Down Expand Up @@ -363,8 +364,8 @@ fi
if grep -Fq -- '- name: Capture tested candidate index' "$PREPARE"; then
fail "candidate index must not be recaptured from mutable release state after materialization"
fi
homebrew_guest_block=$(step_block "$PREPARE" "Prove Homebrew starts from candidate artifacts")
homebrew_guest_step=$(step_run_block "$PREPARE" "Prove Homebrew starts from candidate artifacts")
homebrew_guest_block=$(step_block "$PREPARE" "Build and prove Homebrew guest bootstrap from candidate artifacts")
homebrew_guest_step=$(step_run_block "$PREPARE" "Build and prove Homebrew guest bootstrap from candidate artifacts")
grep -Fq "if: env.PACKAGE_STAGING_REQUIRED == 'true'" <<<"$homebrew_guest_block" || \
fail "candidate-backed Homebrew execution must run for package and ABI staging"
grep -Fq 'build-homebrew-bootstrap.sh --skip-package-resolve' <<<"$homebrew_guest_step" || \
Expand All @@ -373,19 +374,32 @@ grep -Fq -- '--brew-script /home/linuxbrew/.linuxbrew/bin/brew' <<<"$homebrew_gu
fail "candidate-backed Homebrew execution must test the canonical brew entry point"
grep -Fq -- '--brew-script /usr/bin/brew' <<<"$homebrew_guest_step" || \
fail "candidate-backed Homebrew execution must test the /usr/bin/brew alias"
grep -Fq 'homebrew_bootstrap_guest_contract_node.ts' <<<"$homebrew_guest_step" || \
fail "candidate-backed Homebrew execution must prove the complete guest bootstrap contract"
host_dist_clear_count=$(grep -Fc 'rm -rf host/dist' <<<"$homebrew_guest_step")
if [ "$host_dist_clear_count" -ne 2 ]; then
fail "candidate-backed Homebrew execution must clear host/dist before both probes"
if [ "$host_dist_clear_count" -ne 3 ]; then
fail "candidate-backed Homebrew execution must clear host/dist before all three probes"
fi
first_host_dist_clear_line=$(grep -nF 'rm -rf host/dist' <<<"$homebrew_guest_step" | sed -n '1s/:.*//p')
second_host_dist_clear_line=$(grep -nF 'rm -rf host/dist' <<<"$homebrew_guest_step" | sed -n '2s/:.*//p')
third_host_dist_clear_line=$(grep -nF 'rm -rf host/dist' <<<"$homebrew_guest_step" | sed -n '3s/:.*//p')
canonical_brew_line=$(grep -nF -- '--brew-script /home/linuxbrew/.linuxbrew/bin/brew' <<<"$homebrew_guest_step" | cut -d: -f1)
alias_brew_line=$(grep -nF -- '--brew-script /usr/bin/brew' <<<"$homebrew_guest_step" | cut -d: -f1)
guest_contract_line=$(grep -nF 'homebrew_bootstrap_guest_contract_node.ts' <<<"$homebrew_guest_step" | cut -d: -f1)
if [ "$first_host_dist_clear_line" -ge "$canonical_brew_line" ] || \
[ "$canonical_brew_line" -ge "$second_host_dist_clear_line" ] || \
[ "$second_host_dist_clear_line" -ge "$alias_brew_line" ]; then
[ "$second_host_dist_clear_line" -ge "$alias_brew_line" ] || \
[ "$alias_brew_line" -ge "$third_host_dist_clear_line" ] || \
[ "$third_host_dist_clear_line" -ge "$guest_contract_line" ]; then
fail "candidate-backed Homebrew execution must clear host/dist before each ordered entry-point probe"
fi
homebrew_guest_step_line=$(grep -nF -- '- name: Build and prove Homebrew guest bootstrap from candidate artifacts' "$PREPARE" | cut -d: -f1)
pack_workspace_step_line=$(grep -nF -- '- name: Pack prepared test workspace' "$PREPARE" | cut -d: -f1)
if [ "$homebrew_guest_step_line" -ge "$pack_workspace_step_line" ]; then
fail "the exact guest bootstrap must be proved and packed before browser consumers run"
fi
grep -Fq 'target/homebrew-bootstrap/homebrew-bootstrap.vfs' "$PACK_WORKSPACE_SCRIPT" || \
fail "the prepared test workspace must carry the exact Homebrew bootstrap into browser CI"
grep -Fq 'current merge-gate authority changed' "$MARK_READY_SCRIPT" || \
fail "candidate recovery authority replacement must be compare-and-swap"
grep -Fq 'default branch changed after recovery validation' "$MARK_READY_SCRIPT" || \
Expand Down
35 changes: 21 additions & 14 deletions .github/workflows/prepare-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1578,20 +1578,7 @@ jobs:
bash scripts/build-rootfs.sh
'

- name: Pack prepared test workspace
run: |
bash scripts/dev-shell.sh bash scripts/pack-ci-test-workspace.sh \
"$RUNNER_TEMP/test-workspace.tar.zst"

- name: Upload prepared test workspace
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-workspace
path: ${{ runner.temp }}/test-workspace.tar.zst
retention-days: 1
if-no-files-found: error

- name: Prove Homebrew starts from candidate artifacts
- name: Build and prove Homebrew guest bootstrap from candidate artifacts
if: env.PACKAGE_STAGING_REQUIRED == 'true'
run: |
bash scripts/dev-shell.sh bash -c '
Expand All @@ -1614,8 +1601,28 @@ jobs:
--bash binaries/programs/wasm32/bash.wasm \
--brew-script /usr/bin/brew \
--timeout-ms 120000

rm -rf host/dist
host/node_modules/.bin/tsx \
homebrew/test/homebrew_bootstrap_guest_contract_node.ts \
--image target/homebrew-bootstrap/homebrew-bootstrap.vfs \
--bash binaries/programs/wasm32/bash.wasm \
--timeout-ms 300000
'

- name: Pack prepared test workspace
run: |
bash scripts/dev-shell.sh bash scripts/pack-ci-test-workspace.sh \
"$RUNNER_TEMP/test-workspace.tar.zst"

- name: Upload prepared test workspace
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-workspace
path: ${{ runner.temp }}/test-workspace.tar.zst
retention-days: 1
if-no-files-found: error

test-suite:
name: test-suite (${{ matrix.label }})
needs: [synthesize-merge, change-scope, test-gate-prepare]
Expand Down
52 changes: 17 additions & 35 deletions apps/browser-demos/pages/homebrew-vfs-test/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ interface HomebrewVfsAcceptanceRequest {
vfsUrl: string;
executable: string;
argv: string[];
env?: string[];
cwd?: string;
uid?: number;
gid?: number;
lazyUrlBase?: string;
timeoutMs: number;
}

Expand Down Expand Up @@ -106,27 +111,6 @@ interface PackageLayerMachine {
let packageLayerMachine: PackageLayerMachine | null = null;
let packageLayerDiscardedBufferCount = 0;

function readVfsFile(fs: MemoryFileSystem, path: string): Uint8Array {
const stat = fs.stat(path);
const fd = fs.open(path, 0, 0);
try {
const bytes = new Uint8Array(stat.size);
fs.read(fd, bytes, null, bytes.byteLength);
return bytes;
} finally {
fs.close(fd);
}
}

function extractExecutable(image: Uint8Array, path: string): Uint8Array {
const fs = MemoryFileSystem.fromImagePreservingCapacity(image);
try {
return readVfsFile(fs, path);
} finally {
trackTransientImageBuffer(fs.sharedBuffer);
}
}

function appendOutput(current: string, bytes: Uint8Array, label: string): string {
const next = current + new TextDecoder().decode(bytes);
if (new TextEncoder().encode(next).byteLength > MAX_OUTPUT_BYTES) {
Expand Down Expand Up @@ -165,10 +149,6 @@ async function init(): Promise<void> {
ABI_VERSION,
"Homebrew Brewfile VFS image",
);
const executableBytes = extractExecutable(
new Uint8Array(imageBytes),
request.executable,
);
let stdout = "";
let stderr = "";
const kernel = new BrowserKernel({
Expand All @@ -183,18 +163,20 @@ async function init(): Promise<void> {
await kernel.initFromImage({
kernelWasm: kernelBytes,
vfsImage: new Uint8Array(imageBytes),
lazyUrlBase: request.lazyUrlBase,
});
const spawned = await kernel.spawnFromVfs(request.executable, request.argv, {
cwd: request.cwd ?? "/",
env: request.env ?? [
"HOME=/tmp",
"TMPDIR=/tmp",
"PATH=/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin",
],
uid: request.uid,
gid: request.gid,
});
const executable = new Uint8Array(executableBytes.byteLength);
executable.set(executableBytes);
const exitCode = await Promise.race([
kernel.spawn(executable.buffer, request.argv, {
cwd: "/",
env: [
"HOME=/tmp",
"TMPDIR=/tmp",
"PATH=/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin",
],
}),
spawned.exit,
new Promise<never>((_resolve, reject) => {
timer = setTimeout(
() => reject(new Error(`browser acceptance timed out after ${request.timeoutMs}ms`)),
Expand Down
131 changes: 131 additions & 0 deletions apps/browser-demos/test/homebrew-bootstrap-vfs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { createHash } from "node:crypto";
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { expect, test } from "@playwright/test";

import {
createHomebrewBootstrapGuestContractScript,
HOMEBREW_BOOTSTRAP_CONTRACT_MARKER,
HOMEBREW_BOOTSTRAP_GUEST,
HOMEBREW_BOOTSTRAP_GUEST_ENV,
} from "../../../scripts/homebrew-bootstrap-guest-contract";
import { tryResolveBinary } from "../../../host/src/binary-resolver";

interface BootstrapRequest {
vfsUrl: string;
executable: string;
argv: string[];
env?: string[];
cwd?: string;
uid?: number;
gid?: number;
lazyUrlBase?: string;
timeoutMs: number;
}

interface BootstrapResult {
exitCode: number;
stdout: string;
stderr: string;
imageSha256: string;
kernelSha256: string;
}

declare global {
interface Window {
__homebrewVfsTestReady: boolean;
__runHomebrewVfsAcceptance: (
request: BootstrapRequest,
) => Promise<BootstrapResult>;
}
}

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const localImage = resolve(
repoRoot,
"target/homebrew-bootstrap/homebrew-bootstrap.vfs",
);
const publicImage = resolve(
repoRoot,
"apps/browser-demos/public/__kandelo-acceptance/homebrew-bootstrap.vfs",
);
let copiedLocalImage = false;

function sha256(path: string): string {
return createHash("sha256").update(readFileSync(path)).digest("hex");
}

test.beforeAll(() => {
if (process.env.KANDELO_HOMEBREW_BOOTSTRAP_VFS_URL || !existsSync(localImage)) return;
mkdirSync(dirname(publicImage), { recursive: true });
copyFileSync(localImage, publicImage);
copiedLocalImage = true;
});

test.afterAll(() => {
if (copiedLocalImage) rmSync(publicImage, { force: true });
});

test("stock Homebrew bootstrap entrypoints and state work in Chromium", async ({
page,
baseURL,
}) => {
test.setTimeout(600_000);
if (!baseURL) throw new Error("Playwright baseURL is required");

const configuredUrl = process.env.KANDELO_HOMEBREW_BOOTSTRAP_VFS_URL;
const hasLocalImage = existsSync(localImage);
test.skip(!configuredUrl && !hasLocalImage, "Homebrew bootstrap VFS is not available");

const vfsUrl = configuredUrl ??
new URL("/__kandelo-acceptance/homebrew-bootstrap.vfs", baseURL).href;
const imageSha256 = process.env.KANDELO_HOMEBREW_BOOTSTRAP_VFS_SHA256 ??
(hasLocalImage ? sha256(localImage) : undefined);
const kernelPath = tryResolveBinary("kernel.wasm");
const kernelSha256 = process.env.KANDELO_HOMEBREW_BOOTSTRAP_KERNEL_SHA256 ??
(kernelPath ? sha256(kernelPath) : undefined);
if (!imageSha256 || !kernelSha256) {
throw new Error("Homebrew bootstrap image and kernel digests are required");
}

const unexpectedLazyDownloads: string[] = [];
page.on("request", (request) => {
const url = request.url();
if (/\/binaries\/programs\//.test(url)) unexpectedLazyDownloads.push(url);
});

await page.goto(new URL("/pages/homebrew-vfs-test/", baseURL).href);
await expect
.poll(() => page.evaluate(() => window.__homebrewVfsTestReady), {
timeout: 120_000,
})
.toBe(true);

const result = await page.evaluate(
async ({ imageUrl, script, env, guest }) =>
window.__runHomebrewVfsAcceptance({
vfsUrl: imageUrl,
executable: "/usr/bin/bash",
argv: ["/bin/bash", "-c", script],
env,
cwd: guest.cwd,
uid: guest.uid,
gid: guest.gid,
timeoutMs: 300_000,
}),
{
imageUrl: vfsUrl,
script: createHomebrewBootstrapGuestContractScript(),
env: [...HOMEBREW_BOOTSTRAP_GUEST_ENV],
guest: HOMEBREW_BOOTSTRAP_GUEST,
},
);

expect(result.imageSha256).toBe(imageSha256);
expect(result.kernelSha256).toBe(kernelSha256);
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout.split(/\r?\n/)).toContain(HOMEBREW_BOOTSTRAP_CONTRACT_MARKER);
expect(unexpectedLazyDownloads).toEqual([]);
});
5 changes: 5 additions & 0 deletions apps/browser-demos/test/homebrew-brewfile-vfs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ interface AcceptanceRequest {
vfsUrl: string;
executable: string;
argv: string[];
env?: string[];
cwd?: string;
uid?: number;
gid?: number;
lazyUrlBase?: string;
timeoutMs: number;
}

Expand Down
5 changes: 5 additions & 0 deletions apps/browser-demos/test/homebrew-language-runtime-vfs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ interface RuntimeRequest {
vfsUrl: string;
executable: string;
argv: string[];
env?: string[];
cwd?: string;
uid?: number;
gid?: number;
lazyUrlBase?: string;
timeoutMs: number;
}

Expand Down
Loading
Loading