Skip to content
Open
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
13 changes: 13 additions & 0 deletions docs/porting-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ headers from an arbitrary host LLVM install; the libcxx package generates and
ships a version-matched header tree with its `libc++.a` and `libc++abi.a`.
See `packages/registry/mariadb/build-mariadb.sh` for a complete example.

**Header-scanning build steps (cpp linemarkers + `-P`)**: Some language
runtimes discover constants at build time by preprocessing a system header and
scanning the output for `# <line> "file"` linemarkers to learn which headers to
grep (e.g. Perl's `ext/Errno/Errno_pm.PL` finds the `E*` errno constants this
way). perl-cross defines `cpp`/`cpprun`/`cppstdin` as `"$cc -E -P"`, and `-P`
*suppresses* linemarkers — so on the wasm cross target the scan discovers zero
headers and silently produces nothing (Errno then dies "No error definitions
found" and `use Errno` fails, even though the constants are plain `#define E*`
in `$WASM_POSIX_SYSROOT/include/bits/errno.h`). When a `.PL`/config step relies
on cpp linemarkers, point it at the sysroot header directly rather than
depending on linemarker discovery. See the `Errno_pm.PL` fallback patch in
`packages/registry/perl/build-perl.sh`.

### Step 3: Test it

```bash
Expand Down
56 changes: 54 additions & 2 deletions packages/registry/perl/build-perl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,58 @@ PYEOF2
-e "s/whichprog readelf READELF readelf || die \"Cannot find readelf\"/whichprog readelf READELF readelf || true/" \
-e "s/whichprog objdump OBJDUMP objdump || die \"Cannot find objdump\"/whichprog objdump OBJDUMP objdump || true/" \
"$SRC_DIR/cnf/configure_tool.sh"

# Point ext/Errno's errno-constant scan at the sysroot errno headers.
# Errno_pm.PL::get_files() discovers which headers define the E* constants
# by preprocessing `#include <errno.h>` and scanning the output for
# `# <line> "file"` linemarkers -- but perl-cross defines cpp/cpprun/
# cppstdin as "$cc -E -P" (cnf/configure_tool.sh) and -P suppresses
# linemarkers, so on the wasm cross target the scan discovers zero headers,
# collects no constants, and Errno_pm.PL dies "No error definitions found".
# Errno.pm is then never generated/staged and `use Errno` fails. The
# constants exist as plain `#define E* <int>` in the sysroot (musl
# arch/generic bits/errno.h); patch get_files() to fall back to the sysroot
# errno headers when linemarker discovery yields nothing.
chmod u+w "$SRC_DIR/ext/Errno/Errno_pm.PL"
python3 - "$SRC_DIR/ext/Errno/Errno_pm.PL" << 'PYEOF3'
import sys

path = sys.argv[1]
with open(path) as f:
content = f.read()

marker = "kd-gtxa: sysroot errno-header fallback"
if marker in content:
print("Errno_pm.PL already patched for kd-gtxa", file=sys.stderr)
sys.exit(0)

old = " return uniq(@file);"
new = """ # kd-gtxa: sysroot errno-header fallback. perl-cross defines
# cpp/cpprun/cppstdin as "$cc -E -P"; -P suppresses the `# <line> "file"`
# linemarkers get_files() scans for, so on the wasm cross target the loop
# above discovers zero headers -> no E* constants collected -> Errno.pm is
# never generated and write_errno_pm() dies "No error definitions found".
# Point the scan at the sysroot errno headers directly (musl ships the
# constants as plain `#define E* <int>` there). Fallback-only: leaves the
# upstream linemarker discovery intact wherever it already works.
if (!@file) {
my $sysroot = $ENV{WASM_POSIX_SYSROOT} || $Config{sysroot} || '';
push @file, grep { -f $_ }
"$sysroot/include/errno.h", "$sysroot/include/bits/errno.h";
}
return uniq(@file);"""

if old not in content:
print("ERROR: Errno_pm.PL anchor 'return uniq(@file);' not found "
"(perl layout changed?) -- refusing to ship perl without Errno",
file=sys.stderr)
sys.exit(1)

content = content.replace(old, new, 1)
with open(path, "w") as f:
f.write(content)
print("Patched get_files() in ext/Errno/Errno_pm.PL (kd-gtxa sysroot errno fallback)")
PYEOF3
fi

cd "$SRC_DIR"
Expand Down Expand Up @@ -631,15 +683,15 @@ PRIVLIB_SRC="$SRC_DIR/lib"
# are still absent -- do not publish a silently-incomplete runtime. Cwd.pm is
# the file whose absence made File::Spec fail in the original report.
missing=""
for f in XSLoader.pm Config.pm File/Spec.pm File/Spec/Unix.pm Cwd.pm; do
for f in XSLoader.pm Config.pm File/Spec.pm File/Spec/Unix.pm Cwd.pm Errno.pm; do
[ -f "$PRIVLIB_SRC/$f" ] || missing="$missing $f"
done
if [ -n "$missing" ]; then
echo "ERROR: required generated runtime files missing from $PRIVLIB_SRC:$missing" >&2
echo " (make -k rc=$ALL_RC) -- see $WORK_DIR/build-all.log" >&2
exit 1
fi
echo "==> Generated runtime files present (XSLoader.pm, Config.pm, File::Spec, Cwd.pm)."
echo "==> Generated runtime files present (XSLoader.pm, Config.pm, File::Spec, Cwd.pm, Errno.pm)."

# `make perl` above linked perl.wasm before any extension was built; with
# `-Uusedl` the `make -k` pass compiles the core XS extensions and relinks
Expand Down
2 changes: 1 addition & 1 deletion packages/registry/perl/build.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
script_path = "packages/registry/perl/build-perl.sh"
repo_url = "https://github.com/brandonpayton/kandelo.git"
commit = "8c53383229fab78f97b098c3207a655159c03041"
revision = 2
revision = 3

[binary]
index_url = "https://github.com/Automattic/kandelo/releases/download/binaries-abi-v{abi}/index.toml"
150 changes: 150 additions & 0 deletions packages/registry/perl/demo/errno-browser-smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* errno-browser-smoke.ts — kd-gtxa Perl `use Errno` smoke in a real browser
* (headless Chromium via Playwright + the browser-demos test-runner page, which
* drives BrowserKernel).
*
* Confirms the fix is host-agnostic: the same perl.wasm + generated Errno.pm
* that pass under the Node kernel host also load under the browser kernel host.
* Errno.pm is pure-perl constants; loading it is an @INC VFS file-read, the
* identical path that loads Config/File::Spec/POSIX under both hosts. Here we
* inject the minimal `use Errno` load-closure (empirically Errno.pm, Config.pm,
* Config_heavy.pl, Exporter.pm, strict.pm, warnings.pm -- all flat in
* perl-src/lib) into a PERL5LIB dir in the browser VFS and run the same
* constant/tie assertions as the Node smoke.
*
* Runs the real thing when the browser asset bundle is present; otherwise
* SKIPS with a reason (exit 0) rather than failing -- the full browser stdlib
* bundle (kernel + dash/coreutils/grep/sed/gencat + perl.vfs) is tracked by
* kd-yuef, the same boundary #821 defers browser/bottle acceptance to.
*
* Usage (needs playwright + a chromium build available):
* npx tsx packages/registry/perl/demo/errno-browser-smoke.ts
*/
import { readFileSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { spawn, type ChildProcess } from "node:child_process";

const scriptDir = dirname(new URL(import.meta.url).pathname);
const repoRoot = resolve(scriptDir, "../../../..");
const BROWSER_DIR = resolve(repoRoot, "apps/browser-demos");
const LIB = resolve(repoRoot, "packages/registry/perl/perl-src/lib");
const VITE_PORT = 5208;

// The empirically-determined `use Errno` load closure (via %INC under Node).
// All flat in perl-src/lib; injected into /plib and reached via PERL5LIB=/plib.
const CLOSURE = [
"Errno.pm", "Config.pm", "Config_heavy.pl",
"Exporter.pm", "strict.pm", "warnings.pm",
];

// Assertions mirror the Node smoke (exact musl values + the %! tie). Constants
// are called via dynamic method dispatch (Errno->$name()) so there is no
// compile-time "called too early to check prototype" warning. In JS
// double-quoted strings $ and @ are literal; only " and perl's \n are escaped.
const PROG = [
"use strict; no warnings 'prototype';",
"require Errno;",
"my @r;",
"push @r, 'use_Errno=' . ($INC{'Errno.pm'} ? 'ok' : 'FAIL');",
"for my $p ([EPERM=>1],[ENOENT=>2],[EACCES=>13],[EINVAL=>22],[EAGAIN=>11]) {",
" my ($n,$v) = @$p; my $g = Errno->can($n) ? Errno->$n() : -1;",
" push @r, \"$n=\" . ($g==$v ? 'ok' : \"FAIL($g)\");",
"}",
"my $c = grep { Errno->can($_) } @Errno::EXPORT_OK;",
"push @r, 'count=' . ($c>=100 ? 'ok' : \"FAIL($c)\");",
"{ local $! = Errno::ENOENT(); push @r, 'tie=' . (($!{ENOENT} && !$!{EACCES}) ? 'ok' : 'FAIL'); }",
"print 'RESULTS=', join(',', @r), \"\\n\";",
"print((grep { $_ !~ /=ok$/ } @r) ? \"PERL_ERRNO_BROWSER_SMOKE_FAIL\\n\" : \"PERL_ERRNO_BROWSER_SMOKE_PASS\\n\");",
].join("\n");

function skip(reason: string): never {
console.log(`PERL_ERRNO_BROWSER_SMOKE_SKIP: ${reason}`);
process.exit(0);
}

function startVite(): Promise<ChildProcess> {
return new Promise((resolvePromise, reject) => {
const proc = spawn(
"npx",
["vite", "--config", resolve(BROWSER_DIR, "vite.config.ts"), "--port", String(VITE_PORT)],
{ cwd: BROWSER_DIR, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } },
);
let started = false;
const timer = setTimeout(() => { if (!started) { proc.kill(); reject(new Error("Vite did not start in 60s")); } }, 60_000);
proc.stdout!.on("data", (d: Buffer) => {
if (!started && d.toString().includes("Local:")) {
started = true; clearTimeout(timer); setTimeout(() => resolvePromise(proc), 500);
}
});
proc.on("exit", (code) => { if (!started) { clearTimeout(timer); reject(new Error(`Vite exited ${code}`)); } });
});
}

async function main() {
const perlWasm = resolve(repoRoot, "packages/registry/perl/bin/perl.wasm");
if (!existsSync(perlWasm)) skip("perl.wasm not built (run build-perl.sh)");
const missing = CLOSURE.filter((f) => !existsSync(resolve(LIB, f)));
if (missing.length) skip(`perl runtime lib missing ${missing.join(",")} (run build-perl.sh)`);

// Playwright + chromium are optional in some environments.
let chromium: typeof import("playwright").chromium;
try {
({ chromium } = await import("playwright"));
} catch {
skip("playwright not installed");
}

const perlBytes = readFileSync(perlWasm);
const dataFiles = CLOSURE.map((f) => ({
path: `/plib/${f}`,
data: Array.from(readFileSync(resolve(LIB, f))),
}));

let vite: ChildProcess | undefined;
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined;
try {
try {
vite = await startVite();
} catch (e) {
skip(`vite dev server unavailable: ${String(e)}`);
}
try {
browser = await chromium.launch();
} catch (e) {
skip(`chromium unavailable: ${String(e)}`);
}
const page = await browser!.newPage();
await page.goto(`http://localhost:${VITE_PORT}/pages/test-runner/`);
try {
await page.waitForFunction(() => (window as any).__testRunnerReady === true, {}, { timeout: 60_000 });
} catch {
skip("test-runner did not initialize (browser asset bundle incomplete: kernel/dash/coreutils/grep/sed/gencat wasm) -- tracked by kd-yuef");
}

const r: any = await page.evaluate(
async ({ bytes, argv, env, files }) => {
const ab = new Uint8Array(bytes).buffer;
return await (window as any).__runTest(ab, argv, 60_000, { env, dataFiles: files });
},
{
bytes: Array.from(perlBytes),
argv: ["perl", "-e", PROG],
env: ["PERL5LIB=/plib", "LC_ALL=C", "HOME=/tmp", "TMPDIR=/tmp"],
files: dataFiles,
},
);

const stdout = (r.stdout || "").trim();
console.log(`exit=${r.exitCode}`);
console.log(stdout);
if (r.stderr) console.log("STDERR:", r.stderr.trim());
const ok = r.exitCode === 0 && stdout.includes("PERL_ERRNO_BROWSER_SMOKE_PASS");
console.log(ok ? "BROWSER_SMOKE_OK" : "BROWSER_SMOKE_FAIL");
process.exit(ok ? 0 : 1);
} finally {
if (browser) await browser.close();
if (vite) vite.kill();
}
}

main().catch((err) => { console.error("Fatal error:", err); process.exit(1); });
108 changes: 108 additions & 0 deletions packages/registry/perl/demo/errno-smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* errno-smoke.ts — kd-gtxa Perl Errno.pm runtime smoke on Kandelo.
*
* Runs the built perl.wasm under the Node kernel host (host-fs passthrough so
* PERL5LIB resolves) and checks that `use Errno` loads and exposes the wasm
* target's errno constants with musl's numeric values.
*
* Guards the reported gap (kd-gtxa): ext/Errno's Errno_pm.PL discovers which
* headers hold the E* #defines by preprocessing `#include <errno.h>` and
* scanning the output for `# <line> "file"` linemarkers. The wasm target's
* cpp config runs the preprocessor with -P, which inhibits linemarkers, so the
* scan found no headers, collected no constants, and Errno_pm.PL died
* "No error definitions found" -> Errno.pm was never generated/staged and
* `use Errno` failed "Can't locate Errno.pm". The constants themselves are
* plain `#define EPERM 1` in the sysroot (musl arch/generic bits/errno.h);
* they were simply never discovered. build-perl.sh now points the scan at the
* sysroot errno headers directly so Errno.pm generates and ships.
*
* Usage:
* bash build.sh && bash packages/registry/perl/build-perl.sh
* npx tsx packages/registry/perl/demo/errno-smoke.ts [PERL5LIB_DIR]
*
* PERL5LIB_DIR defaults to the built privlib staged by build-perl.sh; pass an
* arg (e.g. an unzipped perl-runtime.zip) to smoke the shippable layout.
*/
import { existsSync } from "fs";
import { resolve, dirname } from "path";
import { runCentralizedProgram } from "../../../../host/test/centralized-test-helper";
import { NodePlatformIO } from "../../../../host/src/platform/node";

const scriptDir = dirname(new URL(import.meta.url).pathname);
const repoRoot = resolve(scriptDir, "../../../..");

// Known musl (arch/generic bits/errno.h) values. Asserting exact numbers proves
// Errno.pm carries the *wasm target's* constants, not the build host's.
const EXPECT: Array<[string, number]> = [
["EPERM", 1],
["ENOENT", 2],
["ESRCH", 3],
["EINTR", 4],
["EIO", 5],
["EBADF", 9],
["EAGAIN", 11],
["ENOMEM", 12],
["EACCES", 13],
["EEXIST", 17],
["ENOTDIR", 20],
["EISDIR", 21],
["EINVAL", 22],
];

const checks = EXPECT.map(
([n, v]) =>
`ck('${n}', sub { Errno::${n}() == ${v} or die 'got '.Errno::${n}() });`,
).join("\n");

const PROG = [
"use strict; use warnings;",
// Errno's constant subs are require'd at runtime, so calling them in this
// same -e is 'too early to check prototype' -- a benign parse-time warning.
"no warnings 'prototype';",
"my @res;",
"sub ck { my ($n,$c)=@_; my $r=eval { $c->() }; push @res, $n.'='.((defined $r && !$@)?'ok':'FAIL('.(($@=~/^(.*?)(?: at |\\n)/)?$1:'err').')'); }",
// The reported gap: Errno.pm must exist and load at all.
"ck('use_Errno', sub { require Errno; $INC{'Errno.pm'} or die 'not loaded'; 1 });",
// Exact musl values for a spread of constants.
checks,
// Enough constants present (musl generic ships 134 E*); a truncated scan
// would collect far fewer, so require a healthy count.
"ck('count>=100', sub { my $n=grep { Errno->can($_) } @Errno::EXPORT_OK; $n>=100 or die \"only $n\" });",
// The tied %! interface (Errno's headline feature) must reflect $!.
"ck('errno_tie', sub { local $! = Errno::ENOENT(); $!{ENOENT} or die; !$!{EACCES} or die 'EACCES leaked'; 1 });",
"print 'PERLVER=',$],\"\\n\";",
"print 'ERRNO_COUNT=',scalar(grep { Errno->can($_) } @Errno::EXPORT_OK),\"\\n\";",
"print 'RESULTS=',join(',',@res),\"\\n\";",
"print((grep { !/=ok$/ } @res) ? \"PERL_ERRNO_SMOKE_FAIL\\n\" : \"PERL_ERRNO_SMOKE_PASS\\n\");",
].join("\n");

async function main() {
const perlWasm = resolve(repoRoot, "packages/registry/perl/bin/perl.wasm");
const perl5lib = process.argv[2] ||
resolve(repoRoot, "packages/registry/perl/perl-src/lib");
if (!existsSync(perlWasm)) {
console.error("perl.wasm not found. Run: bash packages/registry/perl/build-perl.sh");
process.exit(1);
}

const result = await runCentralizedProgram({
programPath: perlWasm,
argv: ["perl", "-e", PROG],
// LC_ALL=C: perl 5.40 panics at startup parsing the composite default
// locale Kandelo's musl setlocale returns ('C.UTF-8;C;C;C;C;C') -- a
// separate platform boundary (kd-dvph), not this package's gap.
env: [`PERL5LIB=${perl5lib}`, `LC_ALL=C`, `HOME=/tmp`, `TMPDIR=/tmp`],
io: new NodePlatformIO(),
timeout: 300_000,
});

process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
const ok = result.exitCode === 0 && result.stdout.includes("PERL_ERRNO_SMOKE_PASS");
process.exit(ok ? 0 : (result.exitCode || 1));
}

main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});
Loading