-
Notifications
You must be signed in to change notification settings - Fork 126
Preserve glibc 2.36 runtime compatibility #365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -114,7 +114,7 @@ jobs: | |||||||
| llvm_artifacts_linux_x64: | ||||||||
| name: test (Linux x64 LLVM artifacts, no clang) | ||||||||
| runs-on: ubuntu-24.04 | ||||||||
| timeout-minutes: 25 | ||||||||
| timeout-minutes: 30 | ||||||||
| steps: | ||||||||
| - uses: actions/checkout@v4 | ||||||||
| - uses: pnpm/action-setup@v4 | ||||||||
|
|
@@ -135,10 +135,10 @@ jobs: | |||||||
| echo "$RUNNER_TEMP/llvm-22.1.8/bin" >> "$GITHUB_PATH" | ||||||||
| - run: pnpm install --frozen-lockfile | ||||||||
| - run: pnpm --filter @scriptc/llvm-linux-x64-gnu build:native | ||||||||
| - run: CC=clang AR=llvm-ar pnpm --filter @scriptc/runtime-linux-x64-gnu build:native | ||||||||
| - uses: vercel-labs/setup-zig@v1 | ||||||||
| with: | ||||||||
| version: 0.16.0 | ||||||||
| - run: CC=zig AR=zig pnpm --filter @scriptc/runtime-linux-x64-gnu build:native | ||||||||
| - name: Build and verify Linux x64 musl release packages | ||||||||
| run: | | ||||||||
| pnpm --filter @scriptc/llvm-linux-x64-musl build:native | ||||||||
|
|
@@ -173,6 +173,14 @@ jobs: | |||||||
| run: | | ||||||||
| node packages/cli/dist/main.js build tests/corpus/001-hello.ts -o "$RUNNER_TEMP/hello" | ||||||||
| test "$("$RUNNER_TEMP/hello")" = 'hello world' | ||||||||
| - name: Debian 12 glibc 2.36 runtime-pack smoke | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| run: | | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" --workdir /work node:24-bookworm-slim sh -ec ' | ||||||||
| apt-get update | ||||||||
| apt-get install --yes --no-install-recommends clang | ||||||||
| SCRIPTC_NO_CACHE=1 SCRIPTC_LEGACY_C_PIPELINE=0 node packages/cli/dist/main.js build tests/corpus/001-hello.ts -o /tmp/hello | ||||||||
| test "$(/tmp/hello)" = "hello world" | ||||||||
| ' | ||||||||
| - name: WASI helper object/runtime-pack smoke | ||||||||
| run: pnpm test packages/cli/test/wasi-runtime-pack.test.ts | ||||||||
|
|
||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -13,12 +13,7 @@ pnpm install && pnpm -r build # build the workspace | |||||
| pnpm test:sandbox # full gate: ~4m custom image, ~9m cold managed fallback | ||||||
| ``` | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| The ordinary workspace build does not rebuild packaged native artifacts. | ||||||
| When changing native assembly/object emission or runtime-pack selection, | ||||||
| install CMake, Ninja, and the pinned LLVM 22 development package, then run the | ||||||
| matching `@scriptc/llvm-<platform>` and `@scriptc/runtime-<platform>` | ||||||
| `build:native` scripts explicitly. The macOS full test suite also needs its | ||||||
| generated artifacts. | ||||||
| The ordinary workspace build does not rebuild packaged native artifacts. When changing native assembly/object emission or runtime-pack selection, install CMake, Ninja, the pinned LLVM 22 development package, and Zig 0.16.0, then run the matching `@scriptc/llvm-<platform>` and `@scriptc/runtime-<platform>` `build:native` scripts explicitly. The macOS full test suite also needs its generated artifacts. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| Use focused local tests while iterating, then use `pnpm test:sandbox` whenever a | ||||||
| full validation gate is required. It loads Sandbox configuration from the | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ FROM ubuntu:24.04 | |||||
|
|
||||||
| ARG NODE_VERSION | ||||||
| ARG PNPM_VERSION=11.1.3 | ||||||
| ARG ZIG_VERSION=0.16.0 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| ENV DEBIAN_FRONTEND=noninteractive | ||||||
|
|
||||||
|
|
@@ -45,6 +46,10 @@ RUN curl --fail --silent --show-error --location \ | |||||
| && tar -xJf /tmp/node.tar.xz --directory /usr/local --strip-components=1 \ | ||||||
| && rm /tmp/node.tar.xz /tmp/SHASUMS256.txt | ||||||
|
|
||||||
| COPY scripts/install-zig.sh /tmp/install-zig.sh | ||||||
| RUN sh /tmp/install-zig.sh "$ZIG_VERSION" \ | ||||||
| && rm /tmp/install-zig.sh | ||||||
|
|
||||||
| RUN npm install --global "pnpm@${PNPM_VERSION}" | ||||||
|
|
||||||
| WORKDIR /workspace | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,12 @@ | ||
| import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; | ||
| if (process.platform !== "linux" || process.arch !== "arm64") { process.stdout.write("@scriptc/runtime-linux-arm64-gnu: skipped on this host\n"); process.exit(0); } | ||
| import { dirname } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { glibcRuntimeToolchain } from "../../runtime-pack-common/scripts/glibc-toolchain.mjs"; | ||
|
|
||
| if (process.platform !== "linux" || process.arch !== "arm64") { | ||
| process.stdout.write("@scriptc/runtime-linux-arm64-gnu: skipped on this host\n"); | ||
| process.exit(0); | ||
| } | ||
| process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); | ||
| process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ platform: "linux", runtimeDefines: ["_GNU_SOURCE"], threadArgs: ["-pthread"], target: { name: "linux-arm64-gnu", llvm_triple: "aarch64-unknown-linux-gnu", architecture: "arm64", object_format: "elf", minimum_os: "glibc 2.36" }, targetArgs: ["-target", "aarch64-unknown-linux-gnu"], compileFlags: ["-ffunction-sections", "-fdata-sections"], systemLibraries: [{ name: "m", predicate: true }] }); | ||
| const { minimumOs, ...toolchain } = glibcRuntimeToolchain("arm64"); | ||
| process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ platform: "linux", runtimeDefines: ["_GNU_SOURCE"], threadArgs: ["-pthread"], target: { name: "linux-arm64-gnu", llvm_triple: "aarch64-unknown-linux-gnu", architecture: "arm64", object_format: "elf", minimum_os: minimumOs }, ...toolchain, compileFlags: ["-ffunction-sections", "-fdata-sections"], systemLibraries: [{ name: "m", predicate: true }] }); | ||
| await import("../../runtime-pack-common/scripts/build.mjs"); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,12 @@ | ||
| import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; | ||
| if (process.platform !== "linux" || process.arch !== "x64") { process.stdout.write("@scriptc/runtime-linux-x64-gnu: skipped on this host\n"); process.exit(0); } | ||
| import { dirname } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { glibcRuntimeToolchain } from "../../runtime-pack-common/scripts/glibc-toolchain.mjs"; | ||
|
|
||
| if (process.platform !== "linux" || process.arch !== "x64") { | ||
| process.stdout.write("@scriptc/runtime-linux-x64-gnu: skipped on this host\n"); | ||
| process.exit(0); | ||
| } | ||
| process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); | ||
| process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ platform: "linux", runtimeDefines: ["_GNU_SOURCE"], threadArgs: ["-pthread"], target: { name: "linux-x64-gnu", llvm_triple: "x86_64-unknown-linux-gnu", architecture: "x64", object_format: "elf", minimum_os: "glibc 2.36" }, targetArgs: ["-target", "x86_64-unknown-linux-gnu"], compileFlags: ["-ffunction-sections", "-fdata-sections"], systemLibraries: [{ name: "m", predicate: true }] }); | ||
| const { minimumOs, ...toolchain } = glibcRuntimeToolchain("x64"); | ||
| process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ platform: "linux", runtimeDefines: ["_GNU_SOURCE"], threadArgs: ["-pthread"], target: { name: "linux-x64-gnu", llvm_triple: "x86_64-unknown-linux-gnu", architecture: "x64", object_format: "elf", minimum_os: minimumOs }, ...toolchain, compileFlags: ["-ffunction-sections", "-fdata-sections"], systemLibraries: [{ name: "m", predicate: true }] }); | ||
| await import("../../runtime-pack-common/scripts/build.mjs"); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { readFile, readdir } from "node:fs/promises"; | ||
| import { join, relative, sep } from "node:path"; | ||
|
|
||
| export async function assertArtifactsExcludeStrings(root, forbiddenStrings) { | ||
| if (!Array.isArray(forbiddenStrings)) { | ||
| throw new Error("runtime-pack forbidden artifact strings must be an array"); | ||
| } | ||
| if (forbiddenStrings.length === 0) return; | ||
| if (forbiddenStrings.some((value) => typeof value !== "string" || value.length === 0)) { | ||
| throw new Error("runtime-pack forbidden artifact strings must be non-empty strings"); | ||
| } | ||
| const forbidden = forbiddenStrings.map((value) => ({ value, bytes: Buffer.from(value) })); | ||
| const visit = async (directory) => { | ||
| for (const entry of await readdir(directory, { withFileTypes: true })) { | ||
| const path = join(directory, entry.name); | ||
| if (entry.isDirectory()) { | ||
| await visit(path); | ||
| continue; | ||
| } | ||
| if (!entry.isFile()) continue; | ||
| const bytes = await readFile(path); | ||
| const match = forbidden.find((candidate) => bytes.includes(candidate.bytes)); | ||
| if (match !== undefined) { | ||
| const artifact = relative(root, path).split(sep).join("/"); | ||
| throw new Error( | ||
| `runtime-pack artifact ${artifact} contains forbidden symbol family ${match.value}`, | ||
| ); | ||
| } | ||
| } | ||
| }; | ||
| await visit(root); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| const GLIBC_RUNTIME_FLOOR = "2.36"; | ||
|
|
||
| const GNU_ARCHITECTURES = { | ||
| x64: "x86_64", | ||
| arm64: "aarch64", | ||
| }; | ||
|
|
||
| export function glibcRuntimeToolchain(architecture) { | ||
| const targetArchitecture = GNU_ARCHITECTURES[architecture]; | ||
| if (targetArchitecture === undefined) { | ||
| throw new Error(`unsupported GNU runtime architecture: ${architecture}`); | ||
| } | ||
| return { | ||
| minimumOs: `glibc ${GLIBC_RUNTIME_FLOOR}`, | ||
| compiler: "zig", | ||
| compilerArgs: ["cc"], | ||
| archiver: "zig", | ||
| archiverArgs: ["ar"], | ||
| targetArgs: ["-target", `${targetArchitecture}-linux-gnu.${GLIBC_RUNTIME_FLOOR}`], | ||
| compilerFlags: ["-fno-sanitize=undefined"], | ||
| forbiddenArtifactStrings: ["__isoc23_", "__ubsan_"], | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; | ||
| import { tmpdir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { expect, test } from "vitest"; | ||
| import { assertArtifactsExcludeStrings } from "../scripts/artifact-policy.mjs"; | ||
|
|
||
| test("accepts artifacts whose imports stay within the declared libc floor", async () => { | ||
| const root = await mkdtemp(join(tmpdir(), "scriptc-artifact-policy-")); | ||
| try { | ||
| await writeFile(join(root, "runtime.o"), Buffer.from("strtol\0__isoc99_sscanf\0")); | ||
| await expect(assertArtifactsExcludeStrings(root, ["__isoc23_", "__ubsan_"])).resolves.toBeUndefined(); | ||
| } finally { | ||
| await rm(root, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
|
|
||
| test.each([ | ||
| ["__isoc23_sscanf", "__isoc23_"], | ||
| ["__ubsan_handle_add_overflow", "__ubsan_"], | ||
| ])("rejects the %s import in nested runtime artifacts", async (symbol, family) => { | ||
| const root = await mkdtemp(join(tmpdir(), "scriptc-artifact-policy-")); | ||
| try { | ||
| await mkdir(join(root, "release", "runtime"), { recursive: true }); | ||
| await writeFile(join(root, "release", "runtime", "scr_lib.o"), Buffer.from(`${symbol}\0`)); | ||
| await expect(assertArtifactsExcludeStrings(root, ["__isoc23_", "__ubsan_"])).rejects.toThrow( | ||
| `runtime-pack artifact release/runtime/scr_lib.o contains forbidden symbol family ${family}`, | ||
| ); | ||
| } finally { | ||
| await rm(root, { recursive: true, force: true }); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| 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"], | ||
| ])("pins the %s GNU runtime to its glibc floor", (architecture, target) => { | ||
| expect(glibcRuntimeToolchain(architecture)).toEqual({ | ||
| minimumOs: "glibc 2.36", | ||
| compiler: "zig", | ||
| compilerArgs: ["cc"], | ||
| archiver: "zig", | ||
| archiverArgs: ["ar"], | ||
| targetArgs: ["-target", target], | ||
| compilerFlags: ["-fno-sanitize=undefined"], | ||
| forbiddenArtifactStrings: ["__isoc23_", "__ubsan_"], | ||
| }); | ||
| }); | ||
|
|
||
| test("rejects unknown GNU runtime architectures", () => { | ||
| expect(() => glibcRuntimeToolchain("riscv64")).toThrow( | ||
| "unsupported GNU runtime architecture: riscv64", | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| #!/bin/sh | ||
| set -eu | ||
|
|
||
| version=${1:?usage: install-zig.sh <version>} | ||
| case "$version" in | ||
| *[!0-9A-Za-z.+-]*) echo "invalid Zig version: $version" >&2; exit 1 ;; | ||
| esac | ||
|
|
||
| case "$(uname -s)-$(uname -m)" in | ||
| Linux-x86_64) target=x86_64-linux ;; | ||
| Linux-aarch64 | Linux-arm64) target=aarch64-linux ;; | ||
| *) echo "unsupported Zig host: $(uname -s)/$(uname -m)" >&2; exit 1 ;; | ||
| esac | ||
|
|
||
| if command -v zig >/dev/null 2>&1 && [ "$(zig version)" = "$version" ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| temporary=$(mktemp -d) | ||
| trap 'rm -rf "$temporary"' EXIT HUP INT TERM | ||
|
|
||
| curl --fail --silent --show-error --location \ | ||
| https://ziglang.org/download/index.json \ | ||
| --output "$temporary/index.json" | ||
| index_size=$(wc -c <"$temporary/index.json" | tr -d ' ') | ||
| if [ "$index_size" -gt 2097152 ]; then | ||
| echo "Zig download index exceeds 2 MiB" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| node --input-type=module - "$version" "$target" "$temporary/index.json" >"$temporary/metadata" <<'NODE' | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const [version, target, indexPath] = process.argv.slice(2); | ||
| const release = JSON.parse(await readFile(indexPath, "utf8"))[version]; | ||
| const artifact = release?.[target]; | ||
| if (!artifact?.tarball || !artifact?.shasum || !artifact?.size) { | ||
| throw new Error(`Zig ${version} does not publish ${target}`); | ||
| } | ||
| const url = new URL(artifact.tarball); | ||
| if (url.protocol !== "https:" || !/^[0-9a-f]{64}$/.test(artifact.shasum)) { | ||
| throw new Error(`Zig ${version} returned invalid ${target} metadata`); | ||
| } | ||
| process.stdout.write(`${url.href}\n${artifact.shasum}\n${artifact.size}\n`); | ||
| NODE | ||
|
|
||
| url=$(sed -n '1p' "$temporary/metadata") | ||
| sha256=$(sed -n '2p' "$temporary/metadata") | ||
| expected_size=$(sed -n '3p' "$temporary/metadata") | ||
| archive="$temporary/zig.tar.xz" | ||
| curl --fail --silent --show-error --location "$url" --output "$archive" | ||
| printf '%s %s\n' "$sha256" "$archive" | sha256sum --check --strict - | ||
| actual_size=$(wc -c <"$archive" | tr -d ' ') | ||
| if [ "$actual_size" != "$expected_size" ]; then | ||
| echo "Zig archive size mismatch: expected $expected_size, got $actual_size" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| as_root() { | ||
| if [ "$(id -u)" -eq 0 ]; then | ||
| "$@" | ||
| else | ||
| sudo "$@" | ||
| fi | ||
| } | ||
|
|
||
| destination="/opt/zig-$version" | ||
| as_root rm -rf "$destination" | ||
| as_root mkdir -p "$destination" | ||
| as_root tar -xJf "$archive" --directory "$destination" --strip-components=1 | ||
| as_root ln -sf "$destination/zig" /usr/local/bin/zig | ||
| test "$(zig version)" = "$version" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.