mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
Make native installs portable and smaller (#599)
* Make native installs portable and smaller - Support glibc 2.34 with portable compiler binaries and static LLVM helpers. - Bundle the host runtime and discover optional cross-target packs from project dependencies. - Add application build benchmarks covering latency, executable size, and memory. * Keep bootstrap tools available in Sandbox validation - Isolate Zig for native compiler bootstrap and glibc entropy contracts. - Preserve the existing toolchain scope for cache and corpus tests. * Fix portable compiler bootstrap linking - Keep native support objects compatible with the destination host linker. - Resolve installed targets using the host declared by the toolchain manifest.
This commit is contained in:
@@ -201,6 +201,27 @@ jobs:
|
||||
helper: "false"
|
||||
- run: pnpm build
|
||||
- run: pnpm test tests/harness/self-hosting-native-driver.test.ts
|
||||
env:
|
||||
SCRIPTC_BOOTSTRAP_PACKAGE_DIR: ${{ runner.temp }}/native-cli-package
|
||||
- name: Verify packed compiler and Linux ABI
|
||||
if: matrix.flavor == 'plain'
|
||||
run: |
|
||||
pnpm test packages/runtime/test/glibc-random.test.ts
|
||||
node scripts/verify-native-cli.mjs "$RUNNER_TEMP/native-cli-package" --run
|
||||
node scripts/smoke-native-install.mjs "$RUNNER_TEMP/native-cli-package"
|
||||
- name: Install and compile on Amazon Linux 2023 (glibc 2.34)
|
||||
if: matrix.flavor == 'plain'
|
||||
run: |
|
||||
NODE_ROOT=$(dirname "$(dirname "$(command -v node)")")
|
||||
docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" \
|
||||
--volume "$RUNNER_TEMP/native-cli-package:/native:ro" --volume "$NODE_ROOT:/node:ro" \
|
||||
--env PATH=/node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin \
|
||||
--workdir /work amazonlinux:2023 sh -c '
|
||||
set -eu
|
||||
dnf install --assumeyes clang tar gzip
|
||||
test "$(getconf GNU_LIBC_VERSION)" = "glibc 2.34"
|
||||
node scripts/smoke-native-install.mjs /native
|
||||
'
|
||||
|
||||
# A real host-clang/glibc build, not the zig cross-target lane: catches
|
||||
# Linux feature-test macro omissions at compile time and separate-libm
|
||||
|
||||
@@ -244,6 +244,18 @@ jobs:
|
||||
pnpm --filter @scriptc/compiler --filter scriptc build
|
||||
pnpm --filter @scriptc/${{ matrix.cli }} build:native
|
||||
node scripts/verify-native-cli.mjs packages/${{ matrix.cli }} --run
|
||||
- name: Verify GNU npm distribution on glibc 2.34
|
||||
if: matrix.platform == 'linux-x64' || matrix.platform == 'linux-arm64'
|
||||
run: |
|
||||
NODE_ROOT=$(dirname "$(dirname "$(command -v node)")")
|
||||
docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" --volume "$NODE_ROOT:/node:ro" \
|
||||
--env PATH=/node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin \
|
||||
--workdir /work amazonlinux:2023 sh -c '
|
||||
set -eu
|
||||
dnf install --assumeyes clang tar gzip
|
||||
test "$(getconf GNU_LIBC_VERSION)" = "glibc 2.34"
|
||||
node scripts/smoke-native-install.mjs packages/${{ matrix.cli }}
|
||||
'
|
||||
- name: Verify packed Darwin helper on its build host
|
||||
if: matrix.platform == 'darwin-arm64'
|
||||
run: |
|
||||
|
||||
@@ -17,6 +17,8 @@ scriptc is experimental and targets macOS, Linux, Windows, and WebAssembly via W
|
||||
|
||||
The installed compiler runs natively on supported macOS, Linux, and Windows hosts. Compilation, compile-time evaluation, and native execution do not require Node. `--emit=ir|llvm|asm|obj` uses the bundled TypeScript checker and LLVM helper without an external compiler, archiver, linker, or SDK. Executable builds additionally need a platform linker driver and SDK/sysroot; precompiled runtime packs supply the C runtime. Set `SCRIPTC_LINKER` to choose that driver. Runtime development with `--sanitize` additionally needs a C compiler. Node.js 24 or newer is needed for npm installation, development from a source checkout, the JavaScript compiler API, and `scriptc run` of WASI modules.
|
||||
|
||||
The native compiler includes its host runtime pack. Cross-compilation uses additional `@scriptc/runtime-<target>` packages installed in your project at the same version as `scriptc --version`. For example, WASI builds use `@scriptc/runtime-wasm32-wasi`. Run the compiler from that project or set `SCRIPTC_RUNTIME_PACK` to the pack directory. Linux GNU distributions require glibc 2.34 or newer; the bundled LLVM helper is statically linked.
|
||||
|
||||
```console
|
||||
$ npm install -g scriptc
|
||||
```
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Category, Product } from "./types.ts";
|
||||
import { lowStock } from "./stock.ts";
|
||||
export function categories(products: Product[]): Category[] {
|
||||
const groups = new Map<string, Category>();
|
||||
for (const product of products) {
|
||||
let group = groups.get(product.category);
|
||||
if (group === undefined) {
|
||||
group = { name: product.category, products: 0, units: 0, value: 0, lowStock: 0 };
|
||||
groups.set(product.category, group);
|
||||
}
|
||||
group.products++;
|
||||
group.units += product.quantity;
|
||||
group.value += product.quantity * product.price;
|
||||
group.lowStock += lowStock(product) ? 1 : 0;
|
||||
}
|
||||
return [...groups.values()];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const revision = 0;
|
||||
export const lowStockThreshold = 8;
|
||||
export const reorderTarget = 30;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { parse } from "./parse.ts";
|
||||
export function load(file: string) { return parse(readFileSync(file, "utf8")); }
|
||||
@@ -0,0 +1,6 @@
|
||||
import { options } from "./options.ts";
|
||||
import { load } from "./load.ts";
|
||||
import { select } from "./select.ts";
|
||||
import { report } from "./report.ts";
|
||||
const args = options(process.argv.slice(2));
|
||||
console.log(report(select(load(args.file), args.category), args.limit));
|
||||
@@ -0,0 +1,3 @@
|
||||
export function money(cents: number): string {
|
||||
return (cents / 100).toFixed(2);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function normalize(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface Options { file: string; category: string; limit: number }
|
||||
export function options(args: string[]): Options {
|
||||
let file = "";
|
||||
let category = "";
|
||||
let limit = 12;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const argument = args[i]!;
|
||||
if (argument === "--category") category = args[++i]!;
|
||||
else if (argument === "--limit") limit = Number(args[++i]);
|
||||
else if (file === "") file = argument;
|
||||
else throw new Error("unexpected argument: " + argument);
|
||||
}
|
||||
if (file === "" || !Number.isInteger(limit) || limit < 1) throw new Error("expected an inventory file and positive limit");
|
||||
return { file, category, limit };
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Product } from "./types.ts";
|
||||
import { normalize } from "./normalize.ts";
|
||||
import { valid } from "./validate.ts";
|
||||
export function parse(source: string): Product[] {
|
||||
const products: Product[] = [];
|
||||
let lineNumber = 0;
|
||||
for (const line of source.split("\n")) {
|
||||
lineNumber++;
|
||||
if (line.trim() === "") continue;
|
||||
const fields = line.split("\t");
|
||||
if (fields.length !== 5) throw new Error("invalid inventory line " + lineNumber);
|
||||
const product = { sku: fields[0]!, category: normalize(fields[1]!), name: fields[2]!, quantity: Number(fields[3]), price: Number(fields[4]) };
|
||||
if (!valid(product)) throw new Error("invalid product on line " + lineNumber);
|
||||
products.push(product);
|
||||
}
|
||||
return products;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Category, Product } from "./types.ts";
|
||||
import { reorderUnits } from "./stock.ts";
|
||||
export function rankCategories(rows: Category[]): Category[] {
|
||||
return rows.sort((a, b) => b.value - a.value || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||
}
|
||||
export function rankReorders(products: Product[]): Product[] {
|
||||
return products.filter(product => reorderUnits(product) > 0).sort((a, b) =>
|
||||
reorderUnits(b) * b.price - reorderUnits(a) * a.price || (a.sku < b.sku ? -1 : a.sku > b.sku ? 1 : 0));
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Category, Product } from "./types.ts";
|
||||
import { money } from "./money.ts";
|
||||
import { reorderUnits } from "./stock.ts";
|
||||
import { revision } from "./config.ts";
|
||||
export function render(groups: Category[], reorders: Product[], limit: number): string {
|
||||
const lines = ["inventory report " + revision, "category,products,units,value,low_stock"];
|
||||
for (const group of groups) lines.push([group.name, group.products, group.units, money(group.value), group.lowStock].join(","));
|
||||
lines.push("reorder_sku,units,cost");
|
||||
for (const product of reorders.slice(0, limit)) lines.push([product.sku, reorderUnits(product), money(reorderUnits(product) * product.price)].join(","));
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Product } from "./types.ts";
|
||||
import { categories } from "./categories.ts";
|
||||
import { rankCategories, rankReorders } from "./rank.ts";
|
||||
import { render } from "./render.ts";
|
||||
export function report(products: Product[], limit: number): string {
|
||||
return render(rankCategories(categories(products)), rankReorders(products), limit);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Product } from "./types.ts";
|
||||
import { normalize } from "./normalize.ts";
|
||||
export function select(products: Product[], category: string): Product[] {
|
||||
const wanted = normalize(category);
|
||||
return wanted === "" ? products : products.filter(product => product.category === wanted);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { Product } from "./types.ts";
|
||||
import { lowStockThreshold, reorderTarget } from "./config.ts";
|
||||
export function lowStock(product: Product): boolean { return product.quantity < lowStockThreshold; }
|
||||
export function reorderUnits(product: Product): number { return lowStock(product) ? reorderTarget - product.quantity : 0; }
|
||||
@@ -0,0 +1,2 @@
|
||||
export interface Product { sku: string; category: string; name: string; quantity: number; price: number }
|
||||
export interface Category { name: string; products: number; units: number; value: number; lowStock: number }
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Product } from "./types.ts";
|
||||
export function valid(product: Product): boolean {
|
||||
return product.sku.length > 0 && product.category.length > 0 && product.name.length > 0 &&
|
||||
Number.isInteger(product.quantity) && product.quantity >= 0 &&
|
||||
Number.isInteger(product.price) && product.price >= 0;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { parse } from "./parse.ts";
|
||||
export interface Route { path: string; count: number; errors: number; milliseconds: number; bytes: number }
|
||||
export function summarize(source: string, minimum: number): Route[] {
|
||||
const routes = new Map<string, Route>();
|
||||
for (const line of source.split("\n")) {
|
||||
const request = parse(line);
|
||||
if (request === null || request.status < minimum) continue;
|
||||
let route = routes.get(request.route);
|
||||
if (route === undefined) {
|
||||
route = { path: request.route, count: 0, errors: 0, milliseconds: 0, bytes: 0 };
|
||||
routes.set(request.route, route);
|
||||
}
|
||||
route.count++;
|
||||
route.errors += request.status >= 400 ? 1 : 0;
|
||||
route.milliseconds += request.milliseconds;
|
||||
route.bytes += request.bytes;
|
||||
}
|
||||
return [...routes.values()].sort((a, b) => b.count - a.count || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Route } from "./aggregate.ts";
|
||||
export const revision = 0;
|
||||
export function format(routes: Route[], limit: number): string {
|
||||
const lines = ["request report " + revision, "route,count,errors,mean_ms,bytes"];
|
||||
for (const row of routes.slice(0, limit)) {
|
||||
lines.push([row.path, row.count, row.errors, Math.round(row.milliseconds / row.count), row.bytes].join(","));
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { options } from "./options.ts";
|
||||
import { summarize } from "./aggregate.ts";
|
||||
import { format } from "./format.ts";
|
||||
const args = options(process.argv.slice(2));
|
||||
console.log(format(summarize(readFileSync(args.file, "utf8"), args.minimum), args.limit));
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface Options { file: string; minimum: number; limit: number }
|
||||
export function options(args: string[]): Options {
|
||||
let file = "";
|
||||
let minimum = 200;
|
||||
let limit = 10;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i]!;
|
||||
if (arg === "--min-status") minimum = Number(args[++i]);
|
||||
else if (arg === "--limit") limit = Number(args[++i]);
|
||||
else if (file === "") file = arg;
|
||||
else throw new Error("unexpected argument: " + arg);
|
||||
}
|
||||
if (file === "" || !Number.isInteger(minimum) || !Number.isInteger(limit) || limit < 1) {
|
||||
throw new Error("usage: log-summary <file> [--min-status <number>] [--limit <number>]");
|
||||
}
|
||||
return { file, minimum, limit };
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface Request { route: string; status: number; milliseconds: number; bytes: number }
|
||||
export function parse(line: string): Request | null {
|
||||
const fields = line.split(" ");
|
||||
if (fields.length !== 4) return null;
|
||||
const status = Number(fields[1]);
|
||||
const milliseconds = Number(fields[2]);
|
||||
const bytes = Number(fields[3]);
|
||||
if (!Number.isFinite(status) || !Number.isFinite(milliseconds) || !Number.isFinite(bytes)) return null;
|
||||
const raw = fields[0]!;
|
||||
const query = raw.indexOf("?");
|
||||
return { route: query < 0 ? raw : raw.slice(0, query), status, milliseconds, bytes };
|
||||
}
|
||||
@@ -6,6 +6,10 @@ The compiler runs natively and bundles its TypeScript checker and LLVM helper. O
|
||||
|
||||
## Cross-compilation via zig
|
||||
|
||||
The installed compiler includes its host runtime pack. Other runtime packs are optional project dependencies, pinned to the same version as <code>scriptc --version</code>. Install the appropriate <code>@scriptc/runtime-<target></code> package, such as <code>@scriptc/runtime-linux-arm64-gnu</code> or <code>@scriptc/runtime-wasm32-wasi</code>, and run the compiler from your project. You can also set <code>SCRIPTC_RUNTIME_PACK</code> to a pack directory, including with standalone compiler archives. Adding a pack does not require reinstalling the compiler. Assembly and object output do not require a runtime pack.
|
||||
|
||||
GNU/Linux compiler distributions and runtime packs support glibc 2.34 or newer. The LLVM helper is statically linked and does not require the build machine's libc or C++ runtime to be installed.
|
||||
|
||||
The installed host LLVM helper emits cross-target program objects and the matching runtime pack supplies precompiled support objects. Install [zig](https://ziglang.org) for Linux, Windows, and WASI cross-linking, then select the output target:
|
||||
|
||||
<dl>
|
||||
|
||||
@@ -14,9 +14,9 @@ set(SCRIPTC_ALLOWED_TARGETS "arm64-apple-macosx14.0.0,x86_64-apple-macosx14.0.0,
|
||||
set(SCRIPTC_HELPER_ARCH "arm64" CACHE STRING
|
||||
"macOS architecture of the helper executable")
|
||||
# LLVM's official Linux archives use the host libstdc++ and glibc ABIs. The
|
||||
# musl package therefore ships this process-isolated helper as a fully static
|
||||
# host executable; its configured output targets and runtime pack remain musl.
|
||||
option(SCRIPTC_MUSL_BUILD "Configure the Linux musl helper package" OFF)
|
||||
# Linux packages ship this process-isolated helper as a fully static host
|
||||
# executable so it runs independently of the destination libc and libstdc++.
|
||||
option(SCRIPTC_STATIC_LINUX_BUILD "Build a static Linux helper executable" OFF)
|
||||
|
||||
# These must be cache variables before project() enables the Apple toolchain.
|
||||
# A target property named OSX_DEPLOYMENT_TARGET is not recognized by CMake and
|
||||
@@ -34,7 +34,7 @@ endif()
|
||||
project(scriptc_llvm_codegen LANGUAGES C CXX)
|
||||
|
||||
if(CMAKE_C_COMPILER_TARGET MATCHES "linux-musl")
|
||||
set(SCRIPTC_MUSL_BUILD ON)
|
||||
set(SCRIPTC_STATIC_LINUX_BUILD ON)
|
||||
endif()
|
||||
|
||||
# Official LLVM binary distributions retain zstd as an imported dependency in
|
||||
@@ -43,16 +43,16 @@ endif()
|
||||
find_package(zstd CONFIG QUIET)
|
||||
|
||||
# LLVM's Linux archives retain ZLIB and zstd target edges in LLVMSupport. The
|
||||
# musl package flattens that imported graph below and appends portable library
|
||||
# static Linux helper flattens that imported graph below and appends portable library
|
||||
# names explicitly, so use interface placeholders instead of embedding host
|
||||
# library paths in the flattened graph.
|
||||
if(SCRIPTC_MUSL_BUILD AND NOT TARGET ZLIB::ZLIB)
|
||||
if(SCRIPTC_STATIC_LINUX_BUILD AND NOT TARGET ZLIB::ZLIB)
|
||||
add_library(ZLIB::ZLIB INTERFACE IMPORTED)
|
||||
endif()
|
||||
if(SCRIPTC_MUSL_BUILD AND NOT TARGET zstd::libzstd_static)
|
||||
if(SCRIPTC_STATIC_LINUX_BUILD AND NOT TARGET zstd::libzstd_static)
|
||||
add_library(zstd::libzstd_static INTERFACE IMPORTED)
|
||||
endif()
|
||||
if(SCRIPTC_MUSL_BUILD AND NOT TARGET zstd::libzstd_shared)
|
||||
if(SCRIPTC_STATIC_LINUX_BUILD AND NOT TARGET zstd::libzstd_shared)
|
||||
add_library(zstd::libzstd_shared INTERFACE IMPORTED)
|
||||
endif()
|
||||
|
||||
@@ -126,11 +126,14 @@ target_compile_definitions(scriptc-llvm-codegen PRIVATE
|
||||
SCRIPTC_ALLOWED_TARGETS="${SCRIPTC_ALLOWED_TARGETS}"
|
||||
)
|
||||
|
||||
if(SCRIPTC_MUSL_BUILD)
|
||||
if(SCRIPTC_STATIC_LINUX_BUILD)
|
||||
# CMake's imported LLVM targets carry transitive archive interfaces that
|
||||
# duplicate most of this large static graph. Flatten it to one absolute
|
||||
# archive path per target for the resource-constrained musl release link.
|
||||
# archive path per target for the Linux release link.
|
||||
function(scriptc_collect_llvm_target target)
|
||||
if(target STREQUAL "ZLIB::ZLIB" OR target MATCHES "^zstd::")
|
||||
return()
|
||||
endif()
|
||||
get_property(seen GLOBAL PROPERTY SCRIPTC_LLVM_LINK_SEEN)
|
||||
if(target IN_LIST seen)
|
||||
return()
|
||||
@@ -157,6 +160,17 @@ if(SCRIPTC_MUSL_BUILD)
|
||||
if(NOT location OR location MATCHES "-NOTFOUND$")
|
||||
get_target_property(location "${target}" IMPORTED_LOCATION)
|
||||
endif()
|
||||
if(NOT location OR location MATCHES "-NOTFOUND$")
|
||||
# Distribution packages can export RELWITHDEBINFO rather than RELEASE.
|
||||
get_target_property(configurations "${target}" IMPORTED_CONFIGURATIONS)
|
||||
foreach(configuration IN LISTS configurations)
|
||||
string(TOUPPER "${configuration}" configuration)
|
||||
get_target_property(location "${target}" "IMPORTED_LOCATION_${configuration}")
|
||||
if(location AND NOT location MATCHES "-NOTFOUND$")
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
if(location AND NOT location MATCHES "-NOTFOUND$")
|
||||
set_property(GLOBAL APPEND PROPERTY SCRIPTC_LLVM_LINK_LIBS "${location}")
|
||||
endif()
|
||||
@@ -175,6 +189,9 @@ if(SCRIPTC_MUSL_BUILD)
|
||||
list(REMOVE_DUPLICATES SCRIPTC_LLVM_LIBS)
|
||||
list(REVERSE SCRIPTC_LLVM_LIBS)
|
||||
list(REMOVE_DUPLICATES SCRIPTC_LLVM_SYSTEM_LIBS)
|
||||
# Some distribution builds attach Z3 to LLVMSupport. The helper never uses
|
||||
# the solver, and its shared library cannot be part of a static executable.
|
||||
list(FILTER SCRIPTC_LLVM_SYSTEM_LIBS EXCLUDE REGEX "(^|/)(lib)?([zZ]3|z|zstd)([.].*)?$")
|
||||
list(APPEND SCRIPTC_LLVM_SYSTEM_LIBS z zstd)
|
||||
else()
|
||||
llvm_map_components_to_libnames(SCRIPTC_LLVM_LIBS
|
||||
|
||||
@@ -24,6 +24,8 @@ $ npm install -g scriptc
|
||||
|
||||
The installed compiler runs natively on supported macOS, Linux, and Windows hosts. Compilation, compile-time evaluation, and native execution do not require Node. `--emit=ir|llvm|asm|obj` uses the bundled TypeScript checker and LLVM helper without an external compiler, archiver, linker, or SDK. Executable builds additionally need a platform linker driver and SDK/sysroot; precompiled runtime packs supply the C runtime. Set `SCRIPTC_LINKER` to choose that driver. Runtime development with `--sanitize` additionally needs a C compiler. Node.js 24 or newer is needed for npm installation and `scriptc run` of WASI modules.
|
||||
|
||||
The native compiler includes its host runtime pack. Cross-compilation uses additional `@scriptc/runtime-<target>` packages installed in your project at the same version as `scriptc --version`. WASI builds use `@scriptc/runtime-wasm32-wasi`, for example. Run the compiler from that project or set `SCRIPTC_RUNTIME_PACK` to the pack directory. Linux GNU distributions require glibc 2.34 or newer.
|
||||
|
||||
Builds use a bounded persistent cache. Unchanged source can reuse the validated frontend result and LLVM program objects. Library identity getters occupy a separate LLVM module, so an identity change can reuse the large program object. Runtime objects come from the installed pack. Executable cache entries verify their native dependencies; FFI builds relink against current external inputs. Set `SCRIPTC_NO_CACHE=1` to bypass the cache or `SCRIPTC_CACHE_DIR` to select its location. An existing POSIX override must already be private.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -35,17 +35,6 @@
|
||||
"@scriptc/cli-linux-arm64-musl": "workspace:*",
|
||||
"@scriptc/cli-linux-x64-gnu": "workspace:*",
|
||||
"@scriptc/cli-linux-x64-musl": "workspace:*",
|
||||
"@scriptc/cli-win32-x64-msvc": "workspace:*",
|
||||
"@scriptc/runtime-darwin-arm64": "workspace:*",
|
||||
"@scriptc/runtime-darwin-x64": "workspace:*",
|
||||
"@scriptc/runtime-linux-x64-gnu": "workspace:*",
|
||||
"@scriptc/runtime-linux-arm64-gnu": "workspace:*",
|
||||
"@scriptc/runtime-linux-x64-musl": "workspace:*",
|
||||
"@scriptc/runtime-linux-arm64-musl": "workspace:*",
|
||||
"@scriptc/runtime-win32-x64-msvc": "workspace:*",
|
||||
"@scriptc/runtime-wasm32-wasi": "workspace:*",
|
||||
"@scriptc/runtime-ios-arm64": "workspace:*",
|
||||
"@scriptc/runtime-ios-simulator-arm64": "workspace:*",
|
||||
"@scriptc/runtime-android-arm64": "workspace:*"
|
||||
"@scriptc/cli-win32-x64-msvc": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,23 +56,6 @@ export function installNativeCli(directory, packageName = nativeCliPackage(proce
|
||||
const bin = join(directory, "bin");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
const manifest = relocateToolchain(original, sourceDirectory, bin);
|
||||
const packs = new Map((manifest.runtime_packs ?? []).map((pack) => [pack.target, pack]));
|
||||
for (const suffix of ["darwin-arm64", "darwin-x64", "linux-x64-gnu", "linux-arm64-gnu", "linux-x64-musl", "linux-arm64-musl",
|
||||
"win32-x64-msvc", "wasm32-wasi", "ios-arm64", "ios-simulator-arm64", "android-arm64"]) {
|
||||
const name = `@scriptc/runtime-${suffix}`;
|
||||
if (packageManifest.optionalDependencies?.[name] === undefined) continue;
|
||||
let path;
|
||||
try { path = require.resolve(`${name}/package.json`); }
|
||||
catch { continue; }
|
||||
const packIdentity = JSON.parse(readFileSync(path, "utf8"));
|
||||
if (packIdentity.name !== name || packIdentity.version !== version) throw new Error(`scriptc requires ${name}@${version}`);
|
||||
const pack = JSON.parse(readFileSync(join(dirname(path), "runtime-pack.json"), "utf8"));
|
||||
if (pack.schema !== "scriptc.runtime-pack.v1" || pack.package !== name || pack.version !== version) {
|
||||
throw new Error(`invalid runtime pack installed for ${name}@${version}`);
|
||||
}
|
||||
if (!packs.has(pack.target.name)) packs.set(pack.target.name, { target: pack.target.name, path: relative(bin, dirname(path)) });
|
||||
}
|
||||
manifest.runtime_packs = [...packs.values()];
|
||||
// npm normalizes modes for payloads outside package bin entries. These
|
||||
// tools are launched directly by the compiler after installation.
|
||||
if (process.platform !== "win32") {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* selected artifact is hashed and copied from the same read into private
|
||||
* storage. This host has no persistent cache or mutable installed link inputs. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { closeSync, constants, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
||||
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import type { NativeTargetSpec } from "./targets.js";
|
||||
@@ -30,6 +30,9 @@ export function selectNativeRuntimePack(
|
||||
root: string, target: NativeTargetSpec, compilerVersion: string,
|
||||
features: NativeLinkFeatures, flavor: "release" | "dev", mode: RuntimePackMode = "executable",
|
||||
): NativeRuntimeSelection {
|
||||
if (!existsSync(join(root, "package.json"))) {
|
||||
throw new RuntimePackError(`runtime pack for ${target.name} is not installed; install ${target.runtimePackPackage}@${compilerVersion} in your project or set SCRIPTC_RUNTIME_PACK to its directory`, "missing");
|
||||
}
|
||||
const packageText = readFileSync(join(root, "package.json"), "utf8");
|
||||
const manifestText = readFileSync(join(root, "runtime-pack.json"), "utf8");
|
||||
const identity = JSON.parse(packageText) as { name?: string; version?: string };
|
||||
|
||||
@@ -121,6 +121,14 @@ describe("native code-generation targets", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test.each(["x64", "arm64"])("GNU %s release links honor an explicit libc floor on the same host", (architecture) => {
|
||||
const triple = `${architecture === "x64" ? "x86_64" : "aarch64"}-linux-gnu.2.34`;
|
||||
expect(nativeCodegenTarget({ SCRIPTC_TARGET: triple }, "linux", architecture, "6.8.0", "gnu"))
|
||||
.toMatchObject({ minimumOs: "glibc 2.34", defaultLinker: "zig", defaultLinkerArgs: ["cc"], linkerTargetTriple: triple });
|
||||
expect(nativeCodegenTarget({ SCRIPTC_TARGET: triple.replace(".2.34", "") }, "darwin", "arm64", "24.0.0"))
|
||||
.toMatchObject({ minimumOs: "glibc 2.34", linkerTargetTriple: triple });
|
||||
});
|
||||
|
||||
test("owns helper executable linker arguments in the target specification", () => {
|
||||
expect(MACOS_ARM64_TARGET.executableLinkerArgs).toEqual([
|
||||
"-target",
|
||||
|
||||
@@ -217,7 +217,7 @@ export const MACOS_X64_TARGET: NativeTargetSpec = {
|
||||
export const LINUX_X64_GNU_TARGET: NativeTargetSpec = {
|
||||
name: "linux-x64-gnu", llvmTriple: "x86_64-unknown-linux-gnu", dataLayout: X64_ELF_LAYOUT,
|
||||
cpu: "generic", features: "", pointerBits: 64, endianness: "little", objectFormat: "elf",
|
||||
relocationModel: "pic", codeModel: "small", minimumOs: "glibc 2.36", helperMinimumOs: "glibc 2.35",
|
||||
relocationModel: "pic", codeModel: "small", minimumOs: "glibc 2.34", helperMinimumOs: "static Linux executable",
|
||||
architecture: "x64", platform: "linux", outputSuffixes: { asm: ".s", obj: ".o", exe: "" },
|
||||
executableLinkerArgs: ["-target", "x86_64-unknown-linux-gnu", "-pthread", "-Wl,--gc-sections"],
|
||||
defaultLinker: "clang", defaultLinkerArgs: [], runtimeSystemLibraries: ["m"],
|
||||
@@ -232,7 +232,7 @@ export const LINUX_X64_GNU_TARGET: NativeTargetSpec = {
|
||||
export const LINUX_ARM64_GNU_TARGET: NativeTargetSpec = {
|
||||
name: "linux-arm64-gnu", llvmTriple: "aarch64-unknown-linux-gnu", dataLayout: ARM64_ELF_LAYOUT,
|
||||
cpu: "generic", features: "", pointerBits: 64, endianness: "little", objectFormat: "elf",
|
||||
relocationModel: "pic", codeModel: "small", minimumOs: "glibc 2.36", helperMinimumOs: "glibc 2.35",
|
||||
relocationModel: "pic", codeModel: "small", minimumOs: "glibc 2.34", helperMinimumOs: "static Linux executable",
|
||||
architecture: "arm64", platform: "linux", outputSuffixes: { asm: ".s", obj: ".o", exe: "" },
|
||||
executableLinkerArgs: ["-target", "aarch64-unknown-linux-gnu", "-pthread", "-Wl,--gc-sections"],
|
||||
defaultLinker: "clang", defaultLinkerArgs: [], runtimeSystemLibraries: ["m"],
|
||||
@@ -264,7 +264,7 @@ export const WINDOWS_X64_MSVC_TARGET: NativeTargetSpec = {
|
||||
export const LINUX_X64_MUSL_TARGET: NativeTargetSpec = {
|
||||
...LINUX_X64_GNU_TARGET,
|
||||
name: "linux-x64-musl", llvmTriple: "x86_64-unknown-linux-musl",
|
||||
minimumOs: "musl 1.2", helperMinimumOs: "glibc 2.35",
|
||||
minimumOs: "musl 1.2", helperMinimumOs: "static Linux executable",
|
||||
executableLinkerArgs: ["-target", "x86_64-unknown-linux-musl", "-pthread", "-Wl,--gc-sections"],
|
||||
defaultLinker: "zig", defaultLinkerArgs: ["cc"], linkerTargetTriple: "x86_64-linux-musl",
|
||||
runtimePackPackage: "@scriptc/runtime-linux-x64-musl",
|
||||
@@ -276,7 +276,7 @@ export const LINUX_X64_MUSL_TARGET: NativeTargetSpec = {
|
||||
export const LINUX_ARM64_MUSL_TARGET: NativeTargetSpec = {
|
||||
...LINUX_ARM64_GNU_TARGET,
|
||||
name: "linux-arm64-musl", llvmTriple: "aarch64-unknown-linux-musl",
|
||||
minimumOs: "musl 1.2", helperMinimumOs: "glibc 2.35",
|
||||
minimumOs: "musl 1.2", helperMinimumOs: "static Linux executable",
|
||||
executableLinkerArgs: ["-target", "aarch64-unknown-linux-musl", "-pthread", "-Wl,--gc-sections"],
|
||||
defaultLinker: "zig", defaultLinkerArgs: ["cc"], linkerTargetTriple: "aarch64-linux-musl",
|
||||
runtimePackPackage: "@scriptc/runtime-linux-arm64-musl",
|
||||
@@ -420,9 +420,11 @@ function requestedTarget(
|
||||
case "x86_64-macos": return MACOS_X64_TARGET;
|
||||
case "x86_64-unknown-linux-gnu":
|
||||
case "x86_64-linux-gnu":
|
||||
case "x86_64-linux-gnu.2.34":
|
||||
case "x86_64-linux-gnu.2.36": return LINUX_X64_GNU_TARGET;
|
||||
case "aarch64-unknown-linux-gnu":
|
||||
case "aarch64-linux-gnu":
|
||||
case "aarch64-linux-gnu.2.34":
|
||||
case "aarch64-linux-gnu.2.36": return LINUX_ARM64_GNU_TARGET;
|
||||
case "x86_64-unknown-linux-musl":
|
||||
case "x86_64-linux-musl": return LINUX_X64_MUSL_TARGET;
|
||||
@@ -445,10 +447,12 @@ export function selectNativeTarget(
|
||||
): NativeTargetSpec | null {
|
||||
const target = requestedTarget(raw, host, hostPlatform as NodeJS.Platform);
|
||||
if (target === null) return null;
|
||||
if (target.platform === "linux" && target.name !== host.name && target.name !== "android-arm64") {
|
||||
if (target.platform === "linux" && target.name !== "android-arm64" &&
|
||||
(target.name !== host.name || raw.endsWith("-gnu.2.34") || raw.endsWith("-gnu.2.36"))) {
|
||||
return {
|
||||
...target, defaultLinker: "zig", defaultLinkerArgs: ["cc"],
|
||||
linkerTargetTriple: `${target.architecture === "x64" ? "x86_64" : "aarch64"}-linux-${target.name.endsWith("musl") ? "musl" : "gnu.2.36"}`,
|
||||
linkerTargetTriple: raw.endsWith("-gnu.2.36") ? raw
|
||||
: `${target.architecture === "x64" ? "x86_64" : "aarch64"}-linux-${target.name.endsWith("musl") ? "musl" : "gnu.2.34"}`,
|
||||
};
|
||||
}
|
||||
if (target.platform === "darwin" && hostPlatform !== "darwin") {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
@@ -21,6 +21,7 @@ test("native toolchain installation paths relocate with the manifest", () => {
|
||||
expect(toolchain.linker).toBe(join(root, "tools/linker"));
|
||||
expect(toolchain.linkerArgs).toEqual(["cc"]);
|
||||
expect(toolchain.target.name).toBe("macos-arm64");
|
||||
expect(loadNativeToolchain(path, { SCRIPTC_TARGET: "aarch64-apple-ios" }).target.name).toBe("ios-arm64");
|
||||
for (const invalid of [null, [], {}, { ...manifest, schema: "other" }, { ...manifest, target: "other" },
|
||||
{ ...manifest, ts7: "" }, { ...manifest, linker_args: [1] }]) {
|
||||
writeFileSync(path, JSON.stringify(invalid));
|
||||
@@ -28,3 +29,50 @@ test("native toolchain installation paths relocate with the manifest", () => {
|
||||
}
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("cross-target packs can be added to a project after the compiler is installed", () => {
|
||||
const root = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-project-pack-"));
|
||||
const installation = join(root, "compiler", "bin");
|
||||
const project = join(root, "project");
|
||||
const nested = join(project, "src", "nested");
|
||||
const pack = join(project, "node_modules/@scriptc/runtime-wasm32-wasi");
|
||||
mkdirSync(installation, { recursive: true });
|
||||
mkdirSync(nested, { recursive: true });
|
||||
const path = join(installation, "scriptc.json");
|
||||
const manifest: NativeToolchainManifest = {
|
||||
schema: "scriptc.native-toolchain.v1", compiler_version: "1.2.3", target: "macos-arm64",
|
||||
ts7: "../lib/typescript/tsc", llvm_package: "../lib/llvm", runtime_pack: "../lib/runtime-darwin-arm64",
|
||||
linker: "clang", linker_args: [], dsymutil: "dsymutil",
|
||||
};
|
||||
writeFileSync(path, JSON.stringify(manifest));
|
||||
const env = { SCRIPTC_TARGET: "wasm32-wasi" };
|
||||
try {
|
||||
// Object emission does not require a pack to be installed.
|
||||
expect(loadNativeToolchain(path, env, nested).target.name).toBe("wasm32-wasi");
|
||||
mkdirSync(pack, { recursive: true });
|
||||
writeFileSync(join(pack, "package.json"), JSON.stringify({ name: "@scriptc/runtime-wasm32-wasi", version: "1.2.3" }));
|
||||
expect(loadNativeToolchain(path, env, nested).runtimePackRoot).toBe(pack);
|
||||
expect(loadNativeToolchain(path, { ...env, SCRIPTC_RUNTIME_PACK: "../explicit" }, nested).runtimePackRoot)
|
||||
.toBe(join(root, "compiler", "explicit"));
|
||||
expect(loadNativeToolchain(path, {}, nested).runtimePackRoot).toBe(join(root, "compiler", "lib/runtime-darwin-arm64"));
|
||||
const registered = join(root, "registered");
|
||||
mkdirSync(registered);
|
||||
writeFileSync(path, JSON.stringify({ ...manifest, runtime_packs: [{ target: "wasm32-wasi", path: registered }] }));
|
||||
expect(loadNativeToolchain(path, env, nested).runtimePackRoot).toBe(registered);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("versioned GNU targets select Zig while ordinary host builds keep the installed linker", () => {
|
||||
const root = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-gnu-linker-"));
|
||||
const path = join(root, "scriptc.json");
|
||||
writeFileSync(path, JSON.stringify({
|
||||
schema: "scriptc.native-toolchain.v1", compiler_version: "1.2.3", target: "linux-x64-gnu",
|
||||
ts7: "tsc", llvm_package: "llvm", runtime_pack: "runtime", linker: "clang", linker_args: [], dsymutil: "dsymutil",
|
||||
}));
|
||||
try {
|
||||
expect(loadNativeToolchain(path).linker).toBe("clang");
|
||||
expect(loadNativeToolchain(path, { SCRIPTC_TARGET: "x86_64-linux-gnu.2.34" })).toMatchObject({
|
||||
linker: "zig", linkerArgs: ["cc"], target: { linkerTargetTriple: "x86_64-linux-gnu.2.34" },
|
||||
});
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { NATIVE_TARGETS, selectNativeTarget, type NativeTargetSpec, type NativeHelperSpec } from "../backend/targets.js";
|
||||
|
||||
@@ -52,7 +52,20 @@ function commandFrom(root: string, value: string): string {
|
||||
return value.includes("/") || value.includes("\\") ? pathFrom(root, value) : value;
|
||||
}
|
||||
|
||||
export function loadNativeToolchain(path: string, env: NodeJS.ProcessEnv = {}): NativeToolchain {
|
||||
/** Additional targets are ordinary project dependencies. Resolve them when
|
||||
* requested so adding a pack never requires reinstalling the compiler. */
|
||||
function installedRuntimePack(start: string, packageName: string): string | undefined {
|
||||
let directory = resolve(start);
|
||||
for (;;) {
|
||||
const candidate = join(directory, "node_modules", packageName);
|
||||
if (existsSync(join(candidate, "package.json"))) return candidate;
|
||||
const parent = dirname(directory);
|
||||
if (parent === directory) return undefined;
|
||||
directory = parent;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadNativeToolchain(path: string, env: NodeJS.ProcessEnv = {}, cwd: string = process.cwd()): NativeToolchain {
|
||||
const raw: unknown = JSON.parse(readFileSync(path, "utf8"));
|
||||
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid native toolchain manifest");
|
||||
const value = raw as Record<string, unknown>;
|
||||
@@ -84,14 +97,20 @@ export function loadNativeToolchain(path: string, env: NodeJS.ProcessEnv = {}):
|
||||
const targets: readonly NativeTargetSpec[] = NATIVE_TARGETS;
|
||||
const host = targets.find((item) => item.name === manifest.target);
|
||||
if (host === undefined || host.platform === "wasi") throw new Error(`unsupported native compiler target: ${manifest.target}`);
|
||||
const target = selectNativeTarget(env["SCRIPTC_TARGET"] ?? "", host);
|
||||
const target = selectNativeTarget(env["SCRIPTC_TARGET"] ?? "", host, host.platform);
|
||||
if (target === null) throw new Error(`unsupported native compiler target: ${env["SCRIPTC_TARGET"]}`);
|
||||
const root = dirname(resolve(path));
|
||||
const helperPackageRoot = pathFrom(root, env["SCRIPTC_LLVM_PACKAGE"] ?? manifest.llvm_package);
|
||||
const runtimePath = manifest.runtime_packs?.find((pack) => pack.target === target.name)?.path
|
||||
let runtimePath = manifest.runtime_packs?.find((pack) => pack.target === target.name)?.path
|
||||
?? (target.name === host.name ? manifest.runtime_pack : join(dirname(manifest.runtime_pack), target.runtimePackPackage.replace("@scriptc/", "")));
|
||||
const linker = env["SCRIPTC_LINKER"] ?? (target.name === host.name ? manifest.linker : target.defaultLinker);
|
||||
const linkerArgs = env["SCRIPTC_LINKER"] !== undefined ? [] : target.name === host.name ? manifest.linker_args : [...target.defaultLinkerArgs];
|
||||
if (target.name !== host.name && env["SCRIPTC_RUNTIME_PACK"] === undefined && !existsSync(pathFrom(root, runtimePath))) {
|
||||
runtimePath = installedRuntimePack(cwd, target.runtimePackPackage)
|
||||
?? installedRuntimePack(root, target.runtimePackPackage)
|
||||
?? runtimePath;
|
||||
}
|
||||
const hostLinker = target.name === host.name && target.defaultLinker === host.defaultLinker;
|
||||
const linker = env["SCRIPTC_LINKER"] ?? (hostLinker ? manifest.linker : target.defaultLinker);
|
||||
const linkerArgs = env["SCRIPTC_LINKER"] !== undefined ? [] : hostLinker ? manifest.linker_args : [...target.defaultLinkerArgs];
|
||||
return {
|
||||
compilerVersion: manifest.compiler_version,
|
||||
ts7Executable: pathFrom(root, manifest.ts7),
|
||||
|
||||
@@ -7,5 +7,5 @@ import { fileURLToPath } from "node:url";
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url))); const repo = fileURLToPath(new URL("../../..", import.meta.url));
|
||||
if (process.platform !== "linux" || process.arch !== "arm64") { process.stdout.write("@scriptc/llvm-linux-arm64-gnu: skipped on this host\n"); process.exit(0); }
|
||||
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-arm64-gnu");
|
||||
execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=aarch64-unknown-linux-gnu", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" });
|
||||
execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=aarch64-unknown-linux-gnu", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32", ...nativeTargetCmakeArgs, "-DSCRIPTC_STATIC_LINUX_BUILD=ON", "-DCMAKE_EXE_LINKER_FLAGS=-static", "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" });
|
||||
execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); mkdirSync(join(root, "bin"), { recursive: true }); const output = join(root, "bin", "scriptc-llvm-codegen"); copyFileSync(join(build, "scriptc-llvm-codegen"), output); execFileSync("strip", [output], { stdio: "inherit" }); chmodSync(output, 0o755);
|
||||
|
||||
@@ -8,5 +8,5 @@ const root = dirname(dirname(fileURLToPath(import.meta.url))); const repo = file
|
||||
if (process.platform !== "linux" || process.arch !== "arm64") { process.stdout.write("@scriptc/llvm-linux-arm64-musl: skipped on this host\n"); process.exit(0); }
|
||||
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-arm64-musl");
|
||||
execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`, `-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`, "-DSCRIPTC_MUSL_BUILD=ON", "-DCMAKE_EXE_LINKER_FLAGS=-static", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=aarch64-unknown-linux-musl", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" });
|
||||
execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`, `-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`, "-DSCRIPTC_STATIC_LINUX_BUILD=ON", "-DCMAKE_EXE_LINKER_FLAGS=-static", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=aarch64-unknown-linux-musl", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" });
|
||||
execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); mkdirSync(join(root, "bin"), { recursive: true }); const output = join(root, "bin", "scriptc-llvm-codegen"); copyFileSync(join(build, "scriptc-llvm-codegen"), output); execFileSync("strip", [output], { stdio: "inherit" }); chmodSync(output, 0o755);
|
||||
|
||||
@@ -10,7 +10,7 @@ const repo = fileURLToPath(new URL("../../..", import.meta.url));
|
||||
if (process.platform !== "linux" || process.arch !== "x64") { process.stdout.write("@scriptc/llvm-linux-x64-gnu: skipped on this host\n"); process.exit(0); }
|
||||
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-x64-gnu");
|
||||
execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-unknown-linux-gnu", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" });
|
||||
execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-unknown-linux-gnu", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", ...nativeTargetCmakeArgs, "-DSCRIPTC_STATIC_LINUX_BUILD=ON", "-DCMAKE_EXE_LINKER_FLAGS=-static", "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" });
|
||||
execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" });
|
||||
mkdirSync(join(root, "bin"), { recursive: true });
|
||||
const output = join(root, "bin", "scriptc-llvm-codegen"); copyFileSync(join(build, "scriptc-llvm-codegen"), output); execFileSync("strip", [output], { stdio: "inherit" }); chmodSync(output, 0o755);
|
||||
|
||||
@@ -10,5 +10,5 @@ const repo = fileURLToPath(new URL("../../..", import.meta.url));
|
||||
if (process.platform !== "linux" || process.arch !== "x64") { process.stdout.write("@scriptc/llvm-linux-x64-musl: skipped on this host\n"); process.exit(0); }
|
||||
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-x64-musl");
|
||||
execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`, `-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`, "-DSCRIPTC_MUSL_BUILD=ON", "-DCMAKE_EXE_LINKER_FLAGS=-static", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-unknown-linux-musl", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" });
|
||||
execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`, `-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`, "-DSCRIPTC_STATIC_LINUX_BUILD=ON", "-DCMAKE_EXE_LINKER_FLAGS=-static", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-unknown-linux-musl", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" });
|
||||
execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); mkdirSync(join(root, "bin"), { recursive: true }); const output = join(root, "bin", "scriptc-llvm-codegen"); copyFileSync(join(build, "scriptc-llvm-codegen"), output); execFileSync("strip", [output], { stdio: "inherit" }); chmodSync(output, 0o755);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const GLIBC_RUNTIME_FLOOR = "2.36";
|
||||
export const GLIBC_RUNTIME_FLOOR = "2.34";
|
||||
|
||||
const GNU_ARCHITECTURES = {
|
||||
x64: "x86_64",
|
||||
|
||||
@@ -2,11 +2,11 @@ import { expect, test } from "vitest";
|
||||
import { glibcRuntimeToolchain } from "../scripts/glibc-toolchain.mjs";
|
||||
|
||||
test.each([
|
||||
["x64", "x86_64-linux-gnu.2.36"],
|
||||
["arm64", "aarch64-linux-gnu.2.36"],
|
||||
["x64", "x86_64-linux-gnu.2.34"],
|
||||
["arm64", "aarch64-linux-gnu.2.34"],
|
||||
])("pins the %s GNU runtime to its glibc floor", (architecture, target) => {
|
||||
expect(glibcRuntimeToolchain(architecture)).toEqual({
|
||||
minimumOs: "glibc 2.36",
|
||||
minimumOs: "glibc 2.34",
|
||||
compiler: "zig",
|
||||
compilerArgs: ["cc"],
|
||||
archiver: "zig",
|
||||
|
||||
@@ -78,6 +78,31 @@ void scr_runtime_abi_v5(void);
|
||||
* structured trap-teaching form before delivery (a message that already
|
||||
* begins with the 0x01 marker passes verbatim) — see ScrLibSinkFn below. */
|
||||
_Noreturn void scr_trap(const char *msg);
|
||||
|
||||
/* glibc added arc4random_buf in 2.36. Older GNU targets use the same kernel
|
||||
* CSPRNG as the musl shim, with no global state or persistent descriptor. */
|
||||
#ifdef __GLIBC__
|
||||
#if !__GLIBC_PREREQ(2, 36)
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/random.h>
|
||||
static inline void scr_glibc_random_buf(void *buf, size_t n) {
|
||||
unsigned char *p = buf;
|
||||
while (n > 0) {
|
||||
ssize_t got = getrandom(p, n, 0);
|
||||
if (got > 0) {
|
||||
p += (size_t)got;
|
||||
n -= (size_t)got;
|
||||
} else if (got < 0 && errno == EINTR) {
|
||||
continue;
|
||||
} else {
|
||||
scr_trap("scriptc: getrandom failed\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
#define arc4random_buf scr_glibc_random_buf
|
||||
#endif
|
||||
#endif
|
||||
_Noreturn void scr_trap_fmt(const char *fmt, ...);
|
||||
|
||||
/* ── library mode (scr_library.c, linked only into library artifacts) ─────
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const report = process.platform === "linux" ? process.report.getReport() as { header: { glibcVersionRuntime?: string } } : undefined;
|
||||
const version = report?.header.glibcVersionRuntime?.split(".").map(Number);
|
||||
const supported = version !== undefined && (version[0]! > 2 || (version[0] === 2 && version[1]! >= 34)) &&
|
||||
(process.arch === "x64" || process.arch === "arm64");
|
||||
test.skipIf(!supported)("glibc 2.34 CSPRNG retries interruptions and partial reads and traps on entropy failure", () => {
|
||||
const directory = mkdtempSync("/tmp/scriptc-glibc-random-");
|
||||
const binary = join(directory, "random");
|
||||
try {
|
||||
execFileSync("zig", ["cc", "-target", `${process.arch === "x64" ? "x86_64" : "aarch64"}-linux-gnu.2.34`,
|
||||
"-std=c11", "-O2", "-D_GNU_SOURCE", "-I", join(import.meta.dirname, "../src"),
|
||||
join(import.meta.dirname, "test_glibc_random.c"), "-o", binary], { timeout: 120_000 });
|
||||
expect(execFileSync(binary, [], { encoding: "utf8" })).toBe("entropy retries and failures verified\n");
|
||||
} finally { rmSync(directory, { recursive: true, force: true }); }
|
||||
}, 120_000);
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "scr_runtime.h"
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static jmp_buf panic;
|
||||
static int calls;
|
||||
static int mode;
|
||||
static unsigned char next_byte;
|
||||
|
||||
ssize_t getrandom(void *buf, size_t n, unsigned int flags) {
|
||||
assert(flags == 0);
|
||||
calls++;
|
||||
if (mode == 1) return 0;
|
||||
if (mode == 2) { errno = EIO; return -1; }
|
||||
if (calls == 1) { errno = EINTR; return -1; }
|
||||
size_t count = n < 3 ? n : 3;
|
||||
unsigned char *bytes = buf;
|
||||
for (size_t i = 0; i < count; i++) bytes[i] = next_byte++;
|
||||
return (ssize_t)count;
|
||||
}
|
||||
|
||||
_Noreturn void scr_trap(const char *message) {
|
||||
assert(strcmp(message, "scriptc: getrandom failed\n") == 0);
|
||||
longjmp(panic, 1);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
unsigned char bytes[17];
|
||||
memset(bytes, 255, sizeof bytes);
|
||||
arc4random_buf(bytes, 0);
|
||||
assert(calls == 0);
|
||||
arc4random_buf(bytes, sizeof bytes);
|
||||
assert(calls == 7);
|
||||
for (size_t i = 0; i < sizeof bytes; i++) assert(bytes[i] == i);
|
||||
for (mode = 1; mode <= 2; mode++) {
|
||||
if (setjmp(panic) == 0) {
|
||||
arc4random_buf(bytes, sizeof bytes);
|
||||
assert(0 && "entropy failure must trap");
|
||||
}
|
||||
}
|
||||
puts("entropy retries and failures verified");
|
||||
return 0;
|
||||
}
|
||||
Generated
-33
@@ -74,39 +74,6 @@ importers:
|
||||
'@scriptc/cli-win32-x64-msvc':
|
||||
specifier: workspace:*
|
||||
version: link:../cli-win32-x64-msvc
|
||||
'@scriptc/runtime-android-arm64':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-android-arm64
|
||||
'@scriptc/runtime-darwin-arm64':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-darwin-arm64
|
||||
'@scriptc/runtime-darwin-x64':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-darwin-x64
|
||||
'@scriptc/runtime-ios-arm64':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-ios-arm64
|
||||
'@scriptc/runtime-ios-simulator-arm64':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-ios-simulator-arm64
|
||||
'@scriptc/runtime-linux-arm64-gnu':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-linux-arm64-gnu
|
||||
'@scriptc/runtime-linux-arm64-musl':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-linux-arm64-musl
|
||||
'@scriptc/runtime-linux-x64-gnu':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-linux-x64-gnu
|
||||
'@scriptc/runtime-linux-x64-musl':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-linux-x64-musl
|
||||
'@scriptc/runtime-wasm32-wasi':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-wasm32-wasi
|
||||
'@scriptc/runtime-win32-x64-msvc':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime-win32-x64-msvc
|
||||
|
||||
packages/cli-darwin-arm64: {}
|
||||
|
||||
|
||||
+90
-22
@@ -1,9 +1,9 @@
|
||||
/* Reproducible CLI build latency: one empty-cache build, exact repeats, and
|
||||
/* Reproducible CLI build latency: empty-cache builds, exact repeats, and
|
||||
* actual edits to an imported module. Each invocation owns a fresh cache so
|
||||
* an earlier benchmark cannot turn its edit cases into native object hits. */
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { access, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -17,8 +17,21 @@ const { values } = parseArgs({ options: {
|
||||
functions: { type: "string", default: "16" },
|
||||
timings: { type: "boolean", default: false },
|
||||
strip: { type: "boolean", default: false },
|
||||
workload: { type: "string", default: "modules" },
|
||||
memory: { type: "boolean", default: false },
|
||||
launches: { type: "string", default: "20" },
|
||||
help: { type: "boolean", default: false },
|
||||
} });
|
||||
if (values.help) {
|
||||
console.log("Usage: pnpm bench:builds [--compiler=<native executable>] [--workload=modules|log-summary|inventory-report] [--iterations=5] [--optimization=dev|release] [--memory] [--timings] [--launches=20] [--modules=16] [--functions=16] [--strip]");
|
||||
console.log("Reports empty-cache builds, unchanged builds, edits, executable size, and end-to-end run time. --memory reports peak process RSS using /usr/bin/time, not summed concurrent memory. The modules workload is generated; log-summary and inventory-report exercise file-processing applications.");
|
||||
process.exit(0);
|
||||
}
|
||||
const iterations = Number(values.iterations);
|
||||
const launches = Number(values.launches);
|
||||
if (!Number.isInteger(launches) || launches < 1 || launches > 1000) throw new Error("--launches must be an integer between 1 and 1000");
|
||||
if (!["modules", "log-summary", "inventory-report"].includes(values.workload)) throw new Error("--workload must be modules, log-summary, or inventory-report");
|
||||
if (values.memory && process.platform !== "darwin" && process.platform !== "linux") throw new Error("--memory requires macOS or Linux /usr/bin/time");
|
||||
if (!Number.isInteger(iterations) || iterations < 1 || iterations > 100) {
|
||||
throw new Error("--iterations must be an integer between 1 and 100");
|
||||
}
|
||||
@@ -43,6 +56,9 @@ delete env.SCRIPTC_CACHE_MAX_MB;
|
||||
delete env.SCRIPTC_TEST_STABLE_TOOLCHAIN;
|
||||
delete env.SCRIPTC_TIMING;
|
||||
const samples = [];
|
||||
let previousOutput;
|
||||
const runtimeArgs = values.workload === "log-summary" ? [join(root, "requests.log"), "--min-status", "200", "--limit", "12"]
|
||||
: values.workload === "inventory-report" ? [join(root, "inventory.tsv")] : [];
|
||||
|
||||
function moduleSource(module, offset) {
|
||||
return Array.from({ length: functions }, (_, fn) =>
|
||||
@@ -50,32 +66,43 @@ function moduleSource(module, offset) {
|
||||
).join("\n") + "\n";
|
||||
}
|
||||
|
||||
function run(command, args, timings = false) {
|
||||
const result = spawnSync(command, args, {
|
||||
function run(command, args, timings = false, memory = false) {
|
||||
const measuredArgs = process.platform === "darwin" ? ["-l", command, ...args] : ["-f", "scriptc_peak_rss_kib=%M", command, ...args];
|
||||
const result = spawnSync(memory ? "/usr/bin/time" : command, memory ? measuredArgs : args, {
|
||||
env: timings ? { ...env, SCRIPTC_TIMING: "1" } : env,
|
||||
encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 120_000,
|
||||
encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 600_000,
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
assert.equal(result.signal, null, `${command} received ${result.signal}`);
|
||||
return { stdout: result.stdout, stderr: result.stderr, status: result.status };
|
||||
let stderr = result.stderr;
|
||||
let peakRssBytes;
|
||||
if (memory) {
|
||||
const match = process.platform === "darwin" ? /\s+(\d+)\s+maximum resident set size/.exec(stderr) : /scriptc_peak_rss_kib=(\d+)/.exec(stderr);
|
||||
assert.ok(match, "time did not report peak RSS");
|
||||
peakRssBytes = Number(match[1]) * (process.platform === "darwin" ? 1 : 1024);
|
||||
stderr = process.platform === "darwin" ? stderr.replace(/\s+\d+(?:\.\d+)? real\s+[\s\S]*$/, "") : stderr.replace(/scriptc_peak_rss_kib=\d+\n?/, "");
|
||||
}
|
||||
return { stdout: result.stdout, stderr, status: result.status, ...(peakRssBytes === undefined ? {} : { peakRssBytes }) };
|
||||
}
|
||||
|
||||
function build(phase) {
|
||||
const start = performance.now();
|
||||
const args = ["build", entry, `--optimization=${values.optimization}`, "-o", binary, ...(values.strip ? ["--strip"] : [])];
|
||||
const result = values.compiler ? run(cli, args, values.timings) : run(process.execPath, [cli, ...args], values.timings);
|
||||
const result = values.compiler ? run(cli, args, values.timings, values.memory) : run(process.execPath, [cli, ...args], values.timings, values.memory);
|
||||
const ms = Math.round((performance.now() - start) * 10) / 10;
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
// Correctness checks are outside the timed build and run after EVERY edit,
|
||||
// so a stale executable cannot masquerade as a faster rebuild.
|
||||
const oracle = run(process.execPath, [entry]);
|
||||
const oracle = run(process.execPath, [entry, ...runtimeArgs]);
|
||||
assert.equal(oracle.status, 0, oracle.stderr);
|
||||
assert.deepEqual(run(binary, []), oracle);
|
||||
if (phase === "edit") assert.notEqual(oracle.stdout, previousOutput, "the edit must change the application's output");
|
||||
assert.deepEqual(run(binary, runtimeArgs), oracle);
|
||||
previousOutput = oracle.stdout;
|
||||
const timings = result.stderr.split("\n").flatMap((line) => {
|
||||
const prefix = "scriptc timing ";
|
||||
return line.startsWith(prefix) ? [JSON.parse(line.slice(prefix.length))] : [];
|
||||
});
|
||||
samples.push({ phase, ms, ...(values.timings ? { timings } : {}) });
|
||||
samples.push({ phase, ms, ...(values.timings ? { timings } : {}), ...(values.memory ? { peak_rss_bytes: result.peakRssBytes } : {}) });
|
||||
process.stderr.write(`${phase}: ${ms} ms\n`);
|
||||
}
|
||||
|
||||
@@ -87,20 +114,54 @@ function median(phase) {
|
||||
|
||||
try {
|
||||
await mkdir(cache, { mode: 0o700 });
|
||||
await Promise.all(Array.from({ length: modules }, (_, i) => writeFile(join(root, `module${i}.ts`), moduleSource(i, 0))));
|
||||
await writeFile(entry, [
|
||||
...Array.from({ length: modules }, (_, i) => `import * as m${i} from './module${i}.ts';`),
|
||||
"let total = 0;",
|
||||
...Array.from({ length: modules }, (_, i) => `total += ${Array.from({ length: functions }, (_, fn) => `m${i}.value${fn}(2)`).join(" + ")};`),
|
||||
"console.log(total);",
|
||||
"",
|
||||
].join("\n"));
|
||||
build("cold");
|
||||
if (values.workload !== "modules") {
|
||||
await cp(fileURLToPath(new URL(`../benchmarks/builds/${values.workload}`, import.meta.url)), root, { recursive: true });
|
||||
await writeFile(join(root, "package.json"), '{"type":"module"}\n');
|
||||
}
|
||||
if (values.workload === "log-summary") {
|
||||
await writeFile(join(root, "requests.log"), Array.from({ length: 10_000 }, (_, i) =>
|
||||
`/route/${i % 37}?request=${i} ${i % 13 === 0 ? 500 : 200} ${i % 250} ${200 + i % 4000}`,
|
||||
).join("\n") + "\n");
|
||||
} else if (values.workload === "inventory-report") {
|
||||
await writeFile(join(root, "inventory.tsv"), Array.from({ length: 10_000 }, (_, i) =>
|
||||
["sku-" + i, "category-" + i % 17, "product " + i, i % 40, 199 + i % 5000].join("\t"),
|
||||
).join("\n") + "\n");
|
||||
} else {
|
||||
await Promise.all(Array.from({ length: modules }, (_, i) => writeFile(join(root, `module${i}.ts`), moduleSource(i, 0))));
|
||||
await writeFile(entry, [
|
||||
...Array.from({ length: modules }, (_, i) => `import * as m${i} from './module${i}.ts';`),
|
||||
"let total = 0;",
|
||||
...Array.from({ length: modules }, (_, i) => `total += ${Array.from({ length: functions }, (_, fn) => `m${i}.value${fn}(2)`).join(" + ")};`),
|
||||
"console.log(total);",
|
||||
"",
|
||||
].join("\n"));
|
||||
}
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
if (i > 0) {
|
||||
await rm(cache, { recursive: true, force: true });
|
||||
await mkdir(cache, { mode: 0o700 });
|
||||
}
|
||||
build("cold");
|
||||
}
|
||||
for (let i = 0; i < iterations; i++) build("unchanged");
|
||||
for (let i = 1; i <= iterations; i++) {
|
||||
await writeFile(join(root, "module0.ts"), moduleSource(0, i));
|
||||
if (values.workload !== "modules") {
|
||||
const path = join(root, values.workload === "log-summary" ? "format.ts" : "config.ts");
|
||||
await writeFile(path, (await readFile(path, "utf8")).replace(/export const revision = \d+;/, `export const revision = ${i};`));
|
||||
} else await writeFile(join(root, "module0.ts"), moduleSource(0, i));
|
||||
build("edit");
|
||||
}
|
||||
const runtimeSamples = [];
|
||||
for (let i = -3; i < launches; i++) {
|
||||
const start = performance.now();
|
||||
const result = run(binary, runtimeArgs);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
if (i >= 0) runtimeSamples.push(performance.now() - start);
|
||||
}
|
||||
runtimeSamples.sort((a, b) => a - b);
|
||||
const memoryResult = values.memory ? run(binary, runtimeArgs, false, true) : undefined;
|
||||
if (memoryResult !== undefined) assert.equal(memoryResult.status, 0, memoryResult.stderr);
|
||||
const runtimeMemory = memoryResult?.peakRssBytes;
|
||||
process.stdout.write(JSON.stringify({
|
||||
compiler: values.compiler ?? "workspace CLI",
|
||||
node: process.version,
|
||||
@@ -108,11 +169,18 @@ try {
|
||||
arch: process.arch,
|
||||
optimization: values.optimization,
|
||||
strip: values.strip,
|
||||
modules: modules + 1,
|
||||
functions: modules * functions,
|
||||
workload: values.workload,
|
||||
...(values.workload === "modules" ? { modules: modules + 1, functions: modules * functions }
|
||||
: { application_modules: values.workload === "log-summary" ? 5 : 15, input_records: 10_000 }),
|
||||
iterations,
|
||||
median_ms: Object.fromEntries(["cold", "unchanged", "edit"].map((phase) => [phase, median(phase)])),
|
||||
samples,
|
||||
executable: {
|
||||
bytes: (await stat(binary)).size,
|
||||
median_run_ms: (runtimeSamples[Math.floor((launches - 1) / 2)] + runtimeSamples[Math.floor(launches / 2)]) / 2,
|
||||
runs: launches,
|
||||
...(runtimeMemory === undefined ? {} : { peak_rss_bytes: runtimeMemory }),
|
||||
},
|
||||
}, null, 2) + "\n");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
|
||||
@@ -11,21 +11,31 @@ import type { NativeToolchainManifest } from "../packages/compiler/src/native/to
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const output = resolve(process.argv[2] ?? join(root, ".scriptc/native-cli"));
|
||||
const target = nativeCodegenTarget();
|
||||
let target = nativeCodegenTarget();
|
||||
if (target === null || target.platform === "wasi") throw new Error("a supported native host is required to build the compiler");
|
||||
// Release executables use the runtime pack's libc baseline, even when the
|
||||
// build host has a newer libc. Sanitizer builds use the host's sanitizer SDK.
|
||||
if (target.name.endsWith("-gnu") && process.env["SCRIPTC_SAN"] !== "1") {
|
||||
const architecture = target.architecture === "x64" ? "x86_64" : "aarch64";
|
||||
process.env["SCRIPTC_TARGET"] = `${architecture}-linux-gnu.2.34`;
|
||||
target = nativeCodegenTarget()!;
|
||||
}
|
||||
const bin = join(output, "bin");
|
||||
const lib = join(output, "lib");
|
||||
const seed = join(dirname(output), ".scriptc", basename(output) + "-seed");
|
||||
for (const dir of [bin, lib, seed]) mkdirSync(dir, { recursive: true });
|
||||
const compiler = process.env["SCRIPTC_CC"] ?? target.defaultLinker;
|
||||
const compilerArgs = process.env["SCRIPTC_CC"] === undefined ? [...target.defaultLinkerArgs] : [];
|
||||
// Zig's C debug mode otherwise inserts UBSan calls into these objects. They
|
||||
// must also link through the destination host's ordinary compiler driver.
|
||||
const cFlags = ["-std=c11", "-fno-sanitize=undefined", "-Wall", "-Wextra", "-Werror", "-target", target.linkerTargetTriple];
|
||||
const nativeSources = join(root, "packages/compiler/native");
|
||||
const functions: unknown[] = [];
|
||||
const libraries: string[] = [];
|
||||
for (const name of ["ts7-process", "host"]) {
|
||||
const object = join(seed, name + target.outputSuffixes.obj);
|
||||
execFileSync(compiler, [
|
||||
...compilerArgs, "-std=c11", "-Wall", "-Wextra", "-Werror", "-target", target.linkerTargetTriple,
|
||||
...compilerArgs, ...cFlags,
|
||||
"-c", join(nativeSources, name + ".c"), "-o", object,
|
||||
], { stdio: "inherit" });
|
||||
const manifest = JSON.parse(readFileSync(join(nativeSources, name + ".ffi.json"), "utf8")) as { functions: unknown[] };
|
||||
@@ -37,7 +47,7 @@ writeFileSync(ffi, JSON.stringify({ ffi_format: 6, functions, libraries }, null,
|
||||
const executable = join(bin, "scriptc" + target.outputSuffixes.exe);
|
||||
const comptime = join(lib, "scriptc-comptime" + target.outputSuffixes.exe);
|
||||
execFileSync(compiler, [
|
||||
...compilerArgs, "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror", "-target", target.linkerTargetTriple,
|
||||
...compilerArgs, ...cFlags, "-O2",
|
||||
"-I", join(root, "packages/runtime/vendor/quickjs-ng"), join(nativeSources, "comptime.c"),
|
||||
join(root, "packages", target.runtimePackPackage.replace("@scriptc/", ""), "artifacts/vendor/quickjs/libscriptc-quickjs.a"),
|
||||
"-lm", "-lpthread", "-o", comptime,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join, relative, resolve } from "node:path";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
@@ -10,7 +10,6 @@ const packages = resolve(process.argv[2] ?? join(repository, "packages"));
|
||||
const output = resolve(process.argv[3] ?? join(repository, ".scriptc/releases"));
|
||||
mkdirSync(output, { recursive: true });
|
||||
const compiler = JSON.parse(readFileSync(join(packages, "compiler/package.json"), "utf8"));
|
||||
const runtimes = Object.keys(compiler.optionalDependencies).filter((name) => name.startsWith("@scriptc/runtime-"));
|
||||
const sums = [];
|
||||
|
||||
for (const name of readdirSync(packages).filter((name) => name.startsWith("cli-")).sort()) {
|
||||
@@ -26,20 +25,6 @@ for (const name of readdirSync(packages).filter((name) => name.startsWith("cli-"
|
||||
const binary = join(bin, name.includes("win32-") ? "scriptc.exe" : "scriptc");
|
||||
const manifest = JSON.parse(readFileSync(binary + ".json", "utf8"));
|
||||
const hostPath = (value) => name.includes("win32-") ? value.replaceAll("\\", "/") : value;
|
||||
const packs = new Map();
|
||||
for (const packageName of runtimes) {
|
||||
const directory = packageName.replace("@scriptc/", "");
|
||||
const from = join(packages, directory);
|
||||
const to = join(distribution, "lib", directory);
|
||||
const runtime = JSON.parse(readFileSync(join(from, "runtime-pack.json"), "utf8"));
|
||||
if (runtime.version !== identity.version || runtime.package !== packageName) throw new Error(`runtime version mismatch: ${packageName}`);
|
||||
for (const member of ["package.json", "runtime-pack.json", "artifacts"]) {
|
||||
cpSync(join(from, member), join(to, member), { recursive: true });
|
||||
}
|
||||
packs.set(runtime.target.name, { target: runtime.target.name, path: relative(bin, to) });
|
||||
}
|
||||
manifest.runtime_packs = [...packs.values()];
|
||||
writeFileSync(binary + ".json", JSON.stringify(manifest, null, 2) + "\n");
|
||||
// GitHub artifact transport drops executable modes. Restore the native
|
||||
// tools before validating and creating the standalone archive.
|
||||
for (const path of [binary, resolve(bin, hostPath(manifest.ts7)), resolve(bin, hostPath(manifest.comptime)),
|
||||
|
||||
@@ -48,6 +48,10 @@ const invariantCaseShardedFiles = ["tests/harness/coverage.test.ts"];
|
||||
// independent cases can spread across the once lane's Sandboxes without
|
||||
// colliding with corpus case selection.
|
||||
const cacheCaseShardedFiles = ["packages/compiler/src/backend/native-toolchain.test.ts"];
|
||||
const nativeBuildFiles = [
|
||||
"tests/harness/self-hosting-native-driver.test.ts",
|
||||
"packages/runtime/test/glibc-random.test.ts",
|
||||
];
|
||||
const caseShardedFiles = [
|
||||
...laneCaseShardedFiles,
|
||||
...invariantCaseShardedFiles,
|
||||
@@ -745,14 +749,17 @@ try {
|
||||
"/workspace",
|
||||
3 * 60_000,
|
||||
);
|
||||
// Zig is a build-only dependency in this lane. Cross-target suites own
|
||||
// the conditional Zig tests; exposing it here would silently expand the
|
||||
// native-cache shard while that shard deliberately disables stable
|
||||
// toolchain caching.
|
||||
// Keep Zig available to the portable compiler bootstrap and entropy
|
||||
// contract, without expanding the conditional cross-target cache tests.
|
||||
await execIn(
|
||||
worker,
|
||||
"sudo",
|
||||
["rm", "-f", "/usr/local/bin/zig"],
|
||||
"sh",
|
||||
["-c", [
|
||||
"set -eu",
|
||||
"mkdir -p /tmp/scriptc-native-build-tools",
|
||||
'ln -sf "$(readlink -f "$(command -v zig)")" /tmp/scriptc-native-build-tools/zig',
|
||||
"sudo rm -f /usr/local/bin/zig",
|
||||
].join("\n")],
|
||||
{},
|
||||
"runtime toolchain cleanup",
|
||||
60_000,
|
||||
@@ -805,6 +812,7 @@ try {
|
||||
"--passWithNoTests",
|
||||
`--shard=${worker.shard}/${shardCount}`,
|
||||
...caseShardedFiles.map((file) => `--exclude=${file}`),
|
||||
...nativeBuildFiles.map((file) => `--exclude=${file}`),
|
||||
...nativeHostInvariantFiles.map((file) => `--exclude=${file}`),
|
||||
...localLaneFiles.map((file) => `--exclude=${file}`),
|
||||
...localCaseShardedFiles.map((file) => `--exclude=${file}`),
|
||||
@@ -837,9 +845,17 @@ try {
|
||||
},
|
||||
"cache",
|
||||
);
|
||||
const nativeBuild = () => execIn(
|
||||
worker,
|
||||
"sh",
|
||||
["-c", 'export PATH="/tmp/scriptc-native-build-tools:$PATH"; exec "$@"', "sh",
|
||||
"pnpm", "test", "--reporter=dot", ...nativeBuildFiles],
|
||||
{ ...sharedTestEnv, SCRIPTC_TEST_WORKERS: "1" },
|
||||
"native bootstrap",
|
||||
);
|
||||
// The corpus owns the remaining pool while file/cache processes share
|
||||
// the reserved side slots (serially when only one slot is available).
|
||||
const sideTasks = [files, ...(runCacheCases ? [cacheCases] : [])];
|
||||
const sideTasks = [...(worker.shard === 1 ? [nativeBuild] : []), files, ...(runCacheCases ? [cacheCases] : [])];
|
||||
if (sideConcurrency === 0) {
|
||||
await cases();
|
||||
await runTaskQueue(sideTasks, 1);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/** Exercise the packed npm command and its bundled host assets in isolation. */
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { prepareNativeCommand } from "../packages/cli/scripts/prepare-native.mjs";
|
||||
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const platform = resolve(process.argv[2]);
|
||||
const identity = JSON.parse(readFileSync(join(platform, "package.json"), "utf8"));
|
||||
const root = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-npm-smoke-"));
|
||||
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const env = { ...process.env, npm_config_cache: join(root, "npm-cache"), npm_config_update_notifier: "false", NODE_PATH: "" };
|
||||
function command(executable, args, cwd, environment = env) {
|
||||
return execFileSync(executable, args, { cwd, env: environment, encoding: "utf8", shell: process.platform === "win32" && executable.endsWith(".cmd"), maxBuffer: 16 * 1024 * 1024 });
|
||||
}
|
||||
function pack(directory) {
|
||||
const result = JSON.parse(command(npm, ["pack", "--ignore-scripts", "--json", "--pack-destination", root], directory));
|
||||
return join(root, result[0].filename);
|
||||
}
|
||||
function bytes(directory) {
|
||||
let size = 0;
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
if (entry.isSymbolicLink()) continue;
|
||||
const path = join(directory, entry.name);
|
||||
size += entry.isDirectory() ? bytes(path) : statSync(path).size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
try {
|
||||
const platformTarball = pack(platform);
|
||||
const wrapper = join(root, "wrapper");
|
||||
mkdirSync(wrapper);
|
||||
cpSync(join(repository, "packages/cli/scripts"), join(wrapper, "scripts"), { recursive: true });
|
||||
const manifest = JSON.parse(readFileSync(join(repository, "packages/cli/package.json"), "utf8"));
|
||||
assert.equal(manifest.version, identity.version);
|
||||
assert.ok(Object.keys(manifest.optionalDependencies).every(name => name.startsWith("@scriptc/cli-")), "the default CLI must only depend on native host packages");
|
||||
manifest.optionalDependencies = Object.fromEntries(Object.keys(manifest.optionalDependencies).map(name => [name, name === identity.name ? "file:" + platformTarball : identity.version]));
|
||||
delete manifest.devDependencies;
|
||||
delete manifest.scripts.prepack;
|
||||
writeFileSync(join(wrapper, "package.json"), JSON.stringify(manifest));
|
||||
prepareNativeCommand(wrapper);
|
||||
const tarball = pack(wrapper);
|
||||
const project = join(root, "project");
|
||||
mkdirSync(project);
|
||||
command(npm, ["install", "--offline", "--no-audit", "--no-fund", tarball], project);
|
||||
const executable = join(project, "node_modules/scriptc/bin/scriptc.exe");
|
||||
assert.equal(command(executable, ["--version"], project, { ...env, PATH: "" }).trim(), identity.version);
|
||||
const entry = join(project, "hello.ts");
|
||||
writeFileSync(entry, 'console.log("Hello, world!");\n');
|
||||
const binary = join(project, process.platform === "win32" ? "hello.exe" : "hello");
|
||||
const compileEnv = { ...env, SCRIPTC_CACHE_DIR: join(root, "build-cache") };
|
||||
command(executable, ["build", entry, "-o", binary], project, compileEnv);
|
||||
const result = spawnSync(binary, [], { cwd: project, env: { PATH: "" }, encoding: "utf8" });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout, "Hello, world!\n");
|
||||
assert.equal(result.stderr, "");
|
||||
const helloBytes = statSync(binary).size;
|
||||
writeFileSync(entry, 'import { randomBytes, randomUUID } from "node:crypto";\nconst bytes = randomBytes(32);\nconsole.log(bytes.length === 32, randomUUID().length === 36, Math.random() >= 0);\n');
|
||||
command(executable, ["build", entry, "-o", binary], project, compileEnv);
|
||||
assert.equal(command(binary, [], project, { PATH: "" }), "true true true\n");
|
||||
const installed = readdirSync(join(project, "node_modules/@scriptc"));
|
||||
assert.deepEqual(installed, [identity.name.slice("@scriptc/".length)]);
|
||||
console.log(JSON.stringify({ package: identity.name, version: identity.version, installed_packages: ["scriptc", ...installed], installed_file_bytes: bytes(join(project, "node_modules")), hello_executable_bytes: helloBytes, crypto_executable_bytes: statSync(binary).size, output: result.stdout }));
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { GLIBC_RUNTIME_FLOOR } from "../packages/runtime-pack-common/scripts/glibc-toolchain.mjs";
|
||||
|
||||
/** Audit every executable, including process helpers, before publishing. */
|
||||
export function verifyLinuxAbi(executable, { staticOnly = false } = {}) {
|
||||
const headers = execFileSync("readelf", ["--wide", "--program-headers", executable], { encoding: "utf8" });
|
||||
if (staticOnly && /\bINTERP\b/.test(headers)) {
|
||||
throw new Error(`${executable} must be a static executable`);
|
||||
}
|
||||
const versions = execFileSync("readelf", ["--wide", "--version-info", executable], { encoding: "utf8" });
|
||||
const [floorMajor, floorMinor] = GLIBC_RUNTIME_FLOOR.split(".").map(Number);
|
||||
for (const match of versions.matchAll(/\bName: (GLIBC_[\w.]+)/g)) {
|
||||
const version = /^GLIBC_(\d+)\.(\d+)(?:\.(\d+))?$/.exec(match[1]);
|
||||
if (version === null) throw new Error(`${executable} requires unsupported glibc ABI ${match[1]}`);
|
||||
const [, major, minor, patch] = version.map(Number);
|
||||
if (major > floorMajor || (major === floorMajor && (minor > floorMinor || (minor === floorMinor && patch > 0)))) {
|
||||
throw new Error(`${executable} requires ${match[0].slice(6)}; the supported baseline is glibc ${GLIBC_RUNTIME_FLOOR}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
||||
import { verifyLinuxAbi } from "./verify-linux-abi.mjs";
|
||||
|
||||
const root = realpathSync(resolve(process.argv[2] ?? "."));
|
||||
const identity = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
@@ -47,6 +48,12 @@ for (const directory of [distribution, join(distribution, "lib/runtime-sources/v
|
||||
if (readdirSync(directory).some((name) => ["node_modules", ".cache", "seed"].includes(name))) throw new Error(`development files present in ${directory}`);
|
||||
}
|
||||
if (process.argv.includes("--run")) {
|
||||
if (target.startsWith("linux-")) {
|
||||
for (const executable of [binary, resolve(dirname(binary), toolchain.ts7), resolve(dirname(binary), toolchain.comptime)]) {
|
||||
verifyLinuxAbi(executable, { staticOnly: target.endsWith("-musl") });
|
||||
}
|
||||
verifyLinuxAbi(resolve(dirname(binary), toolchain.llvm_package, "bin/scriptc-llvm-codegen"), { staticOnly: true });
|
||||
}
|
||||
const version = execFileSync(binary, ["--version"], { encoding: "utf8", env: { ...process.env, PATH: "" } }).trim();
|
||||
if (version !== identity.version) throw new Error(`native command reported ${version}, expected ${identity.version}`);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ Full-suite runs (`vitest run` with no filters) take an advisory machine-wide loc
|
||||
|
||||
### Development build benchmark
|
||||
|
||||
After rebuilding the workspace, `pnpm bench:builds` measures the built CLI on a generated 17-module, 256-function TypeScript program using `--optimization=dev`. It reports one build with an empty scriptc cache, five exact rebuilds, and five rebuilds after changing an imported module. Every run uses a fresh private cache and verifies each binary's stdout, stderr, and exit status against Node, outside the timed build. Temporary files are removed on completion. `pnpm bench:builds --iterations=3` changes the sample count; progress goes to stderr and the JSON result, including individual samples and medians, goes to stdout (use `pnpm --silent bench:builds` when capturing JSON).
|
||||
After rebuilding the workspace, `pnpm bench:builds` measures the built CLI on a generated 17-module, 256-function TypeScript program using `--optimization=dev`. It reports five builds with an empty scriptc cache, five exact rebuilds, and five rebuilds after changing an imported module. Every invocation uses a private cache and verifies each binary's stdout, stderr, and exit status against Node, outside the timed build. Temporary files are removed on completion. `pnpm bench:builds --iterations=3` changes the sample count; progress goes to stderr and the JSON result, including individual samples and medians, goes to stdout (use `pnpm --silent bench:builds` when capturing JSON).
|
||||
|
||||
`--workload=log-summary` exercises a five-module request-log processor; `--workload=inventory-report` exercises a fifteen-module inventory report with parsing, validation, filtering, aggregation, and ranking. Both process 10,000 records and change an imported value that appears in the output for each edit. `--compiler=/path/to/scriptc` measures an installed native compiler. Results also include executable size and median end-to-end run time, including startup and application work. `--memory` adds peak process RSS from `/usr/bin/time` on macOS and Linux; this is not the sum of concurrent processes' memory. `--launches=20` controls the measured runs after three warmups. Empty-cache builds clear scriptc's cache, not the operating system's file cache.
|
||||
|
||||
This is a local latency benchmark, not a timing assertion in CI. Compare the same Node version, target, compiler installation, and machine load; the empty-cache sample does not flush OS filesystem or Node bytecode caches. Unset `SCRIPTC_TARGET` because the benchmark executes the resulting host binary. Use representative application measurements alongside this small module-graph baseline when choosing further optimizations.
|
||||
|
||||
|
||||
@@ -76,7 +76,8 @@ test.each([true, false])("verify and archive a native distribution across packag
|
||||
const manifest = JSON.parse(readFileSync(join(unpacked, "bin", f.executable + ".json"), "utf8"));
|
||||
expect(manifest.ts7).toBe(f.manifest.ts7);
|
||||
const runtime = manifest.runtime_packs[0];
|
||||
expect(JSON.parse(readFileSync(join(unpacked, "bin", runtime.path, "runtime-pack.json"), "utf8")).target.name).toBe(f.manifest.target);
|
||||
expect(JSON.parse(readFileSync(join(unpacked, "bin", runtime.path.replaceAll("\\", "/"), "runtime-pack.json"), "utf8")).target.name).toBe(f.manifest.target);
|
||||
expect(manifest.runtime_packs).toEqual(f.manifest.runtime_packs);
|
||||
expect(readFileSync(f.manifestPath)).toEqual(original);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execFile, spawnSync } from "node:child_process";
|
||||
import { chmodSync, existsSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
@@ -45,6 +45,15 @@ test("the production CLI relocates, builds programs, and rebuilds itself with No
|
||||
// before the sanitizer can exercise the compiler.
|
||||
...options, env: { ...process.env, SCRIPTC_SAN: sanitize ? "1" : "", SCRIPTC_NATIVE_EMIT_IR: "1" },
|
||||
}));
|
||||
// CI reuses this already-built seed for npm and older-libc installation
|
||||
// smoke tests, avoiding another full compiler build on the critical path.
|
||||
const packageDirectory = process.env["SCRIPTC_BOOTSTRAP_PACKAGE_DIR"];
|
||||
if (!sanitize && packageDirectory !== undefined) {
|
||||
if (process.platform !== "linux" || process.arch !== "x64") throw new Error("bootstrap package export requires the Linux x64 GNU CI host");
|
||||
mkdirSync(packageDirectory, { recursive: true });
|
||||
cpSync(join(root, "packages/cli-linux-x64-gnu/package.json"), join(packageDirectory, "package.json"));
|
||||
cpSync(distribution, join(packageDirectory, "dist"), { recursive: true });
|
||||
}
|
||||
// All compiler assets must survive moving the complete distribution.
|
||||
const relocated = join(directory, "relocated");
|
||||
renameSync(distribution, relocated);
|
||||
@@ -162,9 +171,14 @@ test("the production CLI relocates, builds programs, and rebuilds itself with No
|
||||
const linker = join(directory, "zigcc");
|
||||
writeFileSync(linker, `#!/bin/sh\nexec '${absoluteCommand("zig").replaceAll("'", "'\\''")}' cc "$@"\n`, { mode: 0o755 });
|
||||
const output = join(directory, "three.wasm");
|
||||
// Install the cross-target pack after relocating the native compiler.
|
||||
// Its resolver must discover project dependencies without Node on PATH.
|
||||
const scope = join(directory, "node_modules/@scriptc");
|
||||
mkdirSync(scope, { recursive: true });
|
||||
symlinkSync(join(root, "packages/runtime-wasm32-wasi"), join(scope, "runtime-wasm32-wasi"), "dir");
|
||||
const built = await exec(probe, ["build", "--lib", "--profile", threeProfile, "-o", output], {
|
||||
...nativeOptions, env: { ...nativeOptions.env, SCRIPTC_TARGET: "wasm32-wasi", SCRIPTC_LINKER: linker,
|
||||
SCRIPTC_RUNTIME_PACK: join(root, "packages/runtime-wasm32-wasi"), SCRIPTC_NO_CACHE: "1" },
|
||||
...nativeOptions, cwd: directory, env: { ...nativeOptions.env, SCRIPTC_TARGET: "wasm32-wasi", SCRIPTC_LINKER: linker,
|
||||
SCRIPTC_RUNTIME_PACK: undefined, SCRIPTC_NO_CACHE: "1" },
|
||||
});
|
||||
expect(built.stdout.trim()).toBe(output);
|
||||
expect(comparableStderr(built.stderr)).toBe("");
|
||||
|
||||
Reference in New Issue
Block a user