Skip to content

CI: one workflow, planned lanes, toolchain images, and the JavaScriptCore tests - #645

Merged
dylan-conway merged 96 commits into
mainfrom
claude/webkit-ci-testing-a094ce
Sep 14, 2026
Merged

dylan-conway merged 96 commits into
mainfrom
claude/webkit-ci-testing-a094ce

Conversation

@dylan-conway

@dylan-conway dylan-conway commented Sep 13, 2026 •

Copy link
Copy Markdown
Member

Summary

Nothing in CI runs a test today: every job builds and uploads. This adds a test job that runs JavaScriptCore's own suites with the jsc shell the Linux asan lanes already ship, and restructures the build workflow around a plan job so lanes are defined in one place.

One workflow. build.yml (main) and build-preview.yml (pull requests) were wrappers around build-reusable.yml. They are now a single ci.yml, and main and pull requests run exactly the same jobs. What differs is nine lines: the commit (env.REF, a pull request's head), the release tag (autobuild-<sha> vs autobuild-preview-pr-<n>-<sha8>, same names as before), the prerelease flag, the concurrency group, and a one-line gh pr comment --edit-last after publishing that replaces the 60-line comment script. Dropped: the explicit write-access check (a fork's token is read-only, so such a build already fails when it creates the release) and dispatch by pr_number; manual dispatch now builds the head of the branch it is run on, as a prerelease.

Lanes from one table, built by one tool. The 42 lanes were seven near-identical jobs, a hand-kept list of expected assets in release, and six *-release.sh wrappers around docker buildx build that each derived a few settings visible nowhere else (MARCH_FLAG per arch, WIN_TRIPLE_ARCH, ENABLE_MALLOC_HEAP_BREAKDOWN=ON for macOS Debug, ...). .github/scripts/lanes.mjs now holds every lane: label, runner, Dockerfile, every build argument, tested or not. lanes.mjs plan emits the matrices and the asset list; lanes.mjs build <label> --output <dir> builds a lane and is exactly what CI runs, so a lane can be reproduced by hand. The six scripts are deleted. Three workflow files totalling 973 lines become one of 421, test and image jobs included.

Toolchain images. Every lane rebuilt its toolchain (apt, LLVM/GCC debs, xwin, macOS SDK, NDK) on every run. Each Dockerfile's base stage is now a pure toolchain stage, kept as an image in ghcr.io/oven-sh/bun-webkit-build-env tagged with a hash of what goes into it. A new image job (10 legs) builds and pushes an image only when its tag is missing; lanes start from theirs via --build-context base=docker-image://... and fall back to building the toolchain themselves if it is not there. ICU and WebKit stages are unchanged. Modelled on bun-toolchain.yml in oven-sh/rust.

Windows arm64 debug on Linux. The last lane that built on a Windows runner. /MTd needs the static debug CRT, which Microsoft ships for ARM64 in a separate package (CRT.ARM64.Desktop.debug.base) that xwin does not select; Dockerfile.windows now fetches it, pinned by sha256. Every lane is now the same four steps.

Tests. test runs on bun-webkit-linux-{amd64,arm64}-asan (Release + asserts + ASAN/UBSAN; plain Release compiles out $vm and the JIT disassembler). It downloads the tarball from the draft release, extracts bin/, runs testFFI and run-javascriptcore-tests --no-fail-fast (JSTests, LayoutTests/js, PerformanceTests), lists failing files in the job summary and uploads the log and results JSON. The tested lanes build in their own job (build-tested, same steps via an anchor) because needs: cannot name one matrix leg; that way tests start when those two lanes are done, not all 42. release does not wait for test.

Also: drops the unused llvm_version input; pushes to branches other than main no longer start a skipped run. If branch protection names any of the old job names as required checks, those need updating.

Not included: the C++ test binaries (testapi, testmasm, testb3, testair, testdfg), which the release Dockerfiles do not build, and test262.

Test plan

Lane settings: ran the release scripts from main for every lane, with the env the old workflow passes and docker replaced by a stub that records its arguments, and diffed that against lanes.mjs build --dry-run: same Dockerfile, platform and build arguments for all 42 (windows-arm64-debug compared against the cross script). Label set equals the old asset list. Dry-ran the tag and comment lines for main and for a pull request. Ran the build step's env export against a stub script for lanes with spaces, slashes and empty values. Test job: ran the same command with --quick locally against bun-webkit-linux-amd64-asan.tar.gz from autobuild-28f58fb0. The workflow itself is first exercised by this PR's preview build.

Nothing in CI ran a test. Add a `test` job that downloads the jsc shell shipped
by the bun-webkit-linux-{amd64,arm64}-asan lanes and runs
run-javascriptcore-tests (JSTests, LayoutTests/js, PerformanceTests) and testFFI
with it. The job does not gate the release and is continue-on-error until the
known failures on this fork are worked through.
Drop continue-on-error from the test job so a test failure turns the run red.
The release still does not wait for the tests. The preview-build comment is now
posted whenever the release was published rather than only when the whole build
workflow succeeded, so it keeps appearing while tests are failing.
@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The change replaces platform-specific build scripts and workflows with centralized lane planning, Docker toolchain builds, unified CI testing, and release handling. It also updates cross-compilation Dockerfiles, centralizes ICU metadata, and updates JavaScriptCore test execution and diagnostics.

Changes

Unified CI workflow

Layer / File(s) Summary
Platform lane planning
.github/scripts/lanes.mjs, .github/workflows/ci.yml
Defines platform variants, validates lanes, derives toolchain images, and plans build and test matrices.
Platform Docker build modes
Dockerfile, Dockerfile.musl, Dockerfile.android, Dockerfile.freebsd, Dockerfile.macos, Dockerfile.windows, icu/source.json, build-icu.ps1
Adds architecture lanes, cross-compilation inputs, shared ICU host tools, dynamic ICU metadata, sanitizer packaging, CRT inputs, and binary validation.
Workflow migration and documentation
.github/workflows/build*.yml, build.ts, *-release.*, CLAUDE.md, macos-cross/README.md
Removes legacy build workflows and scripts. Documents lane-based builds and the unified CI workflow.
JavaScriptCore validation updates
Tools/Scripts/run-javascriptcore-tests, Source/WTF/wtf/PlatformJSCOnly.cmake, Source/JavaScriptCore/tools/JSDollarVM.cpp, JSTests/*, LayoutTests/*
Adds JSC-only and no-slow test options, updates $vm setup, changes expected diagnostics, and adjusts time-zone and FFI stress-test setup.

Suggested reviewers: gsnedders

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 4b5bd

Some CI runs can select the wrong test configuration, skip JavaScriptCore diagnostics after a testFFI failure, or build the Bun-specific code without the required feature guard. These issues should be resolved before merge.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides a detailed summary and test plan, but it does not follow the required template. It omits the Bugzilla title and link, review status, and formatted changed-file list. Add the Bugzilla bug title and URL, include the required review-status line, and provide the formatted explanation and changed-file list required by the template. Preserve the existing summary and test plan as supporting detail.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: workflow consolidation, planned build lanes, toolchain images, and JavaScriptCore tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


Comment @coderabbitai help to get the list of available commands.

The 42 lanes were spread over seven near-identical jobs plus a hand-kept list
of expected assets in the release job. They are now defined once, in
.github/scripts/plan.mjs; a `plan` job turns that table into the build, test
and expected-asset matrices. Every lane gets the same runner, release script
and settings as before.

The lanes that are tested build in their own job (same steps, via an anchor) so
that the tests start when those lanes are done instead of waiting for all 42.

Also drops the unused llvm_version input, and only triggers CI on pushes to
main rather than starting a skipped run for every branch.
@dylan-conway dylan-conway changed the title CI: run the JavaScriptCore tests against the Linux asan builds CI: run the JavaScriptCore tests, generate the build lanes from a plan job Sep 13, 2026
…usable.yml

build.yml (pushes to main) and build-preview.yml (pull requests) were thin
wrappers that called build-reusable.yml with a different commit, release tag
and prerelease flag. They are now one workflow, ci.yml, with all three triggers:
the plan job works out what is being built, and the pull request comment is a
final job that only runs for pull requests.

Tags, the write-access check for pull requests, manual dispatch with pr_number
and the per-pull-request concurrency group are unchanged.
@dylan-conway dylan-conway changed the title CI: run the JavaScriptCore tests, generate the build lanes from a plan job CI: one workflow with a plan job, and run the JavaScriptCore tests Sep 13, 2026
What differs between a build of main and a build of a pull request is now nine
lines: the commit (env.REF), the release tag, the prerelease flag, the
concurrency group and a one-line `gh pr comment` after publishing. The separate
target-resolution step and the 60-line comment job are gone.

Dropped: the explicit write-access check (a fork's token is read-only, so such a
build already fails when it creates the release) and manual dispatch by
pr_number. Manual dispatch now builds the head of the branch it is run on.
…r lane

It was the one lane that still built on a Windows runner, because /MTd needs the
static debug CRT (libcmtd.lib, libcpmtd.lib, libvcruntimed.lib) and an xwin
splat has none for ARM64. The libs exist: for x64 they are in the same Visual
Studio package as the release CRT, for ARM64 Microsoft ships them in a separate
package, CRT.ARM64.Desktop.debug.base, that xwin does not select.
Dockerfile.windows now fetches that package (pinned by sha256) and unpacks its
libs next to the others.

The lane takes the same settings as bun-webkit-windows-amd64-debug with
WIN_ARCH=arm64. With it gone, the LLVM install and PowerShell steps are gone
from the workflow: every lane is checkout, buildx, build, upload.
…e per lane

Every one of the 42 lanes rebuilt its toolchain from scratch on every run: apt,
the LLVM and GCC debs, the xwin download, the macOS SDK, the NDK. The Dockerfiles'
`base` stages are now pure toolchain stages (no lane settings; the glibc and musl
Dockerfiles get a `lane` stage on top for those), and CI keeps them as images in
ghcr.io/oven-sh/bun-webkit-build-env, tagged with a hash of what goes into them.

A new `image` job, one leg per distinct toolchain (10), builds and pushes an
image only when its tag is missing, i.e. after a change to a `base` stage. Lanes
start from their image (--build-context base=docker-image://...). If an image
is not there a lane builds the toolchain itself, which is also what running a
release script by hand still does. The ICU and WebKit stages are unchanged.

Modelled on the bun-toolchain workflow in oven-sh/rust.
@dylan-conway dylan-conway changed the title CI: one workflow with a plan job, and run the JavaScriptCore tests CI: one workflow, planned lanes, toolchain images, and the JavaScriptCore tests Sep 13, 2026
The image job warned and carried on when a push failed, and lanes fell back to
building the toolchain themselves when their image was missing, so a broken
registry setup would only show up as every lane being slow. Now the image leg
fails, no lane starts, and a lane always builds from its image.
release.sh, musl-release.sh, macos-cross-release.sh, windows-cross-release.sh,
freebsd-release.sh and android-release.sh were six copies of the same wrapper
around `docker buildx build`, each also deriving a few settings that were
visible nowhere else (MARCH_FLAG per architecture, WIN_TRIPLE_ARCH,
ICU_MARCH_FLAG, ENABLE_MALLOC_HEAP_BREAKDOWN=ON for macOS Debug,
FREEBSD_VERSION, ANDROID_API). What a lane was built with was spread over the
plan, a script and the Dockerfile's defaults.

.github/scripts/lanes.mjs (was plan.mjs) now holds every build argument of
every lane and is the only way one is built:

  lanes.mjs build <label> --output <dir> [--base-image <ref>]
  lanes.mjs image <name> --push

CI runs exactly that, so a lane can be reproduced by running the same command.
For all 42 lanes the docker command is the same as the one the scripts on main
produce: same Dockerfile, platform and build arguments.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🔴 .github/workflows/build-preview.yml — notify gates on needs.build.result == 'success', and the reusable workflow's result now includes the new test job: whenever a JavaScriptCore test fails the called workflow's aggregate result is failure, so the PR "Preview Builds" comment is never posted/updated even though release still publishes the tarballs. On base every published preview got a comment; after this change any test flake hides an otherwise-good release from the PR. Fix: gate notify on the release having been published rather than the whole workflow succeeding, e.g. if: always() && needs.build.outputs.release_tag != '' (the reusable workflow already exports that output only on publish).

    Extended reasoning...

    build-preview.yml calls build-reusable.yml as job build. GitHub sets needs.build.result to the aggregate of every job inside the called workflow. This PR adds a test job (build-reusable.yml:226-337) that runs run-javascriptcore-tests --no-fail-fast and exits non-zero on any failing test. When that happens: release (which does not need test) still runs, sees all builds succeeded, and publishes the draft (build-reusable.yml:381-385); but the reusable workflow's overall conclusion is failure because test failed. Back in build-preview.yml, notify has needs: [trigger, build] with no always(), so it is skipped, and its if: needs.build.result == 'success' would fail anyway. The preview release exists on GitHub but the PR comment linking to it is never created or updated. On the base branch there was no test job, so notify fired for every published preview. The PR description says test failures are meant not to gate the release; this side-effect on the PR-comment path is not called out and leaves authors without the release link the workflow is meant to post.

    Verification: normal — The scenario is real and reachable. .github/workflows/build-preview.yml:88 still gates notify on if: needs.build.result == 'success', where build is the reusable-workflow call (uses: ./.github/workflows/build-reusable.yml). GitHub sets a caller job's result to the called workflow's overall conclusion, which is failure if any job inside it fails. This PR adds test to the…

Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml
- A tested lane that fails to build no longer skips the tests of the ones that
  did build: `test` runs whenever `build-tested` ran, and the leg of the lane
  that did not build fails at its download.
- testFFI and run-javascriptcore-tests each run whatever the other did; either
  failing fails the job. Before, a testFFI failure skipped the JavaScriptCore
  tests, leaving no results or log.

Also says what `native` means in lanes.mjs.
@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Preview build of a5ca21e: autobuild-preview-pr-645-a5ca21e5

JSTests/wasm.yaml runs LayoutTests/imported/w3c/web-platform-tests/wasm/{core/js,jsapi}
with the harness in web-platform-tests/resources; the test job's sparse checkout
left them out and run-jsc-stress-tests died in `realpath` before running a test.

The log filter that drops "Skipping <test>" lines also dropped that error,
which was printed onto the end of one: it now only drops lines that are nothing
but a skip.
The glibc and musl Dockerfiles built for whatever architecture the container
was, so the arm64 lanes needed arm64 runners and their own toolchain images.
They now build in the same linux/amd64 container as the x86_64 lanes, with
clang --target and a sysroot:

- glibc: an ubuntu 20.04 arm64 sysroot (the arm64 ubuntu:20.04 image, focal's
  libc6/libc6-dev unpacked over it, and the arm64 half of the gcc-13 mirror),
  plus the aarch64 sanitizer runtimes from the arm64 half of the LLVM mirror.
- musl: an alpine aarch64 sysroot populated by apk from the same repository.
- ICU is a two-stage cross build for aarch64 (the container's own ICU tools live
  in the toolchain image), as in the FreeBSD and Android Dockerfiles.

The x86_64 lanes run the same commands as before. The libc versions do not
change, and that is now checked: the image build fails unless the sysroot's
glibc is 2.31 like the container's (for musl: the same package version as the
container's), and every glibc jsc that is linked must not need a symbol
version newer than GLIBC_2.31.

Every lane now builds on linux-x64-gh; arm64 runners only run tests. 8 toolchain
images instead of 10. The musl stages no longer install packages or build zstd
per lane: that moved into the toolchain image.
Every run queued one `image` leg per toolchain on the large runners only to find
the image already there. `plan` (a standard runner, and it already knows the
tags) now asks the registry itself and hands `image` only the missing ones;
when there are none, `image` is skipped and the lanes start right after `plan`.

The legs that do run, after a change to a Dockerfile's `base` stage, use a
standard runner too: every image is linux/amd64 and building one is mostly
downloading and unpacking. They free the usual disk space first.

The registry token is passed to `docker login` through the environment rather
than written into the script.
The toolchain images took the package installs, SDKs and sysroots out of the
lanes, but every lane still fetched a few things and redid some lane-independent
work: ICU's source tarball (all but macOS), its host tools (Windows, FreeBSD,
Android), zstd, node and `patch` (Windows), and bootstrap_cmds to build `mig`
(macOS). Those are now in each Dockerfile's `base` stage, so in the image. The
stages on top only apply the lane's settings.

On Windows the host ICU tools are therefore built from the sources before the
udata.cpp hook patch is applied rather than after; the hook is null in ICU's
own tools either way.

The macOS image now depends on all of macos-cross/, not just xmac.mjs.
Each image leg used to delete the older versions of its own image as soon as it
pushed a new one, on main only. That missed images nothing uses any more (a
renamed toolchain, a pull request that never landed) and had no notion of age.

A build of main now runs `prune`: every version of the package that main's
lanes do not use and that was pushed more than 14 days ago is deleted, untagged
leftovers included. An image a pull request pushed recently stays, so a
toolchain change can be iterated on before it is merged; if an image a branch
still wants has gone, `plan` finds it missing on that branch's next run and
`image` rebuilds it.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 Dockerfile.macos — nit: this comment still points readers at build-reusable.yml ("Matches the native macOS lane's CMAKE_C_FLAGS (build-reusable.yml)"), which this PR deletes; after merge the reference is dangling and there is no native macOS lane in any workflow. Fix: update the comment to point at .github/scripts/lanes.mjs (or mac-release.bash) so future readers can find where these flags are kept in sync — this is the one remaining reference to the deleted workflow files across the repo.

    Extended reasoning...

    A repo-wide grep for the deleted filenames (build-reusable, release.sh, musl-release.sh, macos-cross-release.sh, etc.) turns up exactly one leftover: Dockerfile.macos:35 references build-reusable.yml. On the base branch that pointed at the CMAKE_C_FLAGS matrix in .github/workflows/build-reusable.yml; after this PR that file no longer exists and the flags live in .github/scripts/lanes.mjs (macos platform args()) and mac-release.bash. Purely a documentation drift — no build behaviour is affected — but it is the sort of stale pointer that misleads the next person bumping DEFAULT_CFLAGS.

    Verification: nit — Dockerfile.macos:35 reads # Matches the native macOS lane's CMAKE_C_FLAGS (build-reusable.yml) plus -g, and this PR deletes .github/workflows/build-reusable.yml (git diff --name-status shows D .github/workflows/build-reusable.yml; only ci.yml and mirror-llvm-debs.yml remain in .github/workflows/). A repo-wide grep for build-reusable returns only this one line, so the… | nit —…

Comment thread .github/workflows/ci.yml
Comment thread .github/scripts/lanes.mjs Outdated
Comment thread .github/workflows/ci.yml Outdated
…guments

- A pull request from a fork gets a read-only token, so it could never publish,
  but nothing stopped its 42 lanes from occupying the large runners for an hour
  before failing at the upload (the old workflow refused such runs up front).
  `plan` is now skipped for them, and everything else needs `plan`.
- A toolchain image's tag hashed its Dockerfile's `base` stage and the files it
  copies, but not the build arguments that stage takes from lanes.mjs
  (FREEBSD_VERSION, MACOS_DEPLOYMENT_TARGET, ANDROID_API, ...): bumping one left
  the tag unchanged and the lanes on the old toolchain. The ARGs a `base` stage
  declares that a lane sets are now part of the hash.
- Dockerfile.macos: a comment still named the deleted build-reusable.yml.
…e builds

- Creating the draft release was a job of its own next to `plan`. It is now the
  last step of `plan`, which already names the release. Every lane needs `plan`,
  so the draft exists before any lane starts, and the loop in which each lane
  waited up to ten minutes for it to appear is gone.
- linux image: there are no *.list files in /etc/apt/sources.list.d in this
  container, so the glob handed to sed stayed literal and sed failed.
- linux-musl image: /usr/share/apk/keys/aarch64/* are symlinks to ../<key>.pub,
  and cp -r copied them as dangling links, so apk trusted no key for the
  aarch64 index and found no packages. They are dereferenced now.
Builds:
- aarch64 glibc sysroot: libc6-dev's libm.so, libpthread.so, libdl.so, ... are
  absolute symlinks into /lib/aarch64-linux-gnu, which dangle when the package
  is unpacked into a sysroot, so the linker quietly took the static archives.
  They are re-pointed into the sysroot, their presence is checked, and every
  glibc jsc must list libm.so.6 as NEEDED.
- The glibc and musl version guards did nothing: under `set -e` a failing test
  that is not last in an `&&` list does not abort. They are separate statements.
- The aarch64 sanitizer runtimes must be the version of the clang installed.

Workflow:
- prune: only on the first attempt of a push to main (a re-run of an old run
  knew an old set of images), and the two newest versions of each toolchain
  main has always stay, so the image main used until a moment ago is not pulled
  from under runs still in flight.
- plan: a branch that predates lanes.mjs gets told so; a registry that cannot
  be asked is an error, not eight missing images; two images may not share a
  name; at least one lane must be tested and one not (no empty matrices).
- test takes the jsc shell from an artifact of build-tested instead of the draft
  release, which `release` deletes the moment any lane fails.
- A lane checks that there is a release to upload to before it builds, not
  after.
- The image tag no longer changes with comments, lane-only ARG defaults or
  macos-cross/README.md.
- Build and test checkouts skip about 2 GB nothing reads; dead outputs, ids and
  an API call left over from the reusable workflow are gone.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request has been reviewed before and this review found new issues. Where they share a root cause, one fix may close them together.

Comment thread Dockerfile Outdated
The `base` stage's LDFLAGS carry -L/usr/lib/x86_64-linux-gnu, where the
distribution's ICU 66 lives (libxml2-dev brings libicu-dev), and that came ahead
of the freshly built libicuuc.a: makeconv failed to link with undefined
uprv_stricmp_78 and friends. The lanes' ICU step already replaces LDFLAGS for
the same reason; the host tools step does now too.
`linux` sat next to `linux-musl` and `android`, which are Linux too; the job was
called "image linux" though only the ten glibc lanes use it.
Dockerfile: TARGETARCH is always amd64, so the gcc-13 and LLVM bundles and their
checksums are named directly, the library-path step is unconditional, and its
`uname -m` twin (which did the same thing a second time) is gone. The LLVM
symlink step ran twice, before and after the last apt-get install; the second
run, whose result is the one that stands, stays.

Dockerfile and Dockerfile.musl: CPU was an ARG and an ENV that no lane passes
and nothing reads. musl never used LLVM_VERSION (clang-21 is spelled out).

Dockerfile.freebsd, .android, .windows: the ICU_* ARGs in build_icu were only
for the ADD that moved to `base`.

Dockerfile.macos: two comments still spoke of native macOS lanes.

No build argument, environment variable that is read, or command that affects
an artifact changes.
Nothing runs them. CI builds every lane, macOS and Windows included, through
lanes.mjs and the Dockerfiles; Bun builds WebKit from source with its own
scripts/build.ts. Each of the three carried its own copy of the compiler flags
and cmake options, already out of step with what ships (build.ts still looked
for ICU under vcpkg_installed/), and a stale script that looks authoritative is
how a wrong flag gets copied somewhere it matters.

build-icu.ps1 stays: Bun runs it when it builds WebKit from source on Windows.
It now says so.
The previous change read PROCESSOR_ARCHITEW6432 and PROCESSOR_ARCHITECTURE.
Strawberry Perl is x64, and an x64 process on Windows on ARM runs emulated: it
has AMD64 in %PROCESSOR_ARCHITECTURE%, and PROCESSOR_ARCHITEW6432 is only set
for 32-bit processes, so that still said x86_64 on windows-11-arm.

HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment's
PROCESSOR_ARCHITECTURE is the machine's, whatever the process is. oven-sh/bun's
scripts/bootstrap.ps1 detects ARM64 the same way, for the same reason.

Not exercised: the Windows lanes are not tested at the moment. The query's
output format is as the pattern expects on a Windows x64 machine (AMD64).
…modes that collect continuously

It segfaulted (exit 139, no output) in the ftl-eager mode on linux-arm64-lto,
once, and with publishing now dependent on the tests that held back the
release.

On x64 with the ftl-eager options it crashes in JIT code, called from
llint_op_call at the top level of the script, 13 times in 320 on a loaded
machine (ASan build; 1 in 320 without ASan). It needs eager tiering and
--collectContinuously=true together: 0 of 320 with either alone. Upstream's own
jsc at ccdcb8a, built the same way, does the same, 19 of 640, so this is
not the fork's; upstream's bots show 1 failure in about 48,000 runs of these
modes. The cause is not known.

dfg-eager, dfg-eager-no-cjit-validate, ftl-eager and ftl-eager-no-cjit are the
default modes that have both ingredients.
…e stack

For a DirectTailCall to a known host function, DFG and FTL shuffle the frame
for the tail call and then call the host function inline, from their own code
(emitCallTarget(): prologue, null CodeBlock slot, call, exception check,
epilogue, ret). After the shuffle no frame names this CodeBlock: the frame is
the host function's, with a null CodeBlock slot. The only thing left that
points into this code is the return address, which the conservative scan does
not map to a CodeBlock. If the host function triggers a collection and nothing
else keeps the caller alive, the CodeBlock dies, its executable memory is freed
(and reused), and the host function returns into it.

stress/shadow-realm-remote-function-copy-length-and-name.js hit it: the
ShadowRealm builtins tail-call the host function createRemoteFunction, which
allocates. With eager tiering and --collectContinuously=true --useGenerationalGC=false
together (the dfg-eager and ftl-eager modes) it segfaulted in 11 to 16 of 640
runs on a loaded x86_64 machine, once on linux-arm64-lto in CI; upstream's jsc
at ccdcb8a does the same, 19 of 640. The freed DFG code, saved before it
was freed, has the inline call, and the fault is at its return address.

The inline call stays. Before it, the CodeBlock's pointer is stored in a stack
slot below the host function's frame, where the conservative scan finds it and
keeps the CodeBlock alive (and in CodeBlockSet's currently-executing set); the
epilogue that was already there drops the slot. 0 of 1,280 runs with the
minimal options and 0 of 1,280 with the ftl-eager mode's. A native tail call
from optimized code costs the same, 18.0 ns against 18.0 ns.

The test runs in every mode again.
`release` needs the builds only, as before b7bc14f. The test job still
runs on the tested lanes and goes red when a test fails; nothing waits for it.

One intermittent failure in about 120,000 test runs (an upstream JIT bug, since
fixed here) was enough to stop a preview from being published.
main has the same bug fixed another way (#649): a direct tail call to a host
function no longer takes the inline call path, and links through the
executable's host call thunk, which lives as long as the VM and returns to the
caller's caller. It comes with a regression test that frees the caller's code
deterministically.

This branch kept the inline call and parked the CodeBlock's pointer in a stack
slot across it (f66e3bc). Both fix the crash. The inline tail call is
about 0.7 ns faster per call (17.6 ns against 18.4 ns); main's version leaves
no JIT code running without a frame that names its CodeBlock, which is the
assumption CodeBlockSet's currently-executing set states. DFGSpeculativeJIT64.cpp
and FTLLowerDFGToB3.cpp are main's.
dylan-conway added a commit to oven-sh/bun that referenced this pull request Sep 14, 2026
The preview release autobuild-preview-pr-645-b6d2430a: #645 merged with
oven-sh/WebKit main, which brings the fix for a direct tail call to a host
function returning into freed JIT code (oven-sh/WebKit#649). Temporary: once
#645 is merged this becomes the sha of the autobuild from main.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

Reverts 46650cc ("The asan lanes are built with USE_SYSTEM_MALLOC") and
1bd0367 (its Windows exception): the asan and debug-asan variants pass no
USE_SYSTEM_MALLOC, and the Dockerfiles no longer take it. They build as they do
on main. No other lane ever set the argument.

With the system allocator, every fastMalloc in WTF, JavaScriptCore and Bun's C++
is an ASan allocation. In Bun's asan test lane that made LeakSanitizer see
allocations it could not see in libpas, freed JavaScriptCore memory count as RSS
through ASan's 256 MB quarantine, and max_allocation_size_mb cap JavaScriptCore's
own allocations. One build of that lane: 81 failing test files against 0, 559
minutes of shard time against 108, 4 of 20 shards past the job's time limit.
Of those, 15 files compare RSS with exact limits that are not to be retuned.
dylan-conway added a commit to oven-sh/bun that referenced this pull request Sep 14, 2026
The preview release autobuild-preview-pr-645-7022a119. Its asan lanes are built
with bmalloc/libpas again, as on oven-sh/WebKit main: with the system allocator
the asan test lane had 81 failing files and took five times as long. Temporary:
once #645 is merged this becomes the sha of the autobuild from main.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

Alpine's clang defines _FORTIFY_SOURCE=2 by itself and looks for the checked
libc wrappers in <sysroot>/usr/include/fortify. In the container they come
with clang (a dependency); the aarch64 sysroot has no clang and never got
them, so since the arm64 lanes became a cross build they were compiled with
the define and nothing behind it (musl has no fortify of its own), while the
x86_64 lanes kept the checks.

Against main's build of the same sources, in bun-webkit-linux-arm64-musl:
libWTF.a's code is 15% smaller (StringImpl.cpp.o 140,544 -> 60,548 bytes,
WTF::equal(StringImpl&, StringImpl&) 6,108 -> 1,612, 38 brk traps -> 0),
libJavaScriptCore.a 0.7%, ICU 1.4 to 3.5%; the bun that links the -lto lane
lost 472 KB of .text. The arm64 glibc lanes are unaffected (every object of
libWTF, libbmalloc and libicuuc has the same code size as main's).

The sysroot's fortify-headers must be the container's version, and the
toolchain check compiles a memcpy for both and requires the trap in each. This
changes `base`, so the musl toolchain image is rebuilt.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

… does

Alpine gives clang its default flags in /etc/clang21/<triple>.cfg, read for the
triple being compiled for. The clang package installs the container's
(x86_64-alpine-linux-musl.cfg: -fstack-clash-protection); Alpine's aarch64 clang
package installs the same file as aarch64-alpine-linux-musl.cfg, and in this
x86_64 container nothing did, so the cross-built arm64 lanes were compiled
without it.

With fortify-headers back, that was what was left of the difference from main's
native build of bun-webkit-linux-arm64-musl: 11 of libWTF.a's 175 objects 144
bytes smaller in all, 55 of libJavaScriptCore.a's 196. The file is copied, so
the aarch64 lanes get whatever Alpine puts there, and the toolchain check
requires clang to pass itself the same protection flags for both targets.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

dylan-conway added a commit to oven-sh/bun that referenced this pull request Sep 14, 2026
The preview release autobuild-preview-pr-645-4d198c60. Its cross-built
linux-arm64-musl lanes have _FORTIFY_SOURCE's checked libc wrappers and
-fstack-clash-protection again, as the x86_64-musl lanes and main's native
arm64-musl build do: every object of libWTF, libbmalloc and ICU has the code
size of main's, and libJavaScriptCore differs only in the two objects #645
changes. Temporary: once #645 is merged this becomes the sha of the autobuild
from main.
…d why

JSTests/BUN-TEST-DIFFERENCES.md. All 120 changed test files and the three
runner scripts, grouped by reason, for whoever has a failing test after an
upstream sync and needs to know whether the test is the thing to change:

- nine behaviors the fork has on purpose (ReferenceError text, one-line native
  toString, module link error wording, import attributes, cyclic array join,
  /u matches and surrogate pairs, when a time-zone change is seen, builtin
  frames' URL, bytecode-cache strings), each with the fork code that causes it
  and the files whose expectations follow it;
- tests narrowed to the modes or builds they can pass in, with the reason;
- the seven skipped with TODO(bun), by the gap each waits on;
- the rules an edit to an upstream test follows, and the steps for sorting a
  new failure.
Under Testing: what the file is, that it is read before a test's expectation is
changed or a test is skipped, and that a changed test is added to it. The CI/CD
paragraph mentions it where the failing tests' summary is described.
JSTests/CLAUDE.md and README.md are upstream's and stay as they are.
@dylan-conway
dylan-conway merged commit 9b02218 into main Sep 14, 2026
47 checks passed

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

- Files (6): `LayoutTests/js/` `array-string-recursion`, `array-tostring-and-join`, `toString-recursion` (each: the
script under `script-tests/` and the `-expected.txt`). `array-string-recursion` is this fork's previous version of
the test (upstream rewrote it to expect the `RangeError`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): A5 cites ArrayPrototype.cpp:329 as the toString recursion guard, but line 329 is inside arrayProtoFuncToLocaleString; Array.prototype.toString is guarded in JSArray::fastToString (JSArray.cpp) instead, which the entry never names. A maintainer following section E ("check that the fork code named there is still in the tree") after a sync that dropped the fastToString guard would still find a StringRecursionChecker at :329 and wrongly conclude the behavior is intact. Fix: label :329 as toLocaleString and add the JSArray::fastToString site so the entry names every guard [a,a].toString() actually depends on.

Extended reasoning...

arrayProtoFuncToString (ArrayPrototype.cpp:259) has no StringRecursionChecker; it delegates to asArray(thisValue)->fastToString() (line 286) or to join. StringRecursionChecker at ArrayPrototype.cpp:329 sits inside arrayProtoFuncToLocaleString (defined at line 315), and the toString path's guard is the #if USE(BUN_JSC_ADDITIONS) block in JSArray::fastToString (JSArray.cpp:1048–1060). The doc's stated workflow (section E step 1: "Check that the fork code named there is still in the tree, then update the expectation") relies on these citations being the code that produces the behavior; here the cited line is a different function's guard, so it can survive a sync that removes toString's. Doc-only; nothing changes at runtime.

Verification: nit — the doc's citation is wrong as described. BUN-TEST-DIFFERENCES.md:84 reads: "StringRecursionChecker in runtime/ArrayPrototype.cpp:329 (toString) and :470 (join)". But in /home/claude/webkit/Source/JavaScriptCore/runtime/ArrayPrototype.cpp: - Line 329 (StringRecursionChecker checker(globalObject, thisObject);) is inside arrayProtoFuncToLocaleString, defined at line 315. -…

dylan-conway added a commit to oven-sh/bun that referenced this pull request Sep 14, 2026
… the build graph (#42556)

### What does this PR do?

Re-lands the parts of #41330 that do not depend on compiling
JavaScriptCore and ICU inside bun's own build graph. bun keeps linking
the prebuilt `bun-webkit-*` tarballs; nothing here changes how WebKit or
ICU are obtained. One commit per topic, so any of them can be reverted
alone.

**Build graph**
- Generated headers were one build late: a header that a depfile names
by absolute path did not match its relative ninja output, so includers
were recompiled on the *next* run. Every build-dir output is also
declared under its absolute path.
- Every build after a build-script edit configured twice (`[0/1]
reconfigure`). Configure stamps the manifest and `ninja -t restat`s it.
- `bun run build` never reached a no-op with an isolated-layout
`node_modules`: the install edge's outputs made ninja pre-create
`node_modules/<dep>/`, and bun's isolated linker then skipped the
symlink.
- The link is an ordinary edge instead of holding ninja's console pool.

**Link-time checks** (`scripts/build/verify-binary.ts`, expectations per
target in `binary-expectations.ts`), as ninja validations of bun's link
in every mode that links: exported symbols against the lists in `src/`;
the exact NEEDED / dylib / import-DLL set and symbol-version ceilings;
forbidden imports; static initializers; nx-stack / no-RWX / PIE /
DllCharacteristics; no symbol strongly defined by two link inputs. A
check whose tool is missing is not emitted. `ninja_required_version` is
1.11 (validations); `rust-toolchain.toml` gains `llvm-tools`.
- `bun.exe` links the prebuilt WebKit, whose bmalloc is compiled with
`BEXPORT=__declspec(dllexport)`, so it exports 22 bmalloc symbols and
`g_config`. They are a named temporary allowance in the PE export
expectations, to be removed once oven-sh/WebKit builds bmalloc with
`BEXPORT` empty.

**Compiler policy, stated instead of inherited from the host compiler**
- `-fno-stack-protector` on Linux, FreeBSD and Android (it was off with
CI's clang, `-fstack-protector-strong` with Arch/Alpine/Fedora's). macOS
states `-fstack-protector`, which every clang turns on for a Darwin
target, so the macOS binaries keep their canaries.
- Vendored deps follow bun's PIC policy instead of the building clang's
default.
- windows-arm64 without `-mtune=ampere1`.

**Windows**
- The exe's import library no longer overwrites the object archive
(`obj/<exe>.import.lib`).
- `bun.exe`'s manifest declares `supportedOS` (Windows 7 through 10/11)
and `asInvoker`, so Windows stops running bun in the Vista compatibility
context. Visible effect: `process.report`'s `header.osRelease` reads
`10.0` instead of `6.1` on Windows 10/11.

**Runtime symbols**
- `napi_internal_*` → `Bun__napi_*`: the `napi*` glob in the version
scripts exported them on Linux and FreeBSD.
- `offsetOfWrapped()` in the generated classes is `constexpr`: 93 fewer
static initializers in `-O0` builds.

**CI**
- Linux images are baked by a Buildkite job on a fresh machine of the
target distro plus a `wait-for-image` step. Only `[build images]` /
`[publish images]` runs change; ordinary runs generate the same
pipeline.
- `verify-baseline` and `trace-order` are drawn beside each target's
build group; step keys and dependencies are unchanged.
- verify-baseline-static: decoder resync.

**Release is LTO by default**, locally as in CI; `--lto=off` for fast
relinks. The `btg` and `ci-release` profiles are gone and fail with what
to use instead.

Not included from #41330: JavaScriptCore/ICU in the build graph, the
ccache environment change, any change to `bootstrap.sh` /
`bootstrap.ps1`.

**WebKit** is bumped from `cf1b36ec8703` to `9b02218df662`,
oven-sh/WebKit `main` with oven-sh/WebKit#645 merged: #636, #634, #632,
#652, #646, #649 (a direct tail call to a host function could return
into freed JIT code) and #645 (a parser `SyntaxError` with no JS frame
on the stack has its `line`/`sourceURL` again; private builtins' source
text stays out of error messages; a wasm-to-JS frame index fix).
`test/js/bun/jsc/webkit-upgrade-9b02218df6.test.ts` covers what Bun can
see of #645. Against main's build of the same sources, every prebuilt
lane differs only in the two JavaScriptCore objects #645 changes.

### How did you verify your code works?

- `test/internal` build tests pass (212/212), including new cases for
the validations and for a missing tool; `scripts/build` typechecks apart
from two files that already fail on main.
- `--configure-only` for debug, release and each `--mode`: the link edge
carries `|@ … binary-verified … duplicate-symbols-checked`; release
resolves the `-lto` WebKit tarball, `--lto=off` the plain one,
`--profile=btg` fails with the retired-profile message.
- The verifier run by hand on existing binaries: passes on
prebuilt-linked release and debug-asan ELF, and on the released Windows
x64 `bun.exe` with the bmalloc allowance; the duplicate-symbol scan
finds none over 1221 inputs including the prebuilt archives.
- The Buildkite pipeline generated offline for seven scenarios:
identical step keys, `depends_on` and dependency closure on ordinary
runs; image runs match what #41330 generated.
- The rest is for CI on this PR.
usrbinkat pushed a commit to usrbinkat/bun that referenced this pull request Sep 15, 2026
…/ICU in the build graph (oven-sh#42556)

### What does this PR do?

Re-lands the parts of oven-sh#41330 that do not depend on compiling
JavaScriptCore and ICU inside bun's own build graph. bun keeps linking
the prebuilt `bun-webkit-*` tarballs; nothing here changes how WebKit or
ICU are obtained. One commit per topic, so any of them can be reverted
alone.

**Build graph**
- Generated headers were one build late: a header that a depfile names
by absolute path did not match its relative ninja output, so includers
were recompiled on the *next* run. Every build-dir output is also
declared under its absolute path.
- Every build after a build-script edit configured twice (`[0/1]
reconfigure`). Configure stamps the manifest and `ninja -t restat`s it.
- `bun run build` never reached a no-op with an isolated-layout
`node_modules`: the install edge's outputs made ninja pre-create
`node_modules/<dep>/`, and bun's isolated linker then skipped the
symlink.
- The link is an ordinary edge instead of holding ninja's console pool.

**Link-time checks** (`scripts/build/verify-binary.ts`, expectations per
target in `binary-expectations.ts`), as ninja validations of bun's link
in every mode that links: exported symbols against the lists in `src/`;
the exact NEEDED / dylib / import-DLL set and symbol-version ceilings;
forbidden imports; static initializers; nx-stack / no-RWX / PIE /
DllCharacteristics; no symbol strongly defined by two link inputs. A
check whose tool is missing is not emitted. `ninja_required_version` is
1.11 (validations); `rust-toolchain.toml` gains `llvm-tools`.
- `bun.exe` links the prebuilt WebKit, whose bmalloc is compiled with
`BEXPORT=__declspec(dllexport)`, so it exports 22 bmalloc symbols and
`g_config`. They are a named temporary allowance in the PE export
expectations, to be removed once oven-sh/WebKit builds bmalloc with
`BEXPORT` empty.

**Compiler policy, stated instead of inherited from the host compiler**
- `-fno-stack-protector` on Linux, FreeBSD and Android (it was off with
CI's clang, `-fstack-protector-strong` with Arch/Alpine/Fedora's). macOS
states `-fstack-protector`, which every clang turns on for a Darwin
target, so the macOS binaries keep their canaries.
- Vendored deps follow bun's PIC policy instead of the building clang's
default.
- windows-arm64 without `-mtune=ampere1`.

**Windows**
- The exe's import library no longer overwrites the object archive
(`obj/<exe>.import.lib`).
- `bun.exe`'s manifest declares `supportedOS` (Windows 7 through 10/11)
and `asInvoker`, so Windows stops running bun in the Vista compatibility
context. Visible effect: `process.report`'s `header.osRelease` reads
`10.0` instead of `6.1` on Windows 10/11.

**Runtime symbols**
- `napi_internal_*` → `Bun__napi_*`: the `napi*` glob in the version
scripts exported them on Linux and FreeBSD.
- `offsetOfWrapped()` in the generated classes is `constexpr`: 93 fewer
static initializers in `-O0` builds.

**CI**
- Linux images are baked by a Buildkite job on a fresh machine of the
target distro plus a `wait-for-image` step. Only `[build images]` /
`[publish images]` runs change; ordinary runs generate the same
pipeline.
- `verify-baseline` and `trace-order` are drawn beside each target's
build group; step keys and dependencies are unchanged.
- verify-baseline-static: decoder resync.

**Release is LTO by default**, locally as in CI; `--lto=off` for fast
relinks. The `btg` and `ci-release` profiles are gone and fail with what
to use instead.

Not included from oven-sh#41330: JavaScriptCore/ICU in the build graph, the
ccache environment change, any change to `bootstrap.sh` /
`bootstrap.ps1`.

**WebKit** is bumped from `cf1b36ec8703` to `9b02218df662`,
oven-sh/WebKit `main` with oven-sh/WebKit#645 merged: oven-sh#636, oven-sh#634, oven-sh#632,
oven-sh#652, oven-sh#646, oven-sh#649 (a direct tail call to a host function could return
into freed JIT code) and oven-sh#645 (a parser `SyntaxError` with no JS frame
on the stack has its `line`/`sourceURL` again; private builtins' source
text stays out of error messages; a wasm-to-JS frame index fix).
`test/js/bun/jsc/webkit-upgrade-9b02218df6.test.ts` covers what Bun can
see of oven-sh#645. Against main's build of the same sources, every prebuilt
lane differs only in the two JavaScriptCore objects oven-sh#645 changes.

### How did you verify your code works?

- `test/internal` build tests pass (212/212), including new cases for
the validations and for a missing tool; `scripts/build` typechecks apart
from two files that already fail on main.
- `--configure-only` for debug, release and each `--mode`: the link edge
carries `|@ … binary-verified … duplicate-symbols-checked`; release
resolves the `-lto` WebKit tarball, `--lto=off` the plain one,
`--profile=btg` fails with the retired-profile message.
- The verifier run by hand on existing binaries: passes on
prebuilt-linked release and debug-asan ELF, and on the released Windows
x64 `bun.exe` with the bmalloc allowance; the duplicate-symbol scan
finds none over 1221 inputs including the prebuilt archives.
- The Buildkite pipeline generated offline for seven scenarios:
identical step keys, `depends_on` and dependency closure on ordinary
runs; image runs match what oven-sh#41330 generated.
- The rest is for CI on this PR.
robobun added a commit that referenced this pull request Sep 16, 2026
…t that pins the text

The lanes run the JavaScriptCore tests (#645), and JSTests/BUN-TEST-DIFFERENCES.md
says how to record a difference that is on purpose.

- 34 trap expectations in 18 JSTests/wasm files lose " (evaluating '...')".
  Each change has a `// Bun:` comment with the suffix upstream expects.
  assert.js's `throws` is a substring check, so they also pass on upstream.
- BUN-TEST-DIFFERENCES.md gets section A10 with the fork code, how to
  recognize the difference, and the file list.
- wasm/stress/trap-message-has-no-source-text.js compares `e.message` exactly
  for a trap (tail call, statement call, direct reference, unreachable) and
  for a SuspendError. It fails without the engine change. It builds its
  module from bytes, so it needs no wabt.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant