mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Host the runtime's event loop on Node's libuv once Node loads (NI-3) (#1696)
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
name: libnode development pack (Linux x64, macOS)
|
||||
description: >-
|
||||
Provide a libnode pack for the Node island tests and export
|
||||
SCRIPTC_LIBNODE: restored from the cache, or on a miss assembled from the
|
||||
pinned, checksum-verified inputs in packages/libnode-common/dev-pack.json
|
||||
for the runner's platform (a prebuilt libnode and Node's headers; the
|
||||
bridge builds in seconds). Temporary until the NI-13 packs exist. Fails
|
||||
the job if no pack results: the island tests must never skip in CI.
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Name the runner's platform
|
||||
id: platform
|
||||
shell: bash
|
||||
run: node -p '`name=${process.platform}-${process.arch}`' >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/cache@v4
|
||||
id: libnode
|
||||
with:
|
||||
path: ${{ runner.temp }}/libnode-dev-pack
|
||||
key: libnode-dev-pack-${{ steps.platform.outputs.name }}-${{ hashFiles('packages/libnode-common/dev-pack.json', 'packages/libnode-common/build-pack.mjs', 'packages/libnode-common/fetch-dev-pack.mjs', 'packages/libnode-common/bridge/**', 'packages/runtime/src/scr_node_bridge.h') }}
|
||||
- name: Assemble the libnode pack
|
||||
if: steps.libnode.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: node packages/libnode-common/fetch-dev-pack.mjs --out "$RUNNER_TEMP/libnode-dev-pack"
|
||||
- name: Export SCRIPTC_LIBNODE
|
||||
shell: bash
|
||||
run: |
|
||||
case "$RUNNER_OS" in macOS) bridge=libscr_node_bridge.dylib ;; *) bridge=libscr_node_bridge.so ;; esac
|
||||
test -f "$RUNNER_TEMP/libnode-dev-pack/manifest.json"
|
||||
test -f "$RUNNER_TEMP/libnode-dev-pack/$bridge"
|
||||
echo "SCRIPTC_LIBNODE=$RUNNER_TEMP/libnode-dev-pack" >> "$GITHUB_ENV"
|
||||
@@ -0,0 +1,64 @@
|
||||
name: libnode pack (Linux x64, macOS)
|
||||
description: >-
|
||||
Provide the runner's libnode pack for the Node island tests and export
|
||||
SCRIPTC_LIBNODE. Restores the libnode build that the libnode-packs
|
||||
workflow cached and assembles @scriptc/libnode-<platform> from it (the
|
||||
bridge compiles in seconds; Node is never built here). Until that cache
|
||||
exists for this ref, it falls back to the pinned, checksum-verified
|
||||
development pack (packages/libnode-common/dev-pack.json). Fails the job if
|
||||
no pack results: the island tests must never skip in CI.
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Name the runner's pack
|
||||
id: target
|
||||
shell: bash
|
||||
run: |
|
||||
case "$RUNNER_OS-$RUNNER_ARCH" in
|
||||
Linux-X64) platform=linux-x64-gnu toolchain=manylinux_2_28-gcc-toolset-14 bridge=libscr_node_bridge.so ;;
|
||||
macOS-ARM64) platform=darwin-arm64 toolchain=macos-15-xcode-clang bridge=libscr_node_bridge.dylib ;;
|
||||
macOS-X64) platform=darwin-x64 toolchain=macos-15-xcode-clang bridge=libscr_node_bridge.dylib ;;
|
||||
*) echo "::error::no libnode pack for $RUNNER_OS $RUNNER_ARCH"; exit 1 ;;
|
||||
esac
|
||||
printf 'platform=%s\ntoolchain=%s\nbridge=%s\n' "$platform" "$toolchain" "$bridge" >> "$GITHUB_OUTPUT"
|
||||
- name: Compute the libnode cache key
|
||||
id: key
|
||||
shell: bash
|
||||
env:
|
||||
SCRIPTC_LIBNODE_TOOLCHAIN: ${{ steps.target.outputs.toolchain }}
|
||||
run: node packages/libnode-common/scripts/cache-key.mjs --platform ${{ steps.target.outputs.platform }} >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/cache/restore@v4
|
||||
id: libnode
|
||||
with:
|
||||
path: .libnode-cache/${{ steps.key.outputs.path }}
|
||||
key: ${{ steps.key.outputs.key }}
|
||||
- name: Assemble @scriptc/libnode-${{ steps.target.outputs.platform }}
|
||||
if: steps.libnode.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
SCRIPTC_LIBNODE_TOOLCHAIN: ${{ steps.target.outputs.toolchain }}
|
||||
SCRIPTC_LIBNODE_CACHE: ${{ github.workspace }}/.libnode-cache
|
||||
run: |
|
||||
node packages/libnode-${{ steps.target.outputs.platform }}/scripts/build.mjs --from-cache
|
||||
node packages/libnode-common/scripts/verify.mjs packages/libnode-${{ steps.target.outputs.platform }}
|
||||
echo "SCRIPTC_LIBNODE=$GITHUB_WORKSPACE/packages/libnode-${{ steps.target.outputs.platform }}" >> "$GITHUB_ENV"
|
||||
- uses: actions/cache@v4
|
||||
if: steps.libnode.outputs.cache-hit != 'true'
|
||||
id: devpack
|
||||
with:
|
||||
path: ${{ runner.temp }}/libnode-dev-pack
|
||||
key: libnode-dev-pack-${{ steps.target.outputs.platform }}-${{ hashFiles('packages/libnode-common/dev-pack.json', 'packages/libnode-common/build-pack.mjs', 'packages/libnode-common/fetch-dev-pack.mjs', 'packages/libnode-common/bridge/**', 'packages/runtime/src/scr_node_bridge.h') }}
|
||||
- name: Assemble the development pack
|
||||
if: steps.libnode.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::notice::no cached libnode-packs build for ${{ steps.key.outputs.key }}; using the development pack"
|
||||
if [ "${{ steps.devpack.outputs.cache-hit }}" != true ]; then
|
||||
node packages/libnode-common/fetch-dev-pack.mjs --out "$RUNNER_TEMP/libnode-dev-pack"
|
||||
fi
|
||||
echo "SCRIPTC_LIBNODE=$RUNNER_TEMP/libnode-dev-pack" >> "$GITHUB_ENV"
|
||||
- name: Check the pack
|
||||
shell: bash
|
||||
run: |
|
||||
test -f "$SCRIPTC_LIBNODE/manifest.json"
|
||||
test -f "$SCRIPTC_LIBNODE/${{ steps.target.outputs.bridge }}"
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
# Node island tests (`// @requires-libnode` corpus programs and
|
||||
# node-island.test.ts) load Node from a libnode pack, and fail rather
|
||||
# than skip in this lane when none is configured.
|
||||
- uses: ./.github/actions/libnode-dev-pack
|
||||
- uses: ./.github/actions/libnode
|
||||
# Ubuntu pipes core dumps to apport, and RLIMIT_CORE does not limit a
|
||||
# piped dump. The kernel then streams every page of the runtime's
|
||||
# reserved allocator address space through the pipe, so each expected
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
- run: pnpm build
|
||||
# Node island tests load Node from a libnode pack, and fail rather
|
||||
# than skip on macOS CI when none is configured.
|
||||
- uses: ./.github/actions/libnode-dev-pack
|
||||
- uses: ./.github/actions/libnode
|
||||
- name: Tests (${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && 'darwin suite' || 'corpus and integration suites' }})
|
||||
run: node scripts/ci-test.mjs
|
||||
|
||||
@@ -707,7 +707,7 @@ jobs:
|
||||
- run: pnpm build
|
||||
# Node island tests load Node from a libnode pack, and fail rather
|
||||
# than skip on macOS CI when none is configured.
|
||||
- uses: ./.github/actions/libnode-dev-pack
|
||||
- uses: ./.github/actions/libnode
|
||||
- name: Node island harness
|
||||
if: matrix.shard == 1
|
||||
run: pnpm test tests/harness/node-island packages/libnode-common
|
||||
@@ -808,7 +808,7 @@ jobs:
|
||||
- run: pnpm build
|
||||
# Node island tests load Node from a libnode pack, and fail rather
|
||||
# than skip on macOS CI when none is configured.
|
||||
- uses: ./.github/actions/libnode-dev-pack
|
||||
- uses: ./.github/actions/libnode
|
||||
- name: Node islands (Intel)
|
||||
run: >-
|
||||
pnpm test tests/harness/node-island packages/libnode-common
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
name: libnode packs
|
||||
|
||||
# Builds the @scriptc/libnode-<platform> packs: the pinned Node.js release
|
||||
# as a shared library plus the scriptc bridge (packages/libnode-common).
|
||||
# A Node build takes about half an hour on 8 cores, so this workflow is
|
||||
# never part of the ordinary build or test path. It runs only when the pin,
|
||||
# the bridge or the pack packages change, or by hand, and it caches each
|
||||
# platform's libnode build by (Node release, configure flags, platform,
|
||||
# toolchain): a bridge-only change restores libnode from the cache and
|
||||
# rebuilds just the bridge in seconds.
|
||||
#
|
||||
# Linux packs build in the manylinux_2_28 images (AlmaLinux 8, glibc 2.28,
|
||||
# gcc-toolset-14), the glibc floor of Node's official Linux builds, on
|
||||
# native runners: Node disables its startup snapshot when cross-compiling.
|
||||
# Every pack is smoke-tested by a C program that loads it the way the
|
||||
# scriptc runtime does (packages/libnode-common/smoke/smoke.c).
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "packages/libnode-*/**"
|
||||
- "packages/runtime/src/scr_node_bridge.h"
|
||||
- ".github/workflows/libnode-packs.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/libnode-*/**"
|
||||
- "packages/runtime/src/scr_node_bridge.h"
|
||||
- ".github/workflows/libnode-packs.yml"
|
||||
|
||||
concurrency:
|
||||
# A fixed name: under workflow_call, github.workflow is the caller's, and
|
||||
# sharing the caller's group would cancel the run as a deadlock.
|
||||
group: libnode-packs-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build libnode pack (${{ matrix.platform }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux-x64-gnu
|
||||
runner: ubuntu-24.04
|
||||
image: quay.io/pypa/manylinux_2_28_x86_64
|
||||
toolchain: manylinux_2_28-gcc-toolset-14
|
||||
- platform: linux-arm64-gnu
|
||||
runner: ubuntu-24.04-arm
|
||||
image: quay.io/pypa/manylinux_2_28_aarch64
|
||||
toolchain: manylinux_2_28-gcc-toolset-14
|
||||
- platform: darwin-arm64
|
||||
runner: macos-15
|
||||
toolchain: macos-15-xcode-clang
|
||||
- platform: darwin-x64
|
||||
runner: macos-15-intel
|
||||
toolchain: macos-15-xcode-clang
|
||||
runs-on: ${{ matrix.runner }}
|
||||
# A cold Linux build is ~30 minutes on 8 cores; the 3- and 4-core macOS
|
||||
# runners are expected to take well over an hour. A cache hit is minutes.
|
||||
timeout-minutes: 240
|
||||
env:
|
||||
SCRIPTC_LIBNODE_TOOLCHAIN: ${{ matrix.toolchain }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Compute the libnode cache key
|
||||
id: key
|
||||
run: node packages/libnode-common/scripts/cache-key.mjs --platform ${{ matrix.platform }} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore the libnode build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .libnode-cache/${{ steps.key.outputs.path }}
|
||||
key: ${{ steps.key.outputs.key }}
|
||||
|
||||
- name: Build the pack (Linux, manylinux_2_28)
|
||||
if: startsWith(matrix.platform, 'linux')
|
||||
run: |
|
||||
NODE_ROOT=$(dirname "$(dirname "$(command -v node)")")
|
||||
docker run --rm --volume "$GITHUB_WORKSPACE:/work" --volume "$NODE_ROOT:/node:ro" \
|
||||
--env SCRIPTC_LIBNODE_TOOLCHAIN --env SCRIPTC_LIBNODE_CACHE=/work/.libnode-cache \
|
||||
--env CC=gcc --env CXX=g++ \
|
||||
--env PATH=/opt/rh/gcc-toolset-14/root/usr/bin:/opt/python/cp312-cp312/bin:/node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
--workdir /work ${{ matrix.image }} sh -c '
|
||||
set -eu
|
||||
dnf install --assumeyes --quiet zstd
|
||||
pip install --quiet ninja
|
||||
node packages/libnode-${{ matrix.platform }}/scripts/build.mjs --compress
|
||||
node packages/libnode-common/scripts/verify.mjs packages/libnode-${{ matrix.platform }}
|
||||
# The glibc 2.28 floor: load the pack where it was built.
|
||||
node packages/libnode-common/scripts/smoke.mjs --platform ${{ matrix.platform }}
|
||||
chown -R "$(stat -c %u:%g /work)" /work/packages /work/.libnode-cache
|
||||
'
|
||||
|
||||
- name: Build the pack (macOS)
|
||||
if: startsWith(matrix.platform, 'darwin')
|
||||
env:
|
||||
CC: clang
|
||||
CXX: clang++
|
||||
SCRIPTC_LIBNODE_CACHE: ${{ github.workspace }}/.libnode-cache
|
||||
run: |
|
||||
brew install ninja zstd
|
||||
node packages/libnode-${{ matrix.platform }}/scripts/build.mjs --compress
|
||||
node packages/libnode-common/scripts/verify.mjs packages/libnode-${{ matrix.platform }}
|
||||
|
||||
- name: Smoke-test the pack on the runner
|
||||
run: node packages/libnode-common/scripts/smoke.mjs --platform ${{ matrix.platform }}
|
||||
|
||||
- name: Report sizes
|
||||
run: |
|
||||
{
|
||||
echo "### @scriptc/libnode-${{ matrix.platform }} (Node ${{ steps.key.outputs.node }})"
|
||||
echo
|
||||
echo '```'
|
||||
ls -l packages/libnode-${{ matrix.platform }} .libnode-cache/compressed/${{ matrix.platform }}
|
||||
cat .libnode-cache/${{ steps.key.outputs.path }}/build.json
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload the pack
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libnode-${{ matrix.platform }}
|
||||
path: |
|
||||
packages/libnode-${{ matrix.platform }}
|
||||
!packages/libnode-${{ matrix.platform }}/scripts
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload compressed libraries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libnode-${{ matrix.platform }}-zst
|
||||
path: .libnode-cache/compressed/${{ matrix.platform }}
|
||||
retention-days: 30
|
||||
@@ -33,6 +33,12 @@ output/
|
||||
/packages/runtime-*/artifacts/
|
||||
/packages/runtime-*/runtime-pack.json
|
||||
/packages/runtime-*/.runtime-pack-*
|
||||
# libnode packs: built by build:native, never committed
|
||||
/packages/libnode-*/libnode.so.*
|
||||
/packages/libnode-*/libnode.*.dylib
|
||||
/packages/libnode-*/libscr_node_bridge.*
|
||||
/packages/libnode-*/manifest.json
|
||||
/packages/libnode-*/LICENSE
|
||||
!tests/fixtures/npm/node_modules/*/dist/
|
||||
!tests/fixtures/npm/node_modules/@*/*/dist/
|
||||
!tests/fixtures/npm/workspace/*/dist/
|
||||
|
||||
@@ -25,3 +25,19 @@ Each published package must have a GitHub Actions trusted publisher for
|
||||
`release.yml` and the `Release` environment. A missing configuration fails
|
||||
before upload. Re-runs skip package versions already present on npm, so a
|
||||
partially published release can be resumed safely.
|
||||
|
||||
## libnode packs
|
||||
|
||||
The `@scriptc/libnode-<platform>` packs (linux-x64-gnu, linux-arm64-gnu, darwin-arm64, darwin-x64) carry the Node.js shared library that programs with Node islands load in process, plus the scriptc bridge compiled against it. They version in lockstep with the other platform packages (`sync-versions.mjs` stamps them); the Node release they contain is pinned separately in `packages/libnode-common/libnode.json`, which must match `.node-version` and `internal/compatibility/node-v24.json`. Each pack's `manifest.json` records the Node version, `NODE_MODULE_VERSION`, the bridge ABI and the size and SHA-256 of both libraries.
|
||||
|
||||
A Node `--shared` build takes about half an hour on 8 cores, so packs never build in `pnpm -r build`, the test suites or the release job. `.github/workflows/libnode-packs.yml` builds them on matching hosts when the pin, the bridge (`packages/libnode-common/bridge/`, `packages/runtime/src/scr_node_bridge.h`) or the pack packages change, or on manual dispatch. Linux packs build in the `manylinux_2_28` images (glibc 2.28, gcc-toolset-14) on native x64 and arm64 runners; Node disables its startup snapshot when cross-compiling, so arm64 is never cross-built. Each platform's libnode build is cached by Node release, configure flags, platform and toolchain; a bridge-only change restores libnode from the cache and rebuilds only the bridge. Every pack is smoke-tested by `packages/libnode-common/smoke/smoke.c`, which loads it the way the runtime does, and uploaded as the `libnode-<platform>` artifact, with zstd copies of both libraries as `libnode-<platform>-zst`. The Linux x64 and macOS test lanes of `ci.yml` restore the cached build for their platform through `.github/actions/libnode`, assemble the pack (`build.mjs --from-cache`, which never builds Node) and set `SCRIPTC_LIBNODE`, so the Node island tests run; without a cached build they fall back to the pinned development pack (`packages/libnode-common/dev-pack.json`). `packages/libnode-common/build-pack.mjs` assembles a pack from any matching libnode and headers, the bridge-only rebuild path.
|
||||
|
||||
To build one pack by hand on its own platform (it needs Python 3.9+, Ninja, a C++20 compiler and zstd; on Linux, build in a manylinux_2_28 container for a distributable pack):
|
||||
|
||||
```bash
|
||||
pnpm --filter @scriptc/libnode-linux-x64-gnu build:native
|
||||
node packages/libnode-common/scripts/verify.mjs packages/libnode-linux-x64-gnu
|
||||
node packages/libnode-common/scripts/smoke.mjs
|
||||
```
|
||||
|
||||
The packs are `private` until the in-process Node island runtime ships. Publishing them then means removing `private`, configuring their npm trusted publishers, and having the release job download the `libnode-<platform>` artifacts of the workflow run for the release commit (or call the workflow, which restores every platform from its cache) and publish them with the other platform packages.
|
||||
|
||||
@@ -70,6 +70,9 @@ async function tryFastPath(): Promise<number | null> {
|
||||
values["profile-generate"] !== undefined ||
|
||||
values["profile-use"] !== undefined ||
|
||||
values.sparse ||
|
||||
// A --libnode=beside build places the pack and the island modules next
|
||||
// to the executable, which a routed hit does not do.
|
||||
values.libnode === "beside" ||
|
||||
// The full compiler resolves `native` against the build host.
|
||||
values["target-cpu"] !== undefined ||
|
||||
(values["external-types"] ?? []).length > 0 ||
|
||||
@@ -173,7 +176,7 @@ async function tryFastPath(): Promise<number | null> {
|
||||
: startup.executableNativeEnvironmentFingerprint(),
|
||||
nodeVersion: process.version,
|
||||
// An entry of a program that calls into Node.js serves only the same pack.
|
||||
libnodeIdentity: () => startup.libnodeCacheKey(values.libnode),
|
||||
libnodeIdentity: () => startup.libnodeCacheKey(values.libnode, process.env, dirname(input)),
|
||||
});
|
||||
if (hit === null) return null;
|
||||
if (!values["keep-llvm"]) await rm(hit.llvmPath, { force: true });
|
||||
|
||||
@@ -123,6 +123,61 @@ test("bootstrap exact builds use the routed cache and source edits fall through"
|
||||
}
|
||||
}, 120_000);
|
||||
|
||||
test.skipIf(!persistentExecutable)(
|
||||
"bootstrap leaves --libnode=beside builds to the full compiler",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-bootstrap-beside-"));
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "program");
|
||||
const preload = join(dir, "reject-full-compiler.mjs");
|
||||
const env = { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") };
|
||||
delete env.SCRIPTC_NO_CACHE;
|
||||
const build = (args: string[], rejectFullCompiler = false) =>
|
||||
execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
...(rejectFullCompiler ? ["--import", preload] : []),
|
||||
bootstrap,
|
||||
"build",
|
||||
entry,
|
||||
"-o",
|
||||
outPath,
|
||||
...args,
|
||||
],
|
||||
{ env, maxBuffer: 4 * 1024 * 1024 },
|
||||
);
|
||||
try {
|
||||
await Promise.all([
|
||||
writeFile(entry, 'console.log("beside");\n'),
|
||||
writeFile(
|
||||
preload,
|
||||
[
|
||||
"import { registerHooks } from 'node:module';",
|
||||
"registerHooks({ load(url, context, nextLoad) {",
|
||||
" if (url.includes('/packages/compiler/dist/index.js')) throw new Error('compiler graph loaded');",
|
||||
" return nextLoad(url, context);",
|
||||
"}});",
|
||||
"",
|
||||
].join("\n"),
|
||||
),
|
||||
]);
|
||||
for (const args of [[], ["--libnode=beside"]]) {
|
||||
await build(args);
|
||||
await build(args);
|
||||
}
|
||||
await expect(build([], true)).resolves.toBeDefined();
|
||||
// A routed hit would skip placing the pack and the island modules.
|
||||
await expect(build(["--libnode=beside"], true)).rejects.toMatchObject({
|
||||
stderr: expect.stringContaining("compiler graph loaded"),
|
||||
});
|
||||
expect((await execFileAsync(outPath)).stdout).toBe("beside\n");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
test.skipIf(!runtimePackHost)(
|
||||
"bootstrap cache hits reuse LLVM executables",
|
||||
async () => {
|
||||
|
||||
@@ -43,7 +43,9 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
...(features.builtinValues ? { builtinValues: true } : {}),
|
||||
...(features.computedRequire ? { computedRequire: true } : {}),
|
||||
events: features.processEvents,
|
||||
// Island code's signal and exit listeners join the events unit's
|
||||
// registries (scope-B NI-3).
|
||||
events: features.processEvents || features.nodeIsland === true,
|
||||
emitter: features.emitter,
|
||||
symbol: features.symbol,
|
||||
bigint: features.bigint,
|
||||
|
||||
@@ -926,7 +926,7 @@ async function prepareExecutableInput(
|
||||
if (!moduleRuntimeFeatures(mod).nodeIsland) {
|
||||
delete mod.nodeIsland;
|
||||
} else if (outputKind === "exe") {
|
||||
const recorded = libnodeRecord(opts.libnode, opts.outPath);
|
||||
const recorded = libnodeRecord(opts.libnode, opts.outPath, process.env, dirname(entryPath));
|
||||
if (typeof recorded === "string")
|
||||
return { ok: false, diagnostics: [nodeIslandNodeDiag(recorded, entryPath)], sourceTexts };
|
||||
mod.nodeIsland = { ...mod.nodeIsland, ...recorded };
|
||||
@@ -1762,7 +1762,7 @@ async function compileTracked(
|
||||
...(opts.detachedExit === true ? { detachedExit: true as const } : {}),
|
||||
// Calls into Node record the libnode pack; their entries record its
|
||||
// identity (node-islands/libnode.ts) and a hit requires it unchanged.
|
||||
libnodeIdentity: () => libnodeCacheKey(opts.libnode),
|
||||
libnodeIdentity: () => libnodeCacheKey(opts.libnode, process.env, dirname(entryPath)),
|
||||
...(opts.largePages === true ? { largePages: true as const } : {}),
|
||||
...targetCpuField(opts.targetCpu),
|
||||
...(opts.cpuDispatch === true ? { cpuDispatch: true as const } : {}),
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
import { NODE_BRIDGE_ABI, libnodeRecord } from "./libnode.js";
|
||||
import {
|
||||
LIBNODE_PACK_SCHEMA,
|
||||
LibnodePackError,
|
||||
findInstalledLibnodePack,
|
||||
hostLibnodeTarget,
|
||||
libnodePackPackage,
|
||||
parseLibnodeSetting,
|
||||
readLibnodePack,
|
||||
writeLibnodeNotices,
|
||||
} from "./libnode-pack.js";
|
||||
|
||||
const repo = fileURLToPath(new URL("../../../..", import.meta.url));
|
||||
const dirs: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function temp(): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-libnode-pack-"));
|
||||
dirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function entry(bytes: string) {
|
||||
return {
|
||||
size: Buffer.byteLength(bytes),
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
};
|
||||
}
|
||||
|
||||
/** A published-style pack with stand-in libraries; `edit` adjusts the
|
||||
* manifest build-pack.mjs would write. */
|
||||
function fakePack(
|
||||
dir: string,
|
||||
edit: (manifest: Record<string, unknown>) => void = () => {},
|
||||
name = "@scriptc/libnode-linux-x64-gnu",
|
||||
): string {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const libnode = "stand-in libnode";
|
||||
const bridge = "stand-in bridge";
|
||||
writeFileSync(join(dir, "libnode.so.137"), libnode);
|
||||
writeFileSync(join(dir, "libscr_node_bridge.so"), bridge);
|
||||
writeFileSync(join(dir, "LICENSE"), "Node.js is licensed for use as follows:\n\nMIT\n");
|
||||
writeFileSync(
|
||||
join(dir, "package.json"),
|
||||
JSON.stringify({ name, version: compilerReleaseVersion() }),
|
||||
);
|
||||
const manifest: Record<string, unknown> = {
|
||||
schema: LIBNODE_PACK_SCHEMA,
|
||||
nodeVersion: "24.22.0",
|
||||
moduleVersion: 137,
|
||||
bridgeAbi: NODE_BRIDGE_ABI,
|
||||
platform: "linux",
|
||||
arch: "x64",
|
||||
libnode: "libnode.so.137",
|
||||
bridge: "libscr_node_bridge.so",
|
||||
package: name,
|
||||
version: compilerReleaseVersion(),
|
||||
target: "linux-x64-gnu",
|
||||
license: "LICENSE",
|
||||
files: { "libnode.so.137": entry(libnode), "libscr_node_bridge.so": entry(bridge) },
|
||||
};
|
||||
edit(manifest);
|
||||
writeFileSync(join(dir, "manifest.json"), JSON.stringify(manifest));
|
||||
return dir;
|
||||
}
|
||||
|
||||
function thrown(run: () => unknown): LibnodePackError {
|
||||
try {
|
||||
run();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(LibnodePackError);
|
||||
return error as LibnodePackError;
|
||||
}
|
||||
throw new Error("expected a LibnodePackError");
|
||||
}
|
||||
|
||||
describe("the pack packages", () => {
|
||||
test("every libnode platform package is the pack of one native target", () => {
|
||||
const packages = readdirSync(join(repo, "packages"))
|
||||
.filter((name) => name.startsWith("libnode-") && name !== "libnode-common")
|
||||
.map(
|
||||
(name) =>
|
||||
JSON.parse(readFileSync(join(repo, "packages", name, "package.json"), "utf8")).name,
|
||||
)
|
||||
.sort();
|
||||
const targets = ["linux-x64-gnu", "linux-arm64-gnu", "macos-arm64", "macos-x64"];
|
||||
expect(targets.map((target) => libnodePackPackage(target)).sort()).toEqual(packages);
|
||||
expect(libnodePackPackage("linux-x64-musl")).toBeNull();
|
||||
expect(libnodePackPackage("windows-x64-msvc")).toBeNull();
|
||||
expect(libnodePackPackage("wasm32-wasi")).toBeNull();
|
||||
});
|
||||
|
||||
test("host targets map to native target names", () => {
|
||||
expect(hostLibnodeTarget("linux", "x64")).toBe("linux-x64-gnu");
|
||||
expect(hostLibnodeTarget("linux", "arm64")).toBe("linux-arm64-gnu");
|
||||
expect(hostLibnodeTarget("darwin", "arm64")).toBe("macos-arm64");
|
||||
expect(hostLibnodeTarget("darwin", "x64")).toBe("macos-x64");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseLibnodeSetting", () => {
|
||||
test("accepts embedded, beside and absolute directories", () => {
|
||||
expect(parseLibnodeSetting("embedded")).toEqual({ kind: "embedded" });
|
||||
expect(parseLibnodeSetting("beside")).toEqual({ kind: "beside" });
|
||||
expect(parseLibnodeSetting("/opt/scriptc/libnode-24.22.0")).toEqual({
|
||||
kind: "path",
|
||||
path: "/opt/scriptc/libnode-24.22.0",
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects relative paths and other values", () => {
|
||||
expect(parseLibnodeSetting("libnode")).toBe(
|
||||
'must be "embedded", "beside" or an absolute directory, not the relative path "libnode"',
|
||||
);
|
||||
expect(parseLibnodeSetting(true)).toBe('must be "embedded", "beside" or an absolute directory');
|
||||
});
|
||||
});
|
||||
|
||||
describe("finding the installed pack", () => {
|
||||
test("in the nearest node_modules above the project", () => {
|
||||
const root = temp();
|
||||
const pack = fakePack(join(root, "node_modules/@scriptc/libnode-linux-x64-gnu"));
|
||||
const project = join(root, "apps/web/src");
|
||||
mkdirSync(project, { recursive: true });
|
||||
expect(
|
||||
findInstalledLibnodePack({
|
||||
targetName: "linux-x64-gnu",
|
||||
projectDir: project,
|
||||
resolveInstalled: () => null,
|
||||
}),
|
||||
).toBe(pack);
|
||||
});
|
||||
|
||||
test("next to the compiler when the project has none", () => {
|
||||
const pack = fakePack(join(temp(), "libnode-darwin-arm64"));
|
||||
expect(
|
||||
findInstalledLibnodePack({
|
||||
targetName: "macos-arm64",
|
||||
projectDir: temp(),
|
||||
resolveInstalled: (name) => (name === "@scriptc/libnode-darwin-arm64" ? pack : null),
|
||||
}),
|
||||
).toBe(pack);
|
||||
expect(
|
||||
findInstalledLibnodePack({
|
||||
targetName: "macos-arm64",
|
||||
projectDir: temp(),
|
||||
resolveInstalled: () => null,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("targets without packs have none", () => {
|
||||
expect(findInstalledLibnodePack({ targetName: "linux-x64-musl", projectDir: temp() })).toBe(
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("libnodeRecord uses the installed pack when nothing names one", () => {
|
||||
const root = temp();
|
||||
const name = libnodePackPackage(hostLibnodeTarget());
|
||||
if (name === null) return;
|
||||
const pack = fakePack(join(root, "node_modules", name), () => {}, name);
|
||||
expect(libnodeRecord(undefined, join(root, "app"), {}, root)).toEqual({
|
||||
libnode: pack,
|
||||
nodeVersion: "24.22.0",
|
||||
});
|
||||
// SCRIPTC_LIBNODE still wins.
|
||||
const other = fakePack(join(temp(), "other"), (m) => {
|
||||
m["nodeVersion"] = "24.13.1";
|
||||
});
|
||||
expect(libnodeRecord(undefined, join(root, "app"), { SCRIPTC_LIBNODE: other }, root)).toEqual({
|
||||
libnode: other,
|
||||
nodeVersion: "24.13.1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("an installed pack of another scriptc version is refused", () => {
|
||||
const root = temp();
|
||||
const name = libnodePackPackage(hostLibnodeTarget());
|
||||
if (name === null) return;
|
||||
const pack = fakePack(
|
||||
join(root, "node_modules", name),
|
||||
(m) => {
|
||||
m["version"] = "0.0.1";
|
||||
},
|
||||
name,
|
||||
);
|
||||
expect(libnodeRecord(undefined, join(root, "app"), {}, root)).toBe(
|
||||
`the installed libnode pack ${name} at ${pack} is version 0.0.1; this scriptc is ${compilerReleaseVersion()}: install ${name}@${compilerReleaseVersion()}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("an installed pack whose files do not match their digests is refused", () => {
|
||||
const root = temp();
|
||||
const name = libnodePackPackage(hostLibnodeTarget());
|
||||
if (name === null) return;
|
||||
const pack = fakePack(join(root, "node_modules", name), () => {}, name);
|
||||
// Same size, other bytes.
|
||||
writeFileSync(join(pack, "libnode.so.137"), "stand-in libnodX");
|
||||
expect(libnodeRecord(undefined, join(root, "app"), {}, root)).toBe(
|
||||
`libnode.so.137 in the libnode pack at ${pack} does not match its manifest`,
|
||||
);
|
||||
});
|
||||
|
||||
describe("readLibnodePack", () => {
|
||||
test("reads a pack in build-pack.mjs's format and checks its package", () => {
|
||||
const pack = fakePack(join(temp(), "pack"));
|
||||
expect(
|
||||
readLibnodePack(pack, { packageName: "@scriptc/libnode-linux-x64-gnu", verifyHashes: true })
|
||||
.manifest.nodeVersion,
|
||||
).toBe("24.22.0");
|
||||
expect(
|
||||
thrown(() => readLibnodePack(pack, { packageName: "@scriptc/libnode-linux-arm64-gnu" }))
|
||||
.message,
|
||||
).toBe(
|
||||
`the libnode pack at ${pack} is @scriptc/libnode-linux-x64-gnu; this target needs @scriptc/libnode-linux-arm64-gnu`,
|
||||
);
|
||||
});
|
||||
|
||||
test("refuses other manifest schemas and file names outside the pack", () => {
|
||||
const schema = fakePack(join(temp(), "pack"), (m) => {
|
||||
m["schema"] = "scriptc.libnode-pack.v0";
|
||||
});
|
||||
expect(thrown(() => readLibnodePack(schema)).kind).toBe("invalid");
|
||||
for (const name of [
|
||||
"lib/libnode.so.137",
|
||||
"../libnode.so.137",
|
||||
"..",
|
||||
"/etc/libnode.so",
|
||||
"a\\b",
|
||||
"a\0b",
|
||||
]) {
|
||||
const pack = fakePack(join(temp(), "pack"), (m) => {
|
||||
m["libnode"] = name;
|
||||
m["files"] = { ...(m["files"] as object), [name]: entry("stand-in libnode") };
|
||||
});
|
||||
expect(thrown(() => readLibnodePack(pack)).message).toBe(
|
||||
`the libnode pack at ${pack} names a file outside the pack (${JSON.stringify(name)})`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("checks sizes always and hashes on request", () => {
|
||||
const pack = fakePack(join(temp(), "pack"));
|
||||
writeFileSync(join(pack, "libnode.so.137"), "stand-in libnodE");
|
||||
expect(readLibnodePack(pack).root).toBe(pack);
|
||||
expect(thrown(() => readLibnodePack(pack, { verifyHashes: true })).message).toBe(
|
||||
`libnode.so.137 in the libnode pack at ${pack} does not match its manifest`,
|
||||
);
|
||||
writeFileSync(join(pack, "libnode.so.137"), "short");
|
||||
expect(thrown(() => readLibnodePack(pack)).kind).toBe("invalid");
|
||||
});
|
||||
|
||||
test("a directory without a manifest is missing", () => {
|
||||
const dir = temp();
|
||||
expect(thrown(() => readLibnodePack(dir)).message).toBe(
|
||||
`no libnode pack at ${dir} (manifest.json is missing or unreadable)`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("the license notices carry Node's LICENSE", () => {
|
||||
const pack = readLibnodePack(fakePack(join(temp(), "pack")));
|
||||
const out = join(temp(), "server");
|
||||
const path = writeLibnodeNotices(pack, out);
|
||||
expect(path).toBe(`${out}.licenses.txt`);
|
||||
expect(readFileSync(path, "utf8")).toBe(
|
||||
[
|
||||
`server runs part of its code in Node.js 24.22.0, loaded from libnode.so.137 (@scriptc/libnode-linux-x64-gnu@${compilerReleaseVersion()}).`,
|
||||
"Node.js and the components it bundles are licensed as follows. The scriptc bridge,",
|
||||
"libscr_node_bridge.so, is licensed under Apache-2.0.",
|
||||
"",
|
||||
"Node.js is licensed for use as follows:",
|
||||
"",
|
||||
"MIT",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("beside refuses an installed pack whose manifest names a file outside it", () => {
|
||||
const root = temp();
|
||||
const name = libnodePackPackage(hostLibnodeTarget());
|
||||
if (name === null) return;
|
||||
const pack = fakePack(
|
||||
join(root, "node_modules", name),
|
||||
(m) => {
|
||||
m["files"] = { ...(m["files"] as object), "../escaped": entry("payload") };
|
||||
},
|
||||
name,
|
||||
);
|
||||
// The pack's "../escaped" and the file it would replace next to the output.
|
||||
writeFileSync(join(pack, "..", "escaped"), "payload");
|
||||
writeFileSync(join(root, "escaped"), "victim");
|
||||
expect(libnodeRecord("beside", join(root, "app"), {}, root)).toBe(
|
||||
`the libnode pack at ${pack} names a file outside the pack ("../escaped")`,
|
||||
);
|
||||
expect(readFileSync(join(root, "escaped"), "utf8")).toBe("victim");
|
||||
expect(existsSync(join(root, "app.libnode"))).toBe(false);
|
||||
});
|
||||
|
||||
test("readLibnodePack refuses any listed file or license outside the pack", () => {
|
||||
for (const edit of [
|
||||
(m: Record<string, unknown>) => {
|
||||
m["files"] = { ...(m["files"] as object), "../escaped": entry("payload") };
|
||||
},
|
||||
(m: Record<string, unknown>) => {
|
||||
m["files"] = { ...(m["files"] as object), "/etc/passwd": entry("payload") };
|
||||
},
|
||||
(m: Record<string, unknown>) => {
|
||||
m["license"] = "../../secret";
|
||||
},
|
||||
(m: Record<string, unknown>) => {
|
||||
m["license"] = 7;
|
||||
},
|
||||
]) {
|
||||
const pack = fakePack(join(temp(), "pack"), edit);
|
||||
expect(thrown(() => readLibnodePack(pack)).kind).toBe("invalid");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
/* The installed libnode packs (scope-B design §2.11, §2.13, §2.16.3):
|
||||
* `@scriptc/libnode-<platform>`, built only by the libnode-packs workflow
|
||||
* from the pinned Node release (packages/libnode-common) and installed like
|
||||
* the runtime packs. A scriptc build never compiles Node.
|
||||
*
|
||||
* libnode.ts decides which pack a build records: --libnode, else
|
||||
* SCRIPTC_LIBNODE, else the installed pack this module finds: the nearest
|
||||
* `node_modules/@scriptc/libnode-<platform>` walking up from the project
|
||||
* directory, else the one next to the compiler. The manifest format is the
|
||||
* one packages/libnode-common/build-pack.mjs writes (schema, nodeVersion,
|
||||
* moduleVersion, bridgeAbi, platform, arch, libnode, bridge, files); the
|
||||
* published packs add package, version, target, license and build inputs.
|
||||
*
|
||||
* Also here: the `nodeIslands.libnode` setting ("embedded" | "beside" |
|
||||
* <absolute directory>) for the project configuration, and the license
|
||||
* notices a program that redistributes Node.js carries. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
export const LIBNODE_PACK_SCHEMA = "scriptc.libnode-pack.v1";
|
||||
|
||||
/** The pack package for each native target that can host Node islands. */
|
||||
const PACK_PACKAGES: Readonly<Record<string, string>> = {
|
||||
"linux-x64-gnu": "@scriptc/libnode-linux-x64-gnu",
|
||||
"linux-arm64-gnu": "@scriptc/libnode-linux-arm64-gnu",
|
||||
"macos-arm64": "@scriptc/libnode-darwin-arm64",
|
||||
"macos-x64": "@scriptc/libnode-darwin-x64",
|
||||
};
|
||||
|
||||
export type LibnodePlacement =
|
||||
| { kind: "embedded" }
|
||||
| { kind: "beside" }
|
||||
| { kind: "path"; path: string };
|
||||
|
||||
export interface LibnodeFile {
|
||||
size: number;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
export interface LibnodePackManifest {
|
||||
schema: string;
|
||||
nodeVersion: string;
|
||||
moduleVersion: number;
|
||||
bridgeAbi: number;
|
||||
/** process.platform and process.arch of the pack. */
|
||||
platform: string;
|
||||
arch: string;
|
||||
/** File names inside the pack directory. */
|
||||
libnode: string;
|
||||
bridge: string;
|
||||
files: Record<string, LibnodeFile>;
|
||||
/** Published packs only. */
|
||||
package?: string;
|
||||
version?: string;
|
||||
target?: string;
|
||||
license?: string;
|
||||
}
|
||||
|
||||
export interface LibnodePack {
|
||||
root: string;
|
||||
manifest: LibnodePackManifest;
|
||||
}
|
||||
|
||||
export class LibnodePackError extends Error {
|
||||
readonly kind: "missing" | "invalid" | "unsupported";
|
||||
|
||||
constructor(message: string, kind: "missing" | "invalid" | "unsupported") {
|
||||
super(message);
|
||||
this.name = "LibnodePackError";
|
||||
this.kind = kind;
|
||||
}
|
||||
}
|
||||
|
||||
/** The libnode pack package for a native target, or null when Node islands
|
||||
* have no pack there (musl, Windows, WASI and mobile targets). */
|
||||
export function libnodePackPackage(targetName: string): string | null {
|
||||
return PACK_PACKAGES[targetName] ?? null;
|
||||
}
|
||||
|
||||
/** The native target name of the host, as libnodePackPackage takes it. */
|
||||
export function hostLibnodeTarget(
|
||||
platform: string = process.platform,
|
||||
arch: string = process.arch,
|
||||
): string {
|
||||
return platform === "darwin" ? `macos-${arch}` : `${platform}-${arch}-gnu`;
|
||||
}
|
||||
|
||||
/** Validates a `nodeIslands.libnode` value; a string result is the reason
|
||||
* it is invalid. */
|
||||
export function parseLibnodeSetting(value: unknown): LibnodePlacement | string {
|
||||
if (value === "embedded") return { kind: "embedded" };
|
||||
if (value === "beside") return { kind: "beside" };
|
||||
if (typeof value === "string" && isAbsolute(value)) return { kind: "path", path: value };
|
||||
if (typeof value === "string") {
|
||||
return `must be "embedded", "beside" or an absolute directory, not the relative path "${value}"`;
|
||||
}
|
||||
return `must be "embedded", "beside" or an absolute directory`;
|
||||
}
|
||||
|
||||
/** Whether `name` names a file directly in the pack directory. */
|
||||
function packFileName(name: string): boolean {
|
||||
return name !== "" && name !== "." && name !== ".." && !/[/\\\0]/.test(name) && !isAbsolute(name);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isFileEntry(value: unknown): value is LibnodeFile {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value["size"] === "number" &&
|
||||
typeof value["sha256"] === "string" &&
|
||||
/^[0-9a-f]{64}$/.test(value["sha256"])
|
||||
);
|
||||
}
|
||||
|
||||
/** Parses a pack's manifest.json; throws LibnodePackError when it is not a
|
||||
* pack manifest. Bridge ABI and Node version policy stay in libnode.ts. */
|
||||
export function parseLibnodePackManifest(json: unknown, where: string): LibnodePackManifest {
|
||||
const invalid = (detail: string): never => {
|
||||
throw new LibnodePackError(`the libnode pack at ${where} ${detail}`, "invalid");
|
||||
};
|
||||
if (!isRecord(json)) return invalid("has a malformed manifest.json");
|
||||
if (json["schema"] !== LIBNODE_PACK_SCHEMA) {
|
||||
return invalid(
|
||||
`has manifest schema ${String(json["schema"])}, expected ${LIBNODE_PACK_SCHEMA}`,
|
||||
);
|
||||
}
|
||||
for (const field of ["nodeVersion", "platform", "arch", "libnode", "bridge"]) {
|
||||
if (typeof json[field] !== "string" || json[field] === "") {
|
||||
return invalid(`has no ${field} in manifest.json`);
|
||||
}
|
||||
}
|
||||
if (typeof json["moduleVersion"] !== "number" || typeof json["bridgeAbi"] !== "number") {
|
||||
return invalid("has no moduleVersion or bridgeAbi in manifest.json");
|
||||
}
|
||||
const files = json["files"];
|
||||
if (!isRecord(files)) return invalid("lists no files in manifest.json");
|
||||
// Every name the pack lists is a file directly in the pack directory.
|
||||
const license = json["license"];
|
||||
if (license !== undefined && typeof license !== "string") {
|
||||
return invalid("has a malformed license in manifest.json");
|
||||
}
|
||||
for (const name of [...Object.keys(files), json["license"] ?? "LICENSE"] as string[]) {
|
||||
if (!packFileName(name)) {
|
||||
return invalid(`names a file outside the pack (${JSON.stringify(name)})`);
|
||||
}
|
||||
}
|
||||
for (const name of [json["libnode"], json["bridge"]] as string[]) {
|
||||
if (!packFileName(name)) {
|
||||
return invalid(`names a file outside the pack (${JSON.stringify(name)})`);
|
||||
}
|
||||
if (!isFileEntry(files[name])) return invalid(`lists no size and SHA-256 for ${name}`);
|
||||
}
|
||||
return json as unknown as LibnodePackManifest;
|
||||
}
|
||||
|
||||
/** Reads and checks the pack in `root`: its manifest, its package when
|
||||
* `packageName` is given, and that each library exists with the recorded
|
||||
* size (and SHA-256 when `verifyHashes`). */
|
||||
export function readLibnodePack(
|
||||
root: string,
|
||||
options: { packageName?: string | undefined; verifyHashes?: boolean | undefined } = {},
|
||||
): LibnodePack {
|
||||
let json: unknown;
|
||||
try {
|
||||
json = JSON.parse(readFileSync(join(root, "manifest.json"), "utf8"));
|
||||
} catch {
|
||||
throw new LibnodePackError(
|
||||
`no libnode pack at ${root} (manifest.json is missing or unreadable)`,
|
||||
"missing",
|
||||
);
|
||||
}
|
||||
const manifest = parseLibnodePackManifest(json, root);
|
||||
if (options.packageName !== undefined && manifest.package !== options.packageName) {
|
||||
throw new LibnodePackError(
|
||||
`the libnode pack at ${root} is ${manifest.package ?? "unnamed"}; this target needs ${options.packageName}`,
|
||||
"invalid",
|
||||
);
|
||||
}
|
||||
for (const name of [manifest.libnode, manifest.bridge]) {
|
||||
const path = join(root, name);
|
||||
const entry = manifest.files[name]!;
|
||||
let size: number;
|
||||
try {
|
||||
size = statSync(path).size;
|
||||
} catch {
|
||||
throw new LibnodePackError(`the libnode pack at ${root} is missing ${name}`, "invalid");
|
||||
}
|
||||
const hashMismatch =
|
||||
options.verifyHashes === true &&
|
||||
createHash("sha256").update(readFileSync(path)).digest("hex") !== entry.sha256;
|
||||
if (size !== entry.size || hashMismatch) {
|
||||
throw new LibnodePackError(
|
||||
`${name} in the libnode pack at ${root} does not match its manifest`,
|
||||
"invalid",
|
||||
);
|
||||
}
|
||||
}
|
||||
return { root, manifest };
|
||||
}
|
||||
|
||||
/** The nearest `node_modules/<name>` directory with a built pack (a
|
||||
* manifest.json) walking up from `from`. */
|
||||
function findInstalled(name: string, from: string): string | null {
|
||||
let dir = resolve(from);
|
||||
for (;;) {
|
||||
const candidate = join(dir, "node_modules", name);
|
||||
if (existsSync(join(candidate, "manifest.json"))) return candidate;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) return null;
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveNextToCompiler(name: string): string | null {
|
||||
try {
|
||||
const dir = dirname(createRequire(import.meta.url).resolve(`${name}/package.json`));
|
||||
// An unbuilt workspace package is not a pack.
|
||||
return existsSync(join(dir, "manifest.json")) ? dir : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The installed pack directory for `targetName`, or null when none is
|
||||
* installed or the target has no packs. */
|
||||
export function findInstalledLibnodePack(options: {
|
||||
targetName: string;
|
||||
projectDir: string;
|
||||
/** Test hook for the compiler-relative lookup. */
|
||||
resolveInstalled?: ((name: string) => string | null) | undefined;
|
||||
}): string | null {
|
||||
const packageName = libnodePackPackage(options.targetName);
|
||||
if (packageName === null) return null;
|
||||
return (
|
||||
findInstalled(packageName, options.projectDir) ??
|
||||
(options.resolveInstalled ?? resolveNextToCompiler)(packageName)
|
||||
);
|
||||
}
|
||||
|
||||
/** Writes the license notices a program that redistributes Node.js must
|
||||
* carry, `<output>.licenses.txt` (design §2.13), and returns its path. */
|
||||
export function writeLibnodeNotices(pack: LibnodePack, outPath: string): string {
|
||||
const { manifest } = pack;
|
||||
const nodeLicense = readFileSync(join(pack.root, manifest.license ?? "LICENSE"), "utf8");
|
||||
const origin =
|
||||
manifest.package !== undefined && manifest.version !== undefined
|
||||
? ` (${manifest.package}@${manifest.version})`
|
||||
: "";
|
||||
const path = `${outPath}.licenses.txt`;
|
||||
writeFileSync(
|
||||
path,
|
||||
[
|
||||
`${basename(outPath)} runs part of its code in Node.js ${manifest.nodeVersion}, loaded from ${manifest.libnode}${origin}.`,
|
||||
`Node.js and the components it bundles are licensed as follows. The scriptc bridge,`,
|
||||
`${manifest.bridge}, is licensed under Apache-2.0.`,
|
||||
"",
|
||||
nodeLicense.trimEnd(),
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
return path;
|
||||
}
|
||||
@@ -11,7 +11,9 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { USES_TIMERS_LIB_FNS } from "../backend/llvm/lib-shared.js";
|
||||
import { libnodeCacheKey, libnodeRecord, NODE_BRIDGE_ABI } from "./libnode.js";
|
||||
import { libnodeCacheKey, libnodeRecord, libnodeTarget, NODE_BRIDGE_ABI } from "./libnode.js";
|
||||
import { libnodePackPackage } from "./libnode-pack.js";
|
||||
import { findInstalledLibnodePack, hostLibnodeTarget } from "./libnode-pack.js";
|
||||
|
||||
function pack(nodeVersion: string, bridgeAbi = NODE_BRIDGE_ABI): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-libnode-"));
|
||||
@@ -39,7 +41,21 @@ describe("the recorded libnode pack", () => {
|
||||
});
|
||||
|
||||
test("refuses a missing, too old or incompatible pack", () => {
|
||||
expect(libnodeRecord(undefined, "/out/app", {})).toMatch(/no libnode pack is installed/);
|
||||
// Unless a workspace build of the host's @scriptc/libnode-<platform>
|
||||
// resolves next to the compiler, which libnodeRecord then uses.
|
||||
const installed = findInstalledLibnodePack({
|
||||
targetName: hostLibnodeTarget(),
|
||||
projectDir: "/nonexistent",
|
||||
});
|
||||
if (installed === null) {
|
||||
expect(libnodeRecord(undefined, "/out/app", {}, "/nonexistent")).toMatch(
|
||||
/no libnode pack is installed/,
|
||||
);
|
||||
} else {
|
||||
expect(libnodeRecord(undefined, "/out/app", {}, "/nonexistent")).toMatchObject({
|
||||
libnode: installed,
|
||||
});
|
||||
}
|
||||
expect(libnodeRecord("/nonexistent", "/out/app", {})).toBe(
|
||||
"no libnode pack at /nonexistent (manifest.json is missing or unreadable)",
|
||||
);
|
||||
@@ -143,9 +159,36 @@ describe("the recorded libnode pack", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the libnode pack in an executable's cache key", () => {
|
||||
test("is absent without --libnode or SCRIPTC_LIBNODE", () => {
|
||||
expect(libnodeCacheKey(undefined, {})).toBeUndefined();
|
||||
describe("the libnode pack identity a cached build keeps", () => {
|
||||
// A workspace build of the target's @scriptc/libnode-<platform> that
|
||||
// resolves next to the compiler (CI assembles one) is installed for every
|
||||
// project, so without a pack in the project the identity names it.
|
||||
const nextToCompiler = findInstalledLibnodePack({
|
||||
targetName: libnodeTarget({}),
|
||||
projectDir: "/nonexistent",
|
||||
});
|
||||
const withoutProjectPack = (key: string | undefined): void => {
|
||||
if (nextToCompiler === null) expect(key).toBeUndefined();
|
||||
else expect(key).toContain(`|${nextToCompiler}|`);
|
||||
};
|
||||
|
||||
test("is absent when no pack is named or installed", () => {
|
||||
withoutProjectPack(libnodeCacheKey(undefined, {}, "/nonexistent"));
|
||||
});
|
||||
|
||||
test("names the installed pack, and changes with it", () => {
|
||||
const project = mkdtempSync(join(tmpdir(), "scriptc-libnode-project-"));
|
||||
const name = libnodePackPackage(libnodeTarget({}));
|
||||
if (name === null) return;
|
||||
const installed = join(project, "node_modules", name);
|
||||
mkdirSync(installed, { recursive: true });
|
||||
writeFileSync(join(installed, "manifest.json"), JSON.stringify({ nodeVersion: "24.22.0" }));
|
||||
const before = libnodeCacheKey(undefined, {}, project);
|
||||
expect(before).toContain(installed);
|
||||
writeFileSync(join(installed, "manifest.json"), JSON.stringify({ nodeVersion: "24.23.0" }));
|
||||
expect(libnodeCacheKey(undefined, {}, project)).not.toBe(before);
|
||||
rmSync(installed, { recursive: true, force: true });
|
||||
withoutProjectPack(libnodeCacheKey(undefined, {}, project));
|
||||
});
|
||||
|
||||
test("changes when the pack in the same directory is upgraded", () => {
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* SCRIPTC_LIBNODE overrides the recorded directory at run time.
|
||||
*
|
||||
* Where the build finds the pack: `--libnode=<pack directory>`, else
|
||||
* SCRIPTC_LIBNODE in the build's environment (until NI-13 publishes
|
||||
* @scriptc/libnode-<platform> packages that an installation carries).
|
||||
* SCRIPTC_LIBNODE in the build's environment, else the installed
|
||||
* @scriptc/libnode-<platform> pack (libnode-pack.ts).
|
||||
* `--libnode=beside` takes that pack and copies it next to the executable
|
||||
* as `<output>.libnode/`, recorded as `$ORIGIN/<output>.libnode`, so the
|
||||
* executable and its Node travel together; a recorded `$ORIGIN/` path is
|
||||
@@ -24,6 +24,16 @@ import {
|
||||
rmSync,
|
||||
} from "node:fs";
|
||||
import { basename, isAbsolute, join, resolve, sep } from "node:path";
|
||||
import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
import { nativeCodegenTarget } from "../backend/targets.js";
|
||||
import {
|
||||
findInstalledLibnodePack,
|
||||
hostLibnodeTarget,
|
||||
LibnodePackError,
|
||||
libnodePackPackage,
|
||||
readLibnodePack,
|
||||
type LibnodePack,
|
||||
} from "./libnode-pack.js";
|
||||
|
||||
/** The oldest Node.js release Node islands support: synchronous
|
||||
* module.registerHooks, which shared modules need, is a release candidate
|
||||
@@ -31,7 +41,7 @@ import { basename, isAbsolute, join, resolve, sep } from "node:path";
|
||||
export const NODE_ISLAND_MIN_NODE = "24.13.1";
|
||||
|
||||
/** The bridge ABI this compiler's runtime speaks (scr_node_bridge.h). */
|
||||
export const NODE_BRIDGE_ABI = 3;
|
||||
export const NODE_BRIDGE_ABI = 4;
|
||||
|
||||
/** The prefix of a recorded path relative to the executable's directory. */
|
||||
export const ORIGIN = "$ORIGIN/";
|
||||
@@ -131,18 +141,56 @@ function realpathOr(path: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** The native target whose libnode pack a build records: the codegen
|
||||
* target (SCRIPTC_TARGET, and the host's libc), else the host. */
|
||||
export function libnodeTarget(env: NodeJS.ProcessEnv = process.env): string {
|
||||
return nativeCodegenTarget(env)?.name ?? hostLibnodeTarget();
|
||||
}
|
||||
|
||||
/** Why the installed pack in `dir` cannot serve this compiler, or null: it
|
||||
* must be the target's package at the compiler's own version. */
|
||||
function installedPackProblem(dir: string, packageName: string): string | null {
|
||||
let pack: LibnodePack;
|
||||
try {
|
||||
// An installed pack is found, not named: check its files' digests too.
|
||||
pack = readLibnodePack(dir, { packageName, verifyHashes: true });
|
||||
} catch (error) {
|
||||
return error instanceof LibnodePackError ? error.message : String(error);
|
||||
}
|
||||
const version = compilerReleaseVersion();
|
||||
if (pack.manifest.version !== version)
|
||||
return `the installed libnode pack ${packageName} at ${dir} is version ${pack.manifest.version ?? "unknown"}; this scriptc is ${version}: install ${packageName}@${version}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** What to record for a program that calls into Node, or the reason the
|
||||
* build cannot (an SC8005 message). `option` is --libnode. */
|
||||
export function libnodeRecord(
|
||||
option: string | undefined,
|
||||
outPath: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
projectDir: string = process.cwd(),
|
||||
): LibnodeRecord | string {
|
||||
const fromEnv = env["SCRIPTC_LIBNODE"];
|
||||
const beside = option === "beside";
|
||||
const source = option !== undefined && !beside ? option : fromEnv;
|
||||
if (source === undefined || source === "")
|
||||
return "this program calls into Node.js (@scriptc/node), but no libnode pack is installed: set SCRIPTC_LIBNODE or pass --libnode=<pack directory>";
|
||||
const explicit = option !== undefined && !beside ? option : fromEnv;
|
||||
const target = libnodeTarget(env);
|
||||
// Without an explicit pack, the installed @scriptc/libnode-<target>.
|
||||
const installed =
|
||||
explicit !== undefined && explicit !== ""
|
||||
? null
|
||||
: findInstalledLibnodePack({ targetName: target, projectDir });
|
||||
const source = installed ?? explicit;
|
||||
if (source === undefined || source === null || source === "") {
|
||||
const pack = libnodePackPackage(target);
|
||||
return pack === null
|
||||
? `this program calls into Node.js (@scriptc/node), but there is no libnode pack for ${target}: set SCRIPTC_LIBNODE or pass --libnode=<pack directory>`
|
||||
: `this program calls into Node.js (@scriptc/node), but no libnode pack is installed: install ${pack}, set SCRIPTC_LIBNODE or pass --libnode=<pack directory>`;
|
||||
}
|
||||
if (installed !== null) {
|
||||
const problem = installedPackProblem(installed, libnodePackPackage(target)!);
|
||||
if (problem !== null) return problem;
|
||||
}
|
||||
if (source.startsWith(ORIGIN)) {
|
||||
// A pack the deployment places next to the executable: nothing to read.
|
||||
return { libnode: source, nodeVersion: "" };
|
||||
@@ -159,18 +207,23 @@ export function libnodeRecord(
|
||||
|
||||
/** The identity of the libnode pack a build that calls into Node.js
|
||||
* records, which the program's executable cache entry keeps
|
||||
* (EarlyExecutableCacheOptions.libnodeIdentity), or undefined when neither
|
||||
* --libnode nor SCRIPTC_LIBNODE is set. It holds the option, the pack
|
||||
* directory and a digest of the pack's manifest.json, so that a pack
|
||||
* upgraded in place (another Node version, bridge ABI or file list)
|
||||
* invalidates the entry instead of restoring its old record. */
|
||||
* (EarlyExecutableCacheOptions.libnodeIdentity): the option, the pack
|
||||
* directory (named by --libnode or SCRIPTC_LIBNODE, or the installed pack
|
||||
* found from `projectDir`), and a digest of the pack's manifest.json. A pack
|
||||
* upgraded in place, removed, or replaced by another installed pack changes
|
||||
* it. Undefined when no pack is named or installed. */
|
||||
export function libnodeCacheKey(
|
||||
option: string | undefined,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
projectDir: string = process.cwd(),
|
||||
): string | undefined {
|
||||
const fromEnv = env["SCRIPTC_LIBNODE"];
|
||||
if (option === undefined && fromEnv === undefined) return undefined;
|
||||
const source = option !== undefined && option !== "beside" ? option : (fromEnv ?? "");
|
||||
const explicit = option !== undefined && option !== "beside" ? option : fromEnv;
|
||||
const source =
|
||||
explicit !== undefined && explicit !== ""
|
||||
? explicit
|
||||
: findInstalledLibnodePack({ targetName: libnodeTarget(env), projectDir });
|
||||
if (source === undefined || source === null) return option === undefined ? undefined : option;
|
||||
let manifest = "none";
|
||||
if (isAbsolute(source)) {
|
||||
try {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6804-node-island-signals/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6805-node-island-exit-code/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6806-node-island-exit-in-call/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6807-node-island-exit-in-island-timer/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6808-node-island-exit-from-program/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6809-node-island-stdio/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6810-node-island-children/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6811-node-island-uncaught-listener/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6822-node-island-gc/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6823-node-island-nested-stack/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6890-node-island-timer-refresh/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -9,8 +9,17 @@
|
||||
// no default signal handling, no debug-signal handler or inspector, no
|
||||
// stdio reinitialization, one platform worker thread. Node's process exit
|
||||
// (process.exit, an uncaught exception) goes to the runtime's exit
|
||||
// sequence. Calls run in a CallbackScope with a nested TryCatch, so ticks
|
||||
// and microtasks drain when the scope closes, as after any libuv callback.
|
||||
// sequence.
|
||||
//
|
||||
// One loop (scope-B NI-3). Once the runtime calls host_loop, its queues
|
||||
// live on Node's: its microtasks are V8 microtasks, its ticks, timers and
|
||||
// immediates are Node's (scheduled through the bootstrap's helpers), and
|
||||
// its I/O is watched from Node's libuv loop (uv_poll handles over the fds
|
||||
// the runtime reports, one prepare handle whose ref is the runtime's
|
||||
// liveness, an idle handle while runtime work is ready without any fd).
|
||||
// Calls into JavaScript therefore do not drain queues themselves: the
|
||||
// runtime's checkpoints do, through `checkpoint`, and Node's own after
|
||||
// every callback it dispatches.
|
||||
#include "scr_node_bridge.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
@@ -20,10 +29,12 @@
|
||||
#include <uv.h>
|
||||
#include <v8.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -49,7 +60,10 @@ node::EmbedderSnapshotData::Pointer g_snapshot;
|
||||
std::unique_ptr<node::CommonEnvironmentSetup> g_setup;
|
||||
Locker *g_locker = nullptr;
|
||||
Global<Function> g_call;
|
||||
Global<Function> g_finish;
|
||||
// The bootstrap's hosted-loop helpers (scr_node_island_host.cjs).
|
||||
Global<Function> g_set_timer, g_clear_timer, g_ref_timer, g_refresh_timer;
|
||||
Global<Function> g_set_immediate, g_clear_immediate, g_ref_immediate;
|
||||
Global<Function> g_next_tick, g_emit;
|
||||
|
||||
char *Copy(const std::string &text, size_t *len) {
|
||||
char *out = static_cast<char *>(std::malloc(text.size() + 1));
|
||||
@@ -60,6 +74,29 @@ char *Copy(const std::string &text, size_t *len) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// V8's stack limit follows the stack the runtime calls in from: its main
|
||||
// thread's, or an async fiber's (a few hundred KiB, mapped elsewhere), so
|
||||
// JavaScript entered from a fiber neither trips a false stack overflow nor
|
||||
// overruns the fiber. Restored when the entry returns.
|
||||
uintptr_t g_stack_limit = 0;
|
||||
|
||||
struct StackLimit {
|
||||
uintptr_t previous;
|
||||
explicit StackLimit(Isolate *isolate) : previous(g_stack_limit) {
|
||||
uintptr_t want = g_host->stack_limit();
|
||||
if (want != 0 && want != g_stack_limit) {
|
||||
isolate->SetStackLimit(want);
|
||||
g_stack_limit = want;
|
||||
}
|
||||
}
|
||||
~StackLimit() {
|
||||
if (previous != 0 && previous != g_stack_limit) {
|
||||
g_setup->isolate()->SetStackLimit(previous);
|
||||
g_stack_limit = previous;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The start of the runtime's stack when the caller runs on it (see the
|
||||
// v8::base::Stack definitions at the end of this file), else 0.
|
||||
uintptr_t HostStackStart() {
|
||||
@@ -114,6 +151,39 @@ void Settle(const FunctionCallbackInfo<Value> &info) {
|
||||
g_host->settle(id, outcome.data(), outcome.size());
|
||||
}
|
||||
|
||||
uint32_t Uint32Arg(const FunctionCallbackInfo<Value> &info, int i) {
|
||||
return info[i]->Uint32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(0);
|
||||
}
|
||||
|
||||
// run(kind, token): a runtime job Node's queues dispatched.
|
||||
void Run(const FunctionCallbackInfo<Value> &info) {
|
||||
g_host->run(Uint32Arg(info, 0), Uint32Arg(info, 1));
|
||||
}
|
||||
|
||||
// listen(event, name, listener, add, once): island process listeners.
|
||||
void Listen(const FunctionCallbackInfo<Value> &info) {
|
||||
std::string name = ToStdString(info.GetIsolate(), info[1]);
|
||||
g_host->listen(Uint32Arg(info, 0), name.c_str(), Uint32Arg(info, 2), Uint32Arg(info, 3) != 0,
|
||||
Uint32Arg(info, 4) != 0);
|
||||
}
|
||||
|
||||
void SetMethod(Local<Context> context, Local<Object> target, const char *name,
|
||||
v8::FunctionCallback callback) {
|
||||
target
|
||||
->Set(context, Utf8(context->GetIsolate(), name),
|
||||
Function::New(context, callback).ToLocalChecked())
|
||||
.Check();
|
||||
}
|
||||
|
||||
bool TakeFunction(Isolate *isolate, Local<Context> context, Local<Object> from, const char *name,
|
||||
Global<Function> *to) {
|
||||
Local<Value> value;
|
||||
if (!from->Get(context, Utf8(isolate, name)).ToLocal(&value) || !value->IsFunction())
|
||||
return false;
|
||||
to->Reset(isolate, value.As<Function>());
|
||||
return true;
|
||||
}
|
||||
|
||||
char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *argv0,
|
||||
uintptr_t stack_limit) {
|
||||
if (g_setup) return nullptr;
|
||||
@@ -167,8 +237,10 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
|
||||
g_locker = new Locker(isolate);
|
||||
isolate->Enter();
|
||||
// The runtime calls into Node on a dedicated stack of its own
|
||||
// (scr_node_island.c), not the thread's: V8's limit follows it.
|
||||
// (scr_node_island.c), not the thread's: V8's limit follows it, and
|
||||
// follows any other stack an entry comes from (StackLimit).
|
||||
if (stack_limit != 0) isolate->SetStackLimit(stack_limit);
|
||||
g_stack_limit = stack_limit;
|
||||
node::SetProcessExitHandler(g_setup->env(),
|
||||
[](node::Environment *, int code) { g_host->exit(code); });
|
||||
HandleScope handles(isolate);
|
||||
@@ -176,9 +248,9 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
|
||||
Context::Scope context_scope(context);
|
||||
Local<Object> global = context->Global();
|
||||
Local<Object> native = Object::New(isolate);
|
||||
native
|
||||
->Set(context, Utf8(isolate, "settle"), Function::New(context, Settle).ToLocalChecked())
|
||||
.Check();
|
||||
SetMethod(context, native, "settle", Settle);
|
||||
SetMethod(context, native, "run", Run);
|
||||
SetMethod(context, native, "listen", Listen);
|
||||
global->Set(context, Utf8(isolate, "__scriptcNodeIslandHost"), native).Check();
|
||||
TryCatch try_catch(isolate);
|
||||
if (node::LoadEnvironment(g_setup->env(), bootstrap).IsEmpty()) {
|
||||
@@ -197,17 +269,21 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
|
||||
return Copy(message, nullptr);
|
||||
}
|
||||
Local<Value> dispatcher;
|
||||
Local<Value> call;
|
||||
Local<Value> finish;
|
||||
if (!global->Get(context, Utf8(isolate, "__scriptcNodeIsland")).ToLocal(&dispatcher) ||
|
||||
!dispatcher->IsObject() ||
|
||||
!dispatcher.As<Object>()->Get(context, Utf8(isolate, "call")).ToLocal(&call) ||
|
||||
!call->IsFunction() ||
|
||||
!dispatcher.As<Object>()->Get(context, Utf8(isolate, "finish")).ToLocal(&finish) ||
|
||||
!finish->IsFunction())
|
||||
!dispatcher->IsObject())
|
||||
return Copy("the Node island bootstrap left no dispatcher", nullptr);
|
||||
g_call.Reset(isolate, call.As<Function>());
|
||||
g_finish.Reset(isolate, finish.As<Function>());
|
||||
Local<Object> d = dispatcher.As<Object>();
|
||||
if (!TakeFunction(isolate, context, d, "call", &g_call) ||
|
||||
!TakeFunction(isolate, context, d, "setTimer", &g_set_timer) ||
|
||||
!TakeFunction(isolate, context, d, "clearTimer", &g_clear_timer) ||
|
||||
!TakeFunction(isolate, context, d, "refTimer", &g_ref_timer) ||
|
||||
!TakeFunction(isolate, context, d, "refreshTimer", &g_refresh_timer) ||
|
||||
!TakeFunction(isolate, context, d, "setImmediate", &g_set_immediate) ||
|
||||
!TakeFunction(isolate, context, d, "clearImmediate", &g_clear_immediate) ||
|
||||
!TakeFunction(isolate, context, d, "refImmediate", &g_ref_immediate) ||
|
||||
!TakeFunction(isolate, context, d, "nextTick", &g_next_tick) ||
|
||||
!TakeFunction(isolate, context, d, "emit", &g_emit))
|
||||
return Copy("the Node island bootstrap left an incomplete dispatcher", nullptr);
|
||||
global->Delete(context, Utf8(isolate, "__scriptcNodeIsland")).FromMaybe(false);
|
||||
global->Delete(context, Utf8(isolate, "__scriptcNodeIslandHost")).FromMaybe(false);
|
||||
return nullptr;
|
||||
@@ -216,12 +292,14 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
|
||||
char *Call(uint32_t id, int sync, const char *file, const char *specifier, const char *name,
|
||||
const char *args, size_t args_len, size_t *len) {
|
||||
Isolate *isolate = g_setup->isolate();
|
||||
StackLimit limit(isolate);
|
||||
HandleScope handles(isolate);
|
||||
Local<Context> context = g_setup->context();
|
||||
Context::Scope context_scope(context);
|
||||
std::string outcome;
|
||||
{
|
||||
node::CallbackScope scope(isolate, context->Global(), node::async_context{0, 0});
|
||||
// No callback scope: the runtime's checkpoint drains what the call
|
||||
// queued, at the end of the runtime callback that made it (NI-3).
|
||||
TryCatch try_catch(isolate);
|
||||
Local<String> args_text;
|
||||
if (!MaybeUtf8(isolate, args, args_len).ToLocal(&args_text)) {
|
||||
@@ -260,43 +338,286 @@ char *Call(uint32_t id, int sync, const char *file, const char *specifier, const
|
||||
|
||||
void Release(void *memory) { std::free(memory); }
|
||||
|
||||
int BackendFd() { return uv_backend_fd(g_setup->event_loop()); }
|
||||
// ── the hosted loop ──────────────────────────────────────────────────────
|
||||
|
||||
int BackendTimeout() { return uv_backend_timeout(g_setup->event_loop()); }
|
||||
struct Enter {
|
||||
Isolate *isolate;
|
||||
StackLimit limit;
|
||||
HandleScope handles;
|
||||
Local<Context> context;
|
||||
Context::Scope scope;
|
||||
Enter()
|
||||
: isolate(g_setup->isolate()), limit(isolate), handles(isolate),
|
||||
context(g_setup->context()), scope(context) {}
|
||||
};
|
||||
|
||||
int Alive() { return uv_loop_alive(g_setup->event_loop()); }
|
||||
|
||||
void Pump() {
|
||||
Isolate *isolate = g_setup->isolate();
|
||||
HandleScope handles(isolate);
|
||||
Context::Scope context_scope(g_setup->context());
|
||||
uv_run(g_setup->event_loop(), UV_RUN_NOWAIT);
|
||||
g_platform->DrainTasks(isolate);
|
||||
// Calls a bootstrap helper; helpers do not throw, a failure answers empty.
|
||||
Local<Value> Helper(Enter &e, Global<Function> &fn, int argc, Local<Value> *argv) {
|
||||
TryCatch try_catch(e.isolate);
|
||||
Local<Value> result;
|
||||
if (!fn.Get(e.isolate)->Call(e.context, v8::Undefined(e.isolate), argc, argv).ToLocal(&result))
|
||||
return Local<Value>();
|
||||
return result;
|
||||
}
|
||||
|
||||
// The program ends with `code`: Node's process ends with it too. Node's
|
||||
// 'beforeExit' runs (and the work it schedules), then 'exit'; the result
|
||||
// is the exit code Node's process ends with (process.exitCode).
|
||||
int Finish(int code) {
|
||||
if (!g_setup) return code;
|
||||
Isolate *isolate = g_setup->isolate();
|
||||
{
|
||||
HandleScope handles(isolate);
|
||||
Local<Context> context = g_setup->context();
|
||||
Context::Scope context_scope(context);
|
||||
TryCatch try_catch(isolate);
|
||||
Local<Value> argv[] = {Integer::New(isolate, code)};
|
||||
(void)g_finish.Get(isolate)->Call(context, v8::Undefined(isolate), 1, argv);
|
||||
Local<Value> Num(Enter &e, double value) { return v8::Number::New(e.isolate, value); }
|
||||
|
||||
uint32_t HelperHandle(Enter &e, Local<Value> result) {
|
||||
if (result.IsEmpty()) return 0;
|
||||
return result->Uint32Value(e.context).FromMaybe(0);
|
||||
}
|
||||
|
||||
struct PollEntry {
|
||||
uv_poll_t handle;
|
||||
int fd;
|
||||
bool seen;
|
||||
bool active;
|
||||
};
|
||||
|
||||
uv_prepare_t g_prepare;
|
||||
uv_check_t g_check;
|
||||
uv_idle_t g_idle;
|
||||
uv_timer_t g_cap;
|
||||
bool g_hosted = false;
|
||||
bool g_trace = false; // SCRIPTC_NODE_ISLANDS_TRACE=loop: the I/O state on stderr
|
||||
bool g_dispatched = false; // the runtime's stations ran this iteration
|
||||
std::unordered_map<int, PollEntry *> g_polls;
|
||||
|
||||
void Dispatch() {
|
||||
if (g_dispatched) return;
|
||||
g_dispatched = true;
|
||||
g_host->io_dispatch();
|
||||
}
|
||||
|
||||
void SyncIo(int phase);
|
||||
|
||||
void OnPoll(uv_poll_t *, int, int) {
|
||||
Dispatch();
|
||||
SyncIo(SCR_NODE_IO_JOBS);
|
||||
}
|
||||
|
||||
void OnIdle(uv_idle_t *) {
|
||||
Dispatch();
|
||||
SyncIo(SCR_NODE_IO_JOBS);
|
||||
}
|
||||
|
||||
void OnCap(uv_timer_t *) {
|
||||
Dispatch();
|
||||
SyncIo(SCR_NODE_IO_JOBS);
|
||||
}
|
||||
|
||||
void OnPrepare(uv_prepare_t *) { SyncIo(SCR_NODE_IO_PREPARE); }
|
||||
|
||||
// The end of an iteration: the next one may dispatch again (from its idle
|
||||
// phase when work is ready, else from its poll phase).
|
||||
void OnCheck(uv_check_t *) {
|
||||
g_dispatched = false;
|
||||
SyncIo(SCR_NODE_IO_CHECK);
|
||||
}
|
||||
|
||||
void ClosePoll(uv_handle_t *handle) { delete reinterpret_cast<PollEntry *>(handle->data); }
|
||||
|
||||
// Mirrors the runtime's I/O state onto the loop: one unref'd poll handle
|
||||
// per fd, the prepare handle's ref as the runtime's liveness, the idle
|
||||
// handle while work is ready, the cap timer for work no fd announces. A
|
||||
// poll handle whose fd leaves the set is stopped, not closed, and started
|
||||
// again when the fd returns (the worker pool's wake pipe comes and goes
|
||||
// with every job): libuv cannot watch an fd with a second handle while the
|
||||
// first is still closing.
|
||||
void SyncIo(int phase) {
|
||||
if (!g_hosted) return;
|
||||
ScrNodeIoState state;
|
||||
state.phase = phase;
|
||||
g_host->io_state(&state);
|
||||
if (g_trace) {
|
||||
std::fprintf(stderr, "[node-islands] %s alive=%d ready=%d cap=%d fds=",
|
||||
phase == SCR_NODE_IO_CHECK ? "check" : phase == SCR_NODE_IO_JOBS ? "jobs" : "prepare",
|
||||
state.alive, state.ready, state.cap_ms);
|
||||
for (int i = 0; i < state.nfds; i++) std::fprintf(stderr, "%s%d", i ? "," : "", state.fds[i]);
|
||||
std::fprintf(stderr, "\n");
|
||||
}
|
||||
return node::SpinEventLoop(g_setup->env()).FromMaybe(code);
|
||||
uv_loop_t *loop = g_setup->event_loop();
|
||||
for (auto &entry : g_polls) entry.second->seen = false;
|
||||
for (int i = 0; i < state.nfds; i++) {
|
||||
int fd = state.fds[i];
|
||||
auto found = g_polls.find(fd);
|
||||
PollEntry *entry;
|
||||
if (found != g_polls.end()) {
|
||||
entry = found->second;
|
||||
} else {
|
||||
entry = new PollEntry();
|
||||
entry->fd = fd;
|
||||
entry->active = false;
|
||||
if (uv_poll_init(loop, &entry->handle, fd) != 0) {
|
||||
delete entry;
|
||||
continue;
|
||||
}
|
||||
entry->handle.data = entry;
|
||||
uv_unref(reinterpret_cast<uv_handle_t *>(&entry->handle));
|
||||
g_polls.emplace(fd, entry);
|
||||
}
|
||||
entry->seen = true;
|
||||
if (!entry->active && uv_poll_start(&entry->handle, UV_READABLE, OnPoll) == 0)
|
||||
entry->active = true;
|
||||
}
|
||||
for (auto &it : g_polls) {
|
||||
if (it.second->seen || !it.second->active) continue;
|
||||
uv_poll_stop(&it.second->handle);
|
||||
it.second->active = false;
|
||||
}
|
||||
if (state.alive) uv_ref(reinterpret_cast<uv_handle_t *>(&g_prepare));
|
||||
else uv_unref(reinterpret_cast<uv_handle_t *>(&g_prepare));
|
||||
if (state.ready) uv_idle_start(&g_idle, OnIdle);
|
||||
else uv_idle_stop(&g_idle);
|
||||
if (state.cap_ms >= 0) {
|
||||
if (!uv_is_active(reinterpret_cast<uv_handle_t *>(&g_cap)))
|
||||
uv_timer_start(&g_cap, OnCap, static_cast<uint64_t>(state.cap_ms), 0);
|
||||
} else {
|
||||
uv_timer_stop(&g_cap);
|
||||
}
|
||||
}
|
||||
|
||||
// After every microtask checkpoint, which ends every callback Node
|
||||
// dispatches (closing-phase ones included): the runtime's rejections, and
|
||||
// its liveness, which the callback may have changed after the turn's check.
|
||||
void OnMicrotasksDone(Isolate *, void *) {
|
||||
if (!g_hosted) return;
|
||||
g_host->microtasks_done();
|
||||
SyncIo(SCR_NODE_IO_JOBS);
|
||||
}
|
||||
|
||||
void HostLoop() {
|
||||
if (g_hosted) return;
|
||||
uv_loop_t *loop = g_setup->event_loop();
|
||||
uv_prepare_init(loop, &g_prepare);
|
||||
uv_prepare_start(&g_prepare, OnPrepare);
|
||||
uv_check_init(loop, &g_check);
|
||||
uv_check_start(&g_check, OnCheck);
|
||||
uv_unref(reinterpret_cast<uv_handle_t *>(&g_check));
|
||||
uv_idle_init(loop, &g_idle);
|
||||
uv_unref(reinterpret_cast<uv_handle_t *>(&g_idle));
|
||||
uv_timer_init(loop, &g_cap);
|
||||
uv_unref(reinterpret_cast<uv_handle_t *>(&g_cap));
|
||||
g_setup->isolate()->AddMicrotasksCompletedCallback(OnMicrotasksDone, nullptr);
|
||||
const char *trace = std::getenv("SCRIPTC_NODE_ISLANDS_TRACE");
|
||||
g_trace = trace != nullptr && std::strcmp(trace, "loop") == 0;
|
||||
g_hosted = true;
|
||||
SyncIo(SCR_NODE_IO_JOBS);
|
||||
}
|
||||
|
||||
void UnhostLoop() {
|
||||
if (!g_hosted) return;
|
||||
g_hosted = false;
|
||||
g_setup->isolate()->RemoveMicrotasksCompletedCallback(OnMicrotasksDone, nullptr);
|
||||
for (auto &entry : g_polls) {
|
||||
uv_poll_stop(&entry.second->handle);
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&entry.second->handle), ClosePoll);
|
||||
}
|
||||
g_polls.clear();
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&g_prepare), nullptr);
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&g_check), nullptr);
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&g_idle), nullptr);
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&g_cap), nullptr);
|
||||
uv_run(g_setup->event_loop(), UV_RUN_NOWAIT);
|
||||
}
|
||||
|
||||
void RunMicrotask(void *data) {
|
||||
g_host->run(SCR_NODE_JOB_MICROTASK, static_cast<uint32_t>(reinterpret_cast<uintptr_t>(data)));
|
||||
}
|
||||
|
||||
void Microtask(uint32_t token) {
|
||||
g_setup->isolate()->EnqueueMicrotask(RunMicrotask,
|
||||
reinterpret_cast<void *>(static_cast<uintptr_t>(token)));
|
||||
}
|
||||
|
||||
void Tick(uint32_t token) {
|
||||
Enter e;
|
||||
Local<Value> argv[] = {Num(e, token)};
|
||||
Helper(e, g_next_tick, 1, argv);
|
||||
}
|
||||
|
||||
uint32_t Timer(double delay, double repeat, int ref, uint32_t token) {
|
||||
Enter e;
|
||||
Local<Value> argv[] = {Num(e, delay), Num(e, repeat), v8::Boolean::New(e.isolate, ref != 0),
|
||||
Num(e, token)};
|
||||
return HelperHandle(e, Helper(e, g_set_timer, 4, argv));
|
||||
}
|
||||
|
||||
void TimerClear(uint32_t handle) {
|
||||
Enter e;
|
||||
Local<Value> argv[] = {Num(e, handle)};
|
||||
Helper(e, g_clear_timer, 1, argv);
|
||||
}
|
||||
|
||||
void TimerRef(uint32_t handle, int ref) {
|
||||
Enter e;
|
||||
Local<Value> argv[] = {Num(e, handle), v8::Boolean::New(e.isolate, ref != 0)};
|
||||
Helper(e, g_ref_timer, 2, argv);
|
||||
}
|
||||
|
||||
void TimerRefresh(uint32_t handle) {
|
||||
Enter e;
|
||||
Local<Value> argv[] = {Num(e, handle)};
|
||||
Helper(e, g_refresh_timer, 1, argv);
|
||||
}
|
||||
|
||||
uint32_t Immediate(int ref, uint32_t token) {
|
||||
Enter e;
|
||||
Local<Value> argv[] = {v8::Boolean::New(e.isolate, ref != 0), Num(e, token)};
|
||||
return HelperHandle(e, Helper(e, g_set_immediate, 2, argv));
|
||||
}
|
||||
|
||||
void ImmediateClear(uint32_t handle) {
|
||||
Enter e;
|
||||
Local<Value> argv[] = {Num(e, handle)};
|
||||
Helper(e, g_clear_immediate, 1, argv);
|
||||
}
|
||||
|
||||
void ImmediateRef(uint32_t handle, int ref) {
|
||||
Enter e;
|
||||
Local<Value> argv[] = {Num(e, handle), v8::Boolean::New(e.isolate, ref != 0)};
|
||||
Helper(e, g_ref_immediate, 2, argv);
|
||||
}
|
||||
|
||||
// The checkpoint after a runtime macrotask: an empty callback scope, whose
|
||||
// close runs Node's tick queue and V8's microtasks (InternalCallbackScope::
|
||||
// Close) unless Node's own dispatch is on the stack, which drains after.
|
||||
void Checkpoint(int microtasks_first) {
|
||||
Enter e;
|
||||
if (microtasks_first) e.context->GetMicrotaskQueue()->PerformCheckpoint(e.isolate);
|
||||
node::CallbackScope scope(e.isolate, Object::New(e.isolate), node::async_context{0, 0});
|
||||
}
|
||||
|
||||
int RunLoop(int continue_turn) {
|
||||
Enter e;
|
||||
// The rest of the turn the switch interrupted: libuv's UV_RUN_ONCE skips
|
||||
// the leading timers phase and runs poll, check, closing, then timers.
|
||||
if (continue_turn) {
|
||||
uv_run(g_setup->event_loop(), UV_RUN_ONCE);
|
||||
g_platform->DrainTasks(e.isolate);
|
||||
}
|
||||
return node::SpinEventLoop(g_setup->env()).FromMaybe(0);
|
||||
}
|
||||
|
||||
// An island listener of a process event: through MakeCallback, so a throw
|
||||
// is an uncaught exception as in Node's own emit.
|
||||
void Emit(uint32_t event, uint32_t listener, const char *name, int arg) {
|
||||
Enter e;
|
||||
Local<Value> argv[] = {Num(e, event), Num(e, listener), Utf8(e.isolate, name), Num(e, arg)};
|
||||
node::MakeCallback(e.isolate, e.context->Global(), g_emit.Get(e.isolate), 4, argv,
|
||||
node::async_context{0, 0});
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
if (!g_setup) return;
|
||||
UnhostLoop();
|
||||
Isolate *isolate = g_setup->isolate();
|
||||
node::Stop(g_setup->env());
|
||||
g_call.Reset();
|
||||
g_finish.Reset();
|
||||
for (Global<Function> *fn :
|
||||
{&g_set_timer, &g_clear_timer, &g_ref_timer, &g_refresh_timer, &g_set_immediate,
|
||||
&g_clear_immediate, &g_ref_immediate, &g_next_tick, &g_emit})
|
||||
fn->Reset();
|
||||
isolate->Exit();
|
||||
delete g_locker;
|
||||
g_locker = nullptr;
|
||||
@@ -307,9 +628,26 @@ void Stop() {
|
||||
}
|
||||
|
||||
const ScrNodeBridge kBridge = {
|
||||
SCR_NODE_BRIDGE_ABI, NODE_VERSION_STRING, NODE_MODULE_VERSION, Start, Call, Release,
|
||||
BackendFd, BackendTimeout, Alive, Pump, Stop,
|
||||
Finish,
|
||||
SCR_NODE_BRIDGE_ABI,
|
||||
NODE_VERSION_STRING,
|
||||
NODE_MODULE_VERSION,
|
||||
Start,
|
||||
Call,
|
||||
Release,
|
||||
HostLoop,
|
||||
Microtask,
|
||||
Tick,
|
||||
Timer,
|
||||
TimerClear,
|
||||
TimerRef,
|
||||
TimerRefresh,
|
||||
Immediate,
|
||||
ImmediateClear,
|
||||
ImmediateRef,
|
||||
Checkpoint,
|
||||
RunLoop,
|
||||
Emit,
|
||||
Stop,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
/* Assembles a libnode pack: the directory a scriptc program with Node
|
||||
* islands loads Node from (scope-B design §2.16.3). Until the pack CI
|
||||
* matrix (NI-13) publishes @scriptc/libnode-<platform> packages, packs are
|
||||
* assembled by hand from a Node `--shared` build and the matching headers:
|
||||
* islands loads Node from (scope-B design §2.16.3), from a Node `--shared`
|
||||
* build and the headers of the same release:
|
||||
*
|
||||
* node packages/libnode-common/build-pack.mjs \
|
||||
* --libnode <path to libnode.so.<ABI> or libnode.<ABI>.dylib> \
|
||||
@@ -10,21 +9,32 @@
|
||||
* --out <pack directory> [--license <Node's LICENSE>] [--cxx <compiler>]
|
||||
*
|
||||
* The pack holds libnode, the bridge (bridge/scr_node_bridge.cc) compiled
|
||||
* against those headers, Node's license and manifest.json, which the build
|
||||
* of a program with Node islands reads (packages/compiler/src/node-islands/
|
||||
* libnode.ts). Programs find a pack through SCRIPTC_LIBNODE or --libnode. */
|
||||
* against those headers and linked to libnode through $ORIGIN /
|
||||
* @loader_path (so the directory can move), Node's license and
|
||||
* manifest.json, which the build of a program with Node islands reads
|
||||
* (packages/compiler/src/node-islands/libnode.ts). Programs find a pack
|
||||
* through --libnode, SCRIPTC_LIBNODE, or the installed
|
||||
* @scriptc/libnode-<platform> package.
|
||||
*
|
||||
* The @scriptc/libnode-<platform> packs call buildPack() from
|
||||
* scripts/build.mjs after building libnode from the pinned release; run by
|
||||
* hand, this is the bridge-only rebuild path (seconds) and the way a
|
||||
* self-builder pairs the bridge with a compatible Node build of their own. */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { copyFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
const here = fileURLToPath(new URL(".", import.meta.url));
|
||||
const runtimeSrc = join(here, "../runtime/src");
|
||||
|
||||
/** The manifest schema; the compiler's pack reader accepts exactly this. */
|
||||
export const LIBNODE_PACK_SCHEMA = "scriptc.libnode-pack.v1";
|
||||
|
||||
/** The bridge ABI the runtime expects (scr_node_bridge.h). */
|
||||
function bridgeAbi() {
|
||||
export function bridgeAbi() {
|
||||
const header = readFileSync(join(runtimeSrc, "scr_node_bridge.h"), "utf8");
|
||||
return Number(/#define SCR_NODE_BRIDGE_ABI (\d+)u/.exec(header)[1]);
|
||||
}
|
||||
@@ -46,38 +56,41 @@ function fileEntry(path) {
|
||||
};
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
libnode: { type: "string" },
|
||||
headers: { type: "string" },
|
||||
out: { type: "string" },
|
||||
license: { type: "string" },
|
||||
cxx: { type: "string", default: process.platform === "darwin" ? "clang++" : "g++" },
|
||||
},
|
||||
});
|
||||
if (!values.libnode || !values.headers || !values.out) {
|
||||
console.error("usage: build-pack.mjs --libnode <lib> --headers <include/node> --out <dir>");
|
||||
process.exit(2);
|
||||
}
|
||||
const out = resolve(values.out);
|
||||
const headers = resolve(values.headers);
|
||||
mkdirSync(out, { recursive: true });
|
||||
const libnodeName = basename(values.libnode);
|
||||
copyFileSync(values.libnode, join(out, libnodeName));
|
||||
const bridgeName =
|
||||
process.platform === "darwin" ? "libscr_node_bridge.dylib" : "libscr_node_bridge.so";
|
||||
const link =
|
||||
process.platform === "darwin"
|
||||
? ["-dynamiclib", join(out, libnodeName), "-Wl,-rpath,@loader_path"]
|
||||
: ["-shared", `-L${out}`, `-l:${libnodeName}`, "-Wl,-rpath,$ORIGIN"];
|
||||
/** The bridge library name on a platform. */
|
||||
export function bridgeName(os = process.platform) {
|
||||
return os === "darwin" ? "libscr_node_bridge.dylib" : "libscr_node_bridge.so";
|
||||
}
|
||||
|
||||
/** Compiles the bridge against `headers` (an include/node directory) and
|
||||
* links it to `libnode`, which must already sit in the bridge's directory. */
|
||||
export function buildBridge({ headers, libnode, out, cxx }) {
|
||||
const darwin = process.platform === "darwin";
|
||||
const compiler = cxx ?? process.env.CXX ?? (darwin ? "clang++" : "g++");
|
||||
const link = darwin
|
||||
? [
|
||||
"-dynamiclib",
|
||||
libnode,
|
||||
"-Wl,-rpath,@loader_path",
|
||||
`-Wl,-install_name,@rpath/${basename(out)}`,
|
||||
]
|
||||
: [
|
||||
"-shared",
|
||||
`-L${dirname(libnode)}`,
|
||||
`-l:${basename(libnode)}`,
|
||||
"-Wl,-rpath,$ORIGIN",
|
||||
"-Wl,-z,defs",
|
||||
"-Wl,--as-needed",
|
||||
"-ldl",
|
||||
"-pthread",
|
||||
];
|
||||
execFileSync(
|
||||
values.cxx,
|
||||
compiler,
|
||||
[
|
||||
"-std=c++20",
|
||||
"-O2",
|
||||
"-fPIC",
|
||||
"-fvisibility=hidden",
|
||||
"-fno-rtti",
|
||||
"-I",
|
||||
runtimeSrc,
|
||||
"-I",
|
||||
@@ -85,24 +98,79 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
join(here, "bridge/scr_node_bridge.cc"),
|
||||
...link,
|
||||
"-o",
|
||||
join(out, bridgeName),
|
||||
out,
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
if (values.license) copyFileSync(values.license, join(out, "LICENSE"));
|
||||
if (darwin) {
|
||||
execFileSync("strip", ["-x", out], { stdio: "inherit" });
|
||||
// Stripping invalidated the linker's ad-hoc signature, which arm64
|
||||
// macOS requires to map the library.
|
||||
execFileSync("codesign", ["--force", "--sign", "-", out], { stdio: "inherit" });
|
||||
} else {
|
||||
execFileSync("strip", ["--strip-unneeded", out], { stdio: "inherit" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles the pack in `out` and returns its manifest. `extra` adds fields
|
||||
* to manifest.json (the published packs record their package, target and
|
||||
* build inputs); schema, nodeVersion, moduleVersion, bridgeAbi, platform,
|
||||
* arch, libnode, bridge and files are always present.
|
||||
*
|
||||
* @param {{ libnode: string, headers: string, out: string, license?: string,
|
||||
* cxx?: string, extra?: Record<string, unknown> }} options
|
||||
*/
|
||||
export function buildPack(options) {
|
||||
const out = resolve(options.out);
|
||||
const headers = resolve(options.headers);
|
||||
mkdirSync(out, { recursive: true });
|
||||
const libnodeName = basename(options.libnode);
|
||||
const libnode = join(out, libnodeName);
|
||||
if (resolve(options.libnode) !== libnode) copyFileSync(options.libnode, libnode);
|
||||
const bridge = join(out, bridgeName());
|
||||
buildBridge({ headers, libnode, out: bridge, cxx: options.cxx });
|
||||
if (options.license) copyFileSync(options.license, join(out, "LICENSE"));
|
||||
const { nodeVersion, moduleVersion } = nodeVersionOf(headers);
|
||||
const manifest = {
|
||||
schema: "scriptc.libnode-pack.v1",
|
||||
schema: LIBNODE_PACK_SCHEMA,
|
||||
nodeVersion,
|
||||
moduleVersion,
|
||||
bridgeAbi: bridgeAbi(),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
libnode: libnodeName,
|
||||
bridge: bridgeName(),
|
||||
...options.extra,
|
||||
files: {
|
||||
[libnodeName]: fileEntry(join(out, libnodeName)),
|
||||
[bridgeName]: fileEntry(join(out, bridgeName)),
|
||||
[libnodeName]: fileEntry(libnode),
|
||||
[bridgeName()]: fileEntry(bridge),
|
||||
},
|
||||
};
|
||||
writeFileSync(join(out, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(`libnode pack for Node ${nodeVersion} in ${out}`);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
libnode: { type: "string" },
|
||||
headers: { type: "string" },
|
||||
out: { type: "string" },
|
||||
license: { type: "string" },
|
||||
cxx: { type: "string" },
|
||||
},
|
||||
});
|
||||
if (!values.libnode || !values.headers || !values.out) {
|
||||
console.error("usage: build-pack.mjs --libnode <lib> --headers <include/node> --out <dir>");
|
||||
process.exit(2);
|
||||
}
|
||||
const manifest = buildPack({
|
||||
libnode: values.libnode,
|
||||
headers: values.headers,
|
||||
out: values.out,
|
||||
license: values.license,
|
||||
cxx: values.cxx,
|
||||
});
|
||||
console.log(`libnode pack for Node ${manifest.nodeVersion} in ${resolve(values.out)}`);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
/* Assembles a development libnode pack from the pinned, checksum-verified
|
||||
* inputs in dev-pack.json: a prebuilt libnode of the repository's Node
|
||||
* release and Node's official headers, with the bridge built against them
|
||||
* by build-pack.mjs. CI's Linux test lanes use it (.github/actions/
|
||||
* libnode-dev-pack) until the NI-13 packs are published.
|
||||
* by build-pack.mjs. CI's Linux x64 and macOS test lanes fall back to it
|
||||
* (.github/actions/libnode) until the libnode-packs workflow has cached a
|
||||
* build of @scriptc/libnode-<platform> for their ref.
|
||||
*
|
||||
* node packages/libnode-common/fetch-dev-pack.mjs --out <pack directory> */
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"nodeVersion": "24.22.0",
|
||||
"tarballSha256": "41f97cff2c666f1f8293dc5f85c65e8dd605c16b43fd9febd49ede32ee1e0329",
|
||||
"moduleVersion": 137,
|
||||
"configureFlags": ["--shared", "--ninja"],
|
||||
"icu": "full"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/libnode-common",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
#!/usr/bin/env node
|
||||
// Builds one libnode pack into its package directory:
|
||||
//
|
||||
// 1. libnode: the pinned Node.js release (libnode.json) from its official
|
||||
// source tarball, SHA-256 verified, configured `--shared` with full ICU,
|
||||
// only the `libnode` target built, stripped, staged with its headers
|
||||
// and LICENSE in a cache directory keyed by libnodeCacheKey(). A cache
|
||||
// hit skips the Node build entirely (about 26 minutes on 8 cores).
|
||||
// 2. the bridge, compiled against those headers (seconds, every run);
|
||||
// 3. the pack: libnode, the bridge, Node's LICENSE and manifest.json in
|
||||
// packages/libnode-<platform>/, plus `--compress` copies (.zst) of both
|
||||
// libraries in <cache>/compressed/ for size reports and embedding.
|
||||
//
|
||||
// This never runs in `pnpm -r build` or the test suites: each platform
|
||||
// package runs it through its explicit `build:native` script, and the
|
||||
// libnode-packs workflow runs that on matching hosts.
|
||||
//
|
||||
// Environment:
|
||||
// SCRIPTC_LIBNODE_CACHE cache root (default node_modules/.cache/scriptc-libnode)
|
||||
// SCRIPTC_LIBNODE_SOURCE an extracted source tree of the pinned release to
|
||||
// build in (seeds the cache from an earlier build tree)
|
||||
// SCRIPTC_LIBNODE_TOOLCHAIN a name for the toolchain, part of the cache key
|
||||
// (CI: "manylinux_2_28-gcc14", "macos-15-clang")
|
||||
// SCRIPTC_LIBNODE_JOBS ninja parallelism (default: available cores)
|
||||
// CC, CXX the compilers for Node and the bridge
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
copyFileSync,
|
||||
createReadStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { availableParallelism } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { buildPack } from "../build-pack.mjs";
|
||||
import {
|
||||
bridgeAbi,
|
||||
bridgeHash,
|
||||
libnodeCacheKey,
|
||||
packFileNames,
|
||||
platformInfo,
|
||||
readPin,
|
||||
repoRoot,
|
||||
} from "./pin.mjs";
|
||||
|
||||
function log(message) {
|
||||
process.stdout.write(`libnode: ${message}\n`);
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
log(`$ ${command} ${args.join(" ")}`);
|
||||
execFileSync(command, args, { stdio: "inherit", ...options });
|
||||
}
|
||||
|
||||
async function sha256File(path) {
|
||||
const hash = createHash("sha256");
|
||||
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
/** Downloads and verifies the pinned source tarball, then extracts it. */
|
||||
async function fetchSource(cacheRoot, pin) {
|
||||
const name = `node-v${pin.nodeVersion}`;
|
||||
const tree = join(cacheRoot, "src", name);
|
||||
if (existsSync(join(tree, "configure.py"))) return tree;
|
||||
const tarball = join(cacheRoot, "src", `${name}.tar.xz`);
|
||||
mkdirSync(join(cacheRoot, "src"), { recursive: true });
|
||||
if (!existsSync(tarball) || (await sha256File(tarball)) !== pin.tarballSha256) {
|
||||
const url = `https://nodejs.org/dist/v${pin.nodeVersion}/${name}.tar.xz`;
|
||||
run("curl", ["-fsSL", "--retry", "3", "-o", `${tarball}.part`, url]);
|
||||
renameSync(`${tarball}.part`, tarball);
|
||||
}
|
||||
const actual = await sha256File(tarball);
|
||||
if (actual !== pin.tarballSha256) {
|
||||
rmSync(tarball, { force: true });
|
||||
throw new Error(`${name}.tar.xz has SHA-256 ${actual}, expected ${pin.tarballSha256}`);
|
||||
}
|
||||
rmSync(tree, { recursive: true, force: true });
|
||||
run("tar", ["-xJf", tarball, "-C", join(cacheRoot, "src")]);
|
||||
return tree;
|
||||
}
|
||||
|
||||
/** The version a source tree's node_version.h declares. */
|
||||
function sourceVersion(tree) {
|
||||
const text = readFileSync(join(tree, "src", "node_version.h"), "utf8");
|
||||
const define = (name) => new RegExp(`#define ${name} (\\d+)`).exec(text)?.[1];
|
||||
return `${define("NODE_MAJOR_VERSION")}.${define("NODE_MINOR_VERSION")}.${define("NODE_PATCH_VERSION")}`;
|
||||
}
|
||||
|
||||
function moduleVersionOf(headers) {
|
||||
const text = readFileSync(join(headers, "include", "node", "node_version.h"), "utf8");
|
||||
return Number(/#define NODE_MODULE_VERSION (\d+)/.exec(text)?.[1]);
|
||||
}
|
||||
|
||||
/** The highest GLIBC_x.y symbol version a Linux library needs. */
|
||||
function minimumGlibc(library) {
|
||||
const dynamic = execFileSync("objdump", ["-T", library], {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 256 * 1024 * 1024,
|
||||
});
|
||||
const versions = [...new Set(dynamic.match(/GLIBC_\d+(\.\d+)+/g) ?? [])].map((v) =>
|
||||
v.slice("GLIBC_".length),
|
||||
);
|
||||
versions.sort((a, b) => {
|
||||
const x = a.split(".").map(Number);
|
||||
const y = b.split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
||||
if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) - (y[i] ?? 0);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return versions.at(-1) ?? null;
|
||||
}
|
||||
|
||||
/** Configures and builds libnode in `tree`, then stages it into `stage`. */
|
||||
function buildLibnode(tree, stage, platform, pin) {
|
||||
const { os } = platformInfo(platform);
|
||||
if (sourceVersion(tree) !== pin.nodeVersion) {
|
||||
throw new Error(`${tree} is Node ${sourceVersion(tree)}, the pin is ${pin.nodeVersion}`);
|
||||
}
|
||||
const python = process.env.PYTHON ?? "python3";
|
||||
// An earlier tree configured with exactly these flags is reused as is, so
|
||||
// a seeded build tree is a ninja no-op instead of a reconfigure.
|
||||
const status = join(tree, "config.status");
|
||||
const configured =
|
||||
existsSync(status) &&
|
||||
existsSync(join(tree, "out", "Release", "build.ninja")) &&
|
||||
readFileSync(status, "utf8")
|
||||
.trimEnd()
|
||||
.endsWith(`configure ${pin.configureFlags.join(" ")}`);
|
||||
const started = Date.now();
|
||||
if (!configured) run("./configure", pin.configureFlags, { cwd: tree });
|
||||
const gypi = readFileSync(join(tree, "config.gypi"), "utf8");
|
||||
for (const [setting, expected] of [
|
||||
["node_shared", "true"],
|
||||
["node_use_node_snapshot", "true"],
|
||||
["icu_small", pin.icu === "small" ? "true" : "false"],
|
||||
]) {
|
||||
if (!gypi.includes(`"${setting}": "${expected}"`)) {
|
||||
throw new Error(`config.gypi does not have ${setting}: ${expected}`);
|
||||
}
|
||||
}
|
||||
const jobs = process.env.SCRIPTC_LIBNODE_JOBS ?? String(availableParallelism());
|
||||
run("ninja", ["-C", join(tree, "out", "Release"), "-j", jobs, "libnode"]);
|
||||
const buildSeconds = Math.round((Date.now() - started) / 1000);
|
||||
|
||||
const names = packFileNames(platform, pin.moduleVersion);
|
||||
const built = [
|
||||
join(tree, "out", "Release", "lib", names.libnode),
|
||||
join(tree, "out", "Release", names.libnode),
|
||||
].find((path) => existsSync(path));
|
||||
if (built === undefined) throw new Error(`the Node build did not produce ${names.libnode}`);
|
||||
const partial = `${stage}.partial`;
|
||||
rmSync(partial, { recursive: true, force: true });
|
||||
mkdirSync(join(partial, "lib"), { recursive: true });
|
||||
const library = join(partial, "lib", names.libnode);
|
||||
if (os === "darwin") {
|
||||
copyFileSync(built, library);
|
||||
run("install_name_tool", ["-id", `@rpath/${names.libnode}`, library]);
|
||||
// -x keeps the global symbols addons and the bridge bind to.
|
||||
run("strip", ["-x", library]);
|
||||
// Editing the library invalidated the linker's ad-hoc signature, which
|
||||
// arm64 macOS requires to map it.
|
||||
run("codesign", ["--force", "--sign", "-", library]);
|
||||
} else {
|
||||
run("strip", ["--strip-unneeded", "-o", library, built]);
|
||||
}
|
||||
run(
|
||||
python,
|
||||
[
|
||||
join(tree, "tools", "install.py"),
|
||||
"install",
|
||||
"--headers-only",
|
||||
"--silent",
|
||||
"--root-dir",
|
||||
tree,
|
||||
"--dest-dir",
|
||||
join(partial, "headers"),
|
||||
"--prefix",
|
||||
"/",
|
||||
],
|
||||
{ cwd: tree },
|
||||
);
|
||||
const moduleVersion = moduleVersionOf(join(partial, "headers"));
|
||||
if (moduleVersion !== pin.moduleVersion) {
|
||||
throw new Error(
|
||||
`Node ${pin.nodeVersion} has NODE_MODULE_VERSION ${moduleVersion}, the pin says ${pin.moduleVersion}`,
|
||||
);
|
||||
}
|
||||
copyFileSync(join(tree, "LICENSE"), join(partial, "LICENSE"));
|
||||
writeFileSync(
|
||||
join(partial, "build.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
nodeVersion: pin.nodeVersion,
|
||||
moduleVersion,
|
||||
configureFlags: pin.configureFlags,
|
||||
icu: pin.icu,
|
||||
toolchain: process.env.SCRIPTC_LIBNODE_TOOLCHAIN ?? "default",
|
||||
buildSeconds,
|
||||
builtBytes: statSync(built).size,
|
||||
minimumGlibc: os === "linux" ? minimumGlibc(library) : null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
rmSync(stage, { recursive: true, force: true });
|
||||
renameSync(partial, stage);
|
||||
}
|
||||
|
||||
/** zstd copies of the pack's libraries, for size reports and embedding. */
|
||||
function compress(pack, names, out) {
|
||||
mkdirSync(out, { recursive: true });
|
||||
const sizes = {};
|
||||
for (const name of [names.libnode, names.bridge]) {
|
||||
const target = join(out, `${name}.zst`);
|
||||
const started = Date.now();
|
||||
run("zstd", ["-19", "-T0", "-q", "-f", join(pack, name), "-o", target]);
|
||||
sizes[name] = { zstdBytes: statSync(target).size, seconds: (Date.now() - started) / 1000 };
|
||||
}
|
||||
return sizes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ platform: string, packageRoot: string, compress?: boolean, skipLibnode?: boolean }} options
|
||||
*/
|
||||
export async function buildLibnodePack(options) {
|
||||
const { platform, packageRoot } = options;
|
||||
const { os, arch } = platformInfo(platform);
|
||||
const pin = readPin();
|
||||
const cacheRoot = resolve(
|
||||
process.env.SCRIPTC_LIBNODE_CACHE ??
|
||||
join(repoRoot, "node_modules", ".cache", "scriptc-libnode"),
|
||||
);
|
||||
const key = libnodeCacheKey(platform);
|
||||
const stage = join(cacheRoot, `${platform}-${pin.nodeVersion}-${key.slice(0, 16)}`);
|
||||
const names = packFileNames(platform, pin.moduleVersion);
|
||||
if (existsSync(join(stage, "build.json"))) {
|
||||
log(`cached libnode ${pin.nodeVersion} for ${platform} in ${stage}`);
|
||||
} else {
|
||||
if (options.skipLibnode) throw new Error(`no cached libnode build in ${stage}`);
|
||||
const tree = process.env.SCRIPTC_LIBNODE_SOURCE
|
||||
? resolve(process.env.SCRIPTC_LIBNODE_SOURCE)
|
||||
: await fetchSource(cacheRoot, pin);
|
||||
buildLibnode(tree, stage, platform, pin);
|
||||
}
|
||||
const build = JSON.parse(readFileSync(join(stage, "build.json"), "utf8"));
|
||||
|
||||
for (const name of [names.libnode, names.bridge, "LICENSE", "manifest.json"]) {
|
||||
rmSync(join(packageRoot, name), { force: true });
|
||||
}
|
||||
const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
||||
const bridgeStarted = Date.now();
|
||||
const manifest = buildPack({
|
||||
libnode: join(stage, "lib", names.libnode),
|
||||
headers: join(stage, "headers", "include", "node"),
|
||||
out: packageRoot,
|
||||
license: join(stage, "LICENSE"),
|
||||
extra: {
|
||||
package: packageJson.name,
|
||||
version: packageJson.version,
|
||||
target: platform,
|
||||
license: "LICENSE",
|
||||
build: {
|
||||
libnodeKey: key,
|
||||
bridgeHash: bridgeHash(),
|
||||
configureFlags: build.configureFlags,
|
||||
icu: build.icu,
|
||||
toolchain: build.toolchain,
|
||||
minimumGlibc: build.minimumGlibc,
|
||||
},
|
||||
},
|
||||
});
|
||||
const bridgeSeconds = (Date.now() - bridgeStarted) / 1000;
|
||||
if (
|
||||
manifest.nodeVersion !== pin.nodeVersion ||
|
||||
manifest.moduleVersion !== pin.moduleVersion ||
|
||||
manifest.bridgeAbi !== bridgeAbi() ||
|
||||
manifest.platform !== os ||
|
||||
manifest.arch !== arch ||
|
||||
manifest.libnode !== names.libnode ||
|
||||
manifest.bridge !== names.bridge
|
||||
) {
|
||||
throw new Error(`the assembled pack does not match ${platform}: ${JSON.stringify(manifest)}`);
|
||||
}
|
||||
const report = {
|
||||
platform,
|
||||
nodeVersion: pin.nodeVersion,
|
||||
libnodeBuildSeconds: build.buildSeconds,
|
||||
libnodeBuiltBytes: build.builtBytes,
|
||||
libnodeStrippedBytes: manifest.files[names.libnode].size,
|
||||
bridgeBytes: manifest.files[names.bridge].size,
|
||||
bridgeSeconds,
|
||||
minimumGlibc: manifest.build.minimumGlibc,
|
||||
};
|
||||
if (options.compress) {
|
||||
report.compressed = compress(packageRoot, names, join(cacheRoot, "compressed", platform));
|
||||
}
|
||||
log(`pack ${packageJson.name} for Node ${pin.nodeVersion}: ${JSON.stringify(report)}`);
|
||||
return report;
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
platform: { type: "string" },
|
||||
"package-root": { type: "string" },
|
||||
compress: { type: "boolean", default: false },
|
||||
"skip-libnode": { type: "boolean", default: false },
|
||||
report: { type: "string" },
|
||||
},
|
||||
});
|
||||
if (!values.platform) {
|
||||
console.error("usage: build.mjs --platform <platform> [--package-root <dir>] [--compress]");
|
||||
process.exit(2);
|
||||
}
|
||||
const packageRoot = resolve(
|
||||
values["package-root"] ?? join(repoRoot, "packages", `libnode-${values.platform}`),
|
||||
);
|
||||
const report = await buildLibnodePack({
|
||||
platform: values.platform,
|
||||
packageRoot,
|
||||
compress: values.compress,
|
||||
skipLibnode: values["skip-libnode"],
|
||||
});
|
||||
if (values.report) writeFileSync(values.report, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
// Prints the CI cache identity of one platform's libnode build as
|
||||
// GitHub Actions outputs: `key` (Node release, configure flags, platform,
|
||||
// toolchain), `path` (the staged build directory under the cache root,
|
||||
// relative to it) and `bridge` (the bridge source hash, which is not part
|
||||
// of the key: a bridge change reuses the cached libnode).
|
||||
//
|
||||
// node packages/libnode-common/scripts/cache-key.mjs --platform <p> [--toolchain <t>]
|
||||
import { parseArgs } from "node:util";
|
||||
import { bridgeHash, libnodeCacheKey, readPin } from "./pin.mjs";
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: { platform: { type: "string" }, toolchain: { type: "string" } },
|
||||
});
|
||||
if (!values.platform) {
|
||||
console.error("usage: cache-key.mjs --platform <platform> [--toolchain <name>]");
|
||||
process.exit(2);
|
||||
}
|
||||
const pin = readPin();
|
||||
const key = libnodeCacheKey(values.platform, values.toolchain);
|
||||
process.stdout.write(
|
||||
[
|
||||
`key=libnode-${values.platform}-${pin.nodeVersion}-${key}`,
|
||||
`path=${values.platform}-${pin.nodeVersion}-${key.slice(0, 16)}`,
|
||||
`bridge=${bridgeHash()}`,
|
||||
`node=${pin.nodeVersion}`,
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
@@ -0,0 +1,90 @@
|
||||
// The libnode pin and the platform table shared by the libnode pack build,
|
||||
// its CI cache keys, the pack verifier, and the smoke test.
|
||||
//
|
||||
// A libnode pack (`@scriptc/libnode-<platform>`) holds a stripped Node.js
|
||||
// shared library built from the pinned release, the C++ bridge compiled
|
||||
// against that release's headers, Node's license, and manifest.json. The
|
||||
// scriptc runtime dlopens the bridge on its first call into Node and talks
|
||||
// to it only through the C ABI in packages/runtime/src/scr_node_bridge.h.
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const commonRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
export const repoRoot = dirname(dirname(commonRoot));
|
||||
export const bridgeSource = join(commonRoot, "bridge", "scr_node_bridge.cc");
|
||||
export const bridgeHeader = join(repoRoot, "packages", "runtime", "src", "scr_node_bridge.h");
|
||||
|
||||
export { LIBNODE_PACK_SCHEMA, bridgeAbi } from "../build-pack.mjs";
|
||||
|
||||
/** Bump to invalidate every cached libnode build (a change to how the
|
||||
* library is built or staged that the pin does not capture). */
|
||||
const BUILD_RECIPE = 1;
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* nodeVersion: string,
|
||||
* tarballSha256: string,
|
||||
* moduleVersion: number,
|
||||
* configureFlags: string[],
|
||||
* icu: "full" | "small",
|
||||
* }} LibnodePin
|
||||
*/
|
||||
|
||||
/** @returns {LibnodePin} */
|
||||
export function readPin() {
|
||||
return JSON.parse(readFileSync(join(commonRoot, "libnode.json"), "utf8"));
|
||||
}
|
||||
|
||||
/** Pack platforms: the npm package suffix, the Node `os`/`arch`, and the
|
||||
* file names inside the pack. The darwin packs build only on macOS hosts. */
|
||||
export const LIBNODE_PLATFORMS = {
|
||||
"linux-x64-gnu": { os: "linux", arch: "x64", target: "linux-x64-gnu" },
|
||||
"linux-arm64-gnu": { os: "linux", arch: "arm64", target: "linux-arm64-gnu" },
|
||||
"darwin-arm64": { os: "darwin", arch: "arm64", target: "macos-arm64" },
|
||||
"darwin-x64": { os: "darwin", arch: "x64", target: "macos-x64" },
|
||||
};
|
||||
|
||||
/** @param {string} platform */
|
||||
export function platformInfo(platform) {
|
||||
const info = LIBNODE_PLATFORMS[/** @type {keyof typeof LIBNODE_PLATFORMS} */ (platform)];
|
||||
if (info === undefined) throw new Error(`unknown libnode pack platform ${platform}`);
|
||||
return info;
|
||||
}
|
||||
|
||||
/** The library names inside a pack for one platform and module version. */
|
||||
export function packFileNames(platform, moduleVersion) {
|
||||
const { os } = platformInfo(platform);
|
||||
return os === "darwin"
|
||||
? { libnode: `libnode.${moduleVersion}.dylib`, bridge: "libscr_node_bridge.dylib" }
|
||||
: { libnode: `libnode.so.${moduleVersion}`, bridge: "libscr_node_bridge.so" };
|
||||
}
|
||||
|
||||
function sha256(text) {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
/** The cache key of the libnode build itself: the Node release, how it is
|
||||
* configured, the platform and toolchain. The bridge is not part of it; a
|
||||
* bridge change rebuilds only the bridge against the cached headers. */
|
||||
export function libnodeCacheKey(platform, toolchain = process.env.SCRIPTC_LIBNODE_TOOLCHAIN) {
|
||||
const pin = readPin();
|
||||
platformInfo(platform);
|
||||
return sha256(
|
||||
JSON.stringify({
|
||||
recipe: BUILD_RECIPE,
|
||||
nodeVersion: pin.nodeVersion,
|
||||
tarballSha256: pin.tarballSha256,
|
||||
configureFlags: pin.configureFlags,
|
||||
icu: pin.icu,
|
||||
platform,
|
||||
toolchain: toolchain ?? "default",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** The hash of everything the bridge is compiled from. */
|
||||
export function bridgeHash() {
|
||||
return sha256(readFileSync(bridgeSource, "utf8") + "\0" + readFileSync(bridgeHeader, "utf8"));
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
// Runs the libnode pack smoke test (smoke/smoke.c): compiles the C program
|
||||
// with the host's C compiler, runs it against a pack directory and compares
|
||||
// its stdout with the expected transcript, which pins the Node version,
|
||||
// the built-in startup snapshot, full ICU, and synchronous, asynchronous
|
||||
// and throwing calls through the bridge's C ABI.
|
||||
//
|
||||
// node packages/libnode-common/scripts/smoke.mjs [--pack <dir>] [--platform <p>]
|
||||
//
|
||||
// The pack defaults to SCRIPTC_LIBNODE, else packages/libnode-<platform>.
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { bridgeAbi, bridgeHeader, commonRoot, readPin, repoRoot } from "./pin.mjs";
|
||||
|
||||
/** The host's pack platform name. */
|
||||
export function hostPlatform() {
|
||||
if (process.platform === "darwin") return `darwin-${process.arch}`;
|
||||
return `${process.platform}-${process.arch}-gnu`;
|
||||
}
|
||||
|
||||
/** The transcript smoke.c prints for a correct pack of the pinned Node. */
|
||||
export function expectedTranscript(pin = readPin(), abi = bridgeAbi()) {
|
||||
const icuSmall = pin.icu === "small";
|
||||
return [
|
||||
`bridge ABI ${abi}, Node ${pin.nodeVersion}, NODE_MODULE_VERSION ${pin.moduleVersion}`,
|
||||
"libnode mapped before dlopen: no",
|
||||
"1 + 2 => R3",
|
||||
`process.versions.node => R"${pin.nodeVersion}"`,
|
||||
"process.config.variables.node_use_node_snapshot => Rtrue",
|
||||
`process.config.variables.icu_small => R${icuSmall}`,
|
||||
`new Intl.NumberFormat('de-DE').format(1234567.891) => R"${icuSmall ? "1,234,567.891" : "1.234.567,891"}"`,
|
||||
`new Intl.DateTimeFormat('ja-JP', { timeZone: 'UTC', dateStyle: 'full' }).format(0) => R"${icuSmall ? "Thursday, January 1, 1970" : "1970年1月1日木曜日"}"`,
|
||||
`require('node:crypto').createHash('sha256').update('scriptc').digest('hex') => R"${sha256Hex("scriptc")}"`,
|
||||
`require('node:zlib').gunzipSync(require('node:zlib').gzipSync('round trip')).toString() => R"round trip"`,
|
||||
`[typeof require('node:fs').readFileSync, require('node:path').join('a', 'b')] => R["function","a/b"]`,
|
||||
`null.property => T{"error":{"name":"TypeError","message":"Cannot read properties of null (reading 'property')"}}`,
|
||||
`await new Promise((resolve) => setTimeout(() => resolve('timer fired'), 5)) => R"timer fired"`,
|
||||
"await require('node:fs/promises').readFile(process.execPath).then((b) => b.length > 0) => Rtrue",
|
||||
`await Promise.reject(new RangeError('rejected')) => T{"error":{"name":"RangeError","message":"rejected"}}`,
|
||||
"ok",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function sha256Hex(text) {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
/** Compiles and runs the smoke test; returns its stdout and stderr. */
|
||||
export function runSmoke(pack, cc = process.env.CC ?? "cc") {
|
||||
const work = mkdtempSync(join(tmpdir(), "scriptc-libnode-smoke-"));
|
||||
try {
|
||||
const binary = join(work, "smoke");
|
||||
execFileSync(
|
||||
cc,
|
||||
[
|
||||
"-std=c11",
|
||||
...(process.platform === "linux" ? ["-D_POSIX_C_SOURCE=200809L", "-D_DEFAULT_SOURCE"] : []),
|
||||
"-O2",
|
||||
"-Wall",
|
||||
"-Werror",
|
||||
"-I",
|
||||
dirname(bridgeHeader),
|
||||
join(commonRoot, "smoke", "smoke.c"),
|
||||
...(process.platform === "linux" ? ["-ldl"] : []),
|
||||
"-o",
|
||||
binary,
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
const result = spawnSync(binary, [pack], { encoding: "utf8", env: { ...process.env } });
|
||||
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
||||
} finally {
|
||||
rmSync(work, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const { values } = parseArgs({
|
||||
options: { pack: { type: "string" }, platform: { type: "string" } },
|
||||
});
|
||||
// An explicit --pack or --platform names the pack; SCRIPTC_LIBNODE only
|
||||
// replaces the host's default.
|
||||
const pack = resolve(
|
||||
values.pack ??
|
||||
(values.platform !== undefined
|
||||
? join(repoRoot, "packages", `libnode-${values.platform}`)
|
||||
: (process.env.SCRIPTC_LIBNODE ?? join(repoRoot, "packages", `libnode-${hostPlatform()}`))),
|
||||
);
|
||||
const manifest = JSON.parse(readFileSync(join(pack, "manifest.json"), "utf8"));
|
||||
const { status, stdout, stderr } = runSmoke(pack);
|
||||
process.stderr.write(stderr);
|
||||
const expected = expectedTranscript();
|
||||
if (status !== 0 || stdout !== expected) {
|
||||
process.stderr.write(
|
||||
`smoke test failed (exit ${status}) for ${pack}\n--- expected\n${expected}--- actual\n${stdout}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(
|
||||
`libnode smoke test passed: ${manifest.package} (Node ${manifest.nodeVersion})\n`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env node
|
||||
// Verifies a built libnode pack before it is packed or published: the
|
||||
// manifest matches the package, the pin and the bridge ABI; every library
|
||||
// it lists exists with the recorded size and SHA-256; the license files are
|
||||
// present.
|
||||
//
|
||||
// node packages/libnode-common/scripts/verify.mjs <package directory>
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { LIBNODE_PACK_SCHEMA, bridgeAbi, packFileNames, platformInfo, readPin } from "./pin.mjs";
|
||||
|
||||
/** The problems with the pack in `root`; empty when it is complete. */
|
||||
export function verifyPack(root) {
|
||||
const problems = [];
|
||||
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
const manifestPath = join(root, "manifest.json");
|
||||
if (!existsSync(manifestPath)) return [`${manifestPath} is missing; run build:native first`];
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
||||
const pin = readPin();
|
||||
const platform = String(packageJson.name).replace(/^@scriptc\/libnode-/, "");
|
||||
const expect = (field, actual, expected) => {
|
||||
if (actual !== expected) problems.push(`manifest ${field} is ${actual}, expected ${expected}`);
|
||||
};
|
||||
expect("schema", manifest.schema, LIBNODE_PACK_SCHEMA);
|
||||
expect("package", manifest.package, packageJson.name);
|
||||
expect("version", manifest.version, packageJson.version);
|
||||
expect("target", manifest.target, platform);
|
||||
expect("platform", manifest.platform, platformInfo(platform).os);
|
||||
expect("arch", manifest.arch, platformInfo(platform).arch);
|
||||
expect("nodeVersion", manifest.nodeVersion, pin.nodeVersion);
|
||||
expect("moduleVersion", manifest.moduleVersion, pin.moduleVersion);
|
||||
expect("bridgeAbi", manifest.bridgeAbi, bridgeAbi());
|
||||
const names = packFileNames(platform, pin.moduleVersion);
|
||||
expect("libnode", manifest.libnode, names.libnode);
|
||||
expect("bridge", manifest.bridge, names.bridge);
|
||||
for (const name of [names.libnode, names.bridge]) {
|
||||
const entry = manifest.files?.[name];
|
||||
const path = join(root, name);
|
||||
if (entry === undefined) problems.push(`manifest lists no ${name}`);
|
||||
else if (!existsSync(path)) problems.push(`${name} is missing`);
|
||||
else if (statSync(path).size !== entry.size) problems.push(`${name} has the wrong size`);
|
||||
else if (createHash("sha256").update(readFileSync(path)).digest("hex") !== entry.sha256) {
|
||||
problems.push(`${name} has the wrong SHA-256`);
|
||||
}
|
||||
if (!packageJson.files.includes(name)) problems.push(`package.json files omits ${name}`);
|
||||
}
|
||||
for (const name of ["LICENSE", "SCRIPTC_LICENSE", "THIRD_PARTY_NOTICES"]) {
|
||||
if (!existsSync(join(root, name))) problems.push(`${name} is missing`);
|
||||
}
|
||||
if (existsSync(join(root, "LICENSE"))) {
|
||||
const license = readFileSync(join(root, "LICENSE"), "utf8");
|
||||
if (!license.startsWith("Node.js is licensed for use as follows:")) {
|
||||
problems.push("LICENSE is not the Node.js LICENSE file");
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const root = resolve(process.argv[2] ?? ".");
|
||||
const problems = verifyPack(root);
|
||||
if (problems.length > 0) {
|
||||
for (const problem of problems) console.error(`libnode pack ${root}: ${problem}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`libnode pack ${root} verified`);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/* Smoke test for a libnode pack, written the way the scriptc runtime loads
|
||||
* one: a plain C program that never includes a Node or V8 header, dlopens
|
||||
* the pack's bridge, checks its ABI and Node version, starts Node (one
|
||||
* environment from the built-in startup snapshot), evaluates expressions
|
||||
* synchronously and asynchronously through the bridge's C ABI, runs Node's
|
||||
* event loop until the asynchronous ones settle, and tears Node down.
|
||||
*
|
||||
* smoke <pack directory> (or SCRIPTC_LIBNODE=<pack directory> smoke)
|
||||
*
|
||||
* stdout is deterministic and compared by scripts/smoke.mjs; timings and
|
||||
* memory go to stderr. */
|
||||
#include "scr_node_bridge.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <poll.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/resource.h>
|
||||
#include <time.h>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#define BRIDGE_NAME "libscr_node_bridge.dylib"
|
||||
#else
|
||||
#define BRIDGE_NAME "libscr_node_bridge.so"
|
||||
#endif
|
||||
|
||||
/* The bootstrap runs as the environment's main script. It hands the bridge
|
||||
* a dispatcher that evaluates `name` as an expression and answers the
|
||||
* bridge's outcome strings: "R<json>" when it returns, "T<json>" when it
|
||||
* throws; an asynchronous call settles through the host. The smoke test
|
||||
* schedules no runtime jobs and listens to no process events, so the rest
|
||||
* of the dispatcher the bridge requires does nothing. */
|
||||
static const char kBootstrap[] =
|
||||
"const host = globalThis.__scriptcNodeIslandHost;\n"
|
||||
"globalThis.require = require;\n"
|
||||
"const outcome = (ok, value) => (ok ? 'R' : 'T') + JSON.stringify(\n"
|
||||
" ok ? value : { error: { name: value?.name, message: String(value?.message ?? value) } });\n"
|
||||
"globalThis.__scriptcNodeIsland = {\n"
|
||||
" call(id, sync, file, specifier, name, args) {\n"
|
||||
" let value;\n"
|
||||
" try { value = (0, eval)(name); } catch (error) { return outcome(false, error); }\n"
|
||||
" if (sync) return outcome(true, value);\n"
|
||||
" Promise.resolve(value).then((v) => host.settle(id, outcome(true, v)),\n"
|
||||
" (e) => host.settle(id, outcome(false, e)));\n"
|
||||
" },\n"
|
||||
" setTimer() { return 0; }, clearTimer() {}, refTimer() {}, refreshTimer() {},\n"
|
||||
" setImmediate() { return 0; }, clearImmediate() {}, refImmediate() {},\n"
|
||||
" nextTick() {}, emit() {},\n"
|
||||
"};\n";
|
||||
|
||||
static double now_ms(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6;
|
||||
}
|
||||
|
||||
static long max_rss_kb(void) {
|
||||
struct rusage usage;
|
||||
getrusage(RUSAGE_SELF, &usage);
|
||||
#if defined(__APPLE__)
|
||||
return usage.ru_maxrss / 1024;
|
||||
#else
|
||||
return usage.ru_maxrss;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Whether a library whose path contains `needle` is mapped (Linux only). */
|
||||
static int mapped(const char *needle) {
|
||||
FILE *maps = fopen("/proc/self/maps", "r");
|
||||
if (maps == NULL) return -1;
|
||||
char line[4096];
|
||||
int found = 0;
|
||||
while (!found && fgets(line, sizeof line, maps) != NULL) found = strstr(line, needle) != NULL;
|
||||
fclose(maps);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* The asynchronous expressions, by call id, and their outcomes. */
|
||||
static const char *const kAsync[] = {
|
||||
"new Promise((resolve) => setTimeout(() => resolve('timer fired'), 5))",
|
||||
"require('node:fs/promises').readFile(process.execPath).then((b) => b.length > 0)",
|
||||
"Promise.reject(new RangeError('rejected'))",
|
||||
};
|
||||
#define ASYNC_COUNT (sizeof kAsync / sizeof kAsync[0])
|
||||
static char *settled[ASYNC_COUNT];
|
||||
|
||||
static void host_settle(uint32_t id, const char *outcome, size_t len) {
|
||||
if (id >= ASYNC_COUNT || settled[id] != NULL) return;
|
||||
settled[id] = malloc(len + 1);
|
||||
memcpy(settled[id], outcome, len);
|
||||
settled[id][len] = '\0';
|
||||
}
|
||||
|
||||
static void host_exit(int code) {
|
||||
fflush(stdout);
|
||||
exit(code);
|
||||
}
|
||||
|
||||
/* The smoke test never calls host_loop, so Node's loop runs alone: no
|
||||
* runtime jobs, I/O or listeners, and V8 keeps the thread's stack limit. */
|
||||
static void host_run(uint32_t kind, uint32_t token) { (void)kind, (void)token; }
|
||||
static void host_io_state(ScrNodeIoState *state) {
|
||||
state->nfds = 0;
|
||||
state->alive = 0;
|
||||
state->ready = 0;
|
||||
state->cap_ms = -1;
|
||||
}
|
||||
static void host_io_dispatch(void) {}
|
||||
static void host_microtasks_done(void) {}
|
||||
static void host_listen(uint32_t event, const char *name, uint32_t listener, int add, int once) {
|
||||
(void)event, (void)name, (void)listener, (void)add, (void)once;
|
||||
}
|
||||
static uintptr_t host_stack_limit(void) { return 0; }
|
||||
/* The smoke test runs on its own thread's stack. */
|
||||
static uintptr_t host_stack_start(void) { return 0; }
|
||||
|
||||
static const ScrNodeBridgeHost kHost = {
|
||||
host_settle, host_exit, host_run, host_io_state,
|
||||
host_io_dispatch, host_microtasks_done, host_listen, host_stack_limit,
|
||||
host_stack_start,
|
||||
};
|
||||
|
||||
static const ScrNodeBridge *bridge;
|
||||
|
||||
static void eval_sync(const char *expression) {
|
||||
size_t len = 0;
|
||||
char *outcome = bridge->call(0, 1, "", "", expression, "[]", 2, &len);
|
||||
printf("%s => %s\n", expression, outcome);
|
||||
bridge->release(outcome);
|
||||
}
|
||||
|
||||
/* Starts every asynchronous expression, then runs Node's event loop until
|
||||
* nothing keeps it alive, as the runtime does at the end of a program. */
|
||||
static void eval_async_all(void) {
|
||||
for (uint32_t id = 0; id < ASYNC_COUNT; id++) {
|
||||
size_t len = 0;
|
||||
char *immediate = bridge->call(id, 0, "", "", kAsync[id], "[]", 2, &len);
|
||||
if (immediate != NULL) bridge->release(immediate);
|
||||
bridge->checkpoint(0);
|
||||
}
|
||||
bridge->run_loop(0);
|
||||
for (uint32_t id = 0; id < ASYNC_COUNT; id++) {
|
||||
printf("await %s => %s\n", kAsync[id], settled[id] != NULL ? settled[id] : "(never settled)");
|
||||
free(settled[id]);
|
||||
settled[id] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *dir = argc > 1 ? argv[1] : getenv("SCRIPTC_LIBNODE");
|
||||
if (dir == NULL || dir[0] == '\0') {
|
||||
fprintf(stderr, "usage: smoke <pack directory> (or set SCRIPTC_LIBNODE)\n");
|
||||
return 2;
|
||||
}
|
||||
char path[4096];
|
||||
snprintf(path, sizeof path, "%s/%s", dir, BRIDGE_NAME);
|
||||
int before = mapped("/libnode.");
|
||||
double t0 = now_ms();
|
||||
void *library = dlopen(path, RTLD_NOW | RTLD_GLOBAL);
|
||||
if (library == NULL) {
|
||||
fprintf(stderr, "smoke: %s\n", dlerror());
|
||||
return 1;
|
||||
}
|
||||
double t1 = now_ms();
|
||||
const ScrNodeBridge *(*entry)(void) =
|
||||
(const ScrNodeBridge *(*)(void))dlsym(library, SCR_NODE_BRIDGE_ENTRY);
|
||||
if (entry == NULL) {
|
||||
fprintf(stderr, "smoke: %s has no %s\n", path, SCR_NODE_BRIDGE_ENTRY);
|
||||
return 1;
|
||||
}
|
||||
bridge = entry();
|
||||
if (bridge->abi != SCR_NODE_BRIDGE_ABI) {
|
||||
fprintf(stderr, "smoke: bridge ABI %u, expected %u\n", bridge->abi, SCR_NODE_BRIDGE_ABI);
|
||||
return 1;
|
||||
}
|
||||
printf("bridge ABI %u, Node %s, NODE_MODULE_VERSION %u\n", bridge->abi, bridge->node_version,
|
||||
bridge->module_version);
|
||||
printf("libnode mapped before dlopen: %s\n", before == 1 ? "yes" : "no");
|
||||
char *error = bridge->start(&kHost, kBootstrap, argv[0], 0);
|
||||
if (error != NULL) {
|
||||
fprintf(stderr, "smoke: %s\n", error);
|
||||
return 1;
|
||||
}
|
||||
double t2 = now_ms();
|
||||
eval_sync("1 + 2");
|
||||
double t3 = now_ms();
|
||||
eval_sync("process.versions.node");
|
||||
eval_sync("process.config.variables.node_use_node_snapshot");
|
||||
eval_sync("process.config.variables.icu_small");
|
||||
eval_sync("new Intl.NumberFormat('de-DE').format(1234567.891)");
|
||||
eval_sync("new Intl.DateTimeFormat('ja-JP', { timeZone: 'UTC', dateStyle: 'full' }).format(0)");
|
||||
eval_sync("require('node:crypto').createHash('sha256').update('scriptc').digest('hex')");
|
||||
eval_sync("require('node:zlib').gunzipSync(require('node:zlib').gzipSync('round trip')).toString()");
|
||||
eval_sync("[typeof require('node:fs').readFileSync, require('node:path').join('a', 'b')]");
|
||||
eval_sync("null.property");
|
||||
eval_async_all();
|
||||
fprintf(stderr,
|
||||
"smoke: dlopen %.1f ms, start %.1f ms, first result %.2f ms, dlopen to first result "
|
||||
"%.1f ms, max RSS %ld KB\n",
|
||||
t1 - t0, t2 - t1, t3 - t2, t3 - t0, max_rss_kb());
|
||||
bridge->stop();
|
||||
printf("ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
* check its ABI, start Node from the snapshot with the runtime's embedded
|
||||
* bootstrap) and makes calls through the bridge: synchronous results and
|
||||
* errors, and an asynchronous call settled by Node's own loop, which this
|
||||
* host drives like the runtime's loop slot. bridge.test.ts compares the
|
||||
* transcript. Usage: bridge-smoke <pack dir> <module path> */
|
||||
* host runs the way the runtime's hosted loop does (host_loop, a
|
||||
* checkpoint, run_loop) with no runtime work of its own. bridge.test.ts
|
||||
* compares the transcript. Usage: bridge-smoke <pack dir> <module path> */
|
||||
#include <dlfcn.h>
|
||||
#include <poll.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -32,7 +32,29 @@ static void on_exit_code(int code) {
|
||||
_Exit(code);
|
||||
}
|
||||
|
||||
static const ScrNodeBridgeHost host = {settle, on_exit_code};
|
||||
static void run(uint32_t kind, uint32_t token) {
|
||||
printf("run %u %u\n", kind, token);
|
||||
}
|
||||
|
||||
static void io_state(ScrNodeIoState *state) {
|
||||
state->nfds = 0;
|
||||
state->alive = 0;
|
||||
state->ready = 0;
|
||||
state->cap_ms = -1;
|
||||
}
|
||||
|
||||
static void io_dispatch(void) {}
|
||||
static void microtasks_done(void) {}
|
||||
|
||||
static void listen(uint32_t event, const char *name, uint32_t listener, int add, int once) {
|
||||
printf("listen %u %s %u %d %d\n", event, name, listener, add, once);
|
||||
}
|
||||
|
||||
static uintptr_t stack_limit(void) { return 0; }
|
||||
static uintptr_t stack_start(void) { return 0; }
|
||||
|
||||
static const ScrNodeBridgeHost host = {settle, on_exit_code, run, io_state, io_dispatch,
|
||||
microtasks_done, listen, stack_limit, stack_start};
|
||||
|
||||
static void call(const ScrNodeBridge *b, uint32_t id, int sync, const char *module,
|
||||
const char *name, const char *args) {
|
||||
@@ -70,11 +92,9 @@ int main(int argc, char **argv) {
|
||||
call(b, 0, 1, file, "fails", "[]");
|
||||
call(b, 0, 1, file, "missing", "[]");
|
||||
call(b, 7, 0, file, "later", "[5]");
|
||||
while (settled == 0 || b->alive()) {
|
||||
struct pollfd fd = {b->backend_fd(), POLLIN, 0};
|
||||
(void)poll(&fd, 1, b->backend_timeout());
|
||||
b->pump();
|
||||
}
|
||||
b->host_loop();
|
||||
b->checkpoint(0);
|
||||
b->run_loop(0);
|
||||
call(b, 0, 1, file, "quit", "[3]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* calls through the bridge with the runtime's embedded bootstrap. Needs a
|
||||
* pack in SCRIPTC_LIBNODE; required in the Linux x64 and macOS CI lanes
|
||||
* (tests/harness/libnode.ts). */
|
||||
import { bridgeAbi } from "../build-pack.mjs";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
@@ -45,7 +46,7 @@ exports.quit = (code) => process.exit(code);
|
||||
expect(result.stderr).toBe("");
|
||||
expect(result.stdout).toBe(
|
||||
[
|
||||
"abi 3 ok 1",
|
||||
`abi ${bridgeAbi()} ok 1`,
|
||||
"sync add -> R5",
|
||||
'sync fails -> T{"error":{"kind":2,"message":"out of range","code":"ERR_X"}}',
|
||||
`sync missing -> T{"error":{"kind":1,"message":"'missing' exported by './smoke.cjs' is not a function"}}`,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
LIBNODE_PLATFORMS,
|
||||
bridgeAbi,
|
||||
bridgeHash,
|
||||
libnodeCacheKey,
|
||||
packFileNames,
|
||||
readPin,
|
||||
repoRoot,
|
||||
} from "../scripts/pin.mjs";
|
||||
import { expectedTranscript } from "../scripts/smoke.mjs";
|
||||
|
||||
const readJson = (path: string) => JSON.parse(readFileSync(join(repoRoot, path), "utf8"));
|
||||
|
||||
describe("the libnode pin", () => {
|
||||
test("is the repository's pinned Node release", () => {
|
||||
const pin = readPin();
|
||||
expect(pin.nodeVersion).toBe(readJson("internal/compatibility/node-v24.json").version);
|
||||
expect(pin.nodeVersion).toBe(readFileSync(join(repoRoot, ".node-version"), "utf8").trim());
|
||||
expect(pin.tarballSha256).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test("builds the shared library with full ICU (design §8 question 2)", () => {
|
||||
const pin = readPin();
|
||||
expect(pin.configureFlags).toContain("--shared");
|
||||
expect(pin.configureFlags.some((flag: string) => flag.startsWith("--with-intl"))).toBe(false);
|
||||
expect(pin.icu).toBe("full");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the platform packages", () => {
|
||||
const dirs = readdirSync(join(repoRoot, "packages")).filter(
|
||||
(name) => name.startsWith("libnode-") && name !== "libnode-common",
|
||||
);
|
||||
|
||||
test("match the platform table", () => {
|
||||
expect(dirs.map((dir) => dir.slice("libnode-".length)).sort()).toEqual(
|
||||
Object.keys(LIBNODE_PLATFORMS).sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test.each(Object.entries(LIBNODE_PLATFORMS))("%s ships its libraries", (platform, info) => {
|
||||
const manifest = readJson(`packages/libnode-${platform}/package.json`);
|
||||
const names = packFileNames(platform, readPin().moduleVersion);
|
||||
expect(manifest.name).toBe(`@scriptc/libnode-${platform}`);
|
||||
expect(manifest.version).toBe(readJson("packages/cli/package.json").version);
|
||||
expect(manifest.os).toEqual([info.os]);
|
||||
expect(manifest.cpu).toEqual([info.arch]);
|
||||
expect(manifest.files).toEqual([
|
||||
names.libnode,
|
||||
names.bridge,
|
||||
"manifest.json",
|
||||
"LICENSE",
|
||||
"SCRIPTC_LICENSE",
|
||||
"THIRD_PARTY_NOTICES",
|
||||
]);
|
||||
// Never built by `pnpm -r build`.
|
||||
expect(manifest.scripts.build).toBe("true");
|
||||
expect(manifest.scripts["build:native"]).toBe("node scripts/build.mjs");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cache identity", () => {
|
||||
test("the libnode key depends on platform and toolchain, not on the bridge", () => {
|
||||
const a = libnodeCacheKey("linux-x64-gnu", "manylinux_2_28-gcc-toolset-14");
|
||||
expect(a).toMatch(/^[0-9a-f]{64}$/);
|
||||
expect(libnodeCacheKey("linux-x64-gnu", "manylinux_2_28-gcc-toolset-14")).toBe(a);
|
||||
expect(libnodeCacheKey("linux-arm64-gnu", "manylinux_2_28-gcc-toolset-14")).not.toBe(a);
|
||||
expect(libnodeCacheKey("linux-x64-gnu", "other")).not.toBe(a);
|
||||
expect(bridgeHash()).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test("unknown platforms are refused", () => {
|
||||
expect(() => libnodeCacheKey("linux-x64-musl", "x")).toThrow(
|
||||
"unknown libnode pack platform linux-x64-musl",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("the smoke transcript pins the Node version, the snapshot and full ICU", () => {
|
||||
const lines = expectedTranscript().split("\n");
|
||||
expect(lines[0]).toBe(
|
||||
`bridge ABI ${bridgeAbi()}, Node ${readPin().nodeVersion}, NODE_MODULE_VERSION ${readPin().moduleVersion}`,
|
||||
);
|
||||
expect(lines).toContain("process.config.variables.node_use_node_snapshot => Rtrue");
|
||||
expect(lines).toContain(`new Intl.NumberFormat('de-DE').format(1234567.891) => R"1.234.567,891"`);
|
||||
expect(lines.at(-2)).toBe("ok");
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// Loads a real libnode pack from a C program (smoke/smoke.c) the way the
|
||||
// scriptc runtime does. Packs are not built by the ordinary build, so this
|
||||
// runs only when SCRIPTC_LIBNODE names a pack directory (a CI artifact of
|
||||
// the libnode-packs workflow, or a local build:native output). CI's
|
||||
// fallback development pack (dev-pack.json) is not a package: it has no
|
||||
// package.json and may carry a different Node patch release, so it is
|
||||
// exercised by the Node island tests instead.
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { verifyPack } from "../scripts/verify.mjs";
|
||||
import { expectedTranscript, runSmoke } from "../scripts/smoke.mjs";
|
||||
|
||||
const pack = process.env["SCRIPTC_LIBNODE"];
|
||||
const isPackage = pack !== undefined && pack !== "" && existsSync(join(pack, "package.json"));
|
||||
|
||||
describe.skipIf(!isPackage)("libnode pack smoke test", () => {
|
||||
test("the pack is complete", () => {
|
||||
expect(verifyPack(pack!)).toEqual([]);
|
||||
});
|
||||
|
||||
test("a C program starts Node from the pack and evaluates expressions", () => {
|
||||
const { status, stdout } = runSmoke(pack!);
|
||||
expect(stdout).toBe(expectedTranscript());
|
||||
expect(status).toBe(0);
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
Copyright scriptc contributors. Licensed under Apache-2.0.
|
||||
@@ -0,0 +1,15 @@
|
||||
This package contains libnode.137.dylib, a stripped build of the Node.js shared
|
||||
library (https://nodejs.org) from the official source release named in
|
||||
manifest.json, built without modification. Node.js is licensed under the
|
||||
MIT license. It bundles third-party components, among them V8, ICU,
|
||||
OpenSSL, libuv, zlib, brotli, nghttp2, nghttp3, ngtcp2, llhttp, c-ares,
|
||||
simdutf, ada and uvwasi, under the BSD, Apache-2.0, Unicode, MIT and
|
||||
other permissive licenses. LICENSE is the LICENSE file of that Node.js
|
||||
release, which carries the complete license text of Node.js and of every
|
||||
bundled component.
|
||||
|
||||
libscr_node_bridge.dylib is the scriptc bridge between the scriptc runtime and Node.js,
|
||||
licensed under Apache-2.0 (SCRIPTC_LICENSE).
|
||||
|
||||
A program that `scriptc build` links against this package redistributes
|
||||
Node.js; scriptc writes these notices next to such a program.
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@scriptc/libnode-darwin-arm64",
|
||||
"version": "0.4.5",
|
||||
"private": true,
|
||||
"description": "Prebuilt Node.js shared library and scriptc bridge for Node islands on macOS arm64",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/libnode-darwin-arm64"
|
||||
},
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"files": [
|
||||
"libnode.137.dylib",
|
||||
"libscr_node_bridge.dylib",
|
||||
"manifest.json",
|
||||
"LICENSE",
|
||||
"SCRIPTC_LICENSE",
|
||||
"THIRD_PARTY_NOTICES"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "true",
|
||||
"build:native": "node scripts/build.mjs",
|
||||
"prepack": "node ../libnode-common/scripts/verify.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
// Builds @scriptc/libnode-darwin-arm64: libnode for the pinned Node.js release
|
||||
// and the bridge (packages/libnode-common/scripts/build.mjs). The Node
|
||||
// build takes about half an hour on 8 cores and is cached by Node version,
|
||||
// configure flags and toolchain; only the libnode-packs workflow and an
|
||||
// explicit `pnpm --filter @scriptc/libnode-darwin-arm64 build:native` run it.
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildLibnodePack } from "../../libnode-common/scripts/build.mjs";
|
||||
|
||||
if (process.platform !== "darwin" || process.arch !== "arm64") {
|
||||
process.stdout.write("@scriptc/libnode-darwin-arm64: skipped on this host\n");
|
||||
process.exit(0);
|
||||
}
|
||||
await buildLibnodePack({
|
||||
platform: "darwin-arm64",
|
||||
packageRoot: dirname(dirname(fileURLToPath(import.meta.url))),
|
||||
compress: process.argv.includes("--compress"),
|
||||
// Assemble from a restored libnode build only (CI test lanes); never
|
||||
// start a Node build.
|
||||
skipLibnode: process.argv.includes("--from-cache"),
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
Copyright scriptc contributors. Licensed under Apache-2.0.
|
||||
@@ -0,0 +1,15 @@
|
||||
This package contains libnode.137.dylib, a stripped build of the Node.js shared
|
||||
library (https://nodejs.org) from the official source release named in
|
||||
manifest.json, built without modification. Node.js is licensed under the
|
||||
MIT license. It bundles third-party components, among them V8, ICU,
|
||||
OpenSSL, libuv, zlib, brotli, nghttp2, nghttp3, ngtcp2, llhttp, c-ares,
|
||||
simdutf, ada and uvwasi, under the BSD, Apache-2.0, Unicode, MIT and
|
||||
other permissive licenses. LICENSE is the LICENSE file of that Node.js
|
||||
release, which carries the complete license text of Node.js and of every
|
||||
bundled component.
|
||||
|
||||
libscr_node_bridge.dylib is the scriptc bridge between the scriptc runtime and Node.js,
|
||||
licensed under Apache-2.0 (SCRIPTC_LICENSE).
|
||||
|
||||
A program that `scriptc build` links against this package redistributes
|
||||
Node.js; scriptc writes these notices next to such a program.
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@scriptc/libnode-darwin-x64",
|
||||
"version": "0.4.5",
|
||||
"private": true,
|
||||
"description": "Prebuilt Node.js shared library and scriptc bridge for Node islands on macOS x64",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/libnode-darwin-x64"
|
||||
},
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"libnode.137.dylib",
|
||||
"libscr_node_bridge.dylib",
|
||||
"manifest.json",
|
||||
"LICENSE",
|
||||
"SCRIPTC_LICENSE",
|
||||
"THIRD_PARTY_NOTICES"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "true",
|
||||
"build:native": "node scripts/build.mjs",
|
||||
"prepack": "node ../libnode-common/scripts/verify.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
// Builds @scriptc/libnode-darwin-x64: libnode for the pinned Node.js release
|
||||
// and the bridge (packages/libnode-common/scripts/build.mjs). The Node
|
||||
// build takes about half an hour on 8 cores and is cached by Node version,
|
||||
// configure flags and toolchain; only the libnode-packs workflow and an
|
||||
// explicit `pnpm --filter @scriptc/libnode-darwin-x64 build:native` run it.
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildLibnodePack } from "../../libnode-common/scripts/build.mjs";
|
||||
|
||||
if (process.platform !== "darwin" || process.arch !== "x64") {
|
||||
process.stdout.write("@scriptc/libnode-darwin-x64: skipped on this host\n");
|
||||
process.exit(0);
|
||||
}
|
||||
await buildLibnodePack({
|
||||
platform: "darwin-x64",
|
||||
packageRoot: dirname(dirname(fileURLToPath(import.meta.url))),
|
||||
compress: process.argv.includes("--compress"),
|
||||
// Assemble from a restored libnode build only (CI test lanes); never
|
||||
// start a Node build.
|
||||
skipLibnode: process.argv.includes("--from-cache"),
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
Copyright scriptc contributors. Licensed under Apache-2.0.
|
||||
@@ -0,0 +1,15 @@
|
||||
This package contains libnode.so.137, a stripped build of the Node.js shared
|
||||
library (https://nodejs.org) from the official source release named in
|
||||
manifest.json, built without modification. Node.js is licensed under the
|
||||
MIT license. It bundles third-party components, among them V8, ICU,
|
||||
OpenSSL, libuv, zlib, brotli, nghttp2, nghttp3, ngtcp2, llhttp, c-ares,
|
||||
simdutf, ada and uvwasi, under the BSD, Apache-2.0, Unicode, MIT and
|
||||
other permissive licenses. LICENSE is the LICENSE file of that Node.js
|
||||
release, which carries the complete license text of Node.js and of every
|
||||
bundled component.
|
||||
|
||||
libscr_node_bridge.so is the scriptc bridge between the scriptc runtime and Node.js,
|
||||
licensed under Apache-2.0 (SCRIPTC_LICENSE).
|
||||
|
||||
A program that `scriptc build` links against this package redistributes
|
||||
Node.js; scriptc writes these notices next to such a program.
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@scriptc/libnode-linux-arm64-gnu",
|
||||
"version": "0.4.5",
|
||||
"private": true,
|
||||
"description": "Prebuilt Node.js shared library and scriptc bridge for Node islands on Linux arm64 glibc",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/libnode-linux-arm64-gnu"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"files": [
|
||||
"libnode.so.137",
|
||||
"libscr_node_bridge.so",
|
||||
"manifest.json",
|
||||
"LICENSE",
|
||||
"SCRIPTC_LICENSE",
|
||||
"THIRD_PARTY_NOTICES"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "true",
|
||||
"build:native": "node scripts/build.mjs",
|
||||
"prepack": "node ../libnode-common/scripts/verify.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
// Builds @scriptc/libnode-linux-arm64-gnu: libnode for the pinned Node.js release
|
||||
// and the bridge (packages/libnode-common/scripts/build.mjs). The Node
|
||||
// build takes about half an hour on 8 cores and is cached by Node version,
|
||||
// configure flags and toolchain; only the libnode-packs workflow and an
|
||||
// explicit `pnpm --filter @scriptc/libnode-linux-arm64-gnu build:native` run it.
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildLibnodePack } from "../../libnode-common/scripts/build.mjs";
|
||||
|
||||
if (process.platform !== "linux" || process.arch !== "arm64") {
|
||||
process.stdout.write("@scriptc/libnode-linux-arm64-gnu: skipped on this host\n");
|
||||
process.exit(0);
|
||||
}
|
||||
await buildLibnodePack({
|
||||
platform: "linux-arm64-gnu",
|
||||
packageRoot: dirname(dirname(fileURLToPath(import.meta.url))),
|
||||
compress: process.argv.includes("--compress"),
|
||||
// Assemble from a restored libnode build only (CI test lanes); never
|
||||
// start a Node build.
|
||||
skipLibnode: process.argv.includes("--from-cache"),
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
Copyright scriptc contributors. Licensed under Apache-2.0.
|
||||
@@ -0,0 +1,15 @@
|
||||
This package contains libnode.so.137, a stripped build of the Node.js shared
|
||||
library (https://nodejs.org) from the official source release named in
|
||||
manifest.json, built without modification. Node.js is licensed under the
|
||||
MIT license. It bundles third-party components, among them V8, ICU,
|
||||
OpenSSL, libuv, zlib, brotli, nghttp2, nghttp3, ngtcp2, llhttp, c-ares,
|
||||
simdutf, ada and uvwasi, under the BSD, Apache-2.0, Unicode, MIT and
|
||||
other permissive licenses. LICENSE is the LICENSE file of that Node.js
|
||||
release, which carries the complete license text of Node.js and of every
|
||||
bundled component.
|
||||
|
||||
libscr_node_bridge.so is the scriptc bridge between the scriptc runtime and Node.js,
|
||||
licensed under Apache-2.0 (SCRIPTC_LICENSE).
|
||||
|
||||
A program that `scriptc build` links against this package redistributes
|
||||
Node.js; scriptc writes these notices next to such a program.
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@scriptc/libnode-linux-x64-gnu",
|
||||
"version": "0.4.5",
|
||||
"private": true,
|
||||
"description": "Prebuilt Node.js shared library and scriptc bridge for Node islands on Linux x64 glibc",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/libnode-linux-x64-gnu"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"files": [
|
||||
"libnode.so.137",
|
||||
"libscr_node_bridge.so",
|
||||
"manifest.json",
|
||||
"LICENSE",
|
||||
"SCRIPTC_LICENSE",
|
||||
"THIRD_PARTY_NOTICES"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "true",
|
||||
"build:native": "node scripts/build.mjs",
|
||||
"prepack": "node ../libnode-common/scripts/verify.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
// Builds @scriptc/libnode-linux-x64-gnu: libnode for the pinned Node.js release
|
||||
// and the bridge (packages/libnode-common/scripts/build.mjs). The Node
|
||||
// build takes about half an hour on 8 cores and is cached by Node version,
|
||||
// configure flags and toolchain; only the libnode-packs workflow and an
|
||||
// explicit `pnpm --filter @scriptc/libnode-linux-x64-gnu build:native` run it.
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildLibnodePack } from "../../libnode-common/scripts/build.mjs";
|
||||
|
||||
if (process.platform !== "linux" || process.arch !== "x64") {
|
||||
process.stdout.write("@scriptc/libnode-linux-x64-gnu: skipped on this host\n");
|
||||
process.exit(0);
|
||||
}
|
||||
await buildLibnodePack({
|
||||
platform: "linux-x64-gnu",
|
||||
packageRoot: dirname(dirname(fileURLToPath(import.meta.url))),
|
||||
compress: process.argv.includes("--compress"),
|
||||
// Assemble from a restored libnode build only (CI test lanes); never
|
||||
// start a Node build.
|
||||
skipLibnode: process.argv.includes("--from-cache"),
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/node",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.5",
|
||||
"description": "Call into Node.js from scriptc programs: run an export of a JavaScript module in a Node.js helper process and copy JSON data both ways. Under Node.js the calls run in-process.",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -254,6 +254,17 @@ export function variantsFor(source) {
|
||||
when: { stdioExtended: true },
|
||||
defines: ["SCR_STDIO_EXTENDED"],
|
||||
});
|
||||
// Island listeners for signals and 'exit' join the registries.
|
||||
variants.push({
|
||||
id: "node-island",
|
||||
when: { nodeIsland: true },
|
||||
defines: ["SCR_NODE_ISLANDS"],
|
||||
});
|
||||
variants.push({
|
||||
id: "stdio-extended-node-island",
|
||||
when: { stdioExtended: true, nodeIsland: true },
|
||||
defines: ["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"],
|
||||
});
|
||||
}
|
||||
// Programs with Node islands link the event loop with the Node island slot;
|
||||
// every other program keeps the default object, byte for byte.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,9 @@
|
||||
* zero bytes and keep their exact historical link line. */
|
||||
#define _XOPEN_SOURCE 700
|
||||
#include "scr_runtime.h"
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
#include "scr_node_island.h"
|
||||
#endif
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
@@ -80,6 +83,11 @@ typedef struct {
|
||||
ScrStr *name; /* aliases of one signum remain distinct event names */
|
||||
bool once;
|
||||
uint64_t id;
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* Nonzero: a listener island code added (its cb is undefined); the
|
||||
* delivery calls it through the bridge at this position. */
|
||||
uint32_t island;
|
||||
#endif
|
||||
} ScrSigListener;
|
||||
|
||||
typedef struct ScrSigReg {
|
||||
@@ -280,6 +288,63 @@ void scr_signal_off(ScrStr *name, ScrDyn *cb) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* Island code's signal listeners (scr_node_island.c forwards Node's
|
||||
* newListener/removeListener for signal events here): each takes its place
|
||||
* in the registry like a native listener, so scriptc owns every signal
|
||||
* disposition and libuv never installs one. Removing the last listener of
|
||||
* either side restores the previous disposition, as for native ones. */
|
||||
void scr_signal_on_island(ScrStr *name, uint32_t island) {
|
||||
#if defined(_WIN32) || defined(__wasi__)
|
||||
(void)name;
|
||||
(void)island;
|
||||
#else
|
||||
if (!scr_context_is_main()) return;
|
||||
int sig = scr_signal_from_name(name);
|
||||
if (sig < 0) sig = 0;
|
||||
ScrSigReg *reg = scr_sig_regs;
|
||||
while (reg && reg->sig != sig) reg = reg->next;
|
||||
if (!reg) {
|
||||
reg = calloc(1, sizeof *reg);
|
||||
if (!reg) scr_events_oom();
|
||||
reg->sig = sig;
|
||||
if (sig > 0) {
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_handler = scr_sig_handler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
if (sig >= SCR_SIG_MAX || sigaction(sig, &sa, ®->prev) != 0) {
|
||||
free(reg);
|
||||
return;
|
||||
}
|
||||
scr_sig_watched++;
|
||||
scr_wake_pipe_init();
|
||||
}
|
||||
reg->next = scr_sig_regs;
|
||||
scr_sig_regs = reg;
|
||||
}
|
||||
if (reg->n == reg->cap) {
|
||||
reg->cap = reg->cap ? reg->cap * 2 : 2;
|
||||
reg->ls = realloc(reg->ls, reg->cap * sizeof *reg->ls);
|
||||
if (!reg->ls) scr_events_oom();
|
||||
}
|
||||
reg->ls[reg->n++] =
|
||||
(ScrSigListener){scr_dyn_retain(scr_dyn_undefined()), scr_str_retain(name), false, ++scr_sig_id, island};
|
||||
#endif
|
||||
}
|
||||
|
||||
void scr_signal_off_island(uint32_t island) {
|
||||
for (ScrSigReg *reg = scr_sig_regs; reg; reg = reg->next) {
|
||||
for (size_t i = 0; i < reg->n; i++) {
|
||||
if (reg->ls[i].island == island) {
|
||||
scr_sig_remove(reg, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Snapshot all pending deliveries before invoking user code. No live
|
||||
* registry pointer survives a callback: off() can remove this signal or
|
||||
* another pending signal and free either registry. */
|
||||
@@ -315,11 +380,19 @@ static void scr_signals_drain(void) {
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
ScrDyn *args[2] = {scr_dyn_new_str(snap[i].name), scr_dyn_new_num(scr_signal_from_name(snap[i].name))};
|
||||
ScrDyn *result = scr_dyn_call(snap[i].cb, args, 2, "listener");
|
||||
scr_dyn_release(result);
|
||||
scr_dyn_release(args[0]);
|
||||
scr_dyn_release(args[1]);
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (snap[i].island != 0) {
|
||||
scr_node_island_emit(SCR_NODE_ISLAND_EVENT_SIGNAL, snap[i].island, snap[i].name,
|
||||
scr_signal_from_name(snap[i].name));
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ScrDyn *args[2] = {scr_dyn_new_str(snap[i].name), scr_dyn_new_num(scr_signal_from_name(snap[i].name))};
|
||||
ScrDyn *result = scr_dyn_call(snap[i].cb, args, 2, "listener");
|
||||
scr_dyn_release(result);
|
||||
scr_dyn_release(args[0]);
|
||||
scr_dyn_release(args[1]);
|
||||
}
|
||||
}
|
||||
scr_dyn_release(snap[i].cb);
|
||||
scr_str_release(snap[i].name);
|
||||
@@ -1541,6 +1614,9 @@ typedef struct {
|
||||
ScrClosure *cb; /* owned */
|
||||
void (*fn)(ScrClosure *cb, double code); /* adapter: 0-param or (code) */
|
||||
bool once;
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
uint32_t island; /* nonzero: island code's listener (cb and fn NULL) */
|
||||
#endif
|
||||
} ScrExitListener;
|
||||
|
||||
static SCR_TL ScrExitListener *scr_exit_ls = NULL;
|
||||
@@ -1554,6 +1630,13 @@ void scr_run_exit_listeners(double code) {
|
||||
for (size_t i = 0; i < scr_exit_n; i++) {
|
||||
#ifdef SCR_WORKERS
|
||||
if (scr_context_stopping()) break;
|
||||
#endif
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (scr_exit_ls[i].island != 0) {
|
||||
ScrStr *exit_name = scr_str_new("exit", 4);
|
||||
scr_node_island_emit(SCR_NODE_ISLAND_EVENT_EXIT, scr_exit_ls[i].island, exit_name, (int)code);
|
||||
scr_str_release(exit_name);
|
||||
} else
|
||||
#endif
|
||||
scr_exit_ls[i].fn(scr_exit_ls[i].cb, code);
|
||||
#ifdef SCR_WORKERS
|
||||
@@ -1571,7 +1654,12 @@ void scr_run_exit_listeners(double code) {
|
||||
_Exit(7);
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < scr_exit_n; i++) scr_closure_release(scr_exit_ls[i].cb);
|
||||
for (size_t i = 0; i < scr_exit_n; i++) {
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (scr_exit_ls[i].cb == NULL) continue;
|
||||
#endif
|
||||
scr_closure_release(scr_exit_ls[i].cb);
|
||||
}
|
||||
free(scr_exit_ls);
|
||||
scr_exit_ls = NULL;
|
||||
scr_exit_n = scr_exit_cap = 0;
|
||||
@@ -1595,11 +1683,43 @@ void scr_process_on_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, doub
|
||||
scr_exit_ls[scr_exit_n].cb = cb;
|
||||
scr_exit_ls[scr_exit_n].fn = fn;
|
||||
scr_exit_ls[scr_exit_n].once = once;
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
scr_exit_ls[scr_exit_n].island = 0;
|
||||
#endif
|
||||
scr_exit_n++;
|
||||
}
|
||||
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* Island code's 'exit' listeners, at their registration position among
|
||||
* the program's own. */
|
||||
void scr_process_on_exit_island(uint32_t island) {
|
||||
if (scr_exit_ran) return;
|
||||
if (scr_exit_n == scr_exit_cap) {
|
||||
scr_exit_cap = scr_exit_cap ? scr_exit_cap * 2 : 2;
|
||||
scr_exit_ls = realloc(scr_exit_ls, scr_exit_cap * sizeof *scr_exit_ls);
|
||||
if (!scr_exit_ls) scr_events_oom();
|
||||
}
|
||||
scr_exit_ls[scr_exit_n] = (ScrExitListener){NULL, NULL, false, island};
|
||||
scr_exit_n++;
|
||||
}
|
||||
|
||||
void scr_process_off_exit_island(uint32_t island) {
|
||||
if (scr_exit_ran) return;
|
||||
for (size_t i = 0; i < scr_exit_n; i++) {
|
||||
if (scr_exit_ls[i].island == island) {
|
||||
memmove(scr_exit_ls + i, scr_exit_ls + i + 1, (scr_exit_n - i - 1) * sizeof *scr_exit_ls);
|
||||
scr_exit_n--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void scr_process_off_exit(ScrClosure *cb /*borrowed*/) {
|
||||
for (size_t i = 0; i < scr_exit_n; i++) {
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (scr_exit_ls[i].cb == NULL) continue;
|
||||
#endif
|
||||
if (scr_closure_identity_equal(scr_exit_ls[i].cb, cb)) {
|
||||
scr_closure_release(scr_exit_ls[i].cb);
|
||||
memmove(scr_exit_ls + i, scr_exit_ls + i + 1, (scr_exit_n - i - 1) * sizeof *scr_exit_ls);
|
||||
|
||||
@@ -9,17 +9,56 @@
|
||||
* are "R<json>" (returned; "R" alone is undefined) or "T<json>" (threw;
|
||||
* the JSON is {"error":{kind,message,code?}}, {"value":...} or
|
||||
* {"undefined":true}, see scr_node_island_host.cjs). Every entry point
|
||||
* runs on the thread that called start. */
|
||||
* runs on the thread that called start, and every host function is called
|
||||
* on it. */
|
||||
#ifndef SCR_NODE_BRIDGE_H
|
||||
#define SCR_NODE_BRIDGE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SCR_NODE_BRIDGE_ABI 3u
|
||||
#define SCR_NODE_BRIDGE_ABI 4u
|
||||
/* The symbol a bridge library exports: `const ScrNodeBridge *(void)`. */
|
||||
#define SCR_NODE_BRIDGE_ENTRY "scr_node_bridge_v1"
|
||||
|
||||
/* Kinds of runtime jobs the bridge schedules on Node's queues (`run`). */
|
||||
enum {
|
||||
SCR_NODE_JOB_MICROTASK = 1, /* V8's microtask queue */
|
||||
SCR_NODE_JOB_TICK = 2, /* Node's process.nextTick queue */
|
||||
SCR_NODE_JOB_TIMER = 3, /* a Node timer (setTimeout / setInterval) */
|
||||
SCR_NODE_JOB_IMMEDIATE = 4, /* Node's check phase (setImmediate) */
|
||||
};
|
||||
|
||||
/* Process events that island code listens to (`listen`): the listener
|
||||
* joins the runtime's own registry at its registration position, so one
|
||||
* registry orders the listeners of both sides and libuv never owns a
|
||||
* signal. */
|
||||
enum {
|
||||
SCR_NODE_EVENT_SIGNAL = 1,
|
||||
SCR_NODE_EVENT_EXIT = 2,
|
||||
};
|
||||
|
||||
/* The runtime's I/O as Node's loop sees it (io_state): the fds to watch
|
||||
* for readability, whether runtime work keeps the loop alive, whether
|
||||
* runtime work is ready without any fd (the loop must not block), and a
|
||||
* bound in milliseconds on the sleep for work no fd announces (-1: none).
|
||||
* The bridge sets `phase` (input): when it asks, so that liveness is
|
||||
* current wherever Node's loop next decides it. */
|
||||
enum {
|
||||
SCR_NODE_IO_PREPARE = 0, /* before a poll phase */
|
||||
SCR_NODE_IO_CHECK = 1, /* after a check phase: the end of a turn */
|
||||
SCR_NODE_IO_JOBS = 2, /* after a microtask checkpoint (any callback) */
|
||||
};
|
||||
#define SCR_NODE_IO_MAX_FDS 16
|
||||
typedef struct ScrNodeIoState {
|
||||
int phase;
|
||||
int fds[SCR_NODE_IO_MAX_FDS];
|
||||
int nfds;
|
||||
int alive;
|
||||
int ready;
|
||||
int cap_ms;
|
||||
} ScrNodeIoState;
|
||||
|
||||
/* What the runtime provides to the bridge. */
|
||||
typedef struct ScrNodeBridgeHost {
|
||||
/* An asynchronous call settled with this outcome. */
|
||||
@@ -27,6 +66,27 @@ typedef struct ScrNodeBridgeHost {
|
||||
/* Node ends the process (process.exit, an uncaught exception): the
|
||||
* runtime runs its exit sequence with this code and never returns. */
|
||||
void (*exit)(int code);
|
||||
/* A runtime job came due on Node's queues (a SCR_NODE_JOB_* kind and the
|
||||
* token the runtime passed when it scheduled it). Runs on the stack of
|
||||
* Node's own dispatch, so Node drains ticks and microtasks after it
|
||||
* exactly as after any of its callbacks. */
|
||||
void (*run)(uint32_t kind, uint32_t token);
|
||||
/* The runtime's I/O hooks: its state before each poll phase and after
|
||||
* each check phase, and one dispatch pass (when a watched fd is readable
|
||||
* or `ready` was set). */
|
||||
void (*io_state)(ScrNodeIoState *state);
|
||||
void (*io_dispatch)(void);
|
||||
/* V8 finished a microtask checkpoint: the runtime reports the unhandled
|
||||
* rejections of its own promises, as Node does at the same point. */
|
||||
void (*microtasks_done)(void);
|
||||
/* Island code added (add=1, once=1 for once listeners) or removed
|
||||
* (add=0) a listener for a process event (SCR_NODE_EVENT_*, `name` its
|
||||
* event name). `listener` identifies it in later `emit` calls. */
|
||||
void (*listen)(uint32_t event, const char *name, uint32_t listener, int add, int once);
|
||||
/* The lowest stack address JavaScript may use on the stack the runtime
|
||||
* is calling in from (its main thread's or an async fiber's), for V8's
|
||||
* stack limit; 0 leaves V8's own. */
|
||||
uintptr_t (*stack_limit)(void);
|
||||
/* The start (highest address) of the stack the caller runs on when the
|
||||
* runtime provides it (Node's own stack), 0 for a thread's own stack.
|
||||
* V8 scans the stack conservatively from the current position up to the
|
||||
@@ -54,20 +114,43 @@ typedef struct ScrNodeBridge {
|
||||
char *(*call)(uint32_t id, int sync, const char *file, const char *specifier,
|
||||
const char *name, const char *args, size_t args_len, size_t *len);
|
||||
void (*release)(void *memory);
|
||||
/* Node's event loop, driven from the runtime's loop until NI-3 hosts the
|
||||
* runtime's queues on it: a pollable backend fd, the timeout to its next
|
||||
* timer (-1: none), whether ref'd work remains, and one non-blocking
|
||||
* turn (libuv callbacks plus platform tasks). */
|
||||
int (*backend_fd)(void);
|
||||
int (*backend_timeout)(void);
|
||||
int (*alive)(void);
|
||||
void (*pump)(void);
|
||||
/* The hosted loop (scope-B NI-3). Once Node is loaded, the runtime's
|
||||
* queues live on Node's: `host_loop` starts the integration (the I/O
|
||||
* handles that watch the runtime's fds, the microtask-checkpoint hook)
|
||||
* and holds a callback scope open until the next `checkpoint`, so work
|
||||
* already pending in the runtime and work the current runtime callback
|
||||
* goes on to queue drain together, at the end of that callback. */
|
||||
void (*host_loop)(void);
|
||||
/* Schedules runtime jobs (SCR_NODE_JOB_*). Timers take Node's delay
|
||||
* coercion; `repeat` > 0 re-arms after each run (setInterval), and
|
||||
* `delay` may differ from `repeat` for intervals migrated mid-period.
|
||||
* Handles are nonzero and name the Node Timeout or Immediate until the
|
||||
* runtime clears them; a cleared or fired handle is never reused. */
|
||||
void (*microtask)(uint32_t token);
|
||||
void (*tick)(uint32_t token);
|
||||
uint32_t (*timer)(double delay, double repeat, int ref, uint32_t token);
|
||||
void (*timer_clear)(uint32_t handle);
|
||||
void (*timer_ref)(uint32_t handle, int ref);
|
||||
void (*timer_refresh)(uint32_t handle);
|
||||
uint32_t (*immediate)(int ref, uint32_t token);
|
||||
void (*immediate_clear)(uint32_t handle);
|
||||
void (*immediate_ref)(uint32_t handle, int ref);
|
||||
/* The checkpoint after a macrotask: closes the scope `host_loop` held
|
||||
* (or opens and closes one), which runs Node's tick queue and V8's
|
||||
* microtasks to exhaustion. `microtasks_first` runs the microtasks once
|
||||
* before the ticks: the end of an ES module main body. */
|
||||
void (*checkpoint)(int microtasks_first);
|
||||
/* Runs Node's event loop until nothing keeps it alive (beforeExit
|
||||
* included). Returns when the program ends normally, with the exit code
|
||||
* island code assigned to process.exitCode (0: none). `continue_turn`:
|
||||
* the switch interrupted a timers or poll phase, so the first iteration
|
||||
* starts at the poll phase instead of the timers phase. */
|
||||
int (*run_loop)(int continue_turn);
|
||||
/* Calls island listener `listener` of a process event: a signal (`name`,
|
||||
* signal number `arg`) or exit (code `arg`). */
|
||||
void (*emit)(uint32_t event, uint32_t listener, const char *name, int arg);
|
||||
/* Tears Node down at process exit. */
|
||||
void (*stop)(void);
|
||||
/* The program ends with `code`, before stop: Node's 'beforeExit' and
|
||||
* 'exit' run, and the result is the code Node's process ends with
|
||||
* (process.exitCode in Node, else `code`). */
|
||||
int (*finish)(int code);
|
||||
} ScrNodeBridge;
|
||||
|
||||
#endif /* SCR_NODE_BRIDGE_H */
|
||||
|
||||
@@ -20,20 +20,36 @@
|
||||
* the bridge (a callback scope, so ticks and microtasks drain when it
|
||||
* returns) and parses the outcome: the result's JSON copy, or the copied
|
||||
* error (see scr_node_island_host.cjs). An asynchronous call registers its
|
||||
* promise first, because the bridge may settle it before returning. Until
|
||||
* NI-3 hosts the runtime's queues on Node's libuv, the runtime's loop drives
|
||||
* Node's: the loop slot keeps the program alive while Node has ref'd work,
|
||||
* sleeps on its backend fd up to its next timer, and runs one non-blocking
|
||||
* turn of it per loop turn.
|
||||
* promise first, because the bridge may settle it before returning.
|
||||
*
|
||||
* One loop (scope-B NI-3). Right after Node starts, scr_loop_host moves the
|
||||
* runtime's pending ticks, promise jobs, timers and immediates onto Node's
|
||||
* queues and from then on schedules them there, and the bridge watches the
|
||||
* runtime's I/O on Node's libuv loop: there is one loop and one set of
|
||||
* queues, so the jobs of both sides interleave as in Node. Calls into
|
||||
* JavaScript do not drain queues themselves; the runtime's checkpoints do
|
||||
* (the bridge's `checkpoint`, and Node's own after every callback it
|
||||
* dispatches).
|
||||
*
|
||||
* Process state is shared, not mirrored: island listeners for signals and
|
||||
* 'exit' join the runtime's registries (scr_events.c) at their position,
|
||||
* so the runtime keeps owning every signal disposition; the exit code
|
||||
* island code leaves in process.exitCode becomes the program's when the
|
||||
* loop ends; Node's process.exit runs the runtime's exit sequence.
|
||||
*
|
||||
* Node's stack. V8 fixes an isolate's stack limit where it is created, and
|
||||
* calls into Node can come from any async fiber (512 KB stacks without a
|
||||
* guard page) or from the main stack. Every entry into Node therefore runs
|
||||
* on one dedicated stack of NI_STACK bytes with a guard page, reached by a
|
||||
* context switch; the runtime's own stack guard follows it there. */
|
||||
* guard page) or from the main stack. Every entry into Node from outside
|
||||
* it runs on one dedicated stack of NI_STACK bytes with a guard page,
|
||||
* reached by a context switch; the runtime's own stack guard follows it
|
||||
* there. Node's loop runs there too once it hosts the runtime's queues, so
|
||||
* a runtime job it dispatches may resume an async fiber that calls into
|
||||
* Node again from the fiber's stack: the bridge sets V8's limit for the
|
||||
* stack each entry comes from (ni_stack_limit). */
|
||||
#define _XOPEN_SOURCE 700
|
||||
#define _DARWIN_C_SOURCE 1
|
||||
#include "scr_runtime.h"
|
||||
#include "scr_events.h"
|
||||
#include "scr_node_bridge.h"
|
||||
#include "scr_node_island.h"
|
||||
|
||||
@@ -76,6 +92,7 @@ void __sanitizer_start_switch_fiber(void **fake_stack_save, const void *bottom,
|
||||
void __sanitizer_finish_switch_fiber(void *fake_stack_save, const void **bottom_old,
|
||||
size_t *size_old);
|
||||
void __lsan_register_root_region(const void *p, size_t size) __attribute__((weak));
|
||||
void __asan_unpoison_memory_region(void const volatile *addr, size_t size);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -773,42 +790,51 @@ static void ni_exit(int code) {
|
||||
ni_fatal("");
|
||||
}
|
||||
|
||||
/* An island listener for a process event was added or removed. */
|
||||
static void ni_listen(uint32_t event, const char *name, uint32_t listener, int add, int once) {
|
||||
(void)once; /* Node's once wrapper removes itself through removeListener */
|
||||
if (event == SCR_NODE_EVENT_SIGNAL) {
|
||||
if (add) {
|
||||
ScrStr *s = scr_str_new(name, strlen(name));
|
||||
scr_signal_on_island(s, listener);
|
||||
scr_str_release(s);
|
||||
} else {
|
||||
scr_signal_off_island(listener);
|
||||
}
|
||||
} else if (event == SCR_NODE_EVENT_EXIT) {
|
||||
if (add) scr_process_on_exit_island(listener);
|
||||
else scr_process_off_exit_island(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/* The start of Node's stack for callers on it (V8's stack-scan bound). */
|
||||
static uintptr_t ni_stack_start(void);
|
||||
|
||||
static const ScrNodeBridgeHost ni_host = {&ni_settle, &ni_exit, &ni_stack_start};
|
||||
/* The main thread's stack, for V8's stack limit (ni_stack_limit). */
|
||||
static uintptr_t ni_main_lo = 0, ni_main_hi = 0;
|
||||
|
||||
/* ── the loop slot ────────────────────────────────────────────────────── */
|
||||
|
||||
static bool ni_alive(void) { return ni_bridge->alive() != 0; }
|
||||
static void ni_pump_job(void *p);
|
||||
static void ni_stop_job(void *p);
|
||||
static void ni_on_node_stack(void (*job)(void *), void *arg);
|
||||
static void ni_pump(void) { ni_on_node_stack(ni_pump_job, NULL); }
|
||||
static int ni_backend_fd(void) { return ni_bridge->backend_fd(); }
|
||||
static int ni_backend_timeout(void) { return ni_bridge->backend_timeout(); }
|
||||
typedef struct {
|
||||
int code;
|
||||
} NiFinish;
|
||||
static void ni_finish_job(void *p);
|
||||
|
||||
/* At exit, after the program's own exit work: Node's process ends with
|
||||
* the program's code ('beforeExit', then 'exit'), and Node stops. A code
|
||||
* Node's process changed (process.exitCode in Node) is the program's. */
|
||||
static void ni_stop_atexit(void) {
|
||||
int code = (int)scr_process_exit_code_or_zero();
|
||||
NiFinish finish = {code};
|
||||
/* Node writes to the same stdout. */
|
||||
fflush(stdout);
|
||||
ni_on_node_stack(ni_finish_job, &finish);
|
||||
ni_on_node_stack(ni_stop_job, NULL);
|
||||
if (finish.code != code) {
|
||||
fflush(NULL);
|
||||
if (ni_temp_root[0] != '\0') ni_remove_temp_root();
|
||||
_Exit(finish.code & 0xff);
|
||||
static void ni_main_stack(void) {
|
||||
#if defined(__APPLE__)
|
||||
pthread_t self = pthread_self();
|
||||
ni_main_hi = (uintptr_t)pthread_get_stackaddr_np(self);
|
||||
ni_main_lo = ni_main_hi - pthread_get_stacksize_np(self);
|
||||
#elif defined(__linux__)
|
||||
pthread_attr_t attr;
|
||||
if (pthread_getattr_np(pthread_self(), &attr) == 0) {
|
||||
void *addr = NULL;
|
||||
size_t size = 0;
|
||||
if (pthread_attr_getstack(&attr, &addr, &size) == 0) {
|
||||
ni_main_lo = (uintptr_t)addr;
|
||||
ni_main_hi = ni_main_lo + size;
|
||||
}
|
||||
pthread_attr_destroy(&attr);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void ni_on_node_stack(void (*job)(void *), void *arg);
|
||||
static const ScrNodeBridgeHost ni_host;
|
||||
|
||||
/* ── Node's stack ─────────────────────────────────────────────────────── */
|
||||
|
||||
static char *ni_stack = NULL; /* the mapping; usable above NI_GUARD */
|
||||
@@ -857,17 +883,103 @@ static void ni_trampoline(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static uintptr_t ni_stack_start(void) {
|
||||
uintptr_t sp = (uintptr_t)__builtin_frame_address(0);
|
||||
return ni_stack != NULL && sp >= (uintptr_t)ni_stack && sp < (uintptr_t)ni_stack + NI_STACK
|
||||
? (uintptr_t)ni_stack + NI_STACK
|
||||
: 0;
|
||||
/* Whether `sp` lies on Node's stack. */
|
||||
static bool ni_stack_has(uintptr_t sp) {
|
||||
return ni_stack != NULL && sp > (uintptr_t)ni_stack && sp <= (uintptr_t)ni_stack + NI_STACK;
|
||||
}
|
||||
|
||||
/* The lowest address of Node's stack in use while runtime code Node's
|
||||
* frames called runs (a host callback below), 0 outside them: the
|
||||
* callback's frame, or lower, where that code switched to a fiber's stack
|
||||
* (scr_node_island_leave_stack) and left its own frames live. Runtime code
|
||||
* may resume an async fiber there, and the fiber may call into Node again:
|
||||
* that entry runs on Node's stack below this mark, so every frame of Node
|
||||
* stays on the one stack V8 scans (heap::base::Stack, bounded by
|
||||
* ni_stack_start), never on a fiber's stack beyond its bound. */
|
||||
static uintptr_t ni_mark = 0;
|
||||
|
||||
/* Host callbacks run runtime code from Node's frames: each records the
|
||||
* mark for its duration. */
|
||||
#define NI_MARK_BEGIN \
|
||||
uintptr_t ni_saved_mark = ni_mark; \
|
||||
do { \
|
||||
uintptr_t ni_here = (uintptr_t)__builtin_frame_address(0); \
|
||||
if (ni_stack_has(ni_here)) ni_mark = ni_here; \
|
||||
} while (0)
|
||||
#define NI_MARK_END (ni_mark = ni_saved_mark)
|
||||
|
||||
void scr_node_island_leave_stack(uintptr_t sp) {
|
||||
if (ni_mark != 0 && ni_stack_has(sp) && sp < ni_mark) ni_mark = sp;
|
||||
}
|
||||
|
||||
/* A nested entry: job(arg) on Node's stack below the mark, from a fiber. */
|
||||
static void (*ni_nested_job)(void *) = NULL;
|
||||
static void *ni_nested_arg = NULL;
|
||||
static ucontext_t *ni_nested_return = NULL;
|
||||
|
||||
#ifdef NI_ASAN
|
||||
static uintptr_t ni_nested_top = 0; /* the nested context's stack top */
|
||||
#endif
|
||||
|
||||
static void ni_nested_trampoline(void) {
|
||||
#ifdef NI_ASAN
|
||||
const void *caller_bottom;
|
||||
size_t caller_size;
|
||||
uintptr_t top = ni_nested_top;
|
||||
__sanitizer_finish_switch_fiber(NULL, &caller_bottom, &caller_size);
|
||||
#endif
|
||||
void (*job)(void *) = ni_nested_job;
|
||||
void *arg = ni_nested_arg;
|
||||
ucontext_t *back = ni_nested_return;
|
||||
job(arg);
|
||||
#ifdef NI_ASAN
|
||||
/* This context ends: no fake stack to keep. Its frame never returns, so
|
||||
* the redzones ASan poisoned around this frame's locals would stay in
|
||||
* the shadow of Node's stack, where Node's own frames (uninstrumented)
|
||||
* later trip over them. Linux's ASan clears a context's stack when it is
|
||||
* switched to (its swapcontext interceptor); macOS's has none, so this
|
||||
* frame's part of the stack is unpoisoned here. */
|
||||
uintptr_t low = ((uintptr_t)__builtin_frame_address(0) - 4096) & ~(uintptr_t)7;
|
||||
if (top > low) __asan_unpoison_memory_region((const void *)low, top - low);
|
||||
__sanitizer_start_switch_fiber(NULL, caller_bottom, caller_size);
|
||||
#endif
|
||||
setcontext(back);
|
||||
}
|
||||
|
||||
static void ni_on_node_stack_below_mark(void (*job)(void *), void *arg) {
|
||||
ucontext_t nested, caller;
|
||||
char *lo = ni_stack + NI_GUARD;
|
||||
/* Below the red zone and 16-byte aligned, as makecontext expects. */
|
||||
uintptr_t hi = (ni_mark - 512) & ~(uintptr_t)15;
|
||||
ni_make_context(&nested, lo, hi - (uintptr_t)lo, ni_nested_trampoline);
|
||||
#ifdef NI_ASAN
|
||||
ni_nested_top = hi;
|
||||
#endif
|
||||
ni_nested_job = job;
|
||||
ni_nested_arg = arg;
|
||||
ni_nested_return = &caller;
|
||||
uintptr_t guard = scr_stack_guard;
|
||||
SCR_STACK_GUARD_SET(scr_stack_guard_for((uintptr_t)lo, hi - (uintptr_t)lo));
|
||||
#ifdef NI_ASAN
|
||||
void *caller_fake = NULL;
|
||||
__sanitizer_start_switch_fiber(&caller_fake, lo, hi - (uintptr_t)lo);
|
||||
#endif
|
||||
swapcontext(&caller, &nested);
|
||||
#ifdef NI_ASAN
|
||||
const void *bottom;
|
||||
size_t size;
|
||||
__sanitizer_finish_switch_fiber(caller_fake, &bottom, &size);
|
||||
#endif
|
||||
SCR_STACK_GUARD_SET(guard);
|
||||
}
|
||||
|
||||
/* Runs job(arg) on Node's stack (directly when already there). */
|
||||
static void ni_on_node_stack(void (*job)(void *), void *arg) {
|
||||
if (ni_on_stack) {
|
||||
job(arg);
|
||||
if (ni_mark != 0 && !ni_stack_has((uintptr_t)__builtin_frame_address(0)))
|
||||
ni_on_node_stack_below_mark(job, arg);
|
||||
else
|
||||
job(arg);
|
||||
return;
|
||||
}
|
||||
if (ni_stack == NULL) {
|
||||
@@ -974,9 +1086,102 @@ static void ni_call_job(void *p) {
|
||||
ni_bridge->call(a->id, a->sync, a->file, a->specifier, a->name, a->args, a->args_len, &a->len);
|
||||
}
|
||||
|
||||
static void ni_pump_job(void *p) {
|
||||
(void)p;
|
||||
ni_bridge->pump();
|
||||
/* V8's stack limit for the stack an entry comes from: Node's own stack
|
||||
* (a reserve above its guard page), the main thread's (Node's default
|
||||
* --stack-size, 984 KiB, below its top), or an async fiber's (the
|
||||
* runtime's guard for it: its mapping's start plus the reserve compiled
|
||||
* code keeps). */
|
||||
static uintptr_t ni_stack_limit(void) {
|
||||
/* The frame address, not a local's: under ASan's use-after-return
|
||||
* detection locals live on a fake stack elsewhere in memory. */
|
||||
uintptr_t sp = (uintptr_t)__builtin_frame_address(0);
|
||||
if (ni_stack_has(sp)) return (uintptr_t)(ni_stack + NI_GUARD) + NI_STACK / 8;
|
||||
if (sp > ni_main_lo && sp <= ni_main_hi) {
|
||||
uintptr_t limit = ni_main_hi > 984 * 1024 ? ni_main_hi - 984 * 1024 : 0;
|
||||
return limit > ni_main_lo ? limit : ni_main_lo + 64 * 1024;
|
||||
}
|
||||
if (scr_stack_guard != 0 && scr_stack_guard < sp) return scr_stack_guard;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The start of Node's stack for callers on it (V8's stack-scan bound). */
|
||||
static uintptr_t ni_stack_start(void) {
|
||||
uintptr_t sp = (uintptr_t)__builtin_frame_address(0);
|
||||
return ni_stack_has(sp) ? (uintptr_t)ni_stack + NI_STACK : 0;
|
||||
}
|
||||
|
||||
/* The host callbacks that run runtime code, with the mark recorded. */
|
||||
static void ni_host_settle(uint32_t id, const char *outcome, size_t len) {
|
||||
NI_MARK_BEGIN;
|
||||
ni_settle(id, outcome, len);
|
||||
NI_MARK_END;
|
||||
}
|
||||
|
||||
static void ni_host_exit(int code) {
|
||||
NI_MARK_BEGIN; /* never returns */
|
||||
(void)ni_saved_mark;
|
||||
ni_exit(code);
|
||||
}
|
||||
|
||||
static void ni_host_run(uint32_t kind, uint32_t token) {
|
||||
NI_MARK_BEGIN;
|
||||
scr_loop_hosted_run(kind, token);
|
||||
NI_MARK_END;
|
||||
}
|
||||
|
||||
static void ni_host_io(void) {
|
||||
NI_MARK_BEGIN;
|
||||
scr_loop_hosted_io();
|
||||
NI_MARK_END;
|
||||
}
|
||||
|
||||
static void ni_host_microtasks_done(void) {
|
||||
NI_MARK_BEGIN;
|
||||
scr_loop_hosted_microtasks_done();
|
||||
NI_MARK_END;
|
||||
}
|
||||
|
||||
static void ni_host_listen(uint32_t event, const char *name, uint32_t listener, int add,
|
||||
int once) {
|
||||
NI_MARK_BEGIN;
|
||||
ni_listen(event, name, listener, add, once);
|
||||
NI_MARK_END;
|
||||
}
|
||||
|
||||
static const ScrNodeBridgeHost ni_host = {
|
||||
&ni_host_settle,
|
||||
&ni_host_exit,
|
||||
&ni_host_run,
|
||||
&scr_loop_hosted_io_state,
|
||||
&ni_host_io,
|
||||
&ni_host_microtasks_done,
|
||||
&ni_host_listen,
|
||||
&ni_stack_limit,
|
||||
&ni_stack_start,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint32_t event, listener;
|
||||
const char *name;
|
||||
int arg;
|
||||
} NiEmit;
|
||||
|
||||
static void ni_emit_job(void *p) {
|
||||
NiEmit *e = p;
|
||||
ni_bridge->emit(e->event, e->listener, e->name, e->arg);
|
||||
}
|
||||
|
||||
/* An island listener of a process event, on Node's stack (an exit listener
|
||||
* runs from the runtime's exit sequence, on whatever stack called it). */
|
||||
void scr_node_island_emit(uint32_t event, uint32_t listener, ScrStr *name, int arg) {
|
||||
if (ni_bridge == NULL) return;
|
||||
char *n = malloc(name->len + 1);
|
||||
if (n == NULL) ni_oom();
|
||||
memcpy(n, name->data, name->len);
|
||||
n[name->len] = '\0';
|
||||
NiEmit e = {event, listener, n, arg};
|
||||
ni_on_node_stack(ni_emit_job, &e);
|
||||
free(n);
|
||||
}
|
||||
|
||||
static void ni_stop_job(void *p) {
|
||||
@@ -984,15 +1189,108 @@ static void ni_stop_job(void *p) {
|
||||
ni_bridge->stop();
|
||||
}
|
||||
|
||||
static void ni_finish_job(void *p) {
|
||||
NiFinish *a = p;
|
||||
a->code = ni_bridge->finish(a->code);
|
||||
/* Process end after a normal return: the 'exit' listeners of both sides
|
||||
* run while Node can still call into JavaScript (this handler runs before
|
||||
* the events unit's own, which then finds them done), then Node stops. */
|
||||
static void ni_stop_atexit(void) {
|
||||
scr_run_exit_listeners((double)scr_exit_code_hint_get());
|
||||
ni_on_node_stack(ni_stop_job, NULL);
|
||||
}
|
||||
|
||||
/* The hosted loop's run_loop on Node's stack. */
|
||||
typedef struct {
|
||||
int continue_turn, code;
|
||||
} NiRunLoop;
|
||||
|
||||
static void ni_run_loop_job(void *p) {
|
||||
NiRunLoop *a = p;
|
||||
a->code = ni_bridge->run_loop(a->continue_turn);
|
||||
}
|
||||
|
||||
static int ni_run_loop(int continue_turn) {
|
||||
NiRunLoop a = {continue_turn, 0};
|
||||
ni_on_node_stack(ni_run_loop_job, &a);
|
||||
return a.code;
|
||||
}
|
||||
|
||||
/* The bridge as the hosted loop sees it. Every entry that runs JavaScript
|
||||
* (scheduling a tick, timer or immediate goes through the bootstrap's
|
||||
* helpers; a checkpoint drains queues; Node's loop) runs on Node's stack,
|
||||
* below the mark when it comes from an async fiber that a host callback
|
||||
* resumed, like every other entry: V8 scans only Node's stack, so no frame
|
||||
* of Node may live on a fiber's stack or the main thread's. */
|
||||
typedef struct {
|
||||
int op;
|
||||
uint32_t a, b;
|
||||
double d0, d1;
|
||||
int i0;
|
||||
uint32_t result;
|
||||
} NiHosted;
|
||||
|
||||
enum {
|
||||
NI_H_MICROTASK,
|
||||
NI_H_TICK,
|
||||
NI_H_TIMER,
|
||||
NI_H_TIMER_CLEAR,
|
||||
NI_H_TIMER_REF,
|
||||
NI_H_TIMER_REFRESH,
|
||||
NI_H_IMMEDIATE,
|
||||
NI_H_IMMEDIATE_CLEAR,
|
||||
NI_H_IMMEDIATE_REF,
|
||||
NI_H_CHECKPOINT,
|
||||
};
|
||||
|
||||
static void ni_hosted_job(void *p) {
|
||||
NiHosted *h = p;
|
||||
switch (h->op) {
|
||||
case NI_H_MICROTASK: ni_bridge->microtask(h->a); break;
|
||||
case NI_H_TICK: ni_bridge->tick(h->a); break;
|
||||
case NI_H_TIMER: h->result = ni_bridge->timer(h->d0, h->d1, h->i0, h->a); break;
|
||||
case NI_H_TIMER_CLEAR: ni_bridge->timer_clear(h->a); break;
|
||||
case NI_H_TIMER_REF: ni_bridge->timer_ref(h->a, h->i0); break;
|
||||
case NI_H_TIMER_REFRESH: ni_bridge->timer_refresh(h->a); break;
|
||||
case NI_H_IMMEDIATE: h->result = ni_bridge->immediate(h->i0, h->a); break;
|
||||
case NI_H_IMMEDIATE_CLEAR: ni_bridge->immediate_clear(h->a); break;
|
||||
case NI_H_IMMEDIATE_REF: ni_bridge->immediate_ref(h->a, h->i0); break;
|
||||
case NI_H_CHECKPOINT: ni_bridge->checkpoint(h->i0); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t ni_hosted_run(int op, uint32_t a, double d0, double d1, int i0) {
|
||||
NiHosted h = {op, a, 0, d0, d1, i0, 0};
|
||||
ni_on_node_stack(ni_hosted_job, &h);
|
||||
return h.result;
|
||||
}
|
||||
|
||||
static void ni_h_microtask(uint32_t token) { ni_hosted_run(NI_H_MICROTASK, token, 0, 0, 0); }
|
||||
static void ni_h_tick(uint32_t token) { ni_hosted_run(NI_H_TICK, token, 0, 0, 0); }
|
||||
static uint32_t ni_h_timer(double delay, double repeat, int ref, uint32_t token) {
|
||||
return ni_hosted_run(NI_H_TIMER, token, delay, repeat, ref);
|
||||
}
|
||||
static void ni_h_timer_clear(uint32_t h) { ni_hosted_run(NI_H_TIMER_CLEAR, h, 0, 0, 0); }
|
||||
static void ni_h_timer_ref(uint32_t h, int ref) { ni_hosted_run(NI_H_TIMER_REF, h, 0, 0, ref); }
|
||||
static void ni_h_timer_refresh(uint32_t h) { ni_hosted_run(NI_H_TIMER_REFRESH, h, 0, 0, 0); }
|
||||
static uint32_t ni_h_immediate(int ref, uint32_t token) {
|
||||
return ni_hosted_run(NI_H_IMMEDIATE, token, 0, 0, ref);
|
||||
}
|
||||
static void ni_h_immediate_clear(uint32_t h) { ni_hosted_run(NI_H_IMMEDIATE_CLEAR, h, 0, 0, 0); }
|
||||
static void ni_h_immediate_ref(uint32_t h, int ref) {
|
||||
ni_hosted_run(NI_H_IMMEDIATE_REF, h, 0, 0, ref);
|
||||
}
|
||||
static void ni_h_checkpoint(int microtasks_first) {
|
||||
ni_hosted_run(NI_H_CHECKPOINT, 0, 0, 0, microtasks_first);
|
||||
}
|
||||
|
||||
static ScrNodeBridge ni_hosted;
|
||||
|
||||
/* ── loading Node ─────────────────────────────────────────────────────── */
|
||||
|
||||
static void ni_start(void) {
|
||||
if (ni_bridge != NULL) return;
|
||||
/* Timers pending now keep their deadlines relative to this moment: the
|
||||
* time Node takes to load is invisible to their order. */
|
||||
double since = scr_now_ms();
|
||||
const char *dir = ni_pack_dir();
|
||||
ni_open_modules();
|
||||
char path[4352];
|
||||
@@ -1035,6 +1333,7 @@ static void ni_start(void) {
|
||||
/* Flush first: Node writes to the same stdout. */
|
||||
fflush(stdout);
|
||||
ni_bridge = bridge;
|
||||
ni_main_stack();
|
||||
char *bootstrap = ni_bootstrap();
|
||||
NiStart start = {bootstrap, argv0, NULL};
|
||||
ni_on_node_stack(ni_start_job, &start);
|
||||
@@ -1046,8 +1345,24 @@ static void ni_start(void) {
|
||||
bridge->release(error);
|
||||
ni_fatal(message);
|
||||
}
|
||||
/* Island listeners need the runtime's event registries and their
|
||||
* delivery; installed before the stop handler, whose 'exit' listeners
|
||||
* run first. */
|
||||
scr_events_install();
|
||||
scr_atexit(ni_stop_atexit);
|
||||
scr_loop_set_node_island(&ni_alive, &ni_pump, &ni_backend_fd, &ni_backend_timeout);
|
||||
ni_hosted = *bridge;
|
||||
ni_hosted.run_loop = &ni_run_loop;
|
||||
ni_hosted.microtask = &ni_h_microtask;
|
||||
ni_hosted.tick = &ni_h_tick;
|
||||
ni_hosted.timer = &ni_h_timer;
|
||||
ni_hosted.timer_clear = &ni_h_timer_clear;
|
||||
ni_hosted.timer_ref = &ni_h_timer_ref;
|
||||
ni_hosted.timer_refresh = &ni_h_timer_refresh;
|
||||
ni_hosted.immediate = &ni_h_immediate;
|
||||
ni_hosted.immediate_clear = &ni_h_immediate_clear;
|
||||
ni_hosted.immediate_ref = &ni_h_immediate_ref;
|
||||
ni_hosted.checkpoint = &ni_h_checkpoint;
|
||||
scr_loop_host(&ni_hosted, since);
|
||||
}
|
||||
|
||||
/* ── calls ────────────────────────────────────────────────────────────── */
|
||||
@@ -1141,6 +1456,13 @@ ScrPromise *scr_node_island_call(ScrStr *file, ScrStr *specifier, ScrStr *name,
|
||||
|
||||
#else /* _WIN32 || __wasi__ */
|
||||
|
||||
void scr_node_island_emit(uint32_t event, uint32_t listener, ScrStr *name, int arg) {
|
||||
(void)event;
|
||||
(void)listener;
|
||||
(void)name;
|
||||
(void)arg;
|
||||
}
|
||||
|
||||
/* Node islands need a libnode pack, which these targets do not have: the
|
||||
* compiler refuses them there, so these entry points only report that. */
|
||||
void scr_node_island_install(const char *libnode, const char *node_version) {
|
||||
|
||||
@@ -34,10 +34,36 @@ ScrDyn *scr_node_island_call_sync(ScrStr *file, ScrStr *specifier, ScrStr *name,
|
||||
* error (also when the arguments cannot be copied). Never throws. */
|
||||
ScrPromise *scr_node_island_call(ScrStr *file, ScrStr *specifier, ScrStr *name, ScrDyn *args);
|
||||
|
||||
/* The loop slot (SCR_NODE_ISLANDS build of scr_async.c): `pending` keeps
|
||||
* the loop alive, `dispatch` runs at every turn top, `pollfd` joins the
|
||||
* idle poll and `timeout` (milliseconds, -1 for none) bounds its sleep. */
|
||||
void scr_loop_set_node_island(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void),
|
||||
int (*timeout)(void));
|
||||
/* The hosted loop (SCR_NODE_ISLANDS build of scr_async.c, scope-B NI-3).
|
||||
* scr_loop_host moves the runtime's pending ticks, promise jobs, timers
|
||||
* and immediates onto Node's queues through `bridge` and schedules every
|
||||
* later one there; `since_ms` (scr_now_ms() before Node began loading) is
|
||||
* the clock pending timers count their remaining delay from. The rest are
|
||||
* the bridge's host callbacks (scr_node_bridge.h). */
|
||||
struct ScrNodeBridge;
|
||||
struct ScrNodeIoState;
|
||||
void scr_loop_host(const struct ScrNodeBridge *bridge, double since_ms);
|
||||
bool scr_loop_is_hosted(void);
|
||||
void scr_loop_hosted_run(uint32_t kind, uint32_t token);
|
||||
void scr_loop_hosted_io_state(struct ScrNodeIoState *state);
|
||||
void scr_loop_hosted_io(void);
|
||||
void scr_loop_hosted_microtasks_done(void);
|
||||
|
||||
/* Runtime code switches stacks (scr_switch) at `sp`: when that is Node's
|
||||
* stack, the frames above stay live until it switches back, so a call
|
||||
* into Node.js from the other stack runs below them. */
|
||||
void scr_node_island_leave_stack(uintptr_t sp);
|
||||
|
||||
/* Process events island code listens to (SCR_NODE_EVENT_* of the bridge
|
||||
* ABI). Their listeners sit in the runtime's registries (the
|
||||
* SCR_NODE_ISLANDS build of scr_events.c) at their registration position,
|
||||
* and a delivery calls them through scr_node_island_emit: a signal with
|
||||
* its number, exit with the code. */
|
||||
enum { SCR_NODE_ISLAND_EVENT_SIGNAL = 1, SCR_NODE_ISLAND_EVENT_EXIT = 2 };
|
||||
void scr_node_island_emit(uint32_t event, uint32_t listener, ScrStr *name, int arg);
|
||||
void scr_signal_on_island(ScrStr *name, uint32_t listener);
|
||||
void scr_signal_off_island(uint32_t listener);
|
||||
void scr_process_on_exit_island(uint32_t listener);
|
||||
void scr_process_off_exit_island(uint32_t listener);
|
||||
|
||||
#endif /* SCR_NODE_ISLAND_H */
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
* scr_node_island_host.h embeds this file as a C string; regenerate it
|
||||
* with `node scripts/node-island-host.mjs` after every change.
|
||||
*
|
||||
* The libnode bridge (scr_node_bridge.cc) installs a native `settle`
|
||||
* function as globalThis.__scriptcNodeIslandHost before this script runs,
|
||||
* and takes the dispatcher this script leaves as
|
||||
* globalThis.__scriptcNodeIsland afterwards; it deletes both globals.
|
||||
* The libnode bridge (scr_node_bridge.cc) installs its natives (`settle`,
|
||||
* and for the hosted loop `run` and `listen`) as
|
||||
* globalThis.__scriptcNodeIslandHost before this script runs, and takes the
|
||||
* dispatcher this script leaves as globalThis.__scriptcNodeIsland
|
||||
* afterwards (`call` and the hosted-loop helpers); it deletes both globals.
|
||||
*
|
||||
* A copy of the call semantics lives in packages/node/index.js, which runs
|
||||
* them in-process under Node: both must produce the same results, errors
|
||||
* and messages. Outcomes are "R<json>" (returned; "R" alone is undefined)
|
||||
* or "T<json>" (threw), the encoding of scr_node_bridge.h. */
|
||||
const { createRequire, registerHooks } = require("node:module");
|
||||
const { constants: osConstants } = require("node:os");
|
||||
const timers = require("node:timers");
|
||||
|
||||
const nativeHost = globalThis.__scriptcNodeIslandHost;
|
||||
|
||||
@@ -154,6 +157,202 @@ function call(id, sync, file, specifier, name, argsJson) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ── the hosted loop (scope-B NI-3) ─────────────────────────────────────
|
||||
// The runtime's ticks, timers and immediates are Node's, created here with
|
||||
// Node's own timers (captured now, so island code replacing the globals
|
||||
// does not reach them) and run through the native `run`. Handles name the
|
||||
// Node objects until the runtime clears them.
|
||||
|
||||
const JOB_TICK = 2;
|
||||
const JOB_TIMER = 3;
|
||||
const JOB_IMMEDIATE = 4;
|
||||
const handles = new Map();
|
||||
let nextHandle = 1;
|
||||
|
||||
function onTick(token) {
|
||||
nativeHost.run(JOB_TICK, token);
|
||||
}
|
||||
|
||||
function onTimer(token) {
|
||||
nativeHost.run(JOB_TIMER, token);
|
||||
}
|
||||
|
||||
function onImmediate(handle, token) {
|
||||
handles.delete(handle);
|
||||
nativeHost.run(JOB_IMMEDIATE, token);
|
||||
}
|
||||
|
||||
function setTimer(delay, repeat, ref, token) {
|
||||
let timer;
|
||||
if (repeat > 0 && delay === repeat) {
|
||||
timer = timers.setInterval(onTimer, repeat, token);
|
||||
} else {
|
||||
timer = timers.setTimeout(onTimer, delay, token);
|
||||
// An interval migrated mid-period: its first run comes after the rest
|
||||
// of the period, later ones every `repeat` (Node re-arms a Timeout
|
||||
// whose _repeat is set, exactly as for setInterval).
|
||||
if (repeat > 0) timer._repeat = repeat;
|
||||
}
|
||||
if (!ref) timer.unref();
|
||||
const handle = nextHandle++;
|
||||
handles.set(handle, timer);
|
||||
return handle;
|
||||
}
|
||||
|
||||
function clearTimer(handle) {
|
||||
const timer = handles.get(handle);
|
||||
if (timer === undefined) return;
|
||||
handles.delete(handle);
|
||||
timers.clearTimeout(timer);
|
||||
}
|
||||
|
||||
function refTimer(handle, ref) {
|
||||
const timer = handles.get(handle);
|
||||
if (timer !== undefined) {
|
||||
if (ref) timer.ref();
|
||||
else timer.unref();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshTimer(handle) {
|
||||
const timer = handles.get(handle);
|
||||
if (timer !== undefined) timer.refresh();
|
||||
}
|
||||
|
||||
function setImmediateJob(ref, token) {
|
||||
const handle = nextHandle++;
|
||||
const immediate = timers.setImmediate(onImmediate, handle, token);
|
||||
if (!ref) immediate.unref();
|
||||
handles.set(handle, immediate);
|
||||
return handle;
|
||||
}
|
||||
|
||||
function clearImmediateJob(handle) {
|
||||
const immediate = handles.get(handle);
|
||||
if (immediate === undefined) return;
|
||||
handles.delete(handle);
|
||||
timers.clearImmediate(immediate);
|
||||
}
|
||||
|
||||
function refImmediate(handle, ref) {
|
||||
refTimer(handle, ref);
|
||||
}
|
||||
|
||||
function nextTick(token) {
|
||||
process.nextTick(onTick, token);
|
||||
}
|
||||
|
||||
// ── process state (scope-B NI-3) ─────────────────────────────────────────
|
||||
// Signal and 'exit' listeners of island code join the runtime's registries
|
||||
// at their registration position, so one registry orders both sides and
|
||||
// the runtime keeps owning every signal disposition: Node's own handlers,
|
||||
// which would hand a signal to libuv and reset it to SIG_DFL when the last
|
||||
// listener goes, are taken off. The listeners stay on `process` (listeners
|
||||
// and listenerCount answer as in Node); the runtime calls them through
|
||||
// `emit`, and Node's own 'exit' emission is left to the runtime's exit
|
||||
// sequence.
|
||||
|
||||
const EVENT_SIGNAL = 1;
|
||||
const EVENT_EXIT = 2;
|
||||
const signalNames = new Set(Object.keys(osConstants.signals));
|
||||
const islandListeners = [];
|
||||
let nextListener = 1;
|
||||
|
||||
function eventKind(event) {
|
||||
if (event === "exit") return EVENT_EXIT;
|
||||
return typeof event === "string" && signalNames.has(event) ? EVENT_SIGNAL : 0;
|
||||
}
|
||||
|
||||
for (const [event, name] of [
|
||||
["newListener", "startListeningIfSignal"],
|
||||
["removeListener", "stopListeningIfSignal"],
|
||||
]) {
|
||||
for (const listener of process.listeners(event)) {
|
||||
if (listener.name === name) process.removeListener(event, listener);
|
||||
}
|
||||
}
|
||||
|
||||
process.on("newListener", (event, listener) => {
|
||||
const kind = eventKind(event);
|
||||
if (kind === 0) return;
|
||||
const id = nextListener++;
|
||||
islandListeners.push({ event, listener, id, raw: undefined });
|
||||
nativeHost.listen(kind, event, id, 1, 0);
|
||||
});
|
||||
|
||||
function forget(i, kind) {
|
||||
const entry = islandListeners[i];
|
||||
islandListeners.splice(i, 1);
|
||||
nativeHost.listen(kind, entry.event, entry.id, 0, 0);
|
||||
}
|
||||
|
||||
process.on("removeListener", (event, listener) => {
|
||||
const kind = eventKind(event);
|
||||
if (kind === 0) return;
|
||||
// Node reports the removed registration as the raw listener it installed (a
|
||||
// once wrapper removing itself reports the wrapper) or as the listener
|
||||
// itself. The raw listener each entry recorded separates a once
|
||||
// registration from an `on` registration of the same function: the removed
|
||||
// one is the entry whose raw listener is no longer on `process`.
|
||||
const live = process.rawListeners(event);
|
||||
let same = -1;
|
||||
for (let i = islandListeners.length - 1; i >= 0; i--) {
|
||||
const entry = islandListeners[i];
|
||||
if (entry.event !== event) continue;
|
||||
if (entry.listener !== listener && entry.raw !== listener) continue;
|
||||
if (entry.raw !== undefined && !live.includes(entry.raw)) {
|
||||
forget(i, kind);
|
||||
return;
|
||||
}
|
||||
if (same < 0) same = i;
|
||||
}
|
||||
// Duplicate registrations of one function share a raw listener; Node
|
||||
// removes the most recent, which is the last matching entry.
|
||||
if (same >= 0) forget(same, kind);
|
||||
});
|
||||
|
||||
// The raw listener is the function handed to the emitter (a once wrapper
|
||||
// for process.once, which registers through process.on): tag each entry
|
||||
// with it as its registration returns. Node emits newListener before it
|
||||
// installs the listener, so the entry learns its raw only afterwards.
|
||||
for (const name of ["on", "addListener", "prependListener"]) {
|
||||
const add = process[name];
|
||||
process[name] = function addListener(event, listener) {
|
||||
const start = islandListeners.length;
|
||||
const result = Reflect.apply(add, this, arguments);
|
||||
if (islandListeners.length > start) {
|
||||
const registered = listener.listener ?? listener;
|
||||
for (let i = start; i < islandListeners.length; i++) {
|
||||
const entry = islandListeners[i];
|
||||
if (entry.event === event && entry.listener === registered && entry.raw === undefined) {
|
||||
entry.raw = listener;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function emit(kind, id, name, arg) {
|
||||
const entry = islandListeners.find((e) => e.id === id);
|
||||
if (entry === undefined) return;
|
||||
// The registered function: the listener itself, or its once wrapper,
|
||||
// which removes itself before calling it.
|
||||
const registered =
|
||||
entry.raw ??
|
||||
process.rawListeners(entry.event).find((raw) => (raw.listener ?? raw) === entry.listener);
|
||||
if (registered === undefined) return;
|
||||
if (kind === EVENT_SIGNAL) Reflect.apply(registered, process, [name, arg]);
|
||||
else Reflect.apply(registered, process, [arg]);
|
||||
}
|
||||
|
||||
const nodeEmit = process.emit;
|
||||
process.emit = function emit(event, ...args) {
|
||||
if (event === "exit") return this.listenerCount("exit") > 0;
|
||||
return Reflect.apply(nodeEmit, this, [event, ...args]);
|
||||
};
|
||||
|
||||
// Writes reach the shared stdout and stderr before control returns to the
|
||||
// program, as the program's own writes do.
|
||||
for (const stream of [process.stdout, process.stderr]) {
|
||||
@@ -161,13 +360,6 @@ for (const stream of [process.stdout, process.stderr]) {
|
||||
stream._handle.setBlocking(true);
|
||||
}
|
||||
|
||||
// The program ends with `code` (the bridge then runs Node's loop to its
|
||||
// end, which emits 'beforeExit' and 'exit'): Node's process shares it, as
|
||||
// it shares one process under Node.
|
||||
function finish(code) {
|
||||
if (code !== 0) process.exitCode = code;
|
||||
}
|
||||
|
||||
// process.execPath is this program, not a node executable: a fork without
|
||||
// an execPath of its own would start the program again.
|
||||
const childProcess = require("node:child_process");
|
||||
@@ -185,4 +377,15 @@ childProcess.fork = function fork(modulePath, args, options) {
|
||||
};
|
||||
require("node:module").syncBuiltinESMExports();
|
||||
|
||||
globalThis.__scriptcNodeIsland = { call, finish };
|
||||
globalThis.__scriptcNodeIsland = {
|
||||
call,
|
||||
setTimer,
|
||||
clearTimer,
|
||||
refTimer,
|
||||
refreshTimer,
|
||||
setImmediate: setImmediateJob,
|
||||
clearImmediate: clearImmediateJob,
|
||||
refImmediate,
|
||||
nextTick,
|
||||
emit,
|
||||
};
|
||||
|
||||
@@ -10,16 +10,19 @@ static const char scr_node_island_host_js[] =
|
||||
" * scr_node_island_host.h embeds this file as a C string; regenerate it\012"
|
||||
" * with `node scripts/node-island-host.mjs` after every change.\012"
|
||||
" *\012"
|
||||
" * The libnode bridge (scr_node_bridge.cc) installs a native `settle`\012"
|
||||
" * function as globalThis.__scriptcNodeIslandHost before this script runs,\012"
|
||||
" * and takes the dispatcher this script leaves as\012"
|
||||
" * globalThis.__scriptcNodeIsland afterwards; it deletes both globals.\012"
|
||||
" * The libnode bridge (scr_node_bridge.cc) installs its natives (`settle`,\012"
|
||||
" * and for the hosted loop `run` and `listen`) as\012"
|
||||
" * globalThis.__scriptcNodeIslandHost before this script runs, and takes the\012"
|
||||
" * dispatcher this script leaves as globalThis.__scriptcNodeIsland\012"
|
||||
" * afterwards (`call` and the hosted-loop helpers); it deletes both globals.\012"
|
||||
" *\012"
|
||||
" * A copy of the call semantics lives in packages/node/index.js, which runs\012"
|
||||
" * them in-process under Node: both must produce the same results, errors\012"
|
||||
" * and messages. Outcomes are \"R<json>\" (returned; \"R\" alone is undefined)\012"
|
||||
" * or \"T<json>\" (threw), the encoding of scr_node_bridge.h. */\012"
|
||||
"const { createRequire, registerHooks } = require(\"node:module\");\012"
|
||||
"const { constants: osConstants } = require(\"node:os\");\012"
|
||||
"const timers = require(\"node:timers\");\012"
|
||||
"\012"
|
||||
"const nativeHost = globalThis.__scriptcNodeIslandHost;\012"
|
||||
"\012"
|
||||
@@ -160,6 +163,202 @@ static const char scr_node_island_host_js[] =
|
||||
" return undefined;\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"// \342\224\200\342\224\200 the hosted loop (scope-B NI-3) \342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\012"
|
||||
"// The runtime's ticks, timers and immediates are Node's, created here with\012"
|
||||
"// Node's own timers (captured now, so island code replacing the globals\012"
|
||||
"// does not reach them) and run through the native `run`. Handles name the\012"
|
||||
"// Node objects until the runtime clears them.\012"
|
||||
"\012"
|
||||
"const JOB_TICK = 2;\012"
|
||||
"const JOB_TIMER = 3;\012"
|
||||
"const JOB_IMMEDIATE = 4;\012"
|
||||
"const handles = new Map();\012"
|
||||
"let nextHandle = 1;\012"
|
||||
"\012"
|
||||
"function onTick(token) {\012"
|
||||
" nativeHost.run(JOB_TICK, token);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function onTimer(token) {\012"
|
||||
" nativeHost.run(JOB_TIMER, token);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function onImmediate(handle, token) {\012"
|
||||
" handles.delete(handle);\012"
|
||||
" nativeHost.run(JOB_IMMEDIATE, token);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function setTimer(delay, repeat, ref, token) {\012"
|
||||
" let timer;\012"
|
||||
" if (repeat > 0 && delay === repeat) {\012"
|
||||
" timer = timers.setInterval(onTimer, repeat, token);\012"
|
||||
" } else {\012"
|
||||
" timer = timers.setTimeout(onTimer, delay, token);\012"
|
||||
" // An interval migrated mid-period: its first run comes after the rest\012"
|
||||
" // of the period, later ones every `repeat` (Node re-arms a Timeout\012"
|
||||
" // whose _repeat is set, exactly as for setInterval).\012"
|
||||
" if (repeat > 0) timer._repeat = repeat;\012"
|
||||
" }\012"
|
||||
" if (!ref) timer.unref();\012"
|
||||
" const handle = nextHandle++;\012"
|
||||
" handles.set(handle, timer);\012"
|
||||
" return handle;\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function clearTimer(handle) {\012"
|
||||
" const timer = handles.get(handle);\012"
|
||||
" if (timer === undefined) return;\012"
|
||||
" handles.delete(handle);\012"
|
||||
" timers.clearTimeout(timer);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function refTimer(handle, ref) {\012"
|
||||
" const timer = handles.get(handle);\012"
|
||||
" if (timer !== undefined) {\012"
|
||||
" if (ref) timer.ref();\012"
|
||||
" else timer.unref();\012"
|
||||
" }\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function refreshTimer(handle) {\012"
|
||||
" const timer = handles.get(handle);\012"
|
||||
" if (timer !== undefined) timer.refresh();\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function setImmediateJob(ref, token) {\012"
|
||||
" const handle = nextHandle++;\012"
|
||||
" const immediate = timers.setImmediate(onImmediate, handle, token);\012"
|
||||
" if (!ref) immediate.unref();\012"
|
||||
" handles.set(handle, immediate);\012"
|
||||
" return handle;\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function clearImmediateJob(handle) {\012"
|
||||
" const immediate = handles.get(handle);\012"
|
||||
" if (immediate === undefined) return;\012"
|
||||
" handles.delete(handle);\012"
|
||||
" timers.clearImmediate(immediate);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function refImmediate(handle, ref) {\012"
|
||||
" refTimer(handle, ref);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function nextTick(token) {\012"
|
||||
" process.nextTick(onTick, token);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"// \342\224\200\342\224\200 process state (scope-B NI-3) \342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\012"
|
||||
"// Signal and 'exit' listeners of island code join the runtime's registries\012"
|
||||
"// at their registration position, so one registry orders both sides and\012"
|
||||
"// the runtime keeps owning every signal disposition: Node's own handlers,\012"
|
||||
"// which would hand a signal to libuv and reset it to SIG_DFL when the last\012"
|
||||
"// listener goes, are taken off. The listeners stay on `process` (listeners\012"
|
||||
"// and listenerCount answer as in Node); the runtime calls them through\012"
|
||||
"// `emit`, and Node's own 'exit' emission is left to the runtime's exit\012"
|
||||
"// sequence.\012"
|
||||
"\012"
|
||||
"const EVENT_SIGNAL = 1;\012"
|
||||
"const EVENT_EXIT = 2;\012"
|
||||
"const signalNames = new Set(Object.keys(osConstants.signals));\012"
|
||||
"const islandListeners = [];\012"
|
||||
"let nextListener = 1;\012"
|
||||
"\012"
|
||||
"function eventKind(event) {\012"
|
||||
" if (event === \"exit\") return EVENT_EXIT;\012"
|
||||
" return typeof event === \"string\" && signalNames.has(event) \077 EVENT_SIGNAL : 0;\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"for (const [event, name] of [\012"
|
||||
" [\"newListener\", \"startListeningIfSignal\"],\012"
|
||||
" [\"removeListener\", \"stopListeningIfSignal\"],\012"
|
||||
"]) {\012"
|
||||
" for (const listener of process.listeners(event)) {\012"
|
||||
" if (listener.name === name) process.removeListener(event, listener);\012"
|
||||
" }\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"process.on(\"newListener\", (event, listener) => {\012"
|
||||
" const kind = eventKind(event);\012"
|
||||
" if (kind === 0) return;\012"
|
||||
" const id = nextListener++;\012"
|
||||
" islandListeners.push({ event, listener, id, raw: undefined });\012"
|
||||
" nativeHost.listen(kind, event, id, 1, 0);\012"
|
||||
"});\012"
|
||||
"\012"
|
||||
"function forget(i, kind) {\012"
|
||||
" const entry = islandListeners[i];\012"
|
||||
" islandListeners.splice(i, 1);\012"
|
||||
" nativeHost.listen(kind, entry.event, entry.id, 0, 0);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"process.on(\"removeListener\", (event, listener) => {\012"
|
||||
" const kind = eventKind(event);\012"
|
||||
" if (kind === 0) return;\012"
|
||||
" // Node reports the removed registration as the raw listener it installed (a\012"
|
||||
" // once wrapper removing itself reports the wrapper) or as the listener\012"
|
||||
" // itself. The raw listener each entry recorded separates a once\012"
|
||||
" // registration from an `on` registration of the same function: the removed\012"
|
||||
" // one is the entry whose raw listener is no longer on `process`.\012"
|
||||
" const live = process.rawListeners(event);\012"
|
||||
" let same = -1;\012"
|
||||
" for (let i = islandListeners.length - 1; i >= 0; i--) {\012"
|
||||
" const entry = islandListeners[i];\012"
|
||||
" if (entry.event !== event) continue;\012"
|
||||
" if (entry.listener !== listener && entry.raw !== listener) continue;\012"
|
||||
" if (entry.raw !== undefined && !live.includes(entry.raw)) {\012"
|
||||
" forget(i, kind);\012"
|
||||
" return;\012"
|
||||
" }\012"
|
||||
" if (same < 0) same = i;\012"
|
||||
" }\012"
|
||||
" // Duplicate registrations of one function share a raw listener; Node\012"
|
||||
" // removes the most recent, which is the last matching entry.\012"
|
||||
" if (same >= 0) forget(same, kind);\012"
|
||||
"});\012"
|
||||
"\012"
|
||||
"// The raw listener is the function handed to the emitter (a once wrapper\012"
|
||||
"// for process.once, which registers through process.on): tag each entry\012"
|
||||
"// with it as its registration returns. Node emits newListener before it\012"
|
||||
"// installs the listener, so the entry learns its raw only afterwards.\012"
|
||||
"for (const name of [\"on\", \"addListener\", \"prependListener\"]) {\012"
|
||||
" const add = process[name];\012"
|
||||
" process[name] = function addListener(event, listener) {\012"
|
||||
" const start = islandListeners.length;\012"
|
||||
" const result = Reflect.apply(add, this, arguments);\012"
|
||||
" if (islandListeners.length > start) {\012"
|
||||
" const registered = listener.listener \077\077 listener;\012"
|
||||
" for (let i = start; i < islandListeners.length; i++) {\012"
|
||||
" const entry = islandListeners[i];\012"
|
||||
" if (entry.event === event && entry.listener === registered && entry.raw === undefined) {\012"
|
||||
" entry.raw = listener;\012"
|
||||
" break;\012"
|
||||
" }\012"
|
||||
" }\012"
|
||||
" }\012"
|
||||
" return result;\012"
|
||||
" };\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"function emit(kind, id, name, arg) {\012"
|
||||
" const entry = islandListeners.find((e) => e.id === id);\012"
|
||||
" if (entry === undefined) return;\012"
|
||||
" // The registered function: the listener itself, or its once wrapper,\012"
|
||||
" // which removes itself before calling it.\012"
|
||||
" const registered =\012"
|
||||
" entry.raw \077\077\012"
|
||||
" process.rawListeners(entry.event).find((raw) => (raw.listener \077\077 raw) === entry.listener);\012"
|
||||
" if (registered === undefined) return;\012"
|
||||
" if (kind === EVENT_SIGNAL) Reflect.apply(registered, process, [name, arg]);\012"
|
||||
" else Reflect.apply(registered, process, [arg]);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"const nodeEmit = process.emit;\012"
|
||||
"process.emit = function emit(event, ...args) {\012"
|
||||
" if (event === \"exit\") return this.listenerCount(\"exit\") > 0;\012"
|
||||
" return Reflect.apply(nodeEmit, this, [event, ...args]);\012"
|
||||
"};\012"
|
||||
"\012"
|
||||
"// Writes reach the shared stdout and stderr before control returns to the\012"
|
||||
"// program, as the program's own writes do.\012"
|
||||
"for (const stream of [process.stdout, process.stderr]) {\012"
|
||||
@@ -167,13 +366,6 @@ static const char scr_node_island_host_js[] =
|
||||
" stream._handle.setBlocking(true);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"// The program ends with `code` (the bridge then runs Node's loop to its\012"
|
||||
"// end, which emits 'beforeExit' and 'exit'): Node's process shares it, as\012"
|
||||
"// it shares one process under Node.\012"
|
||||
"function finish(code) {\012"
|
||||
" if (code !== 0) process.exitCode = code;\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"// process.execPath is this program, not a node executable: a fork without\012"
|
||||
"// an execPath of its own would start the program again.\012"
|
||||
"const childProcess = require(\"node:child_process\");\012"
|
||||
@@ -191,6 +383,17 @@ static const char scr_node_island_host_js[] =
|
||||
"};\012"
|
||||
"require(\"node:module\").syncBuiltinESMExports();\012"
|
||||
"\012"
|
||||
"globalThis.__scriptcNodeIsland = { call, finish };\012";
|
||||
"globalThis.__scriptcNodeIsland = {\012"
|
||||
" call,\012"
|
||||
" setTimer,\012"
|
||||
" clearTimer,\012"
|
||||
" refTimer,\012"
|
||||
" refreshTimer,\012"
|
||||
" setImmediate: setImmediateJob,\012"
|
||||
" clearImmediate: clearImmediateJob,\012"
|
||||
" refImmediate,\012"
|
||||
" nextTick,\012"
|
||||
" emit,\012"
|
||||
"};\012";
|
||||
|
||||
#endif /* SCR_NODE_ISLAND_HOST_H */
|
||||
|
||||
Generated
+10
@@ -236,6 +236,16 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../threads
|
||||
|
||||
packages/libnode-common: {}
|
||||
|
||||
packages/libnode-darwin-arm64: {}
|
||||
|
||||
packages/libnode-darwin-x64: {}
|
||||
|
||||
packages/libnode-linux-arm64-gnu: {}
|
||||
|
||||
packages/libnode-linux-x64-gnu: {}
|
||||
|
||||
packages/llvm-darwin-arm64: {}
|
||||
|
||||
packages/llvm-darwin-x64: {}
|
||||
|
||||
@@ -48,6 +48,10 @@ for (const pkg of [
|
||||
"llvm-linux-x64-musl",
|
||||
"llvm-linux-arm64-musl",
|
||||
"llvm-win32-x64-msvc",
|
||||
"libnode-linux-x64-gnu",
|
||||
"libnode-linux-arm64-gnu",
|
||||
"libnode-darwin-arm64",
|
||||
"libnode-darwin-x64",
|
||||
]) {
|
||||
const path = manifest(pkg);
|
||||
const json = read(path);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// @requires-libnode
|
||||
// X { T 30 { L i-late } } L h-end
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 30 { L i-late }");
|
||||
log("h-end");
|
||||
@@ -0,0 +1,20 @@
|
||||
// @requires-libnode
|
||||
// X { F 50 { L i-done } } L h-end
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("F 50 { L i-done }");
|
||||
log("h-end");
|
||||
@@ -0,0 +1,20 @@
|
||||
// @requires-libnode
|
||||
// X { O { L i-listening } } L h-end
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("O { L i-listening }");
|
||||
log("h-end");
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// X { G R { L i-io E } } B 30 L h-sync T 10 { L h-t10 }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("G R { L i-io E }");
|
||||
busy(30);
|
||||
log("h-sync");
|
||||
setTimeout(() => log("h-t10"), 10);
|
||||
@@ -0,0 +1,24 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// X { T 0 { F 20 { L i-fs-chain } } } T 2 { B 80 L h-sync }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 0 { F 20 { L i-fs-chain } }");
|
||||
setTimeout(() => {
|
||||
busy(80);
|
||||
log("h-sync");
|
||||
}, 2);
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// X { T 0 { N { L i-net } } } T 2 { B 80 L h-sync }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 0 { N { L i-net } }");
|
||||
setTimeout(() => {
|
||||
busy(80);
|
||||
log("h-sync");
|
||||
}, 2);
|
||||
@@ -0,0 +1,24 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// X { T 0 { C { L i-child } } } T 2 { B 150 L h-sync }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 0 { C { L i-child } }");
|
||||
setTimeout(() => {
|
||||
busy(150);
|
||||
log("h-sync");
|
||||
}, 2);
|
||||
@@ -0,0 +1,24 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// X { T 0 { D { L i-dns } } } T 2 { B 80 L h-sync }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 0 { D { L i-dns } }");
|
||||
setTimeout(() => {
|
||||
busy(80);
|
||||
log("h-sync");
|
||||
}, 2);
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// X { T 0 { Z { L i-zlib } } } T 2 { B 80 L h-sync }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 0 { Z { L i-zlib } }");
|
||||
setTimeout(() => {
|
||||
busy(80);
|
||||
log("h-sync");
|
||||
}, 2);
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// X { T 0 { W { L i-worker } } } T 2 { B 300 L h-sync }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 0 { W { L i-worker } }");
|
||||
setTimeout(() => {
|
||||
busy(300);
|
||||
log("h-sync");
|
||||
}, 2);
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// X { T 0 { S { L i-internal-timer } } } T 2 { B 80 L h-sync }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 0 { S { L i-internal-timer } }");
|
||||
setTimeout(() => {
|
||||
busy(80);
|
||||
log("h-sync");
|
||||
}, 2);
|
||||
@@ -0,0 +1,183 @@
|
||||
"use strict";
|
||||
// The island half of the Node island ordering programs (scope-B design
|
||||
// §5.2): `run(src)` interprets a small scheduling language inside Node,
|
||||
// the same in a scriptc build (Node loaded in-process, one event loop) and
|
||||
// under Node (@scriptc/node calls the module directly). Each program's
|
||||
// main.ts is the native half. Ops (bodies in braces):
|
||||
// L name log a line (synchronous write to fd 1)
|
||||
// J log the AsyncLocalStorage store
|
||||
// Y { .. } run the body inside als.run("s", ..)
|
||||
// T ms { .. } setTimeout I { .. } setImmediate
|
||||
// M { .. } queueMicrotask K { .. } process.nextTick
|
||||
// P { .. } Promise.resolve().then(body)
|
||||
// R { .. } fs.readFile, then the body
|
||||
// A { .. } fs.promises.readFile, then the body
|
||||
// F n { .. } n sequential fs.stat callbacks, then the body
|
||||
// N { .. } a TCP round trip on localhost, the body on client data
|
||||
// C { .. } child_process.execFile("/bin/true"), the body in its callback
|
||||
// D { .. } dns.lookup("localhost")
|
||||
// Z { .. } zlib.gzip
|
||||
// W { .. } worker_threads: the body on the worker's first message
|
||||
// S { .. } a socket idle timeout (a Node-internal timer)
|
||||
// O { .. } an unref'd TCP server, the body on 'listening'
|
||||
// E throw; G installs an uncaughtException handler that logs
|
||||
// B ms busy-wait
|
||||
const fs = require("node:fs");
|
||||
const { AsyncLocalStorage } = require("node:async_hooks");
|
||||
|
||||
const als = new AsyncLocalStorage();
|
||||
|
||||
function parse(src) {
|
||||
const toks = src.trim().split(/\s+/).filter(Boolean);
|
||||
let i = 0;
|
||||
function body() {
|
||||
if (toks[i++] !== "{") throw new Error(`expected { at ${i}`);
|
||||
const start = i;
|
||||
let depth = 1;
|
||||
while (depth) {
|
||||
const t = toks[i++];
|
||||
if (t === "{") depth++;
|
||||
else if (t === "}") depth--;
|
||||
}
|
||||
return toks.slice(start, i - 1).join(" ");
|
||||
}
|
||||
const out = [];
|
||||
while (i < toks.length) {
|
||||
const k = toks[i++];
|
||||
if (k === "L") out.push({ k, s: toks[i++] });
|
||||
else if (k === "J" || k === "E" || k === "G") out.push({ k });
|
||||
else if (k === "B") out.push({ k, ms: +toks[i++] });
|
||||
else if (k === "T" || k === "F") {
|
||||
const ms = +toks[i++];
|
||||
out.push({ k, ms, body: body() });
|
||||
} else out.push({ k, body: body() });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const log = (s) => fs.writeSync(1, `${s}\n`);
|
||||
const busy = (ms) => {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end);
|
||||
};
|
||||
|
||||
function exec(src) {
|
||||
for (const op of parse(src)) {
|
||||
switch (op.k) {
|
||||
case "L":
|
||||
log(op.s);
|
||||
break;
|
||||
case "J":
|
||||
log(`store=${als.getStore()}`);
|
||||
break;
|
||||
case "E":
|
||||
throw new Error("boom");
|
||||
case "G":
|
||||
process.on("uncaughtException", (e) => log(`uncaught-${e.message}`));
|
||||
break;
|
||||
case "Y":
|
||||
als.run("s", () => exec(op.body));
|
||||
break;
|
||||
case "B":
|
||||
busy(op.ms);
|
||||
break;
|
||||
default:
|
||||
schedule(op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function schedule(op) {
|
||||
const run = () => exec(op.body);
|
||||
switch (op.k) {
|
||||
case "T":
|
||||
setTimeout(run, op.ms);
|
||||
break;
|
||||
case "I":
|
||||
setImmediate(run);
|
||||
break;
|
||||
case "M":
|
||||
queueMicrotask(run);
|
||||
break;
|
||||
case "K":
|
||||
process.nextTick(run);
|
||||
break;
|
||||
case "P":
|
||||
Promise.resolve().then(run);
|
||||
break;
|
||||
case "R":
|
||||
fs.readFile(__filename, run);
|
||||
break;
|
||||
case "A":
|
||||
fs.promises.readFile(__filename).then(run);
|
||||
break;
|
||||
case "F": {
|
||||
let n = op.ms;
|
||||
const next = () => (n-- > 0 ? fs.stat(__filename, next) : run());
|
||||
next();
|
||||
break;
|
||||
}
|
||||
case "N": {
|
||||
const net = require("node:net");
|
||||
const srv = net
|
||||
.createServer((s) => s.end("x"))
|
||||
.listen(0, "127.0.0.1", () => {
|
||||
const c = net.connect(srv.address().port, "127.0.0.1");
|
||||
c.once("data", () => {
|
||||
c.destroy();
|
||||
srv.close();
|
||||
run();
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "C":
|
||||
require("node:child_process").execFile("/bin/true", run);
|
||||
break;
|
||||
case "D":
|
||||
require("node:dns").lookup("localhost", run);
|
||||
break;
|
||||
case "Z":
|
||||
require("node:zlib").gzip("hello", run);
|
||||
break;
|
||||
case "W": {
|
||||
const { Worker } = require("node:worker_threads");
|
||||
const w = new Worker('require("node:worker_threads").parentPort.postMessage(1)', {
|
||||
eval: true,
|
||||
});
|
||||
w.once("message", () => {
|
||||
w.terminate();
|
||||
run();
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "S": {
|
||||
const net = require("node:net");
|
||||
const srv = net
|
||||
.createServer((s) =>
|
||||
s.setTimeout(10, () => {
|
||||
s.destroy();
|
||||
srv.close();
|
||||
run();
|
||||
}),
|
||||
)
|
||||
.listen(0, "127.0.0.1", () => {
|
||||
const c = net.connect(srv.address().port, "127.0.0.1");
|
||||
c.on("error", () => {});
|
||||
c.on("close", () => {});
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "O": {
|
||||
const s = require("node:net").createServer().listen(0, "127.0.0.1", run);
|
||||
s.unref();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`op ${op.k}`);
|
||||
}
|
||||
}
|
||||
|
||||
exports.run = function run(src) {
|
||||
exec(src);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// @requires-libnode
|
||||
// T 0 { L h1 } X { T 0 { L i1 } } T 0 { L h2 } X { T 0 { L i2 } }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
setTimeout(() => log("h1"), 0);
|
||||
island("T 0 { L i1 }");
|
||||
setTimeout(() => log("h2"), 0);
|
||||
island("T 0 { L i2 }");
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// I { L hI } X { I { L iI } M { L iM } K { L iK } } M { L hM } K { L hK } L sync
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
setImmediate(() => log("hI"));
|
||||
island("I { L iI } M { L iM } K { L iK }");
|
||||
queueMicrotask(() => log("hM"));
|
||||
process.nextTick(() => log("hK"));
|
||||
log("sync");
|
||||
@@ -0,0 +1,21 @@
|
||||
// @requires-libnode
|
||||
// X { T 0 { L i-timer } } B 50 L h-after-busy
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 0 { L i-timer }");
|
||||
busy(50);
|
||||
log("h-after-busy");
|
||||
@@ -0,0 +1,22 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// X { R { L i-io M { L i-io-micro } } } R { L h-io } T 0 { L h-t0 }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("R { L i-io M { L i-io-micro } }");
|
||||
readFile(import.meta.filename, "utf8", () => log("h-io"));
|
||||
setTimeout(() => log("h-t0"), 0);
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// M { L hm1 } X { M { L im1 } } M { L hm2 } X { M { L im2 } } L sync
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
queueMicrotask(() => log("hm1"));
|
||||
island("M { L im1 }");
|
||||
queueMicrotask(() => log("hm2"));
|
||||
island("M { L im2 }");
|
||||
log("sync");
|
||||
@@ -0,0 +1,22 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// T 1 { L h-t1 } X { T 0 { L i-t0 } I { L i-imm } } I { L h-imm }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
setTimeout(() => log("h-t1"), 1);
|
||||
island("T 0 { L i-t0 } I { L i-imm }");
|
||||
setImmediate(() => log("h-imm"));
|
||||
@@ -0,0 +1,22 @@
|
||||
// @requires-libnode
|
||||
// X { T 10 { L i-t10 } } T 20 { L h-t20 } B 30 L h-sync
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 10 { L i-t10 }");
|
||||
setTimeout(() => log("h-t20"), 20);
|
||||
busy(30);
|
||||
log("h-sync");
|
||||
@@ -0,0 +1,24 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// X { T 0 { L i-a K { L i-tick } M { L i-micro } I { L i-imm } } } T 0 { L h-t0 M { L h-micro } }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("T 0 { L i-a K { L i-tick } M { L i-micro } I { L i-imm } }");
|
||||
setTimeout(() => {
|
||||
log("h-t0");
|
||||
queueMicrotask(() => log("h-micro"));
|
||||
}, 0);
|
||||
@@ -0,0 +1,24 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// X { R { L i-io M { L i-io-micro } } } R { L h-io } T 0 { L h-t0 }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("R { L i-io M { L i-io-micro } }");
|
||||
readFile(import.meta.filename, "utf8", () => {
|
||||
log("h-io");
|
||||
});
|
||||
setTimeout(() => log("h-t0"), 0);
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// X { R { L i-io } } B 30 L h-sync T 0 { L h-t0 }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("R { L i-io }");
|
||||
busy(30);
|
||||
log("h-sync");
|
||||
setTimeout(() => log("h-t0"), 0);
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// M { L hm1 } X { P { L ip1 } } M { L hm2 } X { P { L ip2 } } L sync
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
queueMicrotask(() => log("hm1"));
|
||||
island("P { L ip1 }");
|
||||
queueMicrotask(() => log("hm2"));
|
||||
island("P { L ip2 }");
|
||||
log("sync");
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// X { P { L ip1 P { L ip2 } } } M { L hm1 M { L hm2 } }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("P { L ip1 P { L ip2 } }");
|
||||
queueMicrotask(() => {
|
||||
log("hm1");
|
||||
queueMicrotask(() => log("hm2"));
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// @requires-libnode
|
||||
// X { R { L i-io K { L i-tick } P { L i-p } M { L i-m } I { L i-imm } } } B 40 L h-sync
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("R { L i-io K { L i-tick } P { L i-p } M { L i-m } I { L i-imm } }");
|
||||
busy(40);
|
||||
log("h-sync");
|
||||
@@ -0,0 +1,23 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// X { A { L i-a P { L i-ap } } } B 40 L h-sync T 0 { L h-t0 }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("A { L i-a P { L i-ap } }");
|
||||
busy(40);
|
||||
log("h-sync");
|
||||
setTimeout(() => log("h-t0"), 0);
|
||||
@@ -0,0 +1,19 @@
|
||||
// @requires-libnode
|
||||
// X { Y { T 0 { J } I { J } K { J } M { J } R { J } } }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("Y { T 0 { J } I { J } K { J } M { J } R { J } }");
|
||||
@@ -0,0 +1,22 @@
|
||||
// @requires-libnode
|
||||
// X { R { T 0 { L i-t0 } } } B 40 T 0 { L h-t0 } L h-sync
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
island("R { T 0 { L i-t0 } }");
|
||||
busy(40);
|
||||
setTimeout(() => log("h-t0"), 0);
|
||||
log("h-sync");
|
||||
@@ -0,0 +1,25 @@
|
||||
// @requires-libnode
|
||||
// P { L hp1 } X { P { L ip1 P { L ip3 } } } P { L hp2 P { L hp3 } } L sync
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
Promise.resolve().then(() => log("hp1"));
|
||||
island("P { L ip1 P { L ip3 } }");
|
||||
Promise.resolve().then(() => {
|
||||
log("hp2");
|
||||
Promise.resolve().then(() => log("hp3"));
|
||||
});
|
||||
log("sync");
|
||||
@@ -0,0 +1,29 @@
|
||||
// @requires-libnode
|
||||
// async f: await, ha1, await, ha2, await, ha3 || X { P { L ip1 P { L ip2 P { L ip3 } } } }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
async function steps(): Promise<void> {
|
||||
await null;
|
||||
log("ha1");
|
||||
await null;
|
||||
log("ha2");
|
||||
await Promise.resolve();
|
||||
log("ha3");
|
||||
}
|
||||
void steps();
|
||||
island("P { L ip1 P { L ip2 P { L ip3 } } }");
|
||||
log("sync");
|
||||
@@ -0,0 +1,25 @@
|
||||
// @requires-libnode
|
||||
// K { L hk1 K { L hk3 } } X { K { L ik1 M { L im1 } } } K { L hk2 } M { L hm1 }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
process.nextTick(() => {
|
||||
log("hk1");
|
||||
process.nextTick(() => log("hk3"));
|
||||
});
|
||||
island("K { L ik1 M { L im1 } }");
|
||||
process.nextTick(() => log("hk2"));
|
||||
queueMicrotask(() => log("hm1"));
|
||||
@@ -0,0 +1,38 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// Switch: work of every kind is pending in the program's queues when Node loads
|
||||
// during the main body: timers (one an interval), an immediate, a tick, a
|
||||
// microtask and a promise job (fs callbacks: s06).
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
let fired = 0;
|
||||
setTimeout(() => log("h-t5"), 5);
|
||||
setTimeout(() => log("h-t0"), 0);
|
||||
const interval = setInterval(() => {
|
||||
log("h-iv");
|
||||
if (++fired === 3) clearInterval(interval);
|
||||
}, 3);
|
||||
setImmediate(() => log("h-imm"));
|
||||
queueMicrotask(() => log("h-m"));
|
||||
process.nextTick(() => log("h-k"));
|
||||
void Promise.resolve().then(() => log("h-p"));
|
||||
island("T 0 { L i-t0 } T 5 { L i-t5 } I { L i-imm } K { L i-k } M { L i-m } P { L i-p }");
|
||||
setTimeout(() => log("h-t0b"), 0);
|
||||
setImmediate(() => log("h-imm2"));
|
||||
queueMicrotask(() => log("h-m2"));
|
||||
process.nextTick(() => log("h-k2"));
|
||||
log("sync");
|
||||
@@ -0,0 +1,35 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// Switch: Node loads inside a timer callback while timers of the same and later
|
||||
// deadlines, an immediate and an interval are pending.
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
function log(s: string): void {
|
||||
writeSync(1, `${s}\n`);
|
||||
}
|
||||
|
||||
function busy(ms: number): void {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {}
|
||||
}
|
||||
|
||||
function island(src: string): void {
|
||||
callSync("../island.cjs", "run", src);
|
||||
}
|
||||
|
||||
let fired = 0;
|
||||
const interval = setInterval(() => {
|
||||
log("h-iv");
|
||||
if (++fired === 2) clearInterval(interval);
|
||||
}, 1);
|
||||
setTimeout(() => {
|
||||
log("h-a");
|
||||
island("T 0 { L i-t0 } I { L i-imm } K { L i-k } M { L i-m }");
|
||||
queueMicrotask(() => log("h-a-m"));
|
||||
process.nextTick(() => log("h-a-k"));
|
||||
log("h-a-end");
|
||||
}, 1);
|
||||
setTimeout(() => log("h-b"), 1);
|
||||
setTimeout(() => log("h-c"), 4);
|
||||
setImmediate(() => log("h-imm"));
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user