diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c5b888d..ec271005 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,10 +111,61 @@ jobs: pnpm test packages/compiler/test/cc-driver.test.ts --testNamePattern "host-native clang static build" + llvm_artifacts_linux_x64: + name: test (Linux x64 LLVM artifacts, no clang) + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11.1.3 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: pnpm + - name: Install pinned LLVM development distribution + run: | + sudo apt-get update + sudo apt-get install --yes libzstd-dev + curl -fL --retry 3 -o "$RUNNER_TEMP/llvm.tar.xz" https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/LLVM-22.1.8-Linux-X64.tar.xz + tar -xJf "$RUNNER_TEMP/llvm.tar.xz" -C "$RUNNER_TEMP" + mv "$RUNNER_TEMP/LLVM-22.1.8-Linux-X64" "$RUNNER_TEMP/llvm-22.1.8" + echo "LLVM_DIR=$RUNNER_TEMP/llvm-22.1.8/lib/cmake/llvm" >> "$GITHUB_ENV" + 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: pnpm --filter @scriptc/runtime-wasm32-wasi build:native + - run: pnpm build + - name: Linux helper object format and no-clang output contract + run: | + PATH="$RUNNER_TEMP/traps:$PATH" + mkdir -p "$RUNNER_TEMP/traps" + for tool in clang cc gcc ar ld; do printf '#!/bin/sh\nexit 97\n' > "$RUNNER_TEMP/traps/$tool"; chmod +x "$RUNNER_TEMP/traps/$tool"; done + node packages/cli/dist/main.js build tests/corpus/001-hello.ts --emit=obj -o "$RUNNER_TEMP/hello.o" + file "$RUNNER_TEMP/hello.o" | grep 'ELF 64-bit LSB relocatable, x86-64' + - name: Linux helper/runtime-pack executable differential smoke + env: + SCRIPTC_NO_CACHE: "1" + SCRIPTC_LEGACY_C_PIPELINE: "0" + run: | + node packages/cli/dist/main.js build tests/corpus/001-hello.ts -o "$RUNNER_TEMP/hello" + test "$("$RUNNER_TEMP/hello")" = 'hello world' + - name: WASI helper object/runtime-pack smoke + run: pnpm test packages/cli/test/wasi-runtime-pack.test.ts + llvm_artifacts_macos_arm64: name: test (macOS arm64 LLVM artifacts, no clang, ${{ matrix.shard }}/3) runs-on: macos-15 - timeout-minutes: 20 + # Shard 1 additionally runs package/install and link-parity contracts + # before its LLVM differential slice. The slice itself can take ~10 + # minutes on a cold hosted runner, so 20 minutes cancels an otherwise + # healthy job before the differential completes. + timeout-minutes: 35 strategy: fail-fast: false matrix: @@ -191,6 +242,38 @@ jobs: "$RUNNER_TEMP/installed-link.json" "$RUNNER_TEMP/installed-program" test "$("$RUNNER_TEMP/installed-program")" = 'hello world' + llvm_artifacts_macos_x64: + name: test (macOS x64 LLVM artifacts, no clang) + runs-on: macos-15-intel + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11.1.3 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: pnpm + - run: brew install llvm@22 + - run: pnpm install --frozen-lockfile + - run: LLVM_DIR=/usr/local/opt/llvm@22/lib/cmake/llvm pnpm --filter @scriptc/llvm-darwin-x64 build:native + - run: pnpm --filter @scriptc/runtime-darwin-x64 build:native + - run: pnpm build + - name: Intel Mach-O no-clang object contract + run: | + mkdir -p "$RUNNER_TEMP/traps" + for tool in clang cc gcc ar ld; do printf '#!/bin/sh\nexit 97\n' > "$RUNNER_TEMP/traps/$tool"; chmod +x "$RUNNER_TEMP/traps/$tool"; done + PATH="$RUNNER_TEMP/traps:$PATH" node packages/cli/dist/main.js build tests/corpus/001-hello.ts --emit=obj -o "$RUNNER_TEMP/hello.o" + file "$RUNNER_TEMP/hello.o" | grep 'Mach-O 64-bit object x86_64' + - name: Intel helper/runtime-pack executable smoke + env: + SCRIPTC_NO_CACHE: "1" + SCRIPTC_LEGACY_C_PIPELINE: "0" + run: | + node packages/cli/dist/main.js build tests/corpus/001-hello.ts -o "$RUNNER_TEMP/hello" + test "$($RUNNER_TEMP/hello)" = 'hello world' + # Exercises the supported Windows GNU target and the built CLI end to end: # TS7 must open its synthetic project, ambient files must resolve across # slash styles, and the default executable must use the .exe suffix. @@ -262,11 +345,67 @@ jobs: pnpm exec vitest run packages/compiler/test/cc-driver.test.ts --testNamePattern "Windows CA roots honor" + llvm_artifacts_windows_x64: + name: test (Windows x64 MSVC LLVM artifacts) + # The official LLVM 22 Windows archives are MSVC-built and the helper + # selects CMake's VS 2022 generator. `windows-latest` can move to an + # image without that generator, so retain the hosted VS 2022 contract. + runs-on: windows-2022 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11.1.3 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: pnpm + - name: Install pinned LLVM development distribution + shell: pwsh + run: | + # windows-2022 already provides CMake, Ninja, VS 2022, and 7-Zip. + # Use the complete archive because it supplies LLVM's exported CMake + # targets and static libraries. Windows tar.exe takes 25+ minutes to + # materialize it; two-stage 7-Zip extraction takes seconds. + $archive = "$env:RUNNER_TEMP\clang+llvm-22.1.8-x86_64-pc-windows-msvc.tar.xz" + $llvm = "$env:RUNNER_TEMP\llvm-22.1.8" + $expanded = "$env:RUNNER_TEMP\llvm-expanded" + $sevenZip = "$env:ProgramFiles\7-Zip\7z.exe" + if (-not (Test-Path $sevenZip)) { + choco install 7zip --no-progress -y + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + if (-not (Test-Path $sevenZip)) { throw "7-Zip is unavailable" } + curl.exe -fL --retry 3 -o $archive https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/clang%2Bllvm-22.1.8-x86_64-pc-windows-msvc.tar.xz + & $sevenZip x -y "-o$expanded" $archive + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & $sevenZip x -y "-o$expanded" "$expanded\clang+llvm-22.1.8-x86_64-pc-windows-msvc.tar" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Move-Item "$expanded\clang+llvm-22.1.8-x86_64-pc-windows-msvc" $llvm + echo "LLVM_DIR=$llvm\lib\cmake\llvm" >> $env:GITHUB_ENV + echo "$llvm\bin" >> $env:GITHUB_PATH + - run: pnpm install --frozen-lockfile + - name: Build Windows helper + run: pnpm --filter @scriptc/llvm-win32-x64-msvc build:native + - uses: vercel-labs/setup-zig@v1 + with: + version: 0.16.0 + - name: Build Windows runtime pack + run: pnpm --filter @scriptc/runtime-win32-x64-msvc build:native + - run: pnpm build + - name: Windows COFF no-clang object contract + shell: pwsh + run: | + node packages/cli/dist/main.js build tests/corpus/001-hello.ts --emit=obj -o "$env:RUNNER_TEMP\hello.obj" + $bytes = [System.IO.File]::ReadAllBytes("$env:RUNNER_TEMP\hello.obj") + if ($bytes[0] -ne 0x64 -or $bytes[1] -ne 0x86) { throw "expected AMD64 COFF object" } + # Aggregate gate with the pre-matrix job's name, so anything keyed on the # single "test" check (branch protection, badges) keeps resolving. Fails # unless every matrix shard and both platform integration jobs succeeded. test: - needs: [tests, linux_host_clang, llvm_artifacts_macos_arm64, windows_cli] + needs: [tests, linux_host_clang, llvm_artifacts_linux_x64, llvm_artifacts_macos_arm64, llvm_artifacts_macos_x64, windows_cli, llvm_artifacts_windows_x64] if: always() runs-on: ubuntu-latest steps: @@ -274,5 +413,8 @@ jobs: run: | test "${{ needs.tests.result }}" = success test "${{ needs.linux_host_clang.result }}" = success + test "${{ needs.llvm_artifacts_linux_x64.result }}" = success test "${{ needs.llvm_artifacts_macos_arm64.result }}" = success + test "${{ needs.llvm_artifacts_macos_x64.result }}" = success test "${{ needs.windows_cli.result }}" = success + test "${{ needs.llvm_artifacts_windows_x64.result }}" = success diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index abcaf23e..bc14cca0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,9 +49,133 @@ jobs: fi echo "version=$LOCAL_VERSION" >> "$GITHUB_OUTPUT" + build-native-packages: + name: Build native package (${{ matrix.platform }}) + needs: check-release + if: needs.check-release.outputs.should_release == 'true' + strategy: + fail-fast: false + matrix: + include: + - platform: darwin-arm64 + runner: macos-15 + helper: llvm-darwin-arm64 + runtime: runtime-darwin-arm64 + - platform: darwin-x64 + runner: macos-15-intel + helper: llvm-darwin-x64 + runtime: runtime-darwin-x64 + - platform: linux-x64 + runner: ubuntu-24.04 + helper: llvm-linux-x64-gnu + runtime: runtime-linux-x64-gnu + llvm_asset: LLVM-22.1.8-Linux-X64 + - platform: linux-arm64 + runner: ubuntu-24.04-arm + helper: llvm-linux-arm64-gnu + runtime: runtime-linux-arm64-gnu + llvm_asset: LLVM-22.1.8-Linux-ARM64 + - platform: windows-x64 + runner: windows-2022 + helper: llvm-win32-x64-msvc + runtime: runtime-win32-x64-msvc + use_zig: true + - platform: linux-x64-musl + runner: ubuntu-24.04 + helper: llvm-linux-x64-musl + runtime: runtime-linux-x64-musl + llvm_asset: LLVM-22.1.8-Linux-X64 + use_zig: true + - platform: linux-arm64-musl + runner: ubuntu-24.04-arm + helper: llvm-linux-arm64-musl + runtime: runtime-linux-arm64-musl + llvm_asset: LLVM-22.1.8-Linux-ARM64 + use_zig: true + - platform: wasm32-wasi + runner: ubuntu-24.04 + helper: llvm-linux-x64-gnu + runtime: runtime-wasm32-wasi + llvm_asset: LLVM-22.1.8-Linux-X64 + use_zig: true + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11.1.3 + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: pnpm + - name: Install toolchain (macOS) + if: startsWith(matrix.platform, 'darwin') + run: brew install llvm@22 + - name: Install pinned LLVM development distribution (Linux) + if: startsWith(matrix.platform, 'linux') || matrix.platform == 'wasm32-wasi' + run: | + sudo apt-get update + sudo apt-get install --yes libzstd-dev + curl -fL --retry 3 -o "$RUNNER_TEMP/llvm.tar.xz" "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/${{ matrix.llvm_asset }}.tar.xz" + tar -xJf "$RUNNER_TEMP/llvm.tar.xz" -C "$RUNNER_TEMP" + mv "$RUNNER_TEMP/${{ matrix.llvm_asset }}" "$RUNNER_TEMP/llvm-22.1.8" + echo "LLVM_DIR=$RUNNER_TEMP/llvm-22.1.8/lib/cmake/llvm" >> "$GITHUB_ENV" + echo "$RUNNER_TEMP/llvm-22.1.8/bin" >> "$GITHUB_PATH" + - uses: vercel-labs/setup-zig@v1 + if: matrix.use_zig == true + with: + version: 0.16.0 + - name: Install pinned LLVM development distribution (Windows) + if: matrix.platform == 'windows-x64' + shell: pwsh + run: | + # The complete archive supplies LLVM's exported CMake targets and + # static libraries. windows-2022 includes CMake, Ninja, VS 2022, + # and 7-Zip; unlike tar.exe, 7-Zip expands this archive quickly. + $archive = "$env:RUNNER_TEMP\clang+llvm-22.1.8-x86_64-pc-windows-msvc.tar.xz" + $llvm = "$env:RUNNER_TEMP\llvm-22.1.8" + $expanded = "$env:RUNNER_TEMP\llvm-expanded" + $sevenZip = "$env:ProgramFiles\7-Zip\7z.exe" + if (-not (Test-Path $sevenZip)) { + choco install 7zip --no-progress -y + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + if (-not (Test-Path $sevenZip)) { throw "7-Zip is unavailable" } + curl.exe -fL --retry 3 -o $archive https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/clang%2Bllvm-22.1.8-x86_64-pc-windows-msvc.tar.xz + & $sevenZip x -y "-o$expanded" $archive + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & $sevenZip x -y "-o$expanded" "$expanded\clang+llvm-22.1.8-x86_64-pc-windows-msvc.tar" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Move-Item "$expanded\clang+llvm-22.1.8-x86_64-pc-windows-msvc" $llvm + echo "LLVM_DIR=$llvm\lib\cmake\llvm" >> $env:GITHUB_ENV + echo "$llvm\bin" >> $env:GITHUB_PATH + - run: pnpm install --frozen-lockfile + - name: Build native packages (POSIX) + if: matrix.platform != 'windows-x64' + run: | + pnpm --filter @scriptc/${{ matrix.helper }} build:native + if [ "${{ matrix.use_zig }}" = true ]; then + CC=zig AR=zig pnpm --filter @scriptc/${{ matrix.runtime }} build:native + else + CC=clang AR=llvm-ar pnpm --filter @scriptc/${{ matrix.runtime }} build:native + fi + - name: Build native packages (Windows) + if: matrix.platform == 'windows-x64' + run: | + pnpm --filter @scriptc/${{ matrix.helper }} build:native + pnpm --filter @scriptc/${{ matrix.runtime }} build:native + - uses: actions/upload-artifact@v4 + with: + name: native-${{ matrix.platform }} + path: | + packages/${{ matrix.helper }} + packages/${{ matrix.runtime }} + retention-days: 1 + publish: name: Publish to npm - needs: check-release + needs: [check-release, build-native-packages] if: needs.check-release.outputs.should_release == 'true' runs-on: macos-15 timeout-minutes: 15 @@ -63,6 +187,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Download native packages + uses: actions/download-artifact@v4 + with: + pattern: native-* + path: native-artifacts + merge-multiple: true + - name: Setup pnpm uses: pnpm/action-setup@v4 with: @@ -74,14 +205,10 @@ jobs: node-version: "24" registry-url: "https://registry.npmjs.org" - - name: Install pinned LLVM - run: brew install llvm@22 - # Publishing uses npm trusted publishing (OIDC): the job's id-token # permission lets npm mint short-lived credentials, so no npm token - # secret exists anywhere in this repo. All five packages — - # @scriptc/runtime, @scriptc/runtime-darwin-arm64, - # @scriptc/llvm-darwin-arm64, @scriptc/compiler, and scriptc — must each be + # secret exists anywhere in this repo. The runtime, every shipped + # platform runtime/helper package, compiler, and CLI must each be # configured on npmjs.com with a GitHub Actions trusted publisher # pointing at repository vercel-labs/scriptc, workflow release.yml, # environment Release. A package missing that configuration fails @@ -91,14 +218,13 @@ jobs: - name: Install and build run: | pnpm install --frozen-lockfile - pnpm --filter @scriptc/llvm-darwin-arm64 build:native - pnpm --filter @scriptc/runtime-darwin-arm64 build:native + cp -R native-artifacts/. packages/ pnpm -r build - name: Check version sync run: | VERSION="${{ needs.check-release.outputs.version }}" - for pkg in packages/runtime packages/runtime-darwin-arm64 packages/llvm-darwin-arm64 packages/compiler packages/cli; do + for pkg in packages/runtime packages/runtime-darwin-arm64 packages/llvm-darwin-arm64 packages/runtime-linux-x64-gnu packages/llvm-linux-x64-gnu packages/runtime-linux-arm64-gnu packages/llvm-linux-arm64-gnu packages/runtime-linux-x64-musl packages/llvm-linux-x64-musl packages/runtime-linux-arm64-musl packages/llvm-linux-arm64-musl packages/runtime-wasm32-wasi packages/runtime-win32-x64-msvc packages/llvm-win32-x64-msvc packages/compiler packages/cli; do V=$(node -p "require('./$pkg/package.json').version") if [ "$V" != "$VERSION" ]; then echo "Version mismatch: $pkg is $V, expected $VERSION" @@ -152,6 +278,17 @@ jobs: publish_dir packages/runtime publish_dir packages/runtime-darwin-arm64 publish_dir packages/llvm-darwin-arm64 "$HELPER_TARBALL" + publish_dir packages/runtime-linux-x64-gnu + publish_dir packages/llvm-linux-x64-gnu + publish_dir packages/runtime-linux-arm64-gnu + publish_dir packages/llvm-linux-arm64-gnu + publish_dir packages/runtime-linux-x64-musl + publish_dir packages/llvm-linux-x64-musl + publish_dir packages/runtime-linux-arm64-musl + publish_dir packages/llvm-linux-arm64-musl + publish_dir packages/runtime-wasm32-wasi + publish_dir packages/runtime-win32-x64-msvc + publish_dir packages/llvm-win32-x64-msvc publish_dir packages/compiler publish_dir packages/cli env: diff --git a/.gitignore b/.gitignore index 34734916..559d4e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ dist/ /packages/runtime-darwin-arm64/artifacts/ /packages/runtime-darwin-arm64/runtime-pack.json /packages/runtime-darwin-arm64/.runtime-pack-* +/packages/llvm-*/bin/ +/packages/runtime-*/artifacts/ +/packages/runtime-*/runtime-pack.json +/packages/runtime-*/.runtime-pack-* !tests/fixtures/fetch/node_modules/eventsource-parser/dist/ !tests/fixtures/npm/node_modules/*/dist/ !tests/fixtures/npm/workspace/*/dist/ diff --git a/AGENTS.md b/AGENTS.md index e3f7a997..d0487c72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,12 +9,12 @@ pnpm install && pnpm -r build # build the workspace pnpm test:sandbox # full gate: ~4m custom image, ~9m cold managed fallback ``` -The ordinary workspace build does not rebuild packaged macOS native artifacts. +The ordinary workspace build does not rebuild packaged native artifacts. When changing native assembly/object emission or runtime-pack selection, -install CMake, Ninja, and Homebrew `llvm@22`, then run -`pnpm --filter @scriptc/llvm-darwin-arm64 build:native` and -`pnpm --filter @scriptc/runtime-darwin-arm64 build:native` explicitly. The -macOS full test suite also needs those generated artifacts. +install CMake, Ninja, and the pinned LLVM 22 development package, then run the +matching `@scriptc/llvm-` and `@scriptc/runtime-` +`build:native` scripts explicitly. The macOS full test suite also needs its +generated artifacts. Use focused local tests while iterating, then use `pnpm test:sandbox` whenever a full validation gate is required. It loads Sandbox configuration from the diff --git a/README.md b/README.md index e0e9bb90..2bf9306c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ scriptc is experimental and targets macOS, Linux, Windows, and WebAssembly via W ## Installation -The compiler requires Node.js 24 or newer. `--emit=ir|c|llvm` needs only Node. On macOS 15+ arm64, `--emit=asm|obj` additionally uses the optional platform helper installed with scriptc, but needs no compiler, archiver, linker, or SDK. Ordinary LLVM-tier executable builds need a platform linker driver and SDK, but use the bundled helper plus precompiled runtime pack rather than compiling generated or runtime C. Set `SCRIPTC_LINKER` to choose that driver. Explicit C builds, LLVM fallbacks, `--sanitize`, and the deprecated `SCRIPTC_CC=clang|zigcc` compatibility route additionally need a C compiler. The executables it produces do not require Node. +The compiler requires Node.js 24 or newer. `--emit=ir|c|llvm` needs only Node. `--emit=asm|obj` uses the matching optional platform helper installed with scriptc on supported macOS, Linux, and Windows hosts (and for WASI), but needs no compiler, archiver, linker, or SDK. Ordinary LLVM-tier executable builds need a platform linker driver and SDK/sysroot, but use the bundled helper plus precompiled runtime pack rather than compiling generated or runtime C. Set `SCRIPTC_LINKER` to choose that driver. Explicit C builds, LLVM fallbacks, `--sanitize`, and the deprecated `SCRIPTC_CC=clang|zigcc` compatibility route additionally need a C compiler. The executables it produces do not require Node. ```console $ npm install -g scriptc @@ -153,12 +153,11 @@ $ vercel link && vercel env pull # writes a project-scoped VERCEL_OIDC_TOKEN $ pnpm test:sandbox ``` -The normal workspace build needs no local LLVM installation. To rebuild the -optional macOS arm64 native artifacts, install CMake, Ninja, and Homebrew -`llvm@22`, then run -`pnpm --filter @scriptc/llvm-darwin-arm64 build:native` and -`pnpm --filter @scriptc/runtime-darwin-arm64 build:native`. The macOS full test -suite also uses those generated artifacts. +The normal workspace build needs no local LLVM installation. To rebuild a +native helper/runtime pack, install CMake, Ninja, and the pinned LLVM 22 +development package on that target host, then run the matching +`@scriptc/llvm-` and `@scriptc/runtime-` `build:native` +scripts. The macOS full test suite also uses those generated artifacts. `pnpm test:sandbox` loads `.env.local`, preflights Vercel authentication and project access, and uses the managed `vercel/sandbox/universal` image by diff --git a/RELEASING.md b/RELEASING.md index dfd348ff..ae9cd4cf 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,21 +1,21 @@ # Releasing -Releases are manual, single-commit affairs. The maintainer controls the changelog voice and format. The five npm packages — `@scriptc/runtime`, `@scriptc/runtime-darwin-arm64`, `@scriptc/llvm-darwin-arm64`, `@scriptc/compiler`, and `scriptc` — always publish together at the same version. +Releases are manual, single-commit affairs. The maintainer controls the changelog voice and format. `@scriptc/runtime`, every platform helper/runtime pack, `@scriptc/compiler`, and `scriptc` always publish together at the same version. To prepare a release: 1. Bump the version in `packages/cli/package.json` -2. Run `node scripts/sync-versions.mjs` to stamp the same version into `packages/runtime`, `packages/runtime-darwin-arm64`, `packages/llvm-darwin-arm64`, and `packages/compiler`, then `pnpm manifest` to restamp `packages/compiler/surface-manifest.json` with the new version, and commit both (the test suite's staleness guard fails on a version drift) +2. Run `node scripts/sync-versions.mjs` to stamp the same version into the runtime, helper, and compiler packages, then `pnpm manifest` to restamp `packages/compiler/surface-manifest.json` with the new version, and commit both (the test suite's staleness guard fails on a version drift) 3. Fold the `## Unreleased` section of `CHANGELOG.md` into a new `## ` entry (newest first, below `## Unreleased`), and leave `## Unreleased` empty for the next cycle 4. Wrap the new entry in `` and `` markers; this marked block is also the GitHub release body 5. Remove the `` and `` markers from the previous release entry; only the latest release should have markers 6. With Zig on `PATH`, run `SCRIPTC_CROSS=1 pnpm exec vitest run tests/harness/library-cross.test.ts` and require the cross-target library conformance lane to pass 7. Commit to `main` -CI (`.github/workflows/release.yml`) compares the version in `packages/cli/package.json` to what `scriptc` has on npm. If it differs, it builds the workspace, verifies all five package versions match (a mismatch fails with a hint to run `scripts/sync-versions.mjs`), and publishes to npm in dependency order — `@scriptc/runtime`, `@scriptc/runtime-darwin-arm64`, `@scriptc/llvm-darwin-arm64`, `@scriptc/compiler`, then `scriptc` — so each package's dependencies are resolvable the moment it lands. After the publish succeeds, a separate job creates the git tag `v` and the GitHub release with the marked changelog entry as its body, and attaches `surface-manifest.json` — the machine-readable listing of the surface the static tier compiles at that version (stable per-entry ids, so two releases diff mechanically; see `packages/compiler/src/coverage/surface-manifest.ts` for the schema). The job regenerates the manifest from the tree and fails on any byte difference from the committed file before attaching, so the asset is always the manifest of the code being released. The same file ships inside the `@scriptc/compiler` package as `@scriptc/compiler/surface-manifest.json`. +CI (`.github/workflows/release.yml`) compares the version in `packages/cli/package.json` to what `scriptc` has on npm. If it differs, it builds platform packages on matching macOS, Linux, and Windows runners, verifies the version spine, and publishes the runtime, helper, compiler, and CLI packages in dependency order. After the publish succeeds, a separate job creates the git tag `v` and the GitHub release with the marked changelog entry as its body, and attaches `surface-manifest.json` — the machine-readable listing of the surface the static tier compiles at that version (stable per-entry ids, so two releases diff mechanically; see `packages/compiler/src/coverage/surface-manifest.ts` for the schema). The job regenerates the manifest from the tree and fails on any byte difference from the committed file before attaching, so the asset is always the manifest of the code being released. The same file ships inside the `@scriptc/compiler` package as `@scriptc/compiler/surface-manifest.json`. -The release job runs on macOS arm64, builds and strips the pinned LLVM helper, -then builds the matching precompiled runtime pack before publishing both +The release job builds and strips each pinned LLVM helper on its matching host, +then builds the matching precompiled runtime pack before publishing the constrained platform packages ahead of `@scriptc/compiler`. Ordinary LLVM-tier executables use the helper for the program object and the platform pack for runtime objects; the user's toolchain performs only the final platform link. @@ -25,7 +25,7 @@ platform pack is available. The GitHub release remains a tag, release notes, and manifest asset; the npm publish never waits on the GitHub release. Publishing uses npm trusted publishing (OIDC) — there is no npm token secret. -Each of the five packages must have a GitHub Actions trusted publisher for +Each published package must have a GitHub Actions trusted publisher for `release.yml` and the `Release` environment. A missing configuration fails before upload. Re-runs skip package versions already present on npm, so a partially published release can be resumed safely. diff --git a/docs/src/app/cli/page.mdx b/docs/src/app/cli/page.mdx index d7ef7464..deef5f76 100644 --- a/docs/src/app/cli/page.mdx +++ b/docs/src/app/cli/page.mdx @@ -90,7 +90,7 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
Primary artifact path. An explicit path is exact. Defaults are .scriptc/<name>.ir.json, .c, .ll, .s, .o, or the platform executable name.
--emit <ir|c|llvm|asm|obj|exe>
-
Select the invocation's one primary artifact. ir, c, and llvm need only Node. asm and obj use the bundled LLVM helper on macOS 15+ arm64 and emit artifacts targeting macOS 14.0. exe is the default.
+
Select the invocation's one primary artifact. ir, c, and llvm need only Node. asm and obj use a matching bundled LLVM helper on supported macOS, Linux, and Windows hosts and for WASI; they need no installed C compiler. exe is the default.
--print <native-link-info>
Build an object (equivalent to --emit=obj) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed source runtime pack and all link inputs, but does not invoke a linker.
@@ -102,7 +102,7 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
Bind signature-only TypeScript declarations to native C ABI symbols and link the manifest's archive, object, and system-library inputs. See Native FFI.
--backend <c|llvm>
-
Code generator: llvm (default) or c (the readable debugging backend). On macOS arm64, LLVM-tier executable code generation uses the bundled helper and precompiled runtime pack before the platform link. Unset, a native build can fall back to C when the program is outside the LLVM tier. The production wasm32-wasi target never falls back: a missing LLVM lowering is SC3001.
+
Code generator: llvm (default) or c (the readable debugging backend). On supported targets, LLVM-tier executable code generation uses the bundled helper and precompiled runtime pack before the platform link. Unset, a native build can fall back to C when the program is outside the LLVM tier. The production wasm32-wasi target never falls back: a missing LLVM lowering is SC3001.
--npm-static <pkg[,pkg…]|auto>
EXPERIMENTAL. Compile the named npm packages' shipped JS statically as program modules instead of embedding them for the engine (repeatable; auto opts in every eligible direct import). A package the preflight refuses falls back to the island with a coverage-report note. See npm Dependencies for maturity notes.
@@ -159,7 +159,7 @@ fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically ## Backends -The default backend emits textual LLVM IR. On macOS arm64 it is lowered to an object by the bundled helper and linked with the precompiled runtime pack; SCRIPTC_LINKER controls the platform linker driver, separately from C compilation. Other executable targets retain their existing toolchain path. A program outside the LLVM tier is never miscompiled—the native build falls back to the C backend transparently and says so in one stderr line. The production wasm32-wasi target uses LLVM's 32-bit ABI path and never falls back; an LLVM coverage gap is a build diagnostic. Dynamic npm embedding is LLVM surface on every target. +The default backend emits textual LLVM IR. On supported targets it is lowered to an object by the bundled helper and linked with the matching precompiled runtime pack; SCRIPTC_LINKER controls the platform linker driver, separately from C compilation. A program outside the LLVM tier is never miscompiled—the native build falls back to the C backend transparently and says so in one stderr line. The production wasm32-wasi target uses LLVM's 32-bit ABI path and never falls back; an LLVM coverage gap is a build diagnostic. Dynamic npm embedding is LLVM surface on every target. The C backend is a debugging aid: deliberately readable, source-line-annotated output with differential tests against LLVM wherever the two overlap. Pin it when you want to inspect what your program became: diff --git a/docs/src/app/how-it-works/page.mdx b/docs/src/app/how-it-works/page.mdx index 8b920739..1ebb3f05 100644 --- a/docs/src/app/how-it-works/page.mdx +++ b/docs/src/app/how-it-works/page.mdx @@ -11,8 +11,8 @@ TypeScript ──tsc: parse + typecheck──▶ lowering ──▶ typed IR ─ 1. **Frontend** — the real TypeScript compiler parses and type-checks your program against `es2025` (plus `@types/node` when your project has it), honoring your `tsconfig.json` for checker strictness. The frontend then lowers the checked AST into a typed intermediate representation, using tsc's own type and narrowing answers to drive every decision. A construct with no lowering is a precise diagnostic at this stage — never a miscompile later. 2. **Typed IR** — the only interface between the ends: a validated, serializable representation (`--emit=ir` writes it as JSON and stops). Types are concrete here; generics have been monomorphized, unions are tagged values, closures have explicit captures. -3. **Backends** — `--emit=c` writes readable C and stops; `--emit=llvm` writes textual LLVM IR and stops. Neither source-output command discovers or invokes a native toolchain. On macOS 15+ arm64, `--emit=asm|obj` sends LLVM IR to a version-matched out-of-process helper linked to LLVM 22; it needs no clang or linker and emits macOS 14-targeted artifacts. Executable builds default to LLVM and can fall back to C on a native program outside the LLVM tier (one stderr note; `--backend llvm` pins it and fails with a diagnostic instead). The production wasm32-wasi target never falls back. -4. **Link** — on macOS arm64 the release packages contain one precompiled object per runtime feature unit plus QuickJS, libregexp, zlib, and mbedTLS archives. A hashed manifest maps IR feature gates to an ordered typed link plan, so binaries still pay only for what they use. The user needs a platform linker and SDK, but ordinary LLVM-tier builds compile no C. AddressSanitizer, explicit C builds, and LLVM refusals keep the external C-toolchain path. +3. **Backends** — `--emit=c` writes readable C and stops; `--emit=llvm` writes textual LLVM IR and stops. Neither source-output command discovers or invokes a native toolchain. On supported macOS, Linux, and Windows hosts (and for WASI), `--emit=asm|obj` sends LLVM IR to a version-matched out-of-process LLVM 22 helper; it needs no clang or linker. Executable builds default to LLVM and can fall back to C on a native program outside the LLVM tier (one stderr note; `--backend llvm` pins it and fails with a diagnostic instead). The production wasm32-wasi target never falls back. +4. **Link** — release packages contain one precompiled object per runtime feature unit plus QuickJS, libregexp, zlib, and mbedTLS archives for every supported target. A hashed manifest maps IR feature gates to an ordered typed link plan, so binaries still pay only for what they use. The user needs the target platform linker and SDK/sysroot, but ordinary LLVM-tier builds compile no C. AddressSanitizer, explicit C builds, and LLVM refusals keep the external C-toolchain path. Program objects define main and leave their selected scr_* runtime functions undefined. The diff --git a/docs/src/app/native-objects/page.mdx b/docs/src/app/native-objects/page.mdx index 9f57fd26..49ce0d9f 100644 --- a/docs/src/app/native-objects/page.mdx +++ b/docs/src/app/native-objects/page.mdx @@ -1,7 +1,7 @@ # Native Program Objects -`scriptc build --emit=obj` produces one relocatable macOS arm64 program -object without invoking clang, a linker, or an SDK. The object defines +`scriptc build --emit=obj` produces one relocatable program object without +invoking clang, a linker, or an SDK on a supported target. The object defines `main`; it is intended to become the program in an external native link. It is not a host-callable library—use `scriptc build --lib --profile ...` for that interface. @@ -59,9 +59,12 @@ $ ./app-cc 42 ``` -The script compiles each reported source set and gives clang only the link -inputs and system libraries from the document. `--emit=obj` itself remains -clang-free; this compiler invocation belongs to the external runtime build. +The script is the macOS external-link example: it compiles each reported +source set and gives clang only the link inputs and system libraries from the +document. `--emit=obj` itself remains clang-free; this compiler invocation +belongs to the external runtime build. Linux objects need the target glibc or +musl linker/sysroot; Windows MSVC objects need the Windows SDK/MSVC CRT; WASI +objects need a Preview 1 linker/sysroot. ## Native Apple linker diff --git a/docs/src/app/platforms/page.mdx b/docs/src/app/platforms/page.mdx index 56c995d5..fd6ab05f 100644 --- a/docs/src/app/platforms/page.mdx +++ b/docs/src/app/platforms/page.mdx @@ -1,8 +1,8 @@ # Platform Support -## macOS (arm64) +## Owned LLVM targets -The primary platform. Source artifacts selected with --emit=ir|c|llvm need only Node. On macOS 15+ arm64, --emit=asm|obj uses the version-matched @scriptc/llvm-darwin-arm64 helper installed with scriptc, emits arm64-apple-macosx14.0.0 artifacts, and does not invoke a compiler, archiver, linker, or SDK. Ordinary LLVM-tier executables additionally need the Xcode platform linker/SDK; the helper emits the program object and the packaged runtime supplies objects/archives, so clang is only a linker driver (selectable with SCRIPTC_LINKER) and compiles neither program nor runtime C. Explicit C builds, LLVM fallbacks, and --sanitize still require a C compiler. Object output retains undefined runtime references and is not a library archive. The full executable surface is supported: the language, the stdlib, the Node API surface including the server stack, `--dynamic`, and the sanitizer lane. +Source artifacts selected with --emit=ir|c|llvm need only Node. On supported hosts, --emit=asm|obj uses the version-matched platform helper installed with scriptc and does not invoke a compiler, archiver, linker, or SDK. Supported assembly/object targets are macOS arm64/x64 (macOS 14.0 artifacts; helper needs macOS 15+), Linux x64/arm64 glibc, Windows x64 MSVC, Linux x64/arm64 musl, and WASI Preview 1. Ordinary LLVM-tier executables additionally need the platform linker and SDK/sysroot; the helper emits the program object and the packaged runtime supplies objects/archives, so that driver compiles neither program nor runtime C. Explicit C builds, LLVM fallbacks, and --sanitize still require a C compiler. Object output retains undefined runtime references and is not a library archive. ## Cross-compilation via zig @@ -83,19 +83,19 @@ The remaining executable boundary is host capability, not language coverage. WAS - macOS arm64 - native - Primary: full surface, --dynamic, sanitizer lane + macOS arm64 / x64 + native host helper + runtime pack + Full surface, --dynamic, sanitizer lane (external C route) Linux arm64 / x86_64 - SCRIPTC_CC=zigcc SCRIPTC_TARGET=<arch>-linux-gnu.2.36 or <arch>-linux-musl - Static and dynamic surfaces incl. servers, TLS, fetch, fs.watch, and child_process; verified against Linux Node on Bookworm and Alpine + native host helper + runtime pack; SCRIPTC_TARGET=<arch>-linux-musl selects musl + Static and dynamic surfaces incl. servers, TLS, fetch, fs.watch, and child_process; executable linking needs the matching libc/sysroot Windows x86_64 - SCRIPTC_CC=zigcc SCRIPTC_TARGET=x86_64-windows-gnu - Static and dynamic surfaces incl. servers, TLS, fetch, and child_process; verified against Windows Node + native x64 MSVC helper + runtime pack + Static and dynamic surfaces incl. servers, TLS, fetch, and child_process; executable linking needs the Windows SDK/MSVC CRT iOS arm64 (device and simulator) @@ -109,7 +109,7 @@ The remaining executable boundary is host capability, not language coverage. WAS WebAssembly / WASI Preview 1 - SCRIPTC_CC=zigcc SCRIPTC_TARGET=wasm32-wasi + matching host helper + WASI runtime pack Production LLVM target; full language and dynamic tiers, bounded by WASI P1 host capabilities diff --git a/native/llvm-codegen/CMakeLists.txt b/native/llvm-codegen/CMakeLists.txt index 26cebe2d..f91c4fd0 100644 --- a/native/llvm-codegen/CMakeLists.txt +++ b/native/llvm-codegen/CMakeLists.txt @@ -1,10 +1,24 @@ cmake_minimum_required(VERSION 3.24) +set(SCRIPTC_PACKAGE_VERSION "0.0.0-dev" CACHE STRING + "scriptc npm package version reported by the helper protocol") +set(SCRIPTC_DEFAULT_TARGET "arm64-apple-macosx14.0.0" CACHE STRING + "Default target reported by the helper protocol") +set(SCRIPTC_DEFAULT_DATA_LAYOUT + "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32" + CACHE STRING "Data layout for SCRIPTC_DEFAULT_TARGET") +set(SCRIPTC_TARGET_BACKENDS "AArch64" CACHE STRING + "Semicolon-separated LLVM target backends compiled into the helper") +set(SCRIPTC_ALLOWED_TARGETS "arm64-apple-macosx14.0.0" CACHE STRING + "Comma-separated target triples accepted by this helper") +set(SCRIPTC_HELPER_ARCH "arm64" CACHE STRING + "macOS architecture of the helper executable") + # These must be cache variables before project() enables the Apple toolchain. # A target property named OSX_DEPLOYMENT_TARGET is not recognized by CMake and # silently leaves the executable at the build host/SDK's deployment version. -if(CMAKE_HOST_APPLE) - set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING +if(APPLE) + set(CMAKE_OSX_ARCHITECTURES "${SCRIPTC_HELPER_ARCH}" CACHE STRING "Architectures for the macOS helper" FORCE) # Homebrew's pinned LLVM 22 bottle is built for macOS 15. The helper must # tell the truth about that host requirement; the objects it emits retain @@ -15,10 +29,12 @@ endif() project(scriptc_llvm_codegen LANGUAGES C CXX) -find_package(LLVM 22.1.8 EXACT REQUIRED CONFIG) +# Official LLVM binary distributions retain zstd as an imported dependency in +# LLVMSupport. Resolve it before loading LLVM's exported targets so CMake never +# leaves an unresolved `zstd::libzstd_static` edge on Linux or Windows. +find_package(zstd CONFIG QUIET) -set(SCRIPTC_PACKAGE_VERSION "0.0.0-dev" CACHE STRING - "scriptc npm package version reported by the helper protocol") +find_package(LLVM 22.1.8 EXACT REQUIRED CONFIG) # LLVM's exported Support target names zstd's shared target even when the # selected LLVM components are static. The npm sidecar must not retain a @@ -49,9 +65,47 @@ target_compile_definitions(scriptc-llvm-codegen PRIVATE ${LLVM_DEFINITIONS} SCRIPTC_PACKAGE_VERSION="${SCRIPTC_PACKAGE_VERSION}" ) +# The official Linux and Windows LLVM archives are commonly built with RTTI +# disabled. Mixing RTTI modes turns otherwise ordinary virtual typeinfo into +# an unresolved link symbol when the helper links those static components. +if(NOT LLVM_ENABLE_RTTI) + if(MSVC) + target_compile_options(scriptc-llvm-codegen PRIVATE /GR-) + else() + target_compile_options(scriptc-llvm-codegen PRIVATE -fno-rtti) + endif() +endif() + +foreach(SCRIPTC_TARGET_BACKEND IN LISTS SCRIPTC_TARGET_BACKENDS) + if(SCRIPTC_TARGET_BACKEND STREQUAL "AArch64") + list(APPEND SCRIPTC_LLVM_TARGET_LIBS AArch64CodeGen) + target_compile_definitions(scriptc-llvm-codegen PRIVATE SCRIPTC_ENABLE_AARCH64=1) + elseif(SCRIPTC_TARGET_BACKEND STREQUAL "X86") + list(APPEND SCRIPTC_LLVM_TARGET_LIBS X86CodeGen) + target_compile_definitions(scriptc-llvm-codegen PRIVATE SCRIPTC_ENABLE_X86=1) + elseif(SCRIPTC_TARGET_BACKEND STREQUAL "WebAssembly") + list(APPEND SCRIPTC_LLVM_TARGET_LIBS WebAssemblyCodeGen) + target_compile_definitions(scriptc-llvm-codegen PRIVATE SCRIPTC_ENABLE_WEBASSEMBLY=1) + else() + message(FATAL_ERROR "unsupported SCRIPTC_TARGET_BACKENDS entry: ${SCRIPTC_TARGET_BACKEND}") + endif() +endforeach() + +# The WebAssembly target machine emits either an object or final wasm module, +# but LLVM's normal `CodeGen` component remains the shared driver interface. +# Keep the backend list canonical in the protocol rather than relying on the +# host's installed LLVM target registry. + +string(REPLACE ";" "," SCRIPTC_TARGET_BACKENDS_JSON "${SCRIPTC_TARGET_BACKENDS}") +target_compile_definitions(scriptc-llvm-codegen PRIVATE + SCRIPTC_DEFAULT_TARGET="${SCRIPTC_DEFAULT_TARGET}" + SCRIPTC_DEFAULT_DATA_LAYOUT="${SCRIPTC_DEFAULT_DATA_LAYOUT}" + SCRIPTC_TARGET_BACKENDS="${SCRIPTC_TARGET_BACKENDS_JSON}" + SCRIPTC_ALLOWED_TARGETS="${SCRIPTC_ALLOWED_TARGETS}" +) llvm_map_components_to_libnames(SCRIPTC_LLVM_LIBS - AArch64CodeGen + ${SCRIPTC_LLVM_TARGET_LIBS} CodeGen Core Coroutines diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp index 75652f00..053176f5 100644 --- a/native/llvm-codegen/src/emit.cpp +++ b/native/llvm-codegen/src/emit.cpp @@ -71,10 +71,10 @@ static OptimizationLevel optimizationLevel(StringRef Level) { } int emit(const EmitOptions &Options) { - if (Options.Target != DefaultTarget) + if (!supportsTarget(Options.Target)) return reportError("unsupported_target", Twine("unsupported target '") + Options.Target + - "' (supported: " + DefaultTarget + ")", + "' (supported: " + AllowedTargets + ")", Options.DiagnosticFormat); if (Options.FileType != "obj" && Options.FileType != "asm") return reportError("invalid_filetype", "filetype must be obj or asm", @@ -113,12 +113,6 @@ int emit(const EmitOptions &Options) { Triple TargetTriple(Options.Target); Mod->setTargetTriple(TargetTriple); Mod->setDataLayout(Machine->createDataLayout()); - if (Mod->getDataLayoutStr() != DefaultDataLayout) - return reportError("data_layout_mismatch", - Twine("LLVM produced unexpected data layout '") + - Mod->getDataLayoutStr() + "'", - Options.DiagnosticFormat); - std::string VerificationError; raw_string_ostream VerificationStream(VerificationError); if (verifyModule(*Mod, &VerificationStream)) diff --git a/native/llvm-codegen/src/main.cpp b/native/llvm-codegen/src/main.cpp index 3408d786..d511ee37 100644 --- a/native/llvm-codegen/src/main.cpp +++ b/native/llvm-codegen/src/main.cpp @@ -32,13 +32,28 @@ int version(int Argc, char **Argv) { if (!Machine) return scriptc::reportError("target_machine_failed", Error); + json::Array Targets; + StringRef Backends = scriptc::TargetBackends; + while (!Backends.empty()) { + auto Split = Backends.split(','); + Targets.push_back(Split.first); + Backends = Split.second; + } + json::Array SupportedTargets; + StringRef TargetsText = scriptc::AllowedTargets; + while (!TargetsText.empty()) { + auto Split = TargetsText.split(','); + SupportedTargets.push_back(Split.first); + TargetsText = Split.second; + } json::Object Response{ {"ok", true}, {"protocol_version", scriptc::ProtocolVersion}, {"scriptc_package_version", SCRIPTC_PACKAGE_VERSION}, {"llvm_version", LLVM_VERSION_STRING}, {"host_triple", sys::getDefaultTargetTriple()}, - {"targets", json::Array{"AArch64"}}, + {"targets", std::move(Targets)}, + {"supported_targets", std::move(SupportedTargets)}, {"default_target", scriptc::DefaultTarget}, {"data_layout", Machine->createDataLayout().getStringRepresentation()}, }; diff --git a/native/llvm-codegen/src/target.cpp b/native/llvm-codegen/src/target.cpp index 55405b9e..c5162191 100644 --- a/native/llvm-codegen/src/target.cpp +++ b/native/llvm-codegen/src/target.cpp @@ -8,12 +8,30 @@ using namespace llvm; +#if defined(SCRIPTC_ENABLE_AARCH64) extern "C" { void LLVMInitializeAArch64TargetInfo(); void LLVMInitializeAArch64Target(); void LLVMInitializeAArch64TargetMC(); void LLVMInitializeAArch64AsmPrinter(); } +#endif +#if defined(SCRIPTC_ENABLE_X86) +extern "C" { +void LLVMInitializeX86TargetInfo(); +void LLVMInitializeX86Target(); +void LLVMInitializeX86TargetMC(); +void LLVMInitializeX86AsmPrinter(); +} +#endif +#if defined(SCRIPTC_ENABLE_WEBASSEMBLY) +extern "C" { +void LLVMInitializeWebAssemblyTargetInfo(); +void LLVMInitializeWebAssemblyTarget(); +void LLVMInitializeWebAssemblyTargetMC(); +void LLVMInitializeWebAssemblyAsmPrinter(); +} +#endif namespace scriptc { @@ -21,13 +39,62 @@ void initializeTargets() { static bool Initialized = false; if (Initialized) return; +#if defined(SCRIPTC_ENABLE_AARCH64) LLVMInitializeAArch64TargetInfo(); LLVMInitializeAArch64Target(); LLVMInitializeAArch64TargetMC(); LLVMInitializeAArch64AsmPrinter(); +#endif +#if defined(SCRIPTC_ENABLE_X86) + LLVMInitializeX86TargetInfo(); + LLVMInitializeX86Target(); + LLVMInitializeX86TargetMC(); + LLVMInitializeX86AsmPrinter(); +#endif +#if defined(SCRIPTC_ENABLE_WEBASSEMBLY) + LLVMInitializeWebAssemblyTargetInfo(); + LLVMInitializeWebAssemblyTarget(); + LLVMInitializeWebAssemblyTargetMC(); + LLVMInitializeWebAssemblyAsmPrinter(); +#endif Initialized = true; } +bool supportsTarget(StringRef TripleName) { + bool Allowed = false; + StringRef Remaining = AllowedTargets; + while (!Remaining.empty()) { + auto Split = Remaining.split(','); + if (Split.first == TripleName) { + Allowed = true; + break; + } + Remaining = Split.second; + } + if (!Allowed) + return false; + Triple Triple(TripleName); + switch (Triple.getArch()) { +#if defined(SCRIPTC_ENABLE_AARCH64) + case Triple::aarch64: + case Triple::aarch64_be: + return Triple.isOSBinFormatMachO() || Triple.isOSBinFormatELF(); +#endif +#if defined(SCRIPTC_ENABLE_X86) + case Triple::x86_64: + case Triple::x86: + return Triple.isOSBinFormatMachO() || Triple.isOSBinFormatELF() || + Triple.isOSBinFormatCOFF(); +#endif +#if defined(SCRIPTC_ENABLE_WEBASSEMBLY) + case Triple::wasm32: + return Triple.isOSBinFormatWasm(); +#endif + default: + return false; + } +} + static CodeGenOptLevel codeGenLevel(StringRef Level) { if (Level == "0") return CodeGenOptLevel::None; @@ -42,6 +109,10 @@ std::unique_ptr createTargetMachine(StringRef TripleName, StringRef OptLevel, std::string &Error) { initializeTargets(); + if (!supportsTarget(TripleName)) { + Error = (Twine("target '") + TripleName + "' was not compiled into this helper").str(); + return nullptr; + } Triple TargetTriple(TripleName); const Target *Definition = TargetRegistry::lookupTarget(TargetTriple, Error); if (Definition == nullptr) diff --git a/native/llvm-codegen/src/target.h b/native/llvm-codegen/src/target.h index fc6cd7d5..0d8f3ab7 100644 --- a/native/llvm-codegen/src/target.h +++ b/native/llvm-codegen/src/target.h @@ -13,12 +13,27 @@ namespace scriptc { inline constexpr llvm::StringLiteral ProtocolVersion = "1"; inline constexpr llvm::StringLiteral LlvmVersion = "22.1.8"; +#ifndef SCRIPTC_DEFAULT_TARGET +#define SCRIPTC_DEFAULT_TARGET "arm64-apple-macosx14.0.0" +#endif +#ifndef SCRIPTC_DEFAULT_DATA_LAYOUT +#define SCRIPTC_DEFAULT_DATA_LAYOUT "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32" +#endif +#ifndef SCRIPTC_TARGET_BACKENDS +#define SCRIPTC_TARGET_BACKENDS "AArch64" +#endif +#ifndef SCRIPTC_ALLOWED_TARGETS +#define SCRIPTC_ALLOWED_TARGETS "arm64-apple-macosx14.0.0" +#endif inline constexpr llvm::StringLiteral DefaultTarget = - "arm64-apple-macosx14.0.0"; + SCRIPTC_DEFAULT_TARGET; inline constexpr llvm::StringLiteral DefaultDataLayout = - "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32"; + SCRIPTC_DEFAULT_DATA_LAYOUT; +inline constexpr llvm::StringLiteral TargetBackends = SCRIPTC_TARGET_BACKENDS; +inline constexpr llvm::StringLiteral AllowedTargets = SCRIPTC_ALLOWED_TARGETS; void initializeTargets(); +bool supportsTarget(llvm::StringRef Triple); std::unique_ptr createTargetMachine(llvm::StringRef Triple, llvm::StringRef OptLevel, std::string &Error); diff --git a/packages/cli/README.md b/packages/cli/README.md index 41bb8a17..2e4bb75a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -22,7 +22,7 @@ $ scriptc build fib.ts -o fib && ./fib $ npm install -g scriptc ``` -Requires Node.js 24. Executable builds require a platform linker driver and SDK. On macOS 15+ arm64, LLVM-tier executables use the matching optional helper and precompiled runtime pack, so the driver only links; select that driver with `SCRIPTC_LINKER`. Explicit C builds, LLVM fallbacks, `--sanitize`, and the deprecated `SCRIPTC_CC=clang|zigcc` compatibility route still compile C. `--emit=ir|c|llvm` requires only Node, while `--emit=asm|obj` requires neither an external compiler nor a linker. +Requires Node.js 24. Executable builds require a platform linker driver and SDK/sysroot. On supported macOS, Linux, and Windows hosts, LLVM-tier executables use the matching optional helper and precompiled runtime pack, so the driver only links; select that driver with `SCRIPTC_LINKER`. Explicit C builds, LLVM fallbacks, `--sanitize`, and the deprecated `SCRIPTC_CC=clang|zigcc` compatibility route still compile C. `--emit=ir|c|llvm` requires only Node, while `--emit=asm|obj` requires neither an external compiler nor a linker. Builds use a bounded persistent cache by default. Exact unchanged library builds validate their recorded TypeScript/module-resolution inputs and restore the generated C/LLVM unit before starting the frontend. TypeScript comment-only edits can restore validated lowered IR instead, rebasing source locations and regenerating exact-source build identity before emission; directives, JSDoc-bearing JavaScript, token edits, configuration, package resolution, and newly appearing candidates still invalidate it. Library identity getters live in a tiny C translation unit, so build-id-only changes reuse the large compiled program object and compile only that small member before rearchiving. The native cache then applies its independent toolchain checks. Unchanged executables and library archives skip native code generation and linking after fresh compiler metadata probes, while edited builds reuse stable runtime objects. Experimental provenance-source builds bypass the early frontend tier because their fetched-source registry is process state. FFI builds with archive/object inputs or ambient `system_libraries` relink every time but still reuse runtime objects. Mutable compiler input paths such as `CPATH` and `SDKROOT`, and compiler wrappers, bypass persistent native artifacts and objects so same-path dependency edits cannot go stale. Opaque archiver wrappers rebuild library program members and archives while retaining runtime-object reuse. Direct Clang, Apple's system Clang shim, `zig cc`, trusted platform archivers, and `zig ar` retain their applicable persistent tiers. Set `SCRIPTC_NO_CACHE=1` to bypass every cache or `SCRIPTC_CACHE_DIR` to choose its location; an existing POSIX override must already be private, otherwise caching is bypassed without changing its permissions. @@ -35,8 +35,8 @@ Builds use a bounded persistent cache by default. Exact unchanged library builds `scriptc build app.ts --emit=ir|c|llvm|asm|obj` selects serialized typed IR, readable C, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object -emission currently requires a macOS 15+ arm64 host and emits objects targeting -macOS 14.0 (`arm64-apple-macosx14.0.0`). +emission uses the matching helper on supported macOS, Linux, and Windows hosts +(and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v1` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. diff --git a/packages/cli/scripts/runtime-pack-host.mjs b/packages/cli/scripts/runtime-pack-host.mjs index dc18cfdc..6e8933e2 100644 --- a/packages/cli/scripts/runtime-pack-host.mjs +++ b/packages/cli/scripts/runtime-pack-host.mjs @@ -6,5 +6,7 @@ export function hostSupportsRuntimePack( hostRelease = release(), ) { const darwinMajor = Number.parseInt(hostRelease.split(".", 1)[0] ?? "", 10); - return platform === "darwin" && architecture === "arm64" && darwinMajor >= 24; + return (platform === "darwin" && (architecture === "arm64" || architecture === "x64") && darwinMajor >= 24) || + (platform === "linux" && (architecture === "arm64" || architecture === "x64")) || + (platform === "win32" && architecture === "x64"); } diff --git a/packages/cli/scripts/warm-cache.mjs b/packages/cli/scripts/warm-cache.mjs index 5ed13d0f..c22eb186 100644 --- a/packages/cli/scripts/warm-cache.mjs +++ b/packages/cli/scripts/warm-cache.mjs @@ -12,8 +12,8 @@ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); if ( !existsSync(join(packageRoot, "src")) && process.env["SCRIPTC_NO_CACHE"] !== "1" && - // A supported macOS arm64 install already carries immutable release/dev - // runtime artifacts. Older macOS hosts retain the source-toolchain path. + // A supported platform install already carries immutable release/dev + // runtime artifacts. Unsupported hosts retain the source-toolchain path. !hostSupportsRuntimePack() ) { try { diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts index 3904d65e..c67c2c8c 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -3,11 +3,9 @@ import { spawn } from "node:child_process"; import { readFile, rm } from "node:fs/promises"; import { enableCompileCache } from "node:module"; -import { arch } from "node:process"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; -import { hostSupportsRuntimePack } from "../scripts/runtime-pack-host.mjs"; import { LEGACY_C_EXECUTABLE_WARNING, shouldWarnLegacyCExecutable } from "./legacy-c-warning.js"; import { CLI_OPTIONS, USAGE } from "./usage.js"; @@ -83,7 +81,7 @@ async function tryFastPath(): Promise { let buildPlatform: string; try { startup = await import("@scriptc/compiler/startup-cache"); - buildPlatform = startup.targetPlatform(startup.resolveCc()); + buildPlatform = startup.configuredTargetPlatform(); } catch { return null; } @@ -101,16 +99,21 @@ async function tryFastPath(): Promise { const ffiBytes = ffiPath === null ? null : await readFile(ffiPath).catch(() => null); if (ffiPath !== null && ffiBytes === null) return null; const root = await startup.prepareBuildCacheRoot(startup.resolveBuildCacheRoot()); - const helperObjectRoute = hostSupportsRuntimePack(process.platform, arch) && - (process.env["SCRIPTC_TARGET"] ?? "") === "" && - backend !== "c" && !values.sanitize && - process.env["SCRIPTC_RUNTIME_PACK"] !== "0" && - process.env["SCRIPTC_FETCH_CURL"] !== "1" && - !startup.legacyCExecutablePathRequested(); + // This must exactly mirror the ordinary LLVM executable's route in the + // full compiler. Otherwise a valid helper/runtime-pack cache entry has a + // different target/compiler identity and bootstrap must unnecessarily load + // the whole compiler graph to rediscover it. + const helperRuntimePackTarget = backend !== "c" && !values.sanitize + ? startup.precompiledRuntimePackTarget() + : null; + const helperObjectRoute = helperRuntimePackTarget !== null; let nativeEnvironment: string | null; try { nativeEnvironment = helperObjectRoute - ? await startup.executableLinkerEnvironmentFingerprint() + ? await startup.executableLinkerEnvironmentFingerprint( + process.env, + helperRuntimePackTarget.defaultLinker, + ) : await startup.executableNativeEnvironmentFingerprint(); } catch { nativeEnvironment = null; @@ -127,10 +130,12 @@ async function tryFastPath(): Promise { ...(optimization === "dev" ? { optimization: "dev" as const } : {}), npmStatic, ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! }, - target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${arch}:${ + target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${ helperObjectRoute ? "runtime-pack" : "driver-tu" }`, - compiler: [helperObjectRoute ? startup.resolvePlatformLinker() : (process.env["SCRIPTC_CC"] ?? "clang")], + compiler: [helperObjectRoute + ? startup.resolvePlatformLinker(process.env, helperRuntimePackTarget.defaultLinker) + : (process.env["SCRIPTC_CC"] ?? "clang")], nativeEnvironment, nodeVersion: process.version, }); diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index bc9af925..ad2e7711 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -3,7 +3,7 @@ import { existsSync, readFileSync, rmSync, statSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; -import { analyze, buildTargetPlatform, compile, compileExternalC, compileLibrary, isExactExternalTypeSpecifier, renderDiagnostics, renderCoverage, resolveProvenanceSources, setProvenanceSources, warmNativeCaches, type NativeCacheWarmProfile } from "@scriptc/compiler"; +import { analyze, buildTargetPlatform, compile, compileExternalC, compileLibrary, isExactExternalTypeSpecifier, renderDiagnostics, renderCoverage, resolveProvenanceSources, setProvenanceSources, sourceTargetPlatform, warmNativeCaches, type NativeCacheWarmProfile } from "@scriptc/compiler"; import { LEGACY_C_EXECUTABLE_WARNING, shouldWarnLegacyCExecutable } from "./legacy-c-warning.js"; import { resolveOutputOptions } from "./output-options.js"; import { selectOutputPaths } from "./paths.js"; @@ -329,7 +329,7 @@ async function main(): Promise { if (command === "run") { return new Promise((resolveExit) => { let child; - if (buildTargetPlatform() === "wasi") { + if (sourceTargetPlatform() === "wasi") { const builtRunner = fileURLToPath(new URL("./wasi-runner.js", import.meta.url)); const runner = existsSync(builtRunner) ? builtRunner diff --git a/packages/cli/src/paths.ts b/packages/cli/src/paths.ts index fec209e3..4107e8f5 100644 --- a/packages/cli/src/paths.ts +++ b/packages/cli/src/paths.ts @@ -1,6 +1,6 @@ import { tmpdir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; -import { buildTargetPlatform, wasiGuestPath } from "@scriptc/compiler"; +import { buildTargetPlatform, sourceTargetPlatform, wasiGuestPath } from "@scriptc/compiler"; export type CliOutputKind = "ir" | "c" | "llvm" | "asm" | "obj" | "exe"; @@ -25,7 +25,7 @@ export function defaultOutputName( kind: CliOutputKind, platform?: string, ): string { - const selectedPlatform = platform ?? (kind === "exe" ? buildTargetPlatform() : process.platform); + const selectedPlatform = platform ?? (kind === "exe" ? sourceTargetPlatform() : process.platform); if (kind === "exe" && selectedPlatform === "wasi") return `${stem}.wasm`; return `${stem}${(selectedPlatform === "win32" ? WINDOWS_SUFFIXES : POSIX_SUFFIXES)[kind]}`; } diff --git a/packages/cli/src/usage.ts b/packages/cli/src/usage.ts index 8fa1ad04..f5096fdf 100644 --- a/packages/cli/src/usage.ts +++ b/packages/cli/src/usage.ts @@ -19,7 +19,7 @@ Usage: Options: -o, --out primary output path (default: .scriptc/) --emit primary output: ir, c, llvm, asm, obj, or exe - (default: exe). asm/obj currently support macOS 15+ arm64 + (default: exe). asm/obj use the matching platform helper --print print machine-readable metadata instead of the output path (native-link-info implies --emit=obj and never links) --backend code generator. llvm is the default and the output that diff --git a/packages/cli/test/runtime-pack-host.test.ts b/packages/cli/test/runtime-pack-host.test.ts index 76f5904b..8439652a 100644 --- a/packages/cli/test/runtime-pack-host.test.ts +++ b/packages/cli/test/runtime-pack-host.test.ts @@ -2,10 +2,13 @@ import { describe, expect, test } from "vitest"; import { hostSupportsRuntimePack } from "../scripts/runtime-pack-host.mjs"; describe("runtime-pack host support", () => { - test("requires macOS 15 or newer on arm64", () => { + test("recognizes each platform with a release-built runtime pack", () => { expect(hostSupportsRuntimePack("darwin", "arm64", "24.0.0")).toBe(true); + expect(hostSupportsRuntimePack("darwin", "x64", "24.0.0")).toBe(true); expect(hostSupportsRuntimePack("darwin", "arm64", "23.6.0")).toBe(false); - expect(hostSupportsRuntimePack("darwin", "x64", "24.0.0")).toBe(false); - expect(hostSupportsRuntimePack("linux", "arm64", "24.0.0")).toBe(false); + expect(hostSupportsRuntimePack("linux", "x64", "6.8.0")).toBe(true); + expect(hostSupportsRuntimePack("linux", "arm64", "6.8.0")).toBe(true); + expect(hostSupportsRuntimePack("win32", "x64", "10.0.0")).toBe(true); + expect(hostSupportsRuntimePack("linux", "ia32", "6.8.0")).toBe(false); }); }); diff --git a/packages/cli/test/wasi-runtime-pack.test.ts b/packages/cli/test/wasi-runtime-pack.test.ts new file mode 100644 index 00000000..8b3a454a --- /dev/null +++ b/packages/cli/test/wasi-runtime-pack.test.ts @@ -0,0 +1,44 @@ +import { execFile } from "node:child_process"; +import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { afterEach, expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const require = createRequire(import.meta.url); +const repoRoot = join(import.meta.dirname, "../../.."); +const cliEntry = join(repoRoot, "packages/cli/src/main.ts"); +const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs"); +const helperPackage = require.resolve("@scriptc/llvm-darwin-arm64/package.json"); +const helper = join(dirname(helperPackage), "bin", "scriptc-llvm-codegen"); +const runtimePackage = require.resolve("@scriptc/runtime-wasm32-wasi/package.json"); +const runtimeManifest = join(dirname(runtimePackage), "runtime-pack.json"); +const supported = existsSync(helper) && existsSync(runtimeManifest); +const dirs: string[] = []; + +afterEach(async () => { + await Promise.all(dirs.splice(0).map((path) => rm(path, { recursive: true, force: true }))); +}); + +test.runIf(supported)("WASI helper object plus runtime pack builds and runs without SCRIPTC_CC", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-wasi-runtime-pack-")); + dirs.push(dir); + const output = join(dir, "hello.wasm"); + const zigCache = join(dir, "zig-cache"); + await expect(execFileAsync(process.execPath, [ + "--import", tsxLoader, cliEntry, "run", join(repoRoot, "tests/corpus/001-hello.ts"), "-o", output, + ], { + env: { + ...process.env, + SCRIPTC_TARGET: "wasm32-wasi", + SCRIPTC_NO_CACHE: "1", + SCRIPTC_LEGACY_C_PIPELINE: "0", + ZIG_GLOBAL_CACHE_DIR: join(zigCache, "global"), + ZIG_LOCAL_CACHE_DIR: join(zigCache, "local"), + }, + encoding: "utf8", + })).resolves.toMatchObject({ stdout: "hello world\n" }); +}); diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 207fdc03..e68f97fe 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -38,6 +38,19 @@ }, "optionalDependencies": { "@scriptc/llvm-darwin-arm64": "workspace:*", - "@scriptc/runtime-darwin-arm64": "workspace:*" + "@scriptc/llvm-darwin-x64": "workspace:*", + "@scriptc/llvm-linux-x64-gnu": "workspace:*", + "@scriptc/llvm-linux-arm64-gnu": "workspace:*", + "@scriptc/llvm-linux-x64-musl": "workspace:*", + "@scriptc/llvm-linux-arm64-musl": "workspace:*", + "@scriptc/llvm-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:*" } } diff --git a/packages/compiler/src/backend/external-c.ts b/packages/compiler/src/backend/external-c.ts index ebad48f5..16a7ac79 100644 --- a/packages/compiler/src/backend/external-c.ts +++ b/packages/compiler/src/backend/external-c.ts @@ -11,6 +11,7 @@ import { compileC, compileLibArchive, + configuredTargetPlatform, type CcOptions, type LibArchiveOptions, } from "./native-toolchain.js"; @@ -20,6 +21,7 @@ export { compileC, compileLibArchive, compilerDriverSupportsPersistentCache, + configuredTargetPlatform, executableNativeEnvironmentFingerprint, isAndroidTarget, isIosTarget, @@ -72,8 +74,8 @@ export function legacyCExecutablePipelineEnabled( } /** `SCRIPTC_CC` selects the external C compiler route during the migration. - * An unset value lets an LLVM-tier macOS build use the helper plus runtime - * pack and reserve `SCRIPTC_LINKER` for the platform-linker driver. */ + * An unset value lets every supported LLVM-tier target use its helper plus + * runtime pack and reserve `SCRIPTC_LINKER` for the platform-linker driver. */ export function legacyCExecutablePathRequested( env: NodeJS.ProcessEnv = process.env, ): boolean { diff --git a/packages/compiler/src/backend/link-plan.ts b/packages/compiler/src/backend/link-plan.ts index c5d2f41d..0b2d02b7 100644 --- a/packages/compiler/src/backend/link-plan.ts +++ b/packages/compiler/src/backend/link-plan.ts @@ -51,7 +51,9 @@ export async function createNativeLinkPlan(options: { ...(options.ffi?.systemLibraries ?? []), ...runtimePack.systemLibraries, ])], - driverFlags: [...options.target.executableLinkerArgs], + driverFlags: options.target.executableLinkerArgs.map((arg, index, args) => + index > 0 && args[index - 1] === "-target" ? options.target.linkerTargetTriple : arg + ), dependencyPaths: [ ...runtimePack.dependencyPaths, ...(options.ffi?.libraries ?? []), diff --git a/packages/compiler/src/backend/linker.ts b/packages/compiler/src/backend/linker.ts index 9d86e43a..a55546f4 100644 --- a/packages/compiler/src/backend/linker.ts +++ b/packages/compiler/src/backend/linker.ts @@ -28,8 +28,11 @@ const execFileAsync = promisify(execFile); /** The linker (or linker driver) is independent from SCRIPTC_CC. A plain * `clang` is the initial macOS driver because it locates the selected SDK and * CRT inputs; it receives only objects and archives on this route. */ -export function resolvePlatformLinker(env: NodeJS.ProcessEnv = process.env): string { - return env["SCRIPTC_LINKER"] || "clang"; +export function resolvePlatformLinker( + env: NodeJS.ProcessEnv = process.env, + defaultLinker: string = "clang", +): string { + return env["SCRIPTC_LINKER"] || defaultLinker; } /** Exact executable selected for a linker spelling. The object-only route @@ -74,8 +77,9 @@ function platformLinkerIdentity( * only the helper-produced program object and runtime-pack artifacts. */ export async function executableLinkerEnvironmentFingerprint( env: NodeJS.ProcessEnv = process.env, + defaultLinker: string = "clang", ): Promise { - const linker = resolvePlatformLinker(env); + const linker = resolvePlatformLinker(env, defaultLinker); const linkerIdentity = platformLinkerIdentity(env); let effectiveDriverIdentity: string; try { @@ -112,7 +116,9 @@ export async function executableLinkerEnvironmentFingerprint( * a caller-selected SCRIPTC_LINKER may be a wrapper with hidden inputs. */ export function platformLinkerSupportsPersistentCache( env: NodeJS.ProcessEnv = process.env, + target?: Pick, ): boolean { + if (target !== undefined && target.platform !== "darwin") return false; // The Apple system shim is a stable front door to the active SDK/linker; // linkNativeExecutable snapshots the selected transitive inputs before it // publishes the final cache entry. A PATH wrapper is intentionally opaque. @@ -198,7 +204,8 @@ export async function linkNativeExecutable( onArtifactReady?: (artifact: { dependencies: NativeArtifactDependency[] }) => Promise; } = {}, ): Promise { - const linker = options.linker ?? resolvePlatformLinker(); + const explicitLinker = options.linker ?? process.env["SCRIPTC_LINKER"]; + const linker = explicitLinker ?? plan.target.defaultLinker; // ld64 derives an ad-hoc signature identifier from the output basename. // Keep that basename caller-visible while a private sibling directory gives // the link its own inode and preserves an atomic same-filesystem install. @@ -211,6 +218,7 @@ export async function linkNativeExecutable( const staged = await stageRuntimePackArtifacts(plan.runtimePack); stagedRoot = staged.root; const args = [ + ...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []), ...plan.driverFlags, ...plan.inputs.map((input) => staged.replacements.get(input) ?? input), ...plan.systemLibraries.map((name) => `-l${name}`), diff --git a/packages/compiler/src/backend/native-codegen.test.ts b/packages/compiler/src/backend/native-codegen.test.ts index 64186cbf..5c092d0c 100644 --- a/packages/compiler/src/backend/native-codegen.test.ts +++ b/packages/compiler/src/backend/native-codegen.test.ts @@ -2,9 +2,9 @@ import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/pr import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test } from "vitest"; -import { emitNativeArtifact, NativeCodegenError } from "./native-codegen.js"; +import { emitNativeArtifact, NativeCodegenError, validateNativeCodegenVersion } from "./native-codegen.js"; import { nativeArtifactDependenciesStillMatch } from "./native-toolchain.js"; -import { MACOS_ARM64_TARGET } from "./targets.js"; +import { LINUX_X64_GNU_TARGET, MACOS_ARM64_TARGET, WASM32_WASI_TARGET } from "./targets.js"; import { compilerReleaseVersion } from "../library/sidecar.js"; const dirs: string[] = []; @@ -34,6 +34,7 @@ async function fakePackage(options: { llvm_version: "22.1.8", host_triple: "arm64-apple-darwin24.0.0", targets: ["AArch64"], + supported_targets: [MACOS_ARM64_TARGET.llvmTriple], default_target: MACOS_ARM64_TARGET.llvmTriple, data_layout: MACOS_ARM64_TARGET.dataLayout, }); @@ -63,18 +64,57 @@ ${options.emitFailure === true return { packageJson, bin, log, root }; } -function request(root: string, packageJson: string, output = join(root, "program.o")) { +function request(root: string, packageJson: string, output = join(root, "program.o"), target = MACOS_ARM64_TARGET) { return { outputPath: output, llvm: "define i32 @answer() { ret i32 42 }\n", outputKind: "obj" as const, sourcePath: "/source/app.ts", - target: MACOS_ARM64_TARGET, + target, resolvePackageJson: () => packageJson, cacheRoot: join(root, "cache"), }; } +test("resolves the target's matching platform helper rather than a fixed macOS package", async () => { + const pkg = await fakePackage(); + const output = join(pkg.root, "linux.o"); + await writeFile(pkg.packageJson, JSON.stringify({ name: LINUX_X64_GNU_TARGET.helperPackage })); + await writeFile(pkg.bin, (await readFile(pkg.bin, "utf8")) + .replaceAll(MACOS_ARM64_TARGET.llvmTriple, LINUX_X64_GNU_TARGET.helper.defaultTarget) + .replaceAll(MACOS_ARM64_TARGET.dataLayout, LINUX_X64_GNU_TARGET.helper.defaultDataLayout) + .replaceAll('"targets":["AArch64"]', '"targets":["X86"]') + .replaceAll(`"supported_targets":["${MACOS_ARM64_TARGET.llvmTriple}"]`, `"supported_targets":["${LINUX_X64_GNU_TARGET.llvmTriple}"]`)); + await emitNativeArtifact({ + ...request(pkg.root, pkg.packageJson, output, LINUX_X64_GNU_TARGET), + helperHost: { platform: "linux", arch: "x64" }, + }); + expect(await readFile(output, "utf8")).toContain("define i32 @answer"); +}); + +test("requires a portable target backend in addition to the host helper backend", async () => { + const pkg = await fakePackage(); + await expect(emitNativeArtifact({ + ...request(pkg.root, pkg.packageJson, join(pkg.root, "program.wasm"), WASM32_WASI_TARGET), + helperHost: { platform: "darwin", arch: "arm64" }, + })).rejects.toMatchObject({ diagnosticCode: "SC3003", detailCode: "version_mismatch" }); +}); + +test("accepts a WASI-capable helper whose default host target differs", () => { + const helper = WASM32_WASI_TARGET.hostHelpers?.["darwin-arm64"]!; + expect(validateNativeCodegenVersion({ + ok: true, + protocol_version: "1", + scriptc_package_version: compilerReleaseVersion(), + llvm_version: "22.1.8", + host_triple: "arm64-apple-darwin25.0.0", + targets: ["AArch64", "WebAssembly"], + supported_targets: [helper.defaultTarget, WASM32_WASI_TARGET.llvmTriple], + default_target: helper.defaultTarget, + data_layout: helper.defaultDataLayout, + }, WASM32_WASI_TARGET, helper).targets).toContain("WebAssembly"); +}); + test("resolves a package helper, emits atomically, and caches by all native inputs", async () => { const pkg = await fakePackage(); const first = join(pkg.root, "first.o"); diff --git a/packages/compiler/src/backend/native-codegen.ts b/packages/compiler/src/backend/native-codegen.ts index b366b2de..59b87995 100644 --- a/packages/compiler/src/backend/native-codegen.ts +++ b/packages/compiler/src/backend/native-codegen.ts @@ -19,7 +19,13 @@ import { snapshotNativeArtifactDependencies, type NativeArtifactDependency, } from "./native-toolchain.js"; -import { nativeCodegenTarget, nativeCodegenTargetRefusal, type NativeTargetSpec } from "./targets.js"; +import { + nativeCodegenTarget, + nativeCodegenTargetRefusal, + nativeHelperForTarget, + type NativeHelperSpec, + type NativeTargetSpec, +} from "./targets.js"; import { compilerReleaseVersion } from "../library/sidecar.js"; const execFileAsync = promisify(execFile); @@ -46,6 +52,7 @@ export interface NativeCodegenVersion { llvm_version: string; host_triple: string; targets: string[]; + supported_targets: string[]; default_target: string; data_layout: string; } @@ -78,6 +85,8 @@ export interface NativeCodegenOptions { optimization?: "0" | "1" | "2" | "3" | "s" | "z"; sanitize?: boolean; target?: NativeTargetSpec; + /** Test seam for package selection on a simulated host. */ + helperHost?: { platform: NodeJS.Platform; arch: string }; /** Test seam: still resolves a package path, never searches PATH. */ resolvePackageJson?: (specifier: string) => string; /** Internal/test override; omitted production calls use the shared cache. */ @@ -122,12 +131,16 @@ async function invoke(binaryPath: string, args: string[]): Promise<{ stdout: str } } -function validateVersion(value: Record, target: NativeTargetSpec): NativeCodegenVersion { +export function validateNativeCodegenVersion( + value: Record, + target: NativeTargetSpec, + helper: NativeHelperSpec, +): NativeCodegenVersion { const expectedPackageVersion = compilerReleaseVersion(); const mismatch = (field: string, expected: string): never => { throw new NativeCodegenError( "SC3003", - `LLVM native helper is incompatible: ${field} is ${JSON.stringify(value[field])}, expected ${JSON.stringify(expected)}; reinstall scriptc so its compiler and ${target.helperPackage} packages have matching versions`, + `LLVM native helper is incompatible: ${field} is ${JSON.stringify(value[field])}, expected ${JSON.stringify(expected)}; reinstall scriptc so its compiler and ${helper.packageName} packages have matching versions`, "version_mismatch", ); }; @@ -141,14 +154,17 @@ function validateVersion(value: Record, target: NativeTargetSpe if (value["llvm_version"] !== NATIVE_CODEGEN_LLVM_VERSION) { mismatch("llvm_version", NATIVE_CODEGEN_LLVM_VERSION); } - if (value["default_target"] !== target.llvmTriple) { - mismatch("default_target", target.llvmTriple); + if (value["default_target"] !== helper.defaultTarget) { + mismatch("default_target", helper.defaultTarget); } - if (value["data_layout"] !== target.dataLayout) { - mismatch("data_layout", target.dataLayout); + if (value["data_layout"] !== helper.defaultDataLayout) { + mismatch("data_layout", helper.defaultDataLayout); } - if (!Array.isArray(value["targets"]) || !value["targets"].includes("AArch64")) { - mismatch("targets", "an array containing AArch64"); + if (!Array.isArray(value["targets"]) || !value["targets"].includes(target.llvmBackend)) { + mismatch("targets", `an array containing ${target.llvmBackend}`); + } + if (!Array.isArray(value["supported_targets"]) || !value["supported_targets"].includes(target.llvmTriple)) { + mismatch("supported_targets", `an array containing ${target.llvmTriple}`); } if (typeof value["host_triple"] !== "string") mismatch("host_triple", "a string"); return value as unknown as NativeCodegenVersion; @@ -157,20 +173,37 @@ function validateVersion(value: Record, target: NativeTargetSpe async function resolveHelper( target: NativeTargetSpec, resolver?: (specifier: string) => string, + host?: { platform: NodeJS.Platform; arch: string }, ): Promise { + const helper = nativeHelperForTarget( + target, + host?.platform, + host?.arch, + ); + if (helper === null) { + throw new NativeCodegenError( + "SC3002", + `no scriptc LLVM helper supports ${target.name} on this host`, + "unsupported_helper_host", + ); + } const resolvePackageJson = resolver ?? ((specifier: string) => createRequire(import.meta.url).resolve(specifier)); let packageJsonPath: string; try { - packageJsonPath = resolvePackageJson(`${target.helperPackage}/package.json`); + packageJsonPath = resolvePackageJson(`${helper.packageName}/package.json`); } catch { throw new NativeCodegenError( "SC3003", - `LLVM native helper package ${target.helperPackage} is not installed; reinstall scriptc with optional dependencies enabled for macOS arm64`, + `LLVM native helper package ${helper.packageName} is not installed; reinstall scriptc with optional dependencies enabled for this host`, "missing_package", ); } - const binaryPath = join(dirname(packageJsonPath), "bin", "scriptc-llvm-codegen"); + const binaryPath = join( + dirname(packageJsonPath), + "bin", + process.platform === "win32" ? "scriptc-llvm-codegen.exe" : "scriptc-llvm-codegen", + ); let binaryStat; try { binaryStat = await stat(binaryPath); @@ -178,7 +211,7 @@ async function resolveHelper( } catch { throw new NativeCodegenError( "SC3003", - `LLVM native helper package ${target.helperPackage} is incomplete: ${binaryPath} is missing; reinstall scriptc`, + `LLVM native helper package ${helper.packageName} is incomplete: ${binaryPath} is missing; reinstall scriptc`, "missing_binary", ); } @@ -191,7 +224,7 @@ async function resolveHelper( } catch { throw new NativeCodegenError( "SC3003", - `LLVM native helper package ${target.helperPackage} is incomplete: ${binaryPath} is not readable and executable; reinstall scriptc`, + `LLVM native helper package ${helper.packageName} is incomplete: ${binaryPath} is not readable and executable; reinstall scriptc`, "unusable_binary", ); } @@ -201,11 +234,15 @@ async function resolveHelper( } catch { throw new NativeCodegenError( "SC3003", - `LLVM native helper package ${target.helperPackage} changed while its inputs were being inspected; retry the build or reinstall scriptc`, + `LLVM native helper package ${helper.packageName} changed while its inputs were being inspected; retry the build or reinstall scriptc`, "helper_changed", ); } - const cacheKey = JSON.stringify(dependencies); + // A single host helper can own more than one backend (for example its + // host-native ISA plus WebAssembly). Cache only after the target backend + // has also been checked; otherwise a prior X86 lookup could accidentally + // bless a later WASI request against an older X86-only package. + const cacheKey = JSON.stringify({ dependencies, targetBackend: target.llvmBackend }); const load = async (): Promise => { let stdout: string; let binary: Buffer; @@ -217,7 +254,7 @@ async function resolveHelper( } catch (error) { throw new NativeCodegenError( "SC3003", - `LLVM native helper package ${target.helperPackage} could not be read and executed for its identity check: ${error instanceof Error ? error.message : String(error)}; reinstall scriptc`, + `LLVM native helper package ${helper.packageName} could not be read and executed for its identity check: ${error instanceof Error ? error.message : String(error)}; reinstall scriptc`, "identity_probe_failed", ); } @@ -225,14 +262,14 @@ async function resolveHelper( if (rawVersion === null) { throw new NativeCodegenError( "SC3003", - `LLVM native helper returned an invalid version response; reinstall scriptc and ${target.helperPackage}`, + `LLVM native helper returned an invalid version response; reinstall scriptc and ${helper.packageName}`, "invalid_version_response", ); } if (!(await nativeArtifactDependenciesStillMatch(dependencies).catch(() => false))) { throw new NativeCodegenError( "SC3003", - `LLVM native helper package ${target.helperPackage} changed during its identity check; retry the build or reinstall scriptc`, + `LLVM native helper package ${helper.packageName} changed during its identity check; retry the build or reinstall scriptc`, "helper_changed", ); } @@ -241,9 +278,9 @@ async function resolveHelper( binaryPath, dependencies, identity: { - packageName: target.helperPackage, + packageName: helper.packageName, binaryDigest: createHash("sha256").update(binary).digest("hex"), - version: validateVersion(rawVersion, target), + version: validateNativeCodegenVersion(rawVersion, target, helper), }, }; }; @@ -304,6 +341,13 @@ export async function emitNativeArtifact(options: NativeCodegenOptions): Promise "unsupported_target", ); } + if (!target.supports[options.outputKind]) { + throw new NativeCodegenError( + "SC3002", + `--emit=${options.outputKind} is not supported for ${target.name}`, + "unsupported_output_kind", + ); + } if (options.sanitize === true) { throw new NativeCodegenError( "SC3002", @@ -311,7 +355,7 @@ export async function emitNativeArtifact(options: NativeCodegenOptions): Promise "sanitize_unsupported", ); } - const helper = await resolveHelper(target, options.resolvePackageJson); + const helper = await resolveHelper(target, options.resolvePackageJson, options.helperHost); const root = await prepareBuildCacheRoot( options.cacheRoot === undefined ? buildCacheRoot() : options.cacheRoot, ); diff --git a/packages/compiler/src/backend/native-link-info.ts b/packages/compiler/src/backend/native-link-info.ts index 7e3ddc02..d5536eeb 100644 --- a/packages/compiler/src/backend/native-link-info.ts +++ b/packages/compiler/src/backend/native-link-info.ts @@ -68,7 +68,7 @@ export interface NativeLinkInfo { target: { name: NativeTargetSpec["name"]; llvm_triple: NativeTargetSpec["llvmTriple"]; - architecture: "arm64"; + architecture: NativeTargetSpec["architecture"]; object_format: NativeTargetSpec["objectFormat"]; minimum_os: NativeTargetSpec["minimumOs"]; relocation_model: NativeTargetSpec["relocationModel"]; @@ -117,11 +117,8 @@ function runtimeSourceRecipe( env: NodeJS.ProcessEnv, optimization: "release" | "dev", ): { sets: NativeSourceSet[]; systemLibraries: string[]; driverFlags: string[] } { - // Native link info currently describes the supported Mach-O external-object - // recipe. Keep this in the same target-aware helper as compileC so a - // consumer's link has the identical executable reachability semantics. - const executableSectionFlags = executableSectionEliminationFlags("darwin"); const dynamic = features.dynamic; + const executableSectionFlags = executableSectionEliminationFlags(target.platform); const curlFetch = dynamic && features.fetch && env["SCRIPTC_FETCH_CURL"] === "1"; const nativeFetch = features.fetch && !curlFetch; const netIsland = dynamic && (features.netIsland || nativeFetch); @@ -129,8 +126,19 @@ function runtimeSourceRecipe( const http = features.http || nativeFetch || netIsland; const tls = features.tls || nativeFetch || netIsland; const tlsCa = features.tlsCa || tls; + const wasiUnavailableSources = new Set([ + "scr_child.c", "scr_dyn_handle.c", "scr_loop_kqueue.c", "scr_loop_epoll.c", + "scr_loop_wsapoll.c", "scr_net.c", "scr_http.c", "scr_http2.c", "scr_dgram.c", + "scr_watch.c", "scr_tls_ca.c", "scr_tls.c", "scr_fetch.c", "scr_net_island.c", + ]); const runtimeSources = [ - ...EXECUTABLE_RUNTIME_SOURCES, + ...EXECUTABLE_RUNTIME_SOURCES.filter((source) => + target.platform !== "wasi" || !wasiUnavailableSources.has(source) + ), + ...(target.platform === "win32" ? ["scr_win.c"] : []), + ...(target.name === "linux-x64-musl" || target.name === "linux-arm64-musl" + ? ["scr_musl.c"] + : []), ...(features.copying ? ["scr_copying.c"] : []), ...(features.fileHandle ? ["scr_file_handle.c"] : []), ...(features.regex ? ["scr_regex.c"] : []), @@ -174,12 +182,14 @@ function runtimeSourceRecipe( ] : []), ]; - const commonTargetFlags = ["-target", target.llvmTriple]; + const filteredSources = target.platform === "wasi" + ? runtimeSources.filter((source) => !wasiUnavailableSources.has(source)) + : runtimeSources; const sets: NativeSourceSet[] = [{ name: "runtime", output: "objects", suggested_output: "runtime/*.o", - sources: unique(runtimeSources).map((source) => packagePath("src", source)), + sources: unique(filteredSources).map((source) => packagePath("src", source)), include_directories: unique([ "src", ...(features.regex || dynamic ? ["vendor/quickjs-ng"] : []), @@ -187,11 +197,13 @@ function runtimeSourceRecipe( ...(curlFetch ? ["vendor/curl/include"] : []), ]), defines: unique([ + ...target.runtimeCompileDefines, ...(features.textDecoderLegacy ? ["SCR_TEXT_DECODER_LEGACY"] : []), ...(dynamic ? ["SCR_DYNAMIC"] : []), ]), c_flags: [ - "-std=c11", ...commonTargetFlags, "-pthread", + "-std=c11", "-target", target.llvmTriple, + ...(target.platform === "win32" || target.platform === "wasi" ? [] : ["-pthread"]), optimization === "dev" ? "-O0" : "-O2", ...executableSectionFlags.compile, "-fno-math-errno", "-fno-strict-aliasing", "-Wno-deprecated-declarations", @@ -205,7 +217,11 @@ function runtimeSourceRecipe( sources: LRE_SOURCES.map((source) => packagePath("vendor", "quickjs-ng", source)), include_directories: ["vendor/quickjs-ng"], defines: [], - c_flags: ["-std=c11", ...commonTargetFlags, "-Os"], + c_flags: [ + "-std=c11", "-target", target.llvmTriple, + ...target.runtimeCompileDefines.map((define) => `-D${define}`), + "-Os", + ], }); } if (dynamic) { @@ -217,7 +233,9 @@ function runtimeSourceRecipe( include_directories: ["vendor/quickjs-ng"], defines: ["QUICKJS_NG_BUILD", "_GNU_SOURCE", "NDEBUG"], c_flags: [ - "-std=gnu11", ...commonTargetFlags, "-fvisibility=hidden", + "-std=gnu11", "-target", target.llvmTriple, + ...target.runtimeCompileDefines.map((define) => `-D${define}`), + "-fvisibility=hidden", "-funsigned-char", "-Os", ], }); @@ -233,22 +251,29 @@ function runtimeSourceRecipe( sources: [], include_directories: ["vendor/mbedtls/include", "vendor/mbedtls/library"], defines: [], - c_flags: ["-std=c11", ...commonTargetFlags, "-Os"], + c_flags: [ + "-std=c11", "-target", target.llvmTriple, + ...target.runtimeCompileDefines.map((define) => `-D${define}`), + "-Os", + ], }); } + if (target.platform === "wasi") { + const blocked = features.net || features.http || features.http2 || features.dgram || + features.watch || features.tls || features.tlsCa || features.fetch || features.netIsland; + if (blocked) throw new Error("WASI native link info requested an unavailable network/TLS runtime surface"); + } const systemLibraries = unique([ - "System", - ...((features.zlib || nativeFetch) ? ["z"] : []), - ...(dynamic ? ["m"] : []), + ...target.runtimeSystemLibraries, + ...((features.zlib || nativeFetch) && target.platform !== "win32" ? ["z"] : []), + ...(dynamic && target.platform !== "win32" ? ["m"] : []), ...(curlFetch ? ["curl"] : []), ]); return { sets, systemLibraries, driverFlags: [ - ...commonTargetFlags, - "-pthread", - ...executableSectionFlags.link, + ...target.executableLinkerArgs, ], }; } @@ -293,7 +318,7 @@ export async function createNativeLinkInfo(options: { target: { name: options.target.name, llvm_triple: options.target.llvmTriple, - architecture: "arm64", + architecture: options.target.architecture, object_format: options.target.objectFormat, minimum_os: options.target.minimumOs, relocation_model: options.target.relocationModel, diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts new file mode 100644 index 00000000..eec58163 --- /dev/null +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -0,0 +1,66 @@ +import { createHash } from "node:crypto"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { loadRuntimePack, type RuntimePackManifest } from "./runtime-pack.js"; +import { + LINUX_ARM64_GNU_TARGET, + LINUX_ARM64_MUSL_TARGET, + LINUX_X64_GNU_TARGET, + LINUX_X64_MUSL_TARGET, + MACOS_X64_TARGET, + WASM32_WASI_TARGET, + WINDOWS_X64_MSVC_TARGET, + type NativeTargetSpec, +} from "./targets.js"; +import { compilerReleaseVersion } from "../library/sidecar.js"; + +const dirs: string[] = []; +afterEach(async () => Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))); + +async function fixture(target: NativeTargetSpec) { + const root = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-target-")); + dirs.push(root); + const packagePath = join(root, "package.json"); + const bytes = Buffer.from("runtime object"); + const artifact = { path: "artifacts/runtime.o", sha256: createHash("sha256").update(bytes).digest("hex"), size: bytes.length }; + await mkdir(dirname(join(root, artifact.path)), { recursive: true }); + await Promise.all([ + writeFile(join(root, artifact.path), bytes), + writeFile(join(root, "LICENSE"), "license"), + writeFile(packagePath, JSON.stringify({ name: target.runtimePackPackage, version: compilerReleaseVersion() })), + ]); + const manifest: RuntimePackManifest = { + schema: "scriptc.runtime-pack.v1", format: 1, package: target.runtimePackPackage, version: compilerReleaseVersion(), + target: { name: target.name, llvm_triple: target.llvmTriple, architecture: target.architecture, object_format: target.objectFormat, minimum_os: target.minimumOs }, + runtime_abi: { version: 1, marker: "scr_runtime_abi_v1" }, + compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, + macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, + flavors: { + release: { optimization: "-O2", runtime_units: [{ source: "scr_runtime.c", predicate: true, variants: [{ id: "default", when: {}, defines: [], ...artifact }] }] }, + dev: { optimization: "-O0", runtime_units: [{ source: "scr_runtime.c", predicate: true, variants: [{ id: "default", when: {}, defines: [], ...artifact }] }] }, + }, + archives: [], system_libraries: [], licenses: [{ path: "LICENSE", license: "fixture" }], + }; + await writeFile(join(root, "runtime-pack.json"), JSON.stringify(manifest)); + return { packagePath }; +} + +const features = { + dynamic: false, regex: false, copying: false, textDecoderLegacy: false, fileHandle: false, + fetch: false, netIsland: false, zlib: false, assert: false, inspect: false, dynInvoke: false, + dc: false, dynAsync: false, events: false, emitter: false, symbol: false, searchParams: false, + qs: false, parseArgs: false, stream: false, net: false, http: false, http2: false, + dgram: false, watch: false, foreignFfi: false, nodeTest: false, tls: false, tlsCa: false, +}; + +test.each([ + MACOS_X64_TARGET, LINUX_X64_GNU_TARGET, LINUX_ARM64_GNU_TARGET, + WINDOWS_X64_MSVC_TARGET, LINUX_X64_MUSL_TARGET, LINUX_ARM64_MUSL_TARGET, WASM32_WASI_TARGET, +])("runtime-pack validation selects the exact $name package", async (target) => { + const pack = await fixture(target); + const result = await loadRuntimePack({ target, features, optimization: "release", resolver: () => pack.packagePath }); + expect(result.manifest.package).toBe(target.runtimePackPackage); + expect(result.manifest.target).toMatchObject({ name: target.name, architecture: target.architecture, object_format: target.objectFormat }); +}); diff --git a/packages/compiler/src/backend/runtime-pack.ts b/packages/compiler/src/backend/runtime-pack.ts index 0e8548f5..c15358ab 100644 --- a/packages/compiler/src/backend/runtime-pack.ts +++ b/packages/compiler/src/backend/runtime-pack.ts @@ -51,7 +51,7 @@ export interface RuntimePackManifest { target: { name: NativeTargetSpec["name"]; llvm_triple: NativeTargetSpec["llvmTriple"]; - architecture: "arm64"; + architecture: NativeTargetSpec["architecture"]; object_format: NativeTargetSpec["objectFormat"]; minimum_os: NativeTargetSpec["minimumOs"]; }; @@ -157,11 +157,10 @@ export function parseRuntimePackManifest(value: unknown): RuntimePackManifest { if ( manifest?.schema !== RUNTIME_PACK_SCHEMA || manifest.format !== RUNTIME_PACK_FORMAT || typeof manifest.package !== "string" || typeof manifest.version !== "string" || - target?.name !== "macos-arm64" || target.llvm_triple !== "arm64-apple-macosx14.0.0" || - target.architecture !== "arm64" || target.object_format !== "macho" || target.minimum_os !== "14.0" || + !isRuntimePackTarget(target) || abi?.version !== RUNTIME_ABI_VERSION || abi.marker !== RUNTIME_ABI_MARKER || typeof compiler?.command !== "string" || typeof compiler.identity !== "string" || - compiler.target !== target.llvm_triple || + compiler.target !== (target?.llvm_triple) || !Array.isArray(macros?.executable) || !macros.executable.every((entry) => typeof entry === "string") || !Array.isArray(macros.excluded) || !macros.excluded.every((entry) => typeof entry === "string") || macros.sanitizer !== "external-toolchain-required" || @@ -182,6 +181,15 @@ export function parseRuntimePackManifest(value: unknown): RuntimePackManifest { return manifest as unknown as RuntimePackManifest; } +function isRuntimePackTarget(target: Record | null): boolean { + return target !== null && + typeof target.name === "string" && + typeof target.llvm_triple === "string" && + (target.architecture === "arm64" || target.architecture === "x64" || target.architecture === "wasm32") && + (target.object_format === "macho" || target.object_format === "elf" || target.object_format === "coff" || target.object_format === "wasm") && + typeof target.minimum_os === "string"; +} + export function effectiveRuntimeFeatures( features: NativeLinkFeatures, env: NodeJS.ProcessEnv = process.env, @@ -324,16 +332,14 @@ export async function loadRuntimePack(options: { env?: NodeJS.ProcessEnv; resolver?: (specifier: string) => string; }): Promise { - const packageName = options.target.name === "macos-arm64" - ? "@scriptc/runtime-darwin-arm64" - : (() => { throw new RuntimePackError(`no runtime pack supports ${options.target.name}`, "unsupported"); })(); + const packageName = options.target.runtimePackPackage; const resolvePackageJson = options.resolver ?? ((specifier: string) => createRequire(import.meta.url).resolve(specifier)); let packagePath: string; try { packagePath = resolvePackageJson(`${packageName}/package.json`); } catch { throw new RuntimePackError( - `precompiled runtime package ${packageName} is not installed; reinstall scriptc with optional dependencies enabled for macOS arm64`, + `precompiled runtime package ${packageName} is not installed; reinstall scriptc with optional dependencies enabled for ${options.target.name}`, "missing", ); } @@ -368,6 +374,7 @@ export async function loadRuntimePack(options: { if ( manifest.target.name !== options.target.name || manifest.target.llvm_triple !== options.target.llvmTriple || + manifest.target.architecture !== options.target.architecture || manifest.target.object_format !== options.target.objectFormat || manifest.target.minimum_os !== options.target.minimumOs ) throw new RuntimePackError(`runtime pack does not support target ${options.target.name}`, "invalid"); diff --git a/packages/compiler/src/backend/targets.test.ts b/packages/compiler/src/backend/targets.test.ts index d251738a..b69a2bc7 100644 --- a/packages/compiler/src/backend/targets.test.ts +++ b/packages/compiler/src/backend/targets.test.ts @@ -1,12 +1,34 @@ import { describe, expect, test } from "vitest"; -import { MACOS_ARM64_TARGET, nativeCodegenTarget, nativeCodegenTargetRefusal } from "./targets.js"; +import { + LINUX_ARM64_GNU_TARGET, + LINUX_ARM64_MUSL_TARGET, + LINUX_X64_GNU_TARGET, + LINUX_X64_MUSL_TARGET, + MACOS_ARM64_TARGET, + MACOS_X64_TARGET, + WASM32_WASI_TARGET, + WINDOWS_X64_MSVC_TARGET, + nativeCodegenTarget, + nativeCodegenTargetRefusal, +} from "./targets.js"; describe("native code-generation targets", () => { - test("admits only host-native macOS arm64", () => { + test("selects only fully specified host-native targets", () => { expect(nativeCodegenTarget({}, "darwin", "arm64", "24.0.0")).toEqual(MACOS_ARM64_TARGET); + expect(nativeCodegenTarget({}, "darwin", "x64", "24.0.0")).toEqual(MACOS_X64_TARGET); + expect(nativeCodegenTarget({}, "linux", "x64", "6.8.0", "gnu")).toEqual(LINUX_X64_GNU_TARGET); + expect(nativeCodegenTarget({}, "linux", "arm64", "6.8.0", "gnu")).toEqual(LINUX_ARM64_GNU_TARGET); + expect(nativeCodegenTarget({}, "linux", "x64", "6.8.0", "musl")).toEqual(LINUX_X64_MUSL_TARGET); + expect(nativeCodegenTarget({}, "linux", "arm64", "6.8.0", "musl")).toEqual(LINUX_ARM64_MUSL_TARGET); + expect(nativeCodegenTarget({}, "win32", "x64", "10.0.0")).toEqual(WINDOWS_X64_MSVC_TARGET); expect(nativeCodegenTarget({}, "darwin", "arm64", "23.6.0")).toBeNull(); - expect(nativeCodegenTarget({}, "darwin", "x64", "24.0.0")).toBeNull(); - expect(nativeCodegenTarget({}, "linux", "arm64", "6.8.0")).toBeNull(); + expect(nativeCodegenTarget({}, "linux", "ia32", "6.8.0")).toBeNull(); + expect(nativeCodegenTarget({ SCRIPTC_TARGET: "x86_64-linux-musl" }, "linux", "x64", "6.8.0", "musl")) + .toEqual(LINUX_X64_MUSL_TARGET); + expect(nativeCodegenTarget({ SCRIPTC_TARGET: "aarch64-linux-musl" }, "linux", "arm64", "6.8.0", "musl")) + .toEqual(LINUX_ARM64_MUSL_TARGET); + expect(nativeCodegenTarget({ SCRIPTC_TARGET: "wasm32-wasi" }, "darwin", "x64", "24.0.0")) + .toEqual(WASM32_WASI_TARGET); expect(nativeCodegenTarget( { SCRIPTC_TARGET: "aarch64-apple-ios" }, "darwin", "arm64", "24.0.0", )) @@ -14,7 +36,7 @@ describe("native code-generation targets", () => { }); test("refusals name the unsupported host or cross target", () => { - expect(nativeCodegenTargetRefusal({}, "linux", "x64", "6.8.0")).toContain("linux x64"); + expect(nativeCodegenTargetRefusal({}, "linux", "ia32", "6.8.0")).toContain("linux ia32"); expect(nativeCodegenTargetRefusal({}, "darwin", "arm64", "23.6.0")) .toContain("requires macOS 15.0 or newer"); expect(nativeCodegenTargetRefusal( @@ -33,4 +55,8 @@ describe("native code-generation targets", () => { "-Wl,-dead_strip", ]); }); + + test("describes the WASI relocatable-object ABI", () => { + expect(WASM32_WASI_TARGET.supports).toMatchObject({ asm: true, obj: true, exe: true }); + }); }); diff --git a/packages/compiler/src/backend/targets.ts b/packages/compiler/src/backend/targets.ts index 51cd9884..51f75430 100644 --- a/packages/compiler/src/backend/targets.ts +++ b/packages/compiler/src/backend/targets.ts @@ -1,60 +1,360 @@ import { release } from "node:os"; +/** A helper package is host-specific, even when it emits a portable target + * (the WASI helpers are the first intentionally cross-host case). */ +export interface NativeHelperSpec { + packageName: string; + /** The helper protocol reports this primary target and its layout. */ + defaultTarget: string; + defaultDataLayout: string; + backend: "AArch64" | "X86" | "WebAssembly"; +} + +export type NativeTargetName = + | "macos-arm64" + | "macos-x64" + | "linux-x64-gnu" + | "linux-arm64-gnu" + | "windows-x64-msvc" + | "linux-x64-musl" + | "linux-arm64-musl" + | "wasm32-wasi"; + +export type NativeObjectFormat = "macho" | "elf" | "coff" | "wasm"; +export type NativeTargetPlatform = "darwin" | "linux" | "win32" | "wasi"; + export interface NativeTargetSpec { /** Stable scriptc-facing identity used in cache keys and diagnostics. */ - name: "macos-arm64"; - llvmTriple: "arm64-apple-macosx14.0.0"; - dataLayout: "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32"; + name: NativeTargetName; + llvmTriple: string; + dataLayout: string; cpu: "generic"; features: ""; - pointerBits: 64; + pointerBits: 32 | 64; endianness: "little"; - objectFormat: "macho"; + objectFormat: NativeObjectFormat; relocationModel: "pic"; codeModel: "small"; - minimumOs: "14.0"; - helperMinimumOs: "15.0"; - outputSuffixes: { asm: ".s"; obj: ".o"; exe: "" }; + /** OS deployment level, libc floor, or WASI ABI contract. */ + minimumOs: string; + /** Minimum host contract for the helper package itself. */ + helperMinimumOs: string; + architecture: "arm64" | "x64" | "wasm32"; + platform: NativeTargetPlatform; + outputSuffixes: { asm: ".s" | ".asm"; obj: ".o" | ".obj"; exe: "" | ".exe" | ".wasm" }; /** Arguments for the platform linker driver once scriptc has produced an * object. These are target ABI policy, rather than C compiler settings. */ - executableLinkerArgs: readonly ["-target", "arm64-apple-macosx14.0.0", "-pthread", "-Wl,-dead_strip"]; - supports: { asm: true; obj: true; exe: true; library: false }; - helperPackage: "@scriptc/llvm-darwin-arm64"; + executableLinkerArgs: readonly string[]; + defaultLinker: string; + defaultLinkerArgs: readonly string[]; + /** Driver spelling for the native-codegen triple. Linker drivers such as + * zig use a different user-facing triple from LLVM's canonical spelling. */ + linkerTargetTriple: string; + runtimeSystemLibraries: readonly string[]; + /** A source-level flag needed by the matching precompiled runtime units. + * The helper's program object never needs this (it is already LLVM IR). */ + runtimeCompileDefines: readonly string[]; + supports: { asm: boolean; obj: boolean; exe: boolean; library: boolean }; + /** Primary host-native helper. The resolved helper can differ for WASI. */ + helperPackage: string; + helper: NativeHelperSpec; + llvmBackend: NativeHelperSpec["backend"]; + hostHelpers?: Partial>; + runtimePackPackage: string; } -export const MACOS_ARM64_TARGET: NativeTargetSpec = { - name: "macos-arm64", - llvmTriple: "arm64-apple-macosx14.0.0", - dataLayout: "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32", - cpu: "generic", - features: "", - pointerBits: 64, - endianness: "little", - objectFormat: "macho", - relocationModel: "pic", - codeModel: "small", - minimumOs: "14.0", - helperMinimumOs: "15.0", - outputSuffixes: { asm: ".s", obj: ".o", exe: "" }, - executableLinkerArgs: ["-target", "arm64-apple-macosx14.0.0", "-pthread", "-Wl,-dead_strip"], - supports: { asm: true, obj: true, exe: true, library: false }, - helperPackage: "@scriptc/llvm-darwin-arm64", +export type NativeHelperHost = + | "darwin-arm64" + | "darwin-x64" + | "linux-x64-gnu" + | "linux-x64-musl" + | "linux-arm64-gnu" + | "linux-arm64-musl" + | "win32-x64"; + +export type LinuxLibc = "gnu" | "musl"; + +const ARM64_DARWIN_LAYOUT = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32"; +const X64_DARWIN_LAYOUT = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"; +const X64_ELF_LAYOUT = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"; +const ARM64_ELF_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"; +const X64_COFF_LAYOUT = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"; +const WASI_LAYOUT = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-i128:128-n32:64-S128-ni:1:10:20"; + +const DARWIN_ARM64_HELPER: NativeHelperSpec = { + packageName: "@scriptc/llvm-darwin-arm64", + defaultTarget: "arm64-apple-macosx14.0.0", + defaultDataLayout: ARM64_DARWIN_LAYOUT, + backend: "AArch64", +}; +const DARWIN_X64_HELPER: NativeHelperSpec = { + packageName: "@scriptc/llvm-darwin-x64", + defaultTarget: "x86_64-apple-macosx14.0.0", + defaultDataLayout: X64_DARWIN_LAYOUT, + backend: "X86", +}; +const LINUX_X64_HELPER: NativeHelperSpec = { + packageName: "@scriptc/llvm-linux-x64-gnu", + defaultTarget: "x86_64-unknown-linux-gnu", + defaultDataLayout: X64_ELF_LAYOUT, + backend: "X86", +}; +const LINUX_ARM64_HELPER: NativeHelperSpec = { + packageName: "@scriptc/llvm-linux-arm64-gnu", + defaultTarget: "aarch64-unknown-linux-gnu", + defaultDataLayout: ARM64_ELF_LAYOUT, + backend: "AArch64", +}; +const LINUX_X64_MUSL_HELPER: NativeHelperSpec = { + packageName: "@scriptc/llvm-linux-x64-musl", + defaultTarget: "x86_64-unknown-linux-musl", + defaultDataLayout: X64_ELF_LAYOUT, + backend: "X86", +}; +const LINUX_ARM64_MUSL_HELPER: NativeHelperSpec = { + packageName: "@scriptc/llvm-linux-arm64-musl", + defaultTarget: "aarch64-unknown-linux-musl", + defaultDataLayout: ARM64_ELF_LAYOUT, + backend: "AArch64", +}; +const WINDOWS_X64_HELPER: NativeHelperSpec = { + packageName: "@scriptc/llvm-win32-x64-msvc", + defaultTarget: "x86_64-pc-windows-msvc", + defaultDataLayout: X64_COFF_LAYOUT, + backend: "X86", }; +export const MACOS_ARM64_TARGET: NativeTargetSpec = { + name: "macos-arm64", llvmTriple: "arm64-apple-macosx14.0.0", dataLayout: ARM64_DARWIN_LAYOUT, + cpu: "generic", features: "", pointerBits: 64, endianness: "little", objectFormat: "macho", + relocationModel: "pic", codeModel: "small", minimumOs: "14.0", helperMinimumOs: "15.0", + architecture: "arm64", platform: "darwin", outputSuffixes: { asm: ".s", obj: ".o", exe: "" }, + executableLinkerArgs: ["-target", "arm64-apple-macosx14.0.0", "-pthread", "-Wl,-dead_strip"], + defaultLinker: "clang", defaultLinkerArgs: [], runtimeSystemLibraries: ["System"], + linkerTargetTriple: "arm64-apple-macosx14.0.0", + runtimeCompileDefines: [], + supports: { asm: true, obj: true, exe: true, library: false }, + helperPackage: DARWIN_ARM64_HELPER.packageName, helper: DARWIN_ARM64_HELPER, + llvmBackend: "AArch64", + runtimePackPackage: "@scriptc/runtime-darwin-arm64", +}; + +export const MACOS_X64_TARGET: NativeTargetSpec = { + name: "macos-x64", llvmTriple: "x86_64-apple-macosx14.0.0", dataLayout: X64_DARWIN_LAYOUT, + cpu: "generic", features: "", pointerBits: 64, endianness: "little", objectFormat: "macho", + relocationModel: "pic", codeModel: "small", minimumOs: "14.0", helperMinimumOs: "15.0", + architecture: "x64", platform: "darwin", outputSuffixes: { asm: ".s", obj: ".o", exe: "" }, + executableLinkerArgs: ["-target", "x86_64-apple-macosx14.0.0", "-pthread", "-Wl,-dead_strip"], + defaultLinker: "clang", defaultLinkerArgs: [], runtimeSystemLibraries: ["System"], + linkerTargetTriple: "x86_64-apple-macosx14.0.0", + runtimeCompileDefines: [], + supports: { asm: true, obj: true, exe: true, library: false }, + helperPackage: DARWIN_X64_HELPER.packageName, helper: DARWIN_X64_HELPER, + llvmBackend: "X86", + runtimePackPackage: "@scriptc/runtime-darwin-x64", +}; + +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", + 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"], + linkerTargetTriple: "x86_64-unknown-linux-gnu", + runtimeCompileDefines: ["_GNU_SOURCE"], + supports: { asm: true, obj: true, exe: true, library: true }, + helperPackage: LINUX_X64_HELPER.packageName, helper: LINUX_X64_HELPER, + llvmBackend: "X86", + runtimePackPackage: "@scriptc/runtime-linux-x64-gnu", +}; + +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", + 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"], + linkerTargetTriple: "aarch64-unknown-linux-gnu", + runtimeCompileDefines: ["_GNU_SOURCE"], + supports: { asm: true, obj: true, exe: true, library: true }, + helperPackage: LINUX_ARM64_HELPER.packageName, helper: LINUX_ARM64_HELPER, + llvmBackend: "AArch64", + runtimePackPackage: "@scriptc/runtime-linux-arm64-gnu", +}; + +export const WINDOWS_X64_MSVC_TARGET: NativeTargetSpec = { + name: "windows-x64-msvc", llvmTriple: "x86_64-pc-windows-msvc", dataLayout: X64_COFF_LAYOUT, + cpu: "generic", features: "", pointerBits: 64, endianness: "little", objectFormat: "coff", + relocationModel: "pic", codeModel: "small", minimumOs: "Windows 10", helperMinimumOs: "Windows 10", + architecture: "x64", platform: "win32", outputSuffixes: { asm: ".asm", obj: ".obj", exe: ".exe" }, + // The runtime pack is built against Zig's MinGW-compatible Windows sysroot; + // its x64 COFF ABI is compatible with the helper's MSVC-flavored object. + executableLinkerArgs: ["-target", "x86_64-windows-gnu", "-Wl,--gc-sections"], + defaultLinker: "zig", defaultLinkerArgs: ["cc"], runtimeSystemLibraries: ["advapi32", "iphlpapi", "ws2_32"], + linkerTargetTriple: "x86_64-windows-gnu", + runtimeCompileDefines: [], + supports: { asm: true, obj: true, exe: true, library: true }, + helperPackage: WINDOWS_X64_HELPER.packageName, helper: WINDOWS_X64_HELPER, + llvmBackend: "X86", + runtimePackPackage: "@scriptc/runtime-win32-x64-msvc", +}; + +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", + 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", + runtimeCompileDefines: ["_GNU_SOURCE", "SCR_MUSL"], + helperPackage: LINUX_X64_MUSL_HELPER.packageName, + helper: LINUX_X64_MUSL_HELPER, +}; + +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", + executableLinkerArgs: ["-target", "aarch64-unknown-linux-musl", "-pthread", "-Wl,--gc-sections"], + defaultLinker: "zig", defaultLinkerArgs: ["cc"], linkerTargetTriple: "aarch64-linux-musl", + runtimePackPackage: "@scriptc/runtime-linux-arm64-musl", + runtimeCompileDefines: ["_GNU_SOURCE", "SCR_MUSL"], + helperPackage: LINUX_ARM64_MUSL_HELPER.packageName, + helper: LINUX_ARM64_MUSL_HELPER, +}; + +export const WASM32_WASI_TARGET: NativeTargetSpec = { + name: "wasm32-wasi", llvmTriple: "wasm32-unknown-wasi", dataLayout: WASI_LAYOUT, + cpu: "generic", features: "", pointerBits: 32, endianness: "little", objectFormat: "wasm", + relocationModel: "pic", codeModel: "small", minimumOs: "WASI Preview 1", helperMinimumOs: "host helper contract", + architecture: "wasm32", platform: "wasi", outputSuffixes: { asm: ".s", obj: ".o", exe: ".wasm" }, + executableLinkerArgs: ["-target", "wasm32-wasi"], defaultLinker: "zig", defaultLinkerArgs: ["cc"], + linkerTargetTriple: "wasm32-wasi", + runtimeSystemLibraries: ["wasi-emulated-signal", "wasi-emulated-process-clocks"], + runtimeCompileDefines: ["_GNU_SOURCE", "_WASI_EMULATED_SIGNAL", "_WASI_EMULATED_PROCESS_CLOCKS"], + supports: { asm: true, obj: true, exe: true, library: false }, + helperPackage: DARWIN_ARM64_HELPER.packageName, helper: DARWIN_ARM64_HELPER, + llvmBackend: "WebAssembly", + hostHelpers: { + "darwin-arm64": DARWIN_ARM64_HELPER, + "darwin-x64": DARWIN_X64_HELPER, + "linux-x64-gnu": LINUX_X64_HELPER, + "linux-x64-musl": LINUX_X64_MUSL_HELPER, + "linux-arm64-gnu": LINUX_ARM64_HELPER, + "linux-arm64-musl": LINUX_ARM64_MUSL_HELPER, + "win32-x64": WINDOWS_X64_HELPER, + }, + runtimePackPackage: "@scriptc/runtime-wasm32-wasi", +}; + +export const NATIVE_TARGETS = [ + MACOS_ARM64_TARGET, MACOS_X64_TARGET, LINUX_X64_GNU_TARGET, + LINUX_ARM64_GNU_TARGET, WINDOWS_X64_MSVC_TARGET, LINUX_X64_MUSL_TARGET, + LINUX_ARM64_MUSL_TARGET, WASM32_WASI_TARGET, +] as const; + +function detectedLinuxLibc(): LinuxLibc { + // Node exposes glibc's runtime version without any external command or + // filesystem probe. Its absence on Linux is the portable musl signal used + // by npm's own optional-dependency selection conventions. + const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: unknown } } | undefined; + const header = report?.header; + return typeof header?.glibcVersionRuntime === "string" ? "gnu" : "musl"; +} + +function helperHost( + platform: NodeJS.Platform, + arch: string, + linuxLibc: LinuxLibc = detectedLinuxLibc(), +): NativeHelperHost | null { + if (platform === "darwin" && arch === "arm64") return "darwin-arm64"; + if (platform === "darwin" && arch === "x64") return "darwin-x64"; + if (platform === "linux" && arch === "x64") return `linux-x64-${linuxLibc}`; + if (platform === "linux" && arch === "arm64") return `linux-arm64-${linuxLibc}`; + if (platform === "win32" && arch === "x64") return "win32-x64"; + return null; +} + +export function nativeHelperForTarget( + target: NativeTargetSpec, + hostPlatform: NodeJS.Platform = process.platform, + hostArch: string = process.arch, + linuxLibc?: LinuxLibc, +): NativeHelperSpec | null { + const host = helperHost(hostPlatform, hostArch, linuxLibc); + if (host === null) return null; + return target.hostHelpers?.[host] ?? ( + target.platform === hostPlatform && target.architecture === hostArch ? target.helper : null + ); +} + +function nativeHostTarget( + hostPlatform: NodeJS.Platform, + hostArch: string, + hostRelease: string, + linuxLibc: LinuxLibc, +): NativeTargetSpec | null { + if (hostPlatform === "darwin") { + const major = Number.parseInt(hostRelease.split(".", 1)[0] ?? "", 10); + if (!Number.isFinite(major) || major < 24) return null; + return hostArch === "arm64" ? MACOS_ARM64_TARGET : hostArch === "x64" ? MACOS_X64_TARGET : null; + } + if (hostPlatform === "linux") { + return hostArch === "x64" + ? linuxLibc === "musl" ? LINUX_X64_MUSL_TARGET : LINUX_X64_GNU_TARGET + : hostArch === "arm64" + ? linuxLibc === "musl" ? LINUX_ARM64_MUSL_TARGET : LINUX_ARM64_GNU_TARGET + : null; + } + return hostPlatform === "win32" && hostArch === "x64" ? WINDOWS_X64_MSVC_TARGET : null; +} + +function requestedTarget( + raw: string, + host: NativeTargetSpec | null, + hostPlatform: NodeJS.Platform, + hostArch: string, + linuxLibc: LinuxLibc, +): NativeTargetSpec | null { + if (raw === "") return host; + switch (raw) { + case "arm64-apple-macosx14.0.0": return host?.name === "macos-arm64" ? MACOS_ARM64_TARGET : null; + case "x86_64-apple-macosx14.0.0": + case "x86_64-apple-macos": return host?.name === "macos-x64" ? MACOS_X64_TARGET : null; + case "x86_64-unknown-linux-gnu": + case "x86_64-linux-gnu": + case "x86_64-linux-gnu.2.36": return host?.name === "linux-x64-gnu" ? LINUX_X64_GNU_TARGET : null; + case "aarch64-unknown-linux-gnu": + case "aarch64-linux-gnu": + case "aarch64-linux-gnu.2.36": return host?.name === "linux-arm64-gnu" ? LINUX_ARM64_GNU_TARGET : null; + case "x86_64-unknown-linux-musl": + case "x86_64-linux-musl": + return host?.platform === "linux" && host.architecture === "x64" ? LINUX_X64_MUSL_TARGET : null; + case "aarch64-unknown-linux-musl": + case "aarch64-linux-musl": + return host?.platform === "linux" && host.architecture === "arm64" ? LINUX_ARM64_MUSL_TARGET : null; + case "wasm32-wasi": + case "wasm32-unknown-wasi": + return helperHost(hostPlatform, hostArch, linuxLibc) === null ? null : WASM32_WASI_TARGET; + case "x86_64-pc-windows-msvc": return host?.name === "windows-x64-msvc" ? WINDOWS_X64_MSVC_TARGET : null; + default: return null; + } +} + +/** Select only a fully described scriptc target. LLVM accepting an arbitrary + * triple is never evidence of its object ABI, runtime pack, link, or run. */ export function nativeCodegenTarget( env: NodeJS.ProcessEnv = process.env, hostPlatform: NodeJS.Platform = process.platform, hostArch: string = process.arch, hostRelease: string = release(), + linuxLibc: LinuxLibc = detectedLinuxLibc(), ): NativeTargetSpec | null { - // The first helper is host-native only. In particular, do not reinterpret - // an existing SCRIPTC_TARGET cross-build as macOS merely because its OS - // family is Darwin. - if ((env["SCRIPTC_TARGET"] ?? "") !== "") return null; - const darwinMajor = Number.parseInt(hostRelease.split(".", 1)[0] ?? "", 10); - return hostPlatform === "darwin" && hostArch === "arm64" && darwinMajor >= 24 - ? MACOS_ARM64_TARGET - : null; + const host = nativeHostTarget(hostPlatform, hostArch, hostRelease, linuxLibc); + const target = requestedTarget(env["SCRIPTC_TARGET"] ?? "", host, hostPlatform, hostArch, linuxLibc); + return target !== null && nativeHelperForTarget(target, hostPlatform, hostArch, linuxLibc) !== null ? target : null; } export function nativeCodegenTargetRefusal( @@ -62,18 +362,17 @@ export function nativeCodegenTargetRefusal( hostPlatform: NodeJS.Platform = process.platform, hostArch: string = process.arch, hostRelease: string = release(), + linuxLibc: LinuxLibc = detectedLinuxLibc(), ): string | null { - if (nativeCodegenTarget(env, hostPlatform, hostArch, hostRelease) !== null) return null; - const crossTarget = env["SCRIPTC_TARGET"] ?? ""; - if (crossTarget !== "") { - return `native assembly/object emission does not support SCRIPTC_TARGET=${crossTarget}; the first supported target is host-native macOS arm64`; + if (nativeCodegenTarget(env, hostPlatform, hostArch, hostRelease, linuxLibc) !== null) return null; + const requested = env["SCRIPTC_TARGET"] ?? ""; + if (requested !== "") { + return `native assembly/object emission does not support SCRIPTC_TARGET=${requested} on this ${hostPlatform} ${hostArch} host; supported hosts are macOS arm64/x64, Linux x64/arm64, and Windows x64 (with wasm32-wasi on each)`; } - if (hostPlatform === "darwin" && hostArch === "arm64") { - const darwinMajor = Number.parseInt(hostRelease.split(".", 1)[0] ?? "", 10); - const macosMajor = Number.isFinite(darwinMajor) && darwinMajor >= 20 - ? String(darwinMajor - 9) - : "unknown"; - return `native assembly/object emission requires macOS ${MACOS_ARM64_TARGET.helperMinimumOs} or newer; this host is macOS ${macosMajor} (Darwin ${hostRelease})`; + if (hostPlatform === "darwin" && (hostArch === "arm64" || hostArch === "x64")) { + const major = Number.parseInt(hostRelease.split(".", 1)[0] ?? "", 10); + const macos = Number.isFinite(major) && major >= 20 ? String(major - 9) : "unknown"; + return `native assembly/object emission requires macOS 15.0 or newer; this host is macOS ${macos} (Darwin ${hostRelease})`; } - return `native assembly/object emission is supported on macOS arm64 hosts; this host is ${hostPlatform} ${hostArch}`; + return `native assembly/object emission is supported on macOS arm64/x64, Linux x64/arm64, and Windows x64 hosts; this host is ${hostPlatform} ${hostArch}`; } diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 3363738f..45a89e27 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -1089,7 +1089,10 @@ async function compileExecutableNative( programObjectDependencies, }); const cacheableLinker = - onArtifactReady !== undefined && ffi === null && platformLinkerSupportsPersistentCache(); + onArtifactReady !== undefined && ffi === null && platformLinkerSupportsPersistentCache( + process.env, + runtimePackTarget, + ); await linkNativeExecutable(plan, { // A caller-selected linker can be a mutable wrapper with hidden inputs, // and a PATH-selected `clang` can be one too. FFI profiles and mutable @@ -1259,7 +1262,22 @@ async function compileTracked( } let buildPlatform: string; if (outputKind === "exe") { - buildPlatform = buildTargetPlatform(); + // Target semantics are needed before choosing the helper/runtime-pack + // path. Do not make them contingent on the legacy C-driver resolver: + // an LLVM-owned WASI build deliberately needs no SCRIPTC_CC. + try { + buildPlatform = sourceTargetPlatform(); + } catch (err) { + return { + ok: false, + diagnostics: [{ + code: "SC3002", + message: err instanceof Error ? err.message : String(err), + loc: { file: entryPath, start: 0, end: 0 }, + }], + sourceTexts: new Map(), + }; + } } else { try { buildPlatform = sourceTargetPlatform(); @@ -1353,13 +1371,16 @@ async function compileTracked( ? "helper-object" : helperObjectRoute ? "runtime-pack" : "driver-tu" }`, - compiler: [ + compiler: [ helperObjectRoute - ? resolvePlatformLinker() + ? resolvePlatformLinker(process.env, nativeCodegenTarget()?.defaultLinker) : (process.env["SCRIPTC_CC"] ?? "clang"), ], nativeEnvironment: helperObjectRoute - ? await executableLinkerEnvironmentFingerprint() + ? await executableLinkerEnvironmentFingerprint( + process.env, + nativeCodegenTarget()?.defaultLinker, + ) : await executableNativeEnvironmentFingerprint(), nodeVersion: process.version, implementation: implementation.digest, diff --git a/packages/compiler/src/startup-cache.ts b/packages/compiler/src/startup-cache.ts index 165ea6d0..8ec924cd 100644 --- a/packages/compiler/src/startup-cache.ts +++ b/packages/compiler/src/startup-cache.ts @@ -5,6 +5,7 @@ export { readRoutedExecutableCache } from "./executable/executable-cache.js"; export type { EarlyExecutableRouteOptions } from "./executable/executable-cache.js"; export { executableNativeEnvironmentFingerprint, + configuredTargetPlatform, legacyCExecutablePathRequested, resolveCc, targetPlatform, @@ -14,3 +15,18 @@ export { executableLinkerEnvironmentFingerprint, resolvePlatformLinker, } from "./backend/linker.js"; +import { legacyCExecutablePathRequested } from "./backend/external-c.js"; +import { nativeCodegenTarget, type NativeTargetSpec } from "./backend/targets.js"; + +/** Whether an ordinary LLVM executable can take the helper/object plus + * precompiled-runtime-pack route. Keep this startup-sized: the CLI bootstrap + * needs the exact cache identity before deciding whether it must import the + * full compiler graph. */ +export function precompiledRuntimePackTarget( + env: NodeJS.ProcessEnv = process.env, +): NativeTargetSpec | null { + if (env["SCRIPTC_RUNTIME_PACK"] === "0" || + env["SCRIPTC_FETCH_CURL"] === "1" || + legacyCExecutablePathRequested(env)) return null; + return nativeCodegenTarget(env); +} diff --git a/packages/llvm-darwin-arm64/scripts/build.mjs b/packages/llvm-darwin-arm64/scripts/build.mjs index e6c7e7cd..3c64b845 100644 --- a/packages/llvm-darwin-arm64/scripts/build.mjs +++ b/packages/llvm-darwin-arm64/scripts/build.mjs @@ -20,6 +20,8 @@ execFileSync("cmake", [ "-G", "Ninja", `-DLLVM_DIR=${llvmDir}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, + "-DSCRIPTC_TARGET_BACKENDS=AArch64;WebAssembly", + "-DSCRIPTC_ALLOWED_TARGETS=arm64-apple-macosx14.0.0,wasm32-unknown-wasi", "-DCMAKE_BUILD_TYPE=Release", ], { stdio: "inherit" }); execFileSync("cmake", ["--build", buildDir, "--target", "scriptc-llvm-codegen"], { @@ -38,7 +40,9 @@ const version = JSON.parse(execFileSync(output, ["version", "--format=json"], { if ( version.protocol_version !== "1" || version.scriptc_package_version !== manifest.version || - version.llvm_version !== "22.1.8" + version.llvm_version !== "22.1.8" || + !Array.isArray(version.supported_targets) || + !version.supported_targets.includes("arm64-apple-macosx14.0.0") ) { throw new Error(`built helper reported an incompatible identity: ${JSON.stringify(version)}`); } diff --git a/packages/llvm-darwin-x64/LICENSE b/packages/llvm-darwin-x64/LICENSE new file mode 100644 index 00000000..c0d45999 --- /dev/null +++ b/packages/llvm-darwin-x64/LICENSE @@ -0,0 +1 @@ +Apache License 2.0. See https://www.apache.org/licenses/LICENSE-2.0 diff --git a/packages/llvm-darwin-x64/SCRIPTC_LICENSE b/packages/llvm-darwin-x64/SCRIPTC_LICENSE new file mode 100644 index 00000000..bb2b7da9 --- /dev/null +++ b/packages/llvm-darwin-x64/SCRIPTC_LICENSE @@ -0,0 +1 @@ +Copyright scriptc contributors. Licensed under Apache-2.0. diff --git a/packages/llvm-darwin-x64/THIRD_PARTY_NOTICES b/packages/llvm-darwin-x64/THIRD_PARTY_NOTICES new file mode 100644 index 00000000..dcf39787 --- /dev/null +++ b/packages/llvm-darwin-x64/THIRD_PARTY_NOTICES @@ -0,0 +1 @@ +This package contains LLVM components licensed under Apache-2.0 with LLVM Exceptions. diff --git a/packages/llvm-darwin-x64/package.json b/packages/llvm-darwin-x64/package.json new file mode 100644 index 00000000..821b49c4 --- /dev/null +++ b/packages/llvm-darwin-x64/package.json @@ -0,0 +1,13 @@ +{ + "name": "@scriptc/llvm-darwin-x64", + "version": "0.0.36", + "description": "Pinned LLVM code-generation helper for scriptc on macOS x64", + "license": "Apache-2.0 WITH LLVM-exception", + "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/llvm-darwin-x64" }, + "os": ["darwin"], + "cpu": ["x64"], + "files": ["bin", "LICENSE", "SCRIPTC_LICENSE", "THIRD_PARTY_NOTICES"], + "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "test -x bin/scriptc-llvm-codegen" }, + "publishConfig": { "access": "public" } +} diff --git a/packages/llvm-darwin-x64/scripts/build.mjs b/packages/llvm-darwin-x64/scripts/build.mjs new file mode 100644 index 00000000..0f4ddea7 --- /dev/null +++ b/packages/llvm-darwin-x64/scripts/build.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); +const repo = fileURLToPath(new URL("../../..", import.meta.url)); +if (process.platform !== "darwin" || process.arch !== "x64") { + process.stdout.write("@scriptc/llvm-darwin-x64: 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-darwin-x64"); +execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/local/opt/llvm@22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_HELPER_ARCH=x86_64", "-DSCRIPTC_DEFAULT_TARGET=x86_64-apple-macosx14.0.0", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", "-DSCRIPTC_TARGET_BACKENDS=X86;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=x86_64-apple-macosx14.0.0,wasm32-unknown-wasi", "-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", ["-x", output], { stdio: "inherit" }); +chmodSync(output, 0o755); diff --git a/packages/llvm-linux-arm64-gnu/LICENSE b/packages/llvm-linux-arm64-gnu/LICENSE new file mode 100644 index 00000000..c0d45999 --- /dev/null +++ b/packages/llvm-linux-arm64-gnu/LICENSE @@ -0,0 +1 @@ +Apache License 2.0. See https://www.apache.org/licenses/LICENSE-2.0 diff --git a/packages/llvm-linux-arm64-gnu/SCRIPTC_LICENSE b/packages/llvm-linux-arm64-gnu/SCRIPTC_LICENSE new file mode 100644 index 00000000..bb2b7da9 --- /dev/null +++ b/packages/llvm-linux-arm64-gnu/SCRIPTC_LICENSE @@ -0,0 +1 @@ +Copyright scriptc contributors. Licensed under Apache-2.0. diff --git a/packages/llvm-linux-arm64-gnu/THIRD_PARTY_NOTICES b/packages/llvm-linux-arm64-gnu/THIRD_PARTY_NOTICES new file mode 100644 index 00000000..dcf39787 --- /dev/null +++ b/packages/llvm-linux-arm64-gnu/THIRD_PARTY_NOTICES @@ -0,0 +1 @@ +This package contains LLVM components licensed under Apache-2.0 with LLVM Exceptions. diff --git a/packages/llvm-linux-arm64-gnu/package.json b/packages/llvm-linux-arm64-gnu/package.json new file mode 100644 index 00000000..62a1b287 --- /dev/null +++ b/packages/llvm-linux-arm64-gnu/package.json @@ -0,0 +1,4 @@ +{ + "name": "@scriptc/llvm-linux-arm64-gnu", "version": "0.0.36", "description": "Pinned LLVM code-generation helper for scriptc on Linux arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/llvm-linux-arm64-gnu" }, "os": ["linux"], "cpu": ["arm64"], "files": ["bin", "LICENSE", "SCRIPTC_LICENSE", "THIRD_PARTY_NOTICES"], "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "test -x bin/scriptc-llvm-codegen" }, "publishConfig": { "access": "public" } +} diff --git a/packages/llvm-linux-arm64-gnu/scripts/build.mjs b/packages/llvm-linux-arm64-gnu/scripts/build.mjs new file mode 100644 index 00000000..3a2e631b --- /dev/null +++ b/packages/llvm-linux-arm64-gnu/scripts/build.mjs @@ -0,0 +1,10 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +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", "-DSCRIPTC_TARGET_BACKENDS=AArch64;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=aarch64-unknown-linux-gnu,aarch64-unknown-linux-musl,wasm32-unknown-wasi", "-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); diff --git a/packages/llvm-linux-arm64-musl/LICENSE b/packages/llvm-linux-arm64-musl/LICENSE new file mode 100644 index 00000000..c0d45999 --- /dev/null +++ b/packages/llvm-linux-arm64-musl/LICENSE @@ -0,0 +1 @@ +Apache License 2.0. See https://www.apache.org/licenses/LICENSE-2.0 diff --git a/packages/llvm-linux-arm64-musl/SCRIPTC_LICENSE b/packages/llvm-linux-arm64-musl/SCRIPTC_LICENSE new file mode 100644 index 00000000..bb2b7da9 --- /dev/null +++ b/packages/llvm-linux-arm64-musl/SCRIPTC_LICENSE @@ -0,0 +1 @@ +Copyright scriptc contributors. Licensed under Apache-2.0. diff --git a/packages/llvm-linux-arm64-musl/THIRD_PARTY_NOTICES b/packages/llvm-linux-arm64-musl/THIRD_PARTY_NOTICES new file mode 100644 index 00000000..dcf39787 --- /dev/null +++ b/packages/llvm-linux-arm64-musl/THIRD_PARTY_NOTICES @@ -0,0 +1 @@ +This package contains LLVM components licensed under Apache-2.0 with LLVM Exceptions. diff --git a/packages/llvm-linux-arm64-musl/package.json b/packages/llvm-linux-arm64-musl/package.json new file mode 100644 index 00000000..7400be9d --- /dev/null +++ b/packages/llvm-linux-arm64-musl/package.json @@ -0,0 +1,12 @@ +{ + "name": "@scriptc/llvm-linux-arm64-musl", + "version": "0.0.36", + "description": "Statically linked LLVM code-generation helper for scriptc on Linux arm64 musl", + "license": "Apache-2.0 WITH LLVM-exception", + "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/llvm-linux-arm64-musl" }, + "os": ["linux"], "cpu": ["arm64"], + "files": ["bin", "LICENSE", "SCRIPTC_LICENSE", "THIRD_PARTY_NOTICES"], + "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "test -x bin/scriptc-llvm-codegen" }, + "publishConfig": { "access": "public" } +} diff --git a/packages/llvm-linux-arm64-musl/scripts/build.mjs b/packages/llvm-linux-arm64-musl/scripts/build.mjs new file mode 100644 index 00000000..ceae6266 --- /dev/null +++ b/packages/llvm-linux-arm64-musl/scripts/build.mjs @@ -0,0 +1,10 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +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-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=zig", "-DCMAKE_C_COMPILER_ARG1=cc", "-DCMAKE_CXX_COMPILER=zig", "-DCMAKE_CXX_COMPILER_ARG1=c++", "-DCMAKE_C_COMPILER_TARGET=aarch64-linux-musl", "-DCMAKE_CXX_COMPILER_TARGET=aarch64-linux-musl", "-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", "-DSCRIPTC_TARGET_BACKENDS=AArch64;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=aarch64-unknown-linux-musl,wasm32-unknown-wasi", "-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); diff --git a/packages/llvm-linux-x64-gnu/LICENSE b/packages/llvm-linux-x64-gnu/LICENSE new file mode 100644 index 00000000..c0d45999 --- /dev/null +++ b/packages/llvm-linux-x64-gnu/LICENSE @@ -0,0 +1 @@ +Apache License 2.0. See https://www.apache.org/licenses/LICENSE-2.0 diff --git a/packages/llvm-linux-x64-gnu/SCRIPTC_LICENSE b/packages/llvm-linux-x64-gnu/SCRIPTC_LICENSE new file mode 100644 index 00000000..bb2b7da9 --- /dev/null +++ b/packages/llvm-linux-x64-gnu/SCRIPTC_LICENSE @@ -0,0 +1 @@ +Copyright scriptc contributors. Licensed under Apache-2.0. diff --git a/packages/llvm-linux-x64-gnu/THIRD_PARTY_NOTICES b/packages/llvm-linux-x64-gnu/THIRD_PARTY_NOTICES new file mode 100644 index 00000000..dcf39787 --- /dev/null +++ b/packages/llvm-linux-x64-gnu/THIRD_PARTY_NOTICES @@ -0,0 +1 @@ +This package contains LLVM components licensed under Apache-2.0 with LLVM Exceptions. diff --git a/packages/llvm-linux-x64-gnu/package.json b/packages/llvm-linux-x64-gnu/package.json new file mode 100644 index 00000000..57031ad7 --- /dev/null +++ b/packages/llvm-linux-x64-gnu/package.json @@ -0,0 +1,12 @@ +{ + "name": "@scriptc/llvm-linux-x64-gnu", + "version": "0.0.36", + "description": "Pinned LLVM code-generation helper for scriptc on Linux x64", + "license": "Apache-2.0 WITH LLVM-exception", + "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/llvm-linux-x64-gnu" }, + "os": ["linux"], "cpu": ["x64"], + "files": ["bin", "LICENSE", "SCRIPTC_LICENSE", "THIRD_PARTY_NOTICES"], + "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "test -x bin/scriptc-llvm-codegen" }, + "publishConfig": { "access": "public" } +} diff --git a/packages/llvm-linux-x64-gnu/scripts/build.mjs b/packages/llvm-linux-x64-gnu/scripts/build.mjs new file mode 100644 index 00000000..2c38d864 --- /dev/null +++ b/packages/llvm-linux-x64-gnu/scripts/build.mjs @@ -0,0 +1,15 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +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 !== "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", "-DSCRIPTC_TARGET_BACKENDS=X86;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=x86_64-unknown-linux-gnu,x86_64-unknown-linux-musl,wasm32-unknown-wasi", "-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); diff --git a/packages/llvm-linux-x64-musl/LICENSE b/packages/llvm-linux-x64-musl/LICENSE new file mode 100644 index 00000000..c0d45999 --- /dev/null +++ b/packages/llvm-linux-x64-musl/LICENSE @@ -0,0 +1 @@ +Apache License 2.0. See https://www.apache.org/licenses/LICENSE-2.0 diff --git a/packages/llvm-linux-x64-musl/SCRIPTC_LICENSE b/packages/llvm-linux-x64-musl/SCRIPTC_LICENSE new file mode 100644 index 00000000..bb2b7da9 --- /dev/null +++ b/packages/llvm-linux-x64-musl/SCRIPTC_LICENSE @@ -0,0 +1 @@ +Copyright scriptc contributors. Licensed under Apache-2.0. diff --git a/packages/llvm-linux-x64-musl/THIRD_PARTY_NOTICES b/packages/llvm-linux-x64-musl/THIRD_PARTY_NOTICES new file mode 100644 index 00000000..dcf39787 --- /dev/null +++ b/packages/llvm-linux-x64-musl/THIRD_PARTY_NOTICES @@ -0,0 +1 @@ +This package contains LLVM components licensed under Apache-2.0 with LLVM Exceptions. diff --git a/packages/llvm-linux-x64-musl/package.json b/packages/llvm-linux-x64-musl/package.json new file mode 100644 index 00000000..0367d840 --- /dev/null +++ b/packages/llvm-linux-x64-musl/package.json @@ -0,0 +1,12 @@ +{ + "name": "@scriptc/llvm-linux-x64-musl", + "version": "0.0.36", + "description": "Statically linked LLVM code-generation helper for scriptc on Linux x64 musl", + "license": "Apache-2.0 WITH LLVM-exception", + "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/llvm-linux-x64-musl" }, + "os": ["linux"], "cpu": ["x64"], + "files": ["bin", "LICENSE", "SCRIPTC_LICENSE", "THIRD_PARTY_NOTICES"], + "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "test -x bin/scriptc-llvm-codegen" }, + "publishConfig": { "access": "public" } +} diff --git a/packages/llvm-linux-x64-musl/scripts/build.mjs b/packages/llvm-linux-x64-musl/scripts/build.mjs new file mode 100644 index 00000000..1c9e05b2 --- /dev/null +++ b/packages/llvm-linux-x64-musl/scripts/build.mjs @@ -0,0 +1,13 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +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 !== "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=zig", "-DCMAKE_C_COMPILER_ARG1=cc", "-DCMAKE_CXX_COMPILER=zig", "-DCMAKE_CXX_COMPILER_ARG1=c++", "-DCMAKE_C_COMPILER_TARGET=x86_64-linux-musl", "-DCMAKE_CXX_COMPILER_TARGET=x86_64-linux-musl", "-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", "-DSCRIPTC_TARGET_BACKENDS=X86;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=x86_64-unknown-linux-musl,wasm32-unknown-wasi", "-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); diff --git a/packages/llvm-win32-x64-msvc/LICENSE b/packages/llvm-win32-x64-msvc/LICENSE new file mode 100644 index 00000000..c0d45999 --- /dev/null +++ b/packages/llvm-win32-x64-msvc/LICENSE @@ -0,0 +1 @@ +Apache License 2.0. See https://www.apache.org/licenses/LICENSE-2.0 diff --git a/packages/llvm-win32-x64-msvc/SCRIPTC_LICENSE b/packages/llvm-win32-x64-msvc/SCRIPTC_LICENSE new file mode 100644 index 00000000..bb2b7da9 --- /dev/null +++ b/packages/llvm-win32-x64-msvc/SCRIPTC_LICENSE @@ -0,0 +1 @@ +Copyright scriptc contributors. Licensed under Apache-2.0. diff --git a/packages/llvm-win32-x64-msvc/THIRD_PARTY_NOTICES b/packages/llvm-win32-x64-msvc/THIRD_PARTY_NOTICES new file mode 100644 index 00000000..dcf39787 --- /dev/null +++ b/packages/llvm-win32-x64-msvc/THIRD_PARTY_NOTICES @@ -0,0 +1 @@ +This package contains LLVM components licensed under Apache-2.0 with LLVM Exceptions. diff --git a/packages/llvm-win32-x64-msvc/package.json b/packages/llvm-win32-x64-msvc/package.json new file mode 100644 index 00000000..904293bd --- /dev/null +++ b/packages/llvm-win32-x64-msvc/package.json @@ -0,0 +1,4 @@ +{ + "name": "@scriptc/llvm-win32-x64-msvc", "version": "0.0.36", "description": "Pinned LLVM code-generation helper for scriptc on Windows x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/llvm-win32-x64-msvc" }, "os": ["win32"], "cpu": ["x64"], "files": ["bin", "LICENSE", "SCRIPTC_LICENSE", "THIRD_PARTY_NOTICES"], "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "test -f bin/scriptc-llvm-codegen.exe" }, "publishConfig": { "access": "public" } +} diff --git a/packages/llvm-win32-x64-msvc/scripts/build.mjs b/packages/llvm-win32-x64-msvc/scripts/build.mjs new file mode 100644 index 00000000..4eb34544 --- /dev/null +++ b/packages/llvm-win32-x64-msvc/scripts/build.mjs @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { copyFileSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +const root = dirname(dirname(fileURLToPath(import.meta.url))); const repo = fileURLToPath(new URL("../../..", import.meta.url)); +if (process.platform !== "win32" || process.arch !== "x64") { process.stdout.write("@scriptc/llvm-win32-x64-msvc: 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-win32-x64-msvc"); +// LLVM's official Windows development archive contains MSVC-built static +// libraries. Use Visual Studio's generator, rather than inheriting a caller's +// clang/ninja defaults, so it selects the matching CRT, SDK, manifest tools, +// and current MSVC standard-library implementation. +execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Visual Studio 17 2022", "-A", "x64", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "C:/Program Files/LLVM/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-pc-windows-msvc", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", "-DSCRIPTC_TARGET_BACKENDS=X86;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=x86_64-pc-windows-msvc,wasm32-unknown-wasi"], { stdio: "inherit" }); +execFileSync("cmake", ["--build", build, "--config", "Release", "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); mkdirSync(join(root, "bin"), { recursive: true }); copyFileSync(join(build, "Release", "scriptc-llvm-codegen.exe"), join(root, "bin", "scriptc-llvm-codegen.exe")); diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 1dfce977..84bb758c 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -99,10 +99,7 @@ export const RUNTIME_PACK_MATRIX = { object_format: "macho", minimum_os: "14.0", }, - flavors: { - release: { optimization: "-O2" }, - dev: { optimization: "-O0" }, - }, + flavors: { release: { optimization: "-O2" }, dev: { optimization: "-O0" } }, executable_section_elimination: EXECUTABLE_SECTION_ELIMINATION, runtime_units: [ ...BASE_RUNTIME_SOURCES.map((source) => ({ source, predicate: true })), @@ -114,8 +111,5 @@ export const RUNTIME_PACK_MATRIX = { { id: "zlib", predicate: "zlibEffective" }, { id: "mbedtls", predicate: "tlsEffective" }, ], - system_libraries: [ - { name: "System", predicate: true }, - { name: "m", predicate: "dynamic" }, - ], + system_libraries: [{ name: "System", predicate: true }, { name: "m", predicate: "dynamic" }], }; diff --git a/packages/runtime-darwin-x64/package.json b/packages/runtime-darwin-x64/package.json new file mode 100644 index 00000000..b4492090 --- /dev/null +++ b/packages/runtime-darwin-x64/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scriptc/runtime-darwin-x64", "version": "0.0.36", "description": "Precompiled scriptc runtime pack for macOS x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-darwin-x64" }, "os": ["darwin"], "cpu": ["x64"], + "files": ["artifacts", "runtime-pack.json"], "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "node scripts/verify.mjs" }, "publishConfig": { "access": "public" } +} diff --git a/packages/runtime-darwin-x64/scripts/build.mjs b/packages/runtime-darwin-x64/scripts/build.mjs new file mode 100644 index 00000000..029350d9 --- /dev/null +++ b/packages/runtime-darwin-x64/scripts/build.mjs @@ -0,0 +1,5 @@ +import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +if (process.platform !== "darwin" || process.arch !== "x64") { process.stdout.write("@scriptc/runtime-darwin-x64: 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: "darwin", runtimeDefines: [], threadArgs: ["-pthread"], target: { name: "macos-x64", llvm_triple: "x86_64-apple-macosx14.0.0", architecture: "x64", object_format: "macho", minimum_os: "14.0" }, targetArgs: ["-target", "x86_64-apple-macosx14.0.0"], compileFlags: [], systemLibraries: [{ name: "System", predicate: true }, { name: "m", predicate: "dynamic" }] }); +await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-darwin-x64/scripts/verify.mjs b/packages/runtime-darwin-x64/scripts/verify.mjs new file mode 100644 index 00000000..e5bea86c --- /dev/null +++ b/packages/runtime-darwin-x64/scripts/verify.mjs @@ -0,0 +1,2 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); await import("../../runtime-pack-common/scripts/verify.mjs"); diff --git a/packages/runtime-linux-arm64-gnu/package.json b/packages/runtime-linux-arm64-gnu/package.json new file mode 100644 index 00000000..d35b5038 --- /dev/null +++ b/packages/runtime-linux-arm64-gnu/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scriptc/runtime-linux-arm64-gnu", "version": "0.0.36", "description": "Precompiled scriptc runtime pack for Linux arm64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-linux-arm64-gnu" }, "os": ["linux"], "cpu": ["arm64"], + "files": ["artifacts", "runtime-pack.json"], "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "node scripts/verify.mjs" }, "publishConfig": { "access": "public" } +} diff --git a/packages/runtime-linux-arm64-gnu/scripts/build.mjs b/packages/runtime-linux-arm64-gnu/scripts/build.mjs new file mode 100644 index 00000000..32278d0a --- /dev/null +++ b/packages/runtime-linux-arm64-gnu/scripts/build.mjs @@ -0,0 +1,5 @@ +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); } +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 }] }); +await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-arm64-gnu/scripts/verify.mjs b/packages/runtime-linux-arm64-gnu/scripts/verify.mjs new file mode 100644 index 00000000..e5bea86c --- /dev/null +++ b/packages/runtime-linux-arm64-gnu/scripts/verify.mjs @@ -0,0 +1,2 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); await import("../../runtime-pack-common/scripts/verify.mjs"); diff --git a/packages/runtime-linux-arm64-musl/package.json b/packages/runtime-linux-arm64-musl/package.json new file mode 100644 index 00000000..d1f8d181 --- /dev/null +++ b/packages/runtime-linux-arm64-musl/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scriptc/runtime-linux-arm64-musl", "version": "0.0.36", "description": "Precompiled scriptc runtime pack for Linux arm64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-linux-arm64-musl" }, "os": ["linux"], "cpu": ["arm64"], + "files": ["artifacts", "runtime-pack.json"], "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "node scripts/verify.mjs" }, "publishConfig": { "access": "public" } +} diff --git a/packages/runtime-linux-arm64-musl/scripts/build.mjs b/packages/runtime-linux-arm64-musl/scripts/build.mjs new file mode 100644 index 00000000..10fa626d --- /dev/null +++ b/packages/runtime-linux-arm64-musl/scripts/build.mjs @@ -0,0 +1,5 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +if (process.platform !== "linux" || process.arch !== "arm64") { process.stdout.write("@scriptc/runtime-linux-arm64-musl: 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", "SCR_MUSL"], threadArgs: ["-pthread"], extraRuntimeSources: ["scr_musl.c"], compiler: "zig", compilerArgs: ["cc"], archiver: "zig", archiverArgs: ["ar"], target: { name: "linux-arm64-musl", llvm_triple: "aarch64-unknown-linux-musl", architecture: "arm64", object_format: "elf", minimum_os: "musl 1.2" }, targetArgs: ["-target", "aarch64-linux-musl"], compileFlags: ["-ffunction-sections", "-fdata-sections"], systemLibraries: [{ name: "m", predicate: true }] }); +await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-arm64-musl/scripts/verify.mjs b/packages/runtime-linux-arm64-musl/scripts/verify.mjs new file mode 100644 index 00000000..e5bea86c --- /dev/null +++ b/packages/runtime-linux-arm64-musl/scripts/verify.mjs @@ -0,0 +1,2 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); await import("../../runtime-pack-common/scripts/verify.mjs"); diff --git a/packages/runtime-linux-x64-gnu/package.json b/packages/runtime-linux-x64-gnu/package.json new file mode 100644 index 00000000..ddaee8e1 --- /dev/null +++ b/packages/runtime-linux-x64-gnu/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scriptc/runtime-linux-x64-gnu", "version": "0.0.36", "description": "Precompiled scriptc runtime pack for Linux x64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-linux-x64-gnu" }, "os": ["linux"], "cpu": ["x64"], + "files": ["artifacts", "runtime-pack.json"], "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "node scripts/verify.mjs" }, "publishConfig": { "access": "public" } +} diff --git a/packages/runtime-linux-x64-gnu/scripts/build.mjs b/packages/runtime-linux-x64-gnu/scripts/build.mjs new file mode 100644 index 00000000..ea547733 --- /dev/null +++ b/packages/runtime-linux-x64-gnu/scripts/build.mjs @@ -0,0 +1,5 @@ +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); } +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 }] }); +await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-x64-gnu/scripts/verify.mjs b/packages/runtime-linux-x64-gnu/scripts/verify.mjs new file mode 100644 index 00000000..e5bea86c --- /dev/null +++ b/packages/runtime-linux-x64-gnu/scripts/verify.mjs @@ -0,0 +1,2 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); await import("../../runtime-pack-common/scripts/verify.mjs"); diff --git a/packages/runtime-linux-x64-musl/package.json b/packages/runtime-linux-x64-musl/package.json new file mode 100644 index 00000000..0a6073be --- /dev/null +++ b/packages/runtime-linux-x64-musl/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scriptc/runtime-linux-x64-musl", "version": "0.0.36", "description": "Precompiled scriptc runtime pack for Linux x64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-linux-x64-musl" }, "os": ["linux"], "cpu": ["x64"], + "files": ["artifacts", "runtime-pack.json"], "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "node scripts/verify.mjs" }, "publishConfig": { "access": "public" } +} diff --git a/packages/runtime-linux-x64-musl/scripts/build.mjs b/packages/runtime-linux-x64-musl/scripts/build.mjs new file mode 100644 index 00000000..7897aa16 --- /dev/null +++ b/packages/runtime-linux-x64-musl/scripts/build.mjs @@ -0,0 +1,5 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +if (process.platform !== "linux" || process.arch !== "x64") { process.stdout.write("@scriptc/runtime-linux-x64-musl: 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", "SCR_MUSL"], threadArgs: ["-pthread"], extraRuntimeSources: ["scr_musl.c"], compiler: "zig", compilerArgs: ["cc"], archiver: "zig", archiverArgs: ["ar"], target: { name: "linux-x64-musl", llvm_triple: "x86_64-unknown-linux-musl", architecture: "x64", object_format: "elf", minimum_os: "musl 1.2" }, targetArgs: ["-target", "x86_64-linux-musl"], compileFlags: ["-ffunction-sections", "-fdata-sections"], systemLibraries: [{ name: "m", predicate: true }] }); +await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-x64-musl/scripts/verify.mjs b/packages/runtime-linux-x64-musl/scripts/verify.mjs new file mode 100644 index 00000000..e5bea86c --- /dev/null +++ b/packages/runtime-linux-x64-musl/scripts/verify.mjs @@ -0,0 +1,2 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); await import("../../runtime-pack-common/scripts/verify.mjs"); diff --git a/packages/runtime-pack-common/package.json b/packages/runtime-pack-common/package.json new file mode 100644 index 00000000..7500f2b8 --- /dev/null +++ b/packages/runtime-pack-common/package.json @@ -0,0 +1,6 @@ +{ + "name": "@scriptc/runtime-pack-common", + "version": "0.0.36", + "private": true, + "type": "module" +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs new file mode 100644 index 00000000..a4ea2400 --- /dev/null +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -0,0 +1,93 @@ +/** + * Shared release-runtime pack matrix. Platform package scripts provide the + * target descriptor, then this module fixes feature reachability and the + * compile/link flags required by every generated runtime object. + */ +const any = (...features) => ({ any: features }); +const all = (...features) => ({ all: features }); + +const BASE_RUNTIME_SOURCES = [ + "scr_number.c", "scr_string.c", "scr_array.c", "scr_bytes.c", + "scr_bytes_io.c", "scr_map.c", "scr_closure.c", "scr_ffi.c", + "scr_object.c", "scr_union.c", "scr_exception.c", "scr_error.c", + "scr_console.c", "scr_lib.c", "scr_path.c", "scr_url.c", "scr_json.c", + "scr_async.c", "scr_child.c", "scr_cycle.c", +]; + +const OPTIONAL = [ + ["scr_copying.c", "copying"], ["scr_file_handle.c", "fileHandle"], + ["scr_regex.c", "regex"], ["scr_assert.c", any("assert", "regex", "symbol")], + ["scr_inspect.c", "inspect"], ["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch")], + ["scr_dc.c", "dc"], ["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "nativeFetch")], + ["scr_zlib.c", "zlib"], ["scr_zlib_island.c", all("zlib", "dynamic")], + ["scr_events.c", "events"], ["scr_readline.c", "events"], + ["scr_events_emitter.c", "emitter"], ["scr_dyn_handle.c", any("emitter", "netEffective")], + ["scr_symbol.c", "symbol"], ["scr_url_params.c", "searchParams"], + ["scr_qs.c", "qs"], ["scr_util.c", "parseArgs"], ["scr_stream.c", "stream"], + ["scr_loop_kqueue.c", any("netEffective", "dgram")], + ["scr_loop_epoll.c", any("netEffective", "dgram")], + ["scr_loop_wsapoll.c", any("netEffective", "dgram")], + ["scr_net.c", "netEffective"], ["scr_http.c", "httpEffective"], + ["scr_http2.c", "http2"], ["scr_dgram.c", "dgram"], ["scr_watch.c", "watch"], + ["scr_ffi_queue.c", "foreignFfi"], ["scr_test.c", "nodeTest"], + ["scr_tls_ca.c", "tlsCaEffective"], ["scr_tls.c", "tlsEffective"], + ["scr_fetch.c", "nativeFetch"], ["scr_island.c", "dynamic"], + ["scr_web.c", "dynamic"], ["scr_inspect_island.c", all("dynamic", "inspect")], + ["scr_net_island.c", "netIslandEffective"], +]; + +function variantsFor(source) { + const dynamicOnly = new Set([ + "scr_zlib_island.c", "scr_island.c", "scr_web.c", + "scr_inspect_island.c", "scr_net_island.c", + ]); + if (dynamicOnly.has(source)) { + return [{ id: "dynamic", when: { dynamic: true }, defines: ["SCR_DYNAMIC"] }]; + } + const variants = [{ id: "default", when: {}, defines: [] }]; + if (source === "scr_bytes.c") { + variants.push({ id: "text-decoder-legacy", when: { textDecoderLegacy: true }, defines: ["SCR_TEXT_DECODER_LEGACY"] }); + } + variants.push({ id: "dynamic", when: { dynamic: true }, defines: ["SCR_DYNAMIC"] }); + if (source === "scr_bytes.c") { + variants.push({ + id: "dynamic-text-decoder-legacy", when: { dynamic: true, textDecoderLegacy: true }, + defines: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"], + }); + } + return variants; +} + +export function createRuntimePackMatrix({ + target, + compileFlags, + systemLibraries, + extraRuntimeSources = [], + omitRuntimeSources = [], + omitOptionalSources = [], + omitArchives = [], +}) { + const vendorArchives = [ + { id: "quickjs", predicate: "dynamic" }, + { id: "libregexp", predicate: { all: ["regex"], not: ["dynamic"] } }, + { id: "zlib", predicate: "zlibEffective" }, + { id: "mbedtls", predicate: "tlsEffective" }, + ]; + return { + schema: "scriptc.runtime-pack-matrix.v1", + target, + flavors: { release: { optimization: "-O2" }, dev: { optimization: "-O0" } }, + executable_section_elimination: { compile_flags: compileFlags }, + runtime_units: [ + ...BASE_RUNTIME_SOURCES + .filter((source) => !omitRuntimeSources.includes(source)) + .map((source) => ({ source, predicate: true })), + ...extraRuntimeSources.map((source) => ({ source, predicate: true })), + ...OPTIONAL + .filter(([source]) => !omitOptionalSources.includes(source)) + .map(([source, predicate]) => ({ source, predicate })), + ].map((unit) => ({ ...unit, variants: variantsFor(unit.source) })), + archives: vendorArchives.filter(({ id }) => !omitArchives.includes(id)), + system_libraries: systemLibraries, + }; +} diff --git a/packages/runtime-pack-common/scripts/archive.mjs b/packages/runtime-pack-common/scripts/archive.mjs new file mode 100644 index 00000000..b9e3e9bc --- /dev/null +++ b/packages/runtime-pack-common/scripts/archive.mjs @@ -0,0 +1,35 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const run = promisify(execFile); +const globalHeader = "!\n"; +const memberHeaderSize = 60; + +function replaceField(bytes, offset, width, value) { + bytes.write(value.padEnd(width, " "), offset, width, "ascii"); +} + +async function normalizeMetadata(output) { + const bytes = await readFile(output); + if (bytes.subarray(0, globalHeader.length).toString("ascii") !== globalHeader) throw new Error(`archiver produced an invalid archive: ${output}`); + let offset = globalHeader.length; + while (offset < bytes.length) { + const end = offset + memberHeaderSize; + if (end > bytes.length || bytes.subarray(offset + 58, end).toString("ascii") !== "`\n") throw new Error(`archiver produced a malformed member header: ${output}`); + const size = Number(bytes.subarray(offset + 48, offset + 58).toString("ascii").trim()); + if (!Number.isSafeInteger(size) || size < 0 || end + size > bytes.length) throw new Error(`archiver produced an invalid member size: ${output}`); + replaceField(bytes, offset + 16, 12, "0"); + replaceField(bytes, offset + 28, 6, "0"); + replaceField(bytes, offset + 34, 6, "0"); + replaceField(bytes, offset + 40, 8, "100644"); + offset = end + size + (size % 2); + } + if (offset !== bytes.length) throw new Error(`archiver produced a truncated archive: ${output}`); + await writeFile(output, bytes); +} + +export async function createDeterministicArchive(archiver, output, objects, args = []) { + await run(archiver, [...args, "rcs", output, ...objects], { env: { ...process.env, ZERO_AR_DATE: "1" } }); + await normalizeMetadata(output); +} diff --git a/packages/runtime-pack-common/scripts/build-state.mjs b/packages/runtime-pack-common/scripts/build-state.mjs new file mode 100644 index 00000000..740ed815 --- /dev/null +++ b/packages/runtime-pack-common/scripts/build-state.mjs @@ -0,0 +1 @@ +export { installRuntimePack, withBuildLock } from "../../runtime-darwin-arm64/scripts/build-state.mjs"; diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs new file mode 100644 index 00000000..bef41b2d --- /dev/null +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -0,0 +1,125 @@ +#!/usr/bin/env node +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { availableParallelism, tmpdir } from "node:os"; +import { copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; +import { basename, dirname, join, relative, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { createRuntimePackMatrix } from "../runtime-pack-matrix.mjs"; +import { createDeterministicArchive } from "./archive.mjs"; +import { installRuntimePack, withBuildLock } from "./build-state.mjs"; + +const run = promisify(execFile); +const packageRoot = process.env.SCRIPTC_RUNTIME_PACK_ROOT; +const configText = process.env.SCRIPTC_RUNTIME_PACK_CONFIG; +if (!packageRoot || !configText) throw new Error("runtime-pack build requires package wrapper configuration"); +const config = JSON.parse(configText); +const repoRoot = fileURLToPath(new URL("../../..", import.meta.url)); +const runtimeRoot = join(repoRoot, "packages/runtime"); +const runtimeSrc = join(runtimeRoot, "src"); +const vendorRoot = join(runtimeRoot, "vendor"); +const outputRoot = join(packageRoot, "artifacts"); +const manifestPath = join(packageRoot, "runtime-pack.json"); +const matrix = createRuntimePackMatrix(config); + +async function build() { + const buildRoot = join(packageRoot, `.runtime-pack-build-${process.pid}-${Math.random().toString(36).slice(2)}`); + const stagedOutputRoot = join(buildRoot, "artifacts"); + const stagedManifestPath = join(buildRoot, "runtime-pack.json"); + const artifactPath = (path) => ["artifacts", ...relative(stagedOutputRoot, path).split(sep)].join("/"); + const sourcePathFlags = [`-ffile-prefix-map=${buildRoot}=${packageRoot}`, `-ffile-prefix-map=${repoRoot}=.`]; + const packageManifest = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")); + const compiler = process.env.CC ?? config.compiler ?? "clang"; + const compilerArgs = config.compilerArgs ?? []; + const archiver = process.env.AR ?? config.archiver ?? "ar"; + const archiverArgs = config.archiverArgs ?? []; + const commonFlags = [ + ...config.targetArgs, "-std=c11", ...(config.threadArgs ?? []), "-fno-math-errno", "-fno-strict-aliasing", + ...matrix.executable_section_elimination.compile_flags, "-Wno-deprecated-declarations", "-I", runtimeSrc, + ...(config.runtimeDefines ?? []).map((define) => `-D${define}`), + ]; + const quickjs = join(vendorRoot, "quickjs-ng"); + const zlib = join(vendorRoot, "zlib"); + const mbedtls = join(vendorRoot, "mbedtls"); + const qjsSources = ["dtoa.c", "libregexp.c", "libunicode.c", "quickjs.c"]; + const lreSources = ["libregexp.c", "libunicode.c"]; + const zlibSources = ["adler32.c", "compress.c", "crc32.c", "deflate.c", "infback.c", "inffast.c", "inflate.c", "inftrees.c", "trees.c", "uncompr.c", "zutil.c"]; + const sha256 = async (path) => createHash("sha256").update(await readFile(path)).digest("hex"); + const compile = async (source, output, flags) => { + await mkdir(dirname(output), { recursive: true }); + await run(compiler, [...compilerArgs, ...sourcePathFlags, ...flags, "-c", source, "-o", output]); + }; + const parallel = async (items, task) => { + const width = Math.max(1, Math.min(8, availableParallelism())); + for (let i = 0; i < items.length; i += width) await Promise.all(items.slice(i, i + width).map(task)); + }; + const archive = async (id, sources, sourceRoot, flags) => { + const root = join(stagedOutputRoot, "vendor", id); + const objectRoot = join(root, "objects"); + await parallel(sources, async (source) => compile(join(sourceRoot, source), join(objectRoot, source.replace(/\.c$/, ".o")), flags)); + const output = join(root, `libscriptc-${id}.a`); + await createDeterministicArchive(archiver, output, sources.map((source) => join(objectRoot, source.replace(/\.c$/, ".o"))), archiverArgs); + await rm(objectRoot, { recursive: true, force: true }); + return { id, path: artifactPath(output), sha256: await sha256(output), size: (await stat(output)).size }; + }; + await rm(buildRoot, { recursive: true, force: true }); + try { + await mkdir(stagedOutputRoot, { recursive: true }); + // Zig 0.16 creates a zero-byte `a.o` in its working directory for + // `zig cc --version`. Probe from a private temporary directory so a + // runtime-pack build never leaves that compiler byproduct in the package. + const versionProbeRoot = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-version-")); + let compilerVersion; + try { + compilerVersion = (await run(compiler, [...compilerArgs, "--version"], { + cwd: versionProbeRoot, + })).stdout.split("\n", 1)[0].trim(); + } finally { + await rm(versionProbeRoot, { recursive: true, force: true }); + } + const flavors = {}; + for (const [flavor, flavorSpec] of Object.entries(matrix.flavors)) { + const units = []; + for (const unit of matrix.runtime_units) { + const variants = []; + for (const variant of unit.variants) { + const output = join(stagedOutputRoot, flavor, "runtime", variant.id, unit.source.replace(/\.c$/, ".o")); + const includeFlags = [ + ...(unit.source === "scr_regex.c" || variant.defines.includes("SCR_DYNAMIC") ? ["-I", quickjs] : []), + ...(unit.source === "scr_tls.c" ? ["-I", join(mbedtls, "include")] : []), + ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" ? ["-I", zlib] : []), + ]; + await compile(join(runtimeSrc, unit.source), output, [...commonFlags, flavorSpec.optimization, ...variant.defines.map((define) => `-D${define}`), ...includeFlags]); + variants.push({ id: variant.id, when: variant.when, defines: variant.defines, path: artifactPath(output), sha256: await sha256(output), size: (await stat(output)).size }); + } + units.push({ source: unit.source, predicate: unit.predicate, variants }); + } + flavors[flavor] = { optimization: flavorSpec.optimization, runtime_units: units }; + } + const mbedtlsSources = (await readdir(join(mbedtls, "library"))).filter((name) => !name.startsWith(".") && name.endsWith(".c")).sort(); + // Vendored QuickJS reads `clock_gettime` for its monotonic clock. WASI + // exposes that declaration only when its process-clock emulation ABI is + // selected, so vendor sources inherit the target's runtime defines too. + const vendorTarget = [ + ...config.targetArgs, + ...(config.runtimeDefines ?? []).map((define) => `-D${define}`), + ]; + const requestedArchives = new Set(matrix.archives.map((entry) => entry.id)); + const archives = config.vendorArchives === false ? [] : [ + ...(requestedArchives.has("quickjs") ? [await archive("quickjs", qjsSources, quickjs, [...vendorTarget, "-std=gnu11", "-fvisibility=hidden", "-funsigned-char", "-DQUICKJS_NG_BUILD", "-D_GNU_SOURCE", "-DNDEBUG", "-Os", "-I", quickjs])] : []), + ...(requestedArchives.has("libregexp") ? [await archive("libregexp", lreSources, quickjs, [...vendorTarget, "-std=c11", "-Os", "-I", quickjs])] : []), + ...(requestedArchives.has("zlib") ? [await archive("zlib", zlibSources, zlib, [...vendorTarget, "-std=c11", "-Os", "-I", zlib])] : []), + ...(requestedArchives.has("mbedtls") ? [await archive("mbedtls", mbedtlsSources, join(mbedtls, "library"), [...vendorTarget, "-std=c11", "-Os", "-I", join(mbedtls, "include"), "-I", join(mbedtls, "library")])] : []), + ]; + const archiveSpecs = new Map(matrix.archives.map((entry) => [entry.id, entry])); + const licensed = [[join(runtimeRoot, "LICENSE"), "artifacts/licenses/scriptc-runtime.txt", "Apache-2.0"], [join(quickjs, "LICENSE"), "artifacts/licenses/quickjs-ng.txt", "MIT"], [join(vendorRoot, "ryu", "LICENSE-Boost"), "artifacts/licenses/ryu.txt", "BSL-1.0"], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"]]; + await Promise.all(licensed.map(async ([source, destination]) => { const output = join(buildRoot, destination); await mkdir(dirname(output), { recursive: true }); await copyFile(source, output); })); + const manifest = { schema: "scriptc.runtime-pack.v1", format: 1, package: packageManifest.name, version: packageManifest.version, target: matrix.target, runtime_abi: { version: 1, marker: "scr_runtime_abi_v1" }, compiler: { command: compiler, identity: compilerVersion, target: matrix.target.llvm_triple }, macros: { executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"], excluded: ["SCR_LIB", "SCR_THREAD_INSTANCES", "SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required" }, flavors, archives: archives.map((entry) => ({ ...entry, predicate: archiveSpecs.get(entry.id).predicate })), system_libraries: matrix.system_libraries, licenses: licensed.map(([, path, license]) => ({ path, license })) }; + await writeFile(stagedManifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + const suffix = `${process.pid}-${Math.random().toString(36).slice(2)}`; + await installRuntimePack({ outputRoot, manifestPath, stagedOutputRoot, stagedManifestPath, backupRoot: join(packageRoot, `.runtime-pack-artifacts-backup-${suffix}`), backupManifestPath: join(packageRoot, `.runtime-pack-manifest-backup-${suffix}`) }); + process.stdout.write(`built ${packageManifest.name}@${packageManifest.version}: ${Object.keys(flavors).length} flavors, ${archives.length} vendor archives\n`); + } finally { await rm(buildRoot, { recursive: true, force: true }); } +} +await withBuildLock(join(packageRoot, ".runtime-pack-build.lock"), build); diff --git a/packages/runtime-pack-common/scripts/verify.mjs b/packages/runtime-pack-common/scripts/verify.mjs new file mode 100644 index 00000000..614a88ce --- /dev/null +++ b/packages/runtime-pack-common/scripts/verify.mjs @@ -0,0 +1,17 @@ +#!/usr/bin/env node +import { createHash } from "node:crypto"; +import { access, readFile } from "node:fs/promises"; +import { join } from "node:path"; + +const root = process.env.SCRIPTC_RUNTIME_PACK_ROOT; +if (!root) throw new Error("runtime-pack verification requires package wrapper configuration"); +const manifest = JSON.parse(await readFile(join(root, "runtime-pack.json"), "utf8")); +const packageManifest = JSON.parse(await readFile(join(root, "package.json"), "utf8")); +if (manifest.schema !== "scriptc.runtime-pack.v1" || manifest.format !== 1 || manifest.package !== packageManifest.name || manifest.version !== packageManifest.version) throw new Error("runtime pack identity does not match package.json"); +const artifacts = [...Object.values(manifest.flavors).flatMap((flavor) => flavor.runtime_units.flatMap((unit) => unit.variants)), ...manifest.archives]; +for (const artifact of artifacts) { + const bytes = await readFile(join(root, artifact.path)); + if (bytes.length !== artifact.size || createHash("sha256").update(bytes).digest("hex") !== artifact.sha256) throw new Error(`runtime pack hash mismatch: ${artifact.path}`); +} +for (const license of manifest.licenses) await access(join(root, license.path)); +process.stdout.write(`verified ${manifest.package}@${manifest.version}: ${artifacts.length} artifacts\n`); diff --git a/packages/runtime-wasm32-wasi/package.json b/packages/runtime-wasm32-wasi/package.json new file mode 100644 index 00000000..895b65e0 --- /dev/null +++ b/packages/runtime-wasm32-wasi/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scriptc/runtime-wasm32-wasi", "version": "0.0.36", "description": "Precompiled scriptc runtime pack for WASI Preview 1", "license": "Apache-2.0", "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-wasm32-wasi" }, + "files": ["artifacts", "runtime-pack.json"], "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "node scripts/verify.mjs" }, "publishConfig": { "access": "public" } +} diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs new file mode 100644 index 00000000..228a70ce --- /dev/null +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -0,0 +1,4 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); +process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ platform: "wasi", runtimeDefines: ["_GNU_SOURCE", "_WASI_EMULATED_SIGNAL", "_WASI_EMULATED_PROCESS_CLOCKS"], omitRuntimeSources: ["scr_child.c"], omitOptionalSources: ["scr_dyn_handle.c", "scr_loop_kqueue.c", "scr_loop_epoll.c", "scr_loop_wsapoll.c", "scr_net.c", "scr_http.c", "scr_http2.c", "scr_dgram.c", "scr_watch.c", "scr_tls_ca.c", "scr_tls.c", "scr_fetch.c", "scr_net_island.c"], omitArchives: ["mbedtls"], compiler: "zig", compilerArgs: ["cc"], archiver: "zig", archiverArgs: ["ar"], target: { name: "wasm32-wasi", llvm_triple: "wasm32-unknown-wasi", architecture: "wasm32", object_format: "wasm", minimum_os: "WASI Preview 1" }, targetArgs: ["-target", "wasm32-wasi"], compileFlags: [], systemLibraries: [{ name: "wasi-emulated-signal", predicate: true }, { name: "wasi-emulated-process-clocks", predicate: true }] }); +await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-wasm32-wasi/scripts/verify.mjs b/packages/runtime-wasm32-wasi/scripts/verify.mjs new file mode 100644 index 00000000..e5bea86c --- /dev/null +++ b/packages/runtime-wasm32-wasi/scripts/verify.mjs @@ -0,0 +1,2 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); await import("../../runtime-pack-common/scripts/verify.mjs"); diff --git a/packages/runtime-win32-x64-msvc/package.json b/packages/runtime-win32-x64-msvc/package.json new file mode 100644 index 00000000..c2a52b4e --- /dev/null +++ b/packages/runtime-win32-x64-msvc/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scriptc/runtime-win32-x64-msvc", "version": "0.0.36", "description": "Precompiled scriptc runtime pack for Windows x64 MSVC", "license": "Apache-2.0", "homepage": "https://scriptc.dev", + "repository": { "type": "git", "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-win32-x64-msvc" }, "os": ["win32"], "cpu": ["x64"], + "files": ["artifacts", "runtime-pack.json"], "scripts": { "build": "true", "build:native": "node scripts/build.mjs", "prepack": "node scripts/verify.mjs" }, "publishConfig": { "access": "public" } +} diff --git a/packages/runtime-win32-x64-msvc/scripts/build.mjs b/packages/runtime-win32-x64-msvc/scripts/build.mjs new file mode 100644 index 00000000..c41d6fb6 --- /dev/null +++ b/packages/runtime-win32-x64-msvc/scripts/build.mjs @@ -0,0 +1,9 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +if (process.platform !== "win32" || process.arch !== "x64") { process.stdout.write("@scriptc/runtime-win32-x64-msvc: skipped on this host\n"); process.exit(0); } +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); +// The helper emits x64 COFF objects, whose calling convention is compatible +// with Zig's MinGW runtime. The runtime itself intentionally uses the +// POSIX-shaped Windows surface (unistd, dirent, clock_gettime), so compile it +// against Zig's Windows GNU sysroot rather than MSVC's headers. +process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ platform: "win32", runtimeDefines: [], extraRuntimeSources: ["scr_win.c"], compiler: "zig", compilerArgs: ["cc"], archiver: "zig", archiverArgs: ["ar"], target: { name: "windows-x64-msvc", llvm_triple: "x86_64-pc-windows-msvc", architecture: "x64", object_format: "coff", minimum_os: "Windows 10" }, targetArgs: ["-target", "x86_64-windows-gnu"], compileFlags: ["-ffunction-sections", "-fdata-sections"], systemLibraries: [{ name: "advapi32", predicate: true }, { name: "iphlpapi", predicate: true }, { name: "ws2_32", predicate: true }, { name: "bcrypt", predicate: "tlsEffective" }, { name: "crypt32", predicate: "tlsCaEffective" }] }); +await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-win32-x64-msvc/scripts/verify.mjs b/packages/runtime-win32-x64-msvc/scripts/verify.mjs new file mode 100644 index 00000000..e5bea86c --- /dev/null +++ b/packages/runtime-win32-x64-msvc/scripts/verify.mjs @@ -0,0 +1,2 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); await import("../../runtime-pack-common/scripts/verify.mjs"); diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 7334bc1e..8195bd8f 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -19,6 +19,12 @@ * Win32's missing POSIX/BSD functions live in scr_win.c. Zig's musl sysroot * additionally lacks arc4random_buf; scr_musl.c supplies it from Linux's * getrandom syscall. Both files are selected by native-toolchain.ts only for their target. */ +#if defined(_WIN32) && defined(_MSC_VER) +/* MSVC's C headers do not define POSIX ssize_t. Keep the runtime's byte-count + * interfaces pointer-sized on Windows without requiring a Windows SDK header + * (and leave the Zig/MinGW compatibility route's native typedef intact). */ +typedef intptr_t ssize_t; +#endif #ifdef _WIN32 #include /* time_t / struct tm for the gmtime_r shim */ char *stpcpy(char *dst, const char *src); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b071865..405e9289 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,16 +54,83 @@ importers: '@scriptc/llvm-darwin-arm64': specifier: workspace:* version: link:../llvm-darwin-arm64 + '@scriptc/llvm-darwin-x64': + specifier: workspace:* + version: link:../llvm-darwin-x64 + '@scriptc/llvm-linux-arm64-gnu': + specifier: workspace:* + version: link:../llvm-linux-arm64-gnu + '@scriptc/llvm-linux-arm64-musl': + specifier: workspace:* + version: link:../llvm-linux-arm64-musl + '@scriptc/llvm-linux-x64-gnu': + specifier: workspace:* + version: link:../llvm-linux-x64-gnu + '@scriptc/llvm-linux-x64-musl': + specifier: workspace:* + version: link:../llvm-linux-x64-musl + '@scriptc/llvm-win32-x64-msvc': + specifier: workspace:* + version: link:../llvm-win32-x64-msvc '@scriptc/runtime-darwin-arm64': specifier: workspace:* version: link:../runtime-darwin-arm64 + '@scriptc/runtime-darwin-x64': + specifier: workspace:* + version: link:../runtime-darwin-x64 + '@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/llvm-darwin-arm64: {} + packages/llvm-darwin-x64: {} + + packages/llvm-linux-arm64-gnu: {} + + packages/llvm-linux-arm64-musl: {} + + packages/llvm-linux-x64-gnu: {} + + packages/llvm-linux-x64-musl: {} + + packages/llvm-win32-x64-msvc: {} + packages/runtime: {} packages/runtime-darwin-arm64: {} + packages/runtime-darwin-x64: {} + + packages/runtime-linux-arm64-gnu: {} + + packages/runtime-linux-arm64-musl: {} + + packages/runtime-linux-x64-gnu: {} + + packages/runtime-linux-x64-musl: {} + + packages/runtime-pack-common: {} + + packages/runtime-wasm32-wasi: {} + + packages/runtime-win32-x64-msvc: {} + packages: '@bytecodealliance/preview2-shim@0.17.6': diff --git a/scripts/surface-manifest.mjs b/scripts/surface-manifest.mjs index e48cea28..c211dcdd 100644 --- a/scripts/surface-manifest.mjs +++ b/scripts/surface-manifest.mjs @@ -28,7 +28,17 @@ const root = fileURLToPath(new URL("..", import.meta.url)); const readJson = (path) => JSON.parse(readFileSync(root + path, "utf8")); const version = readJson("packages/cli/package.json").version; -for (const pkg of ["runtime", "runtime-darwin-arm64", "llvm-darwin-arm64", "compiler"]) { +for (const pkg of [ + "runtime", + "runtime-darwin-arm64", "runtime-darwin-x64", + "runtime-linux-x64-gnu", "runtime-linux-arm64-gnu", + "runtime-linux-x64-musl", "runtime-linux-arm64-musl", + "runtime-win32-x64-msvc", "runtime-wasm32-wasi", + "llvm-darwin-arm64", "llvm-darwin-x64", + "llvm-linux-x64-gnu", "llvm-linux-arm64-gnu", + "llvm-linux-x64-musl", "llvm-linux-arm64-musl", "llvm-win32-x64-msvc", + "compiler", +]) { const v = readJson(`packages/${pkg}/package.json`).version; if (v !== version) { console.error( diff --git a/scripts/sync-versions.mjs b/scripts/sync-versions.mjs index 85d859e9..5c5b18e0 100644 --- a/scripts/sync-versions.mjs +++ b/scripts/sync-versions.mjs @@ -16,7 +16,17 @@ if (typeof version !== "string" || version.length === 0) { process.exit(1); } -for (const pkg of ["runtime", "runtime-darwin-arm64", "compiler", "llvm-darwin-arm64"]) { +for (const pkg of [ + "runtime", + "runtime-darwin-arm64", "runtime-darwin-x64", + "runtime-linux-x64-gnu", "runtime-linux-arm64-gnu", + "runtime-linux-x64-musl", "runtime-linux-arm64-musl", + "runtime-win32-x64-msvc", "runtime-wasm32-wasi", + "compiler", + "llvm-darwin-arm64", "llvm-darwin-x64", + "llvm-linux-x64-gnu", "llvm-linux-arm64-gnu", + "llvm-linux-x64-musl", "llvm-linux-arm64-musl", "llvm-win32-x64-msvc", +]) { const path = manifest(pkg); const json = read(path); if (json.version === version) { diff --git a/scripts/verify-llvm-package.mjs b/scripts/verify-llvm-package.mjs index ce47ade2..9b5263a2 100644 --- a/scripts/verify-llvm-package.mjs +++ b/scripts/verify-llvm-package.mjs @@ -48,7 +48,9 @@ try { version.protocol_version !== "1" || version.scriptc_package_version !== manifest.version || version.llvm_version !== "22.1.8" || - version.default_target !== "arm64-apple-macosx14.0.0" + version.default_target !== "arm64-apple-macosx14.0.0" || + !Array.isArray(version.supported_targets) || + !version.supported_targets.includes(version.default_target) ) throw new Error(`packed helper identity mismatch: ${JSON.stringify(version)}`); const dependencies = execFileSync("otool", ["-L", binary], { encoding: "utf8" }); const nonSystemDependencies = dependencies.trim().split("\n").slice(1) diff --git a/tests/harness/surface-manifest.test.ts b/tests/harness/surface-manifest.test.ts index 3cbf8f00..88f9c387 100644 --- a/tests/harness/surface-manifest.test.ts +++ b/tests/harness/surface-manifest.test.ts @@ -63,12 +63,25 @@ describe("surface manifest generation", () => { test("the version spine is the exact published version string", () => { const parsed = JSON.parse(committed) as SurfaceManifest; expect(parsed.compilerVersion).toBe(releaseVersion); - // The five packages publish in lockstep; a drifted stamp would make + // The compiler and every shipped native package publish in lockstep; a drifted stamp would make // the spine ambiguous for a version pin. expect(readJson("packages/compiler/package.json").version).toBe(releaseVersion); expect(readJson("packages/runtime/package.json").version).toBe(releaseVersion); expect(readJson("packages/runtime-darwin-arm64/package.json").version).toBe(releaseVersion); + expect(readJson("packages/runtime-darwin-x64/package.json").version).toBe(releaseVersion); + expect(readJson("packages/runtime-linux-x64-gnu/package.json").version).toBe(releaseVersion); + expect(readJson("packages/runtime-linux-arm64-gnu/package.json").version).toBe(releaseVersion); + expect(readJson("packages/runtime-linux-x64-musl/package.json").version).toBe(releaseVersion); + expect(readJson("packages/runtime-linux-arm64-musl/package.json").version).toBe(releaseVersion); + expect(readJson("packages/runtime-win32-x64-msvc/package.json").version).toBe(releaseVersion); + expect(readJson("packages/runtime-wasm32-wasi/package.json").version).toBe(releaseVersion); expect(readJson("packages/llvm-darwin-arm64/package.json").version).toBe(releaseVersion); + expect(readJson("packages/llvm-darwin-x64/package.json").version).toBe(releaseVersion); + expect(readJson("packages/llvm-linux-x64-gnu/package.json").version).toBe(releaseVersion); + expect(readJson("packages/llvm-linux-arm64-gnu/package.json").version).toBe(releaseVersion); + expect(readJson("packages/llvm-linux-x64-musl/package.json").version).toBe(releaseVersion); + expect(readJson("packages/llvm-linux-arm64-musl/package.json").version).toBe(releaseVersion); + expect(readJson("packages/llvm-win32-x64-msvc/package.json").version).toBe(releaseVersion); }); test("schema: ids unique and sorted, enums valid, codes on every non-static entry", () => {