Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 105 additions & 8 deletions .buildkite/ci.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -525,13 +525,15 @@ function getTestAgent(platform, options) {
* @param {Target} target
* @param {PipelineOptions} options
* @param {"cpp-only" | "rust-only" | "link-only"} mode
* @param {{standalone?: boolean}} [extra]
* @returns {string}
*/
function getBuildArgs(target, options, mode) {
function getBuildArgs(target, options, mode, extra = {}) {
const { os, arch, abi, baseline, profile, crossCompile } = target;
const { canary } = options;

const args = [`--profile=ci-${mode}`];
if (extra.standalone) args.push("--standalone=on");

// rust-only cross-compiles (linux host → linux/freebsd targets); os/arch/abi
// must all be explicit — host detection (detectLinuxAbi checks
Expand Down Expand Up @@ -570,9 +572,10 @@ function getBuildArgs(target, options, mode) {
* @param {Target} target
* @param {PipelineOptions} options
* @param {"cpp-only" | "rust-only" | "link-only"} mode
* @param {{standalone?: boolean}} [extra]
* @returns {string}
*/
function getBuildCommand(target, options, mode) {
function getBuildCommand(target, options, mode, extra) {
// Windows code signing is handled by a dedicated 'windows-sign' step after
// all Windows builds complete — see getWindowsSignStep(). smctl is x64-only,
// so signing on the build agent wouldn't work for ARM64 anyway.
Expand All @@ -582,7 +585,7 @@ function getBuildCommand(target, options, mode) {
// is wrong. PATH on the agent has node via bootstrap.sh.
// --experimental-strip-types for Node 24's .ts support (unflagged in
// 25+; drop once CI bumps past the ABI-141 blocker).
return `node --experimental-strip-types scripts/build.ts ${getBuildArgs(target, options, mode)}`;
return `node --experimental-strip-types scripts/build.ts ${getBuildArgs(target, options, mode, extra)}`;
}

/**
Expand Down Expand Up @@ -659,6 +662,63 @@ function getLinkBunStep(platform, options) {
};
}

/**
* Second cargo build for the reduced-footprint `bun-standalone` runtime
* (`--features standalone`). Same agent fan-out as build-rust.
*
* @param {Platform} platform
* @param {PipelineOptions} options
* @returns {Step}
*/
function getBuildRustStandaloneStep(platform, options) {
return {
key: `${getTargetKey(platform)}-build-rust-standalone`,
retry: getRetry(),
label: `${getTargetLabel(platform)} - build-rust-standalone`,
agents: getRustAgent(platform, options),
cancel_on_build_failing: isMergeQueue(),
command: getBuildCommand(platform, options, "rust-only", { standalone: true }),
timeout_in_minutes: 35,
};
}

/**
* Second link for `bun-standalone`. Reuses the same `build-cpp` archive
* (the C++ side is identical); only the Rust staticlib differs.
*
* @param {Platform} platform
* @param {PipelineOptions} options
* @returns {Step}
*/
function getLinkBunStandaloneStep(platform, options) {
return {
key: `${getTargetKey(platform)}-build-bun-standalone`,
label: `${getTargetLabel(platform)} - build-bun-standalone`,
depends_on: [`${getTargetKey(platform)}-build-cpp`, `${getTargetKey(platform)}-build-rust-standalone`],
agents: getLinkBunAgent(platform, options),
retry: getRetry(),
cancel_on_build_failing: isMergeQueue(),
env: {
ASAN_OPTIONS: "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=0",
},
command: getBuildCommand(platform, options, "link-only", { standalone: true }),
};
}

/**
* Whether to build `bun-standalone` for this platform. Only the plain
* release lanes that ship to users for `bun build --compile` — not asan,
* and not Android/FreeBSD until --compile supports those targets.
*
* @param {Platform} platform
*/
function shouldBuildStandalone(platform) {
if ((platform.profile ?? "release") !== "release") return false;
if (platform.abi === "android") return false;
if (platform.os === "freebsd") return false;
return true;
}

/**
* Returns the artifact triplet for a platform, e.g. "bun-linux-aarch64" or "bun-linux-x64-musl-baseline".
* Matches the naming convention in cmake/targets/BuildBun.cmake.
Expand Down Expand Up @@ -803,6 +863,11 @@ function getTestBunStep(platform, options, testOptions = {}) {
const { buildId, testFiles } = testOptions;

const args = [`--step=${getTargetKey(platform)}-build-bun`];
// bun-standalone is built by a sibling step; runner.node.mjs downloads it
// best-effort and exports BUN_STANDALONE_EXE for test/cli/standalone-binary.test.ts.
if (shouldBuildStandalone(platform)) {
args.push(`--standalone-step=${getTargetKey(platform)}-build-bun-standalone`);
}
if (buildId) {
args.push(`--build-id=${buildId}`);
}
Expand All @@ -817,6 +882,11 @@ function getTestBunStep(platform, options, testOptions = {}) {
const depends = [];
if (!buildId) {
depends.push(`${getTargetKey(platform)}-build-bun`);
if (shouldBuildStandalone(platform)) {
// Soft dependency: wait for the standalone build so the artifact exists,
// but don't block tests if that step failed.
depends.push({ step: `${getTargetKey(platform)}-build-bun-standalone`, allow_failure: true });
}
}

return {
Expand Down Expand Up @@ -904,6 +974,12 @@ function getWindowsSignStep(windowsPlatforms, options) {
const stepKey = `${getTargetKey(platform)}-build-bun`;
artifacts.push(`${triplet}-profile.zip`, `${triplet}.zip`);
buildSteps.push(stepKey, stepKey);
if (shouldBuildStandalone(platform)) {
const standaloneTriplet = triplet.replace(/^bun-/, "bun-standalone-");
const standaloneStepKey = `${getTargetKey(platform)}-build-bun-standalone`;
artifacts.push(`${standaloneTriplet}-profile.zip`, `${standaloneTriplet}.zip`);
buildSteps.push(standaloneStepKey, standaloneStepKey);
}
Comment on lines +977 to +982

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Centralize standalone triplet derivation.

The standalone artifact prefix is derived twice with triplet.replace(/^bun-/, "bun-standalone-"). Please route both call sites through a helper such as getTargetTriplet(platform, { standalone: true }) so signing, size metadata, and packaging stay on one naming contract. As per coding guidelines, “One source of truth; update every consumer atomically.”

Also applies to: 1020-1027

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.buildkite/ci.mjs around lines 977 - 982, The standalone artifact prefix is
derived multiple times using triplet.replace(/^bun-/, "bun-standalone-"),
violating the single source of truth principle. Centralize this derivation by
creating or using an existing helper function such as getTargetTriplet(platform,
{ standalone: true }) and replace all direct invocations of the
triplet.replace() pattern with calls to this helper throughout the code. This
ensures that signing, size metadata, and packaging all reference the same naming
contract from one location, making future updates atomic and reducing
duplication.

Source: Coding guidelines

}

// Signing runs on a real Windows x64 machine (smctl; doesn't work on
Expand All @@ -912,7 +988,10 @@ function getWindowsSignStep(windowsPlatforms, options) {
return {
key: "windows-sign",
label: `${getBuildkiteEmoji("windows")} sign`,
depends_on: windowsPlatforms.map(p => `${getTargetKey(p)}-build-bun`),
depends_on: windowsPlatforms.flatMap(p => [
`${getTargetKey(p)}-build-bun`,
...(shouldBuildStandalone(p) ? [`${getTargetKey(p)}-build-bun-standalone`] : []),
]),
agents: getEc2Agent({ os: "windows", arch: "x64", release: "2019" }, options, {
instanceType: getAzureVmSize("windows", "x64", "test"),
}),
Expand All @@ -938,7 +1017,14 @@ function getWindowsSignStep(windowsPlatforms, options) {
* @returns {Step}
*/
function getBinarySizeStep(releasePlatforms, options, { recordOnly = false } = {}) {
const targets = releasePlatforms.map(p => ({ triplet: getTargetTriplet(p) }));
const standalone = releasePlatforms.filter(shouldBuildStandalone);
const targets = [
...releasePlatforms.map(p => ({ triplet: getTargetTriplet(p) })),
// packageAndUpload sets `binary-size:bun-standalone-<triplet>` from the
// standalone link step; track those alongside the full binary so size
// regressions in either variant trip the threshold.
...standalone.map(p => ({ triplet: getTargetTriplet(p).replace(/^bun-/, "bun-standalone-") })),
];
const args = [`--targets '${JSON.stringify(targets)}'`, `--threshold-mb ${BINARY_SIZE_THRESHOLD_MB}`];
if (recordOnly) args.push("--no-fail");
if (!options.canary) args.push("--release");
Expand All @@ -951,7 +1037,10 @@ function getBinarySizeStep(releasePlatforms, options, { recordOnly = false } = {
options,
{ instanceType: "c8g.large" },
),
depends_on: releasePlatforms.map(p => `${getTargetKey(p)}-build-bun`),
depends_on: [
...releasePlatforms.map(p => `${getTargetKey(p)}-build-bun`),
...standalone.map(p => `${getTargetKey(p)}-build-bun-standalone`),
],
allow_dependency_failure: true,
soft_fail: !!options.skipSizeCheck,
retry: {
Expand All @@ -977,9 +1066,13 @@ function getReleaseStep(buildPlatforms, options, { signed = false } = {}) {

// When signing ran, depend on windows-sign instead of the raw Windows builds
// so we wait for signed artifacts before releasing.
const buildKeys = p => [
`${getTargetKey(p)}-build-bun`,
...(shouldBuildStandalone(p) ? [`${getTargetKey(p)}-build-bun-standalone`] : []),
];
const depends_on = signed
? [...buildPlatforms.filter(p => p.os !== "windows").map(p => `${getTargetKey(p)}-build-bun`), "windows-sign"]
: buildPlatforms.map(platform => `${getTargetKey(platform)}-build-bun`);
? [...buildPlatforms.filter(p => p.os !== "windows").flatMap(buildKeys), "windows-sign"]
: buildPlatforms.flatMap(buildKeys);

return {
key: "release",
Expand Down Expand Up @@ -1468,6 +1561,10 @@ async function getPipeline(options = {}) {
steps.push(getBuildCppStep(target, options));
steps.push(getBuildRustStep(target, options));
steps.push(getLinkBunStep(target, options));
if (shouldBuildStandalone(target)) {
steps.push(getBuildRustStandaloneStep(target, options));
steps.push(getLinkBunStandaloneStep(target, options));
}

if (needsBaselineVerification(target)) {
steps.push(getVerifyBaselineStep(target, options));
Expand Down
41 changes: 40 additions & 1 deletion .buildkite/scripts/upload-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ function download_buildkite_artifact() {
# (build-bun unsigned, windows-sign signed). Pin to the sign step to
# guarantee we get the signed one.
local step_args=()
if [[ -n "$WINDOWS_ARTIFACT_STEP" && "$name" == bun-windows-* ]]; then
if [[ -n "$WINDOWS_ARTIFACT_STEP" && ( "$name" == bun-windows-* || "$name" == bun-standalone-windows-* ) ]]; then
step_args=(--step "$WINDOWS_ARTIFACT_STEP")
fi
run_command buildkite-agent artifact download "$name" "$dir" "${step_args[@]}"
Expand Down Expand Up @@ -238,6 +238,36 @@ function create_release() {
bun-windows-aarch64-profile.zip
)

# Reduced-footprint --compile runtime. Same triplets minus android/freebsd
# (see shouldBuildStandalone in .buildkite/ci.mjs). buildkite-agent artifact
# download without --step searches the whole build, so these are picked up
# from the *-build-bun-standalone steps.
local standalone_artifacts=(
bun-standalone-darwin-aarch64.zip
bun-standalone-darwin-aarch64-profile.zip
bun-standalone-darwin-x64.zip
bun-standalone-darwin-x64-profile.zip
bun-standalone-linux-aarch64.zip
bun-standalone-linux-aarch64-profile.zip
bun-standalone-linux-x64.zip
bun-standalone-linux-x64-profile.zip
bun-standalone-linux-x64-baseline.zip
bun-standalone-linux-x64-baseline-profile.zip
bun-standalone-linux-aarch64-musl.zip
bun-standalone-linux-aarch64-musl-profile.zip
bun-standalone-linux-x64-musl.zip
bun-standalone-linux-x64-musl-profile.zip
bun-standalone-linux-x64-musl-baseline.zip
bun-standalone-linux-x64-musl-baseline-profile.zip
bun-standalone-windows-x64.zip
bun-standalone-windows-x64-profile.zip
bun-standalone-windows-x64-baseline.zip
bun-standalone-windows-x64-baseline-profile.zip
bun-standalone-windows-aarch64.zip
bun-standalone-windows-aarch64-profile.zip
)
artifacts+=("${standalone_artifacts[@]}")

Comment on lines +269 to +270

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix contradictory standalone upload flow (mandatory + duplicate best-effort pass).

Line 269 makes standalone artifacts required by appending them to artifacts, but Lines 292-295 treat them as best-effort. This can abort releases on missing standalone zips and also generates duplicate uploads plus bogus bun-standalone-standalone-* lookups.

Suggested fix
-  artifacts+=("${standalone_artifacts[@]}")
@@
-  for artifact in "${artifacts[@]}"; do
-    local standalone="${artifact/bun-/bun-standalone-}"
-    ( upload_artifact "$standalone" ) || echo "warn: skipping missing standalone artifact: $standalone"
-  done
+  for standalone in "${standalone_artifacts[@]}"; do
+    ( upload_artifact "$standalone" ) || echo "warn: skipping missing standalone artifact: $standalone"
+  done

Also applies to: 292-295

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.buildkite/scripts/upload-release.sh around lines 269 - 270, The standalone
upload flow has a contradiction where line 269 makes standalone artifacts
mandatory by appending them to the artifacts array, while lines 292-295 handle
them as best-effort with a separate upload pass. This causes releases to
potentially abort if standalone zips are missing and creates duplicate uploads
with malformed artifact names like bun-standalone-standalone-*. Remove the line
at 269 that appends standalone_artifacts to the artifacts array (the line
containing artifacts+=("${standalone_artifacts[@]}")) so that standalone
artifacts are only handled through the best-effort upload mechanism at lines
292-295, making them optional and preventing duplicates.

function upload_artifact() {
local artifact="$1"
download_buildkite_artifact "$artifact"
Expand All @@ -255,6 +285,15 @@ function create_release() {
upload_artifact "$artifact"
done

# bun-standalone-* zips ship alongside the regular zips. Derived from the
# main artifact list so a new platform can't be forgotten here. Best-effort:
# a missing standalone artifact warns but doesn't abort the release
# (download_buildkite_artifact's `exit 1` only kills the subshell).
for artifact in "${artifacts[@]}"; do
local standalone="${artifact/bun-/bun-standalone-}"
( upload_artifact "$standalone" ) || echo "warn: skipping missing standalone artifact: $standalone"
done

Comment on lines +288 to +296

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Two mutually-exclusive approaches for uploading standalone artifacts are both active: line 269 appends standalone_artifacts into artifacts (so the first loop already uploads them with hard-fail semantics), and the second loop at 292-295 then iterates the same extended array applying ${artifact/bun-/bun-standalone-}, re-uploading every standalone zip a second time and deriving bogus names like bun-standalone-standalone-darwin-aarch64.zip and bun-standalone-linux-aarch64-android.zip. Either drop artifacts+=(...) (and keep the derivation loop over the original 30 entries) or drop the second loop — as written every release does ~22 redundant uploads plus ~30 failed downloads, and the "best-effort" comment is wrong since the first loop's download_buildkite_artifact will exit 1 the whole script if any standalone zip is missing.

Extended reasoning...

What the bug is

create_release() now contains two conflicting implementations of "upload the standalone zips" that are both executing:

  1. Explicit list, appended to the main array (lines 245-269): a hand-maintained standalone_artifacts=(...) array is appended to artifacts via artifacts+=("${standalone_artifacts[@]}"). After this, artifacts has 52 entries (30 regular + 22 standalone).
  2. Derivation loop (lines 288-295): iterates "${artifacts[@]}" again and computes ${artifact/bun-/bun-standalone-} for each, uploading the result in a subshell.

The comment on the second loop says "Derived from the main artifact list so a new platform can't be forgotten here" — but the main artifact list is no longer just the 30 regular zips; it's the 52-element combined array.

Step-by-step trace

After line 269, artifacts = 30 regular entries + 22 bun-standalone-* entries.

First loop (284-286)for artifact in "${artifacts[@]}"; do upload_artifact "$artifact"; done:

  • Uploads all 30 regular zips (correct).
  • Uploads all 22 bun-standalone-* zips. upload_artifact calls download_buildkite_artifact not in a subshell, and that function does exit 1 on line 134 if the artifact is missing. So if any standalone build failed to produce an artifact, the entire release script aborts here — directly contradicting the "Best-effort: a missing standalone artifact warns but doesn't abort the release" comment on the second loop.

Second loop (292-295) — iterates the same 52 entries and applies ${artifact/bun-/bun-standalone-} (replaces the first bun- occurrence):

  • bun-darwin-aarch64.zipbun-standalone-darwin-aarch64.zipalready uploaded by loop 1; downloaded and uploaded to S3/GitHub a second time. (22 such duplicates.)
  • bun-linux-aarch64-android.zipbun-standalone-linux-aarch64-android.zip — never built (shouldBuildStandalone excludes android/freebsd); download_buildkite_artifact fails, the subshell exits 1, prints warn: skipping.... (8 such bogus android/freebsd derivations.)
  • bun-standalone-darwin-aarch64.zipbun-standalone-standalone-darwin-aarch64.zip — nonsense filename, doesn't exist; warns. (22 such double-prefixed names.)

Net per release: 22 redundant download+upload cycles (~hundreds of MB to S3/GitHub) and 30 spurious "warn: skipping missing standalone artifact" lines.

Why existing code doesn't prevent it

gh release upload --clobber and S3 cp are idempotent, so the duplicate uploads don't corrupt the release — they just waste time and bandwidth. The subshell wrapper on the second loop catches the exit 1 for the bogus names, so the script still completes. But the first loop has no such wrapper, so the "best-effort" intent for standalone artifacts is not actually honored: a single missing bun-standalone-*.zip kills the canary release before it reaches update_github_release.

Impact

  • Every canary release performs ~22 redundant artifact downloads + 66 redundant uploads (S3 ×2 + GitHub) and emits ~30 warning lines for nonexistent files.
  • A missing standalone artifact aborts the entire release (hard exit 1 from the first loop), contrary to the documented best-effort intent.
  • The "derived so a new platform can't be forgotten" comment is misleading — the explicit standalone_artifacts list is now the source of truth.

How to fix

Pick one approach:

Option A (keep the explicit list): delete the second loop (lines 288-295) entirely. The first loop already handles everything. If best-effort semantics are desired for standalone, wrap those entries in a subshell or iterate standalone_artifacts separately with the ( ... ) || warn pattern.

Option B (keep derivation): delete artifacts+=("${standalone_artifacts[@]}") and the standalone_artifacts array; have the second loop iterate the original 30-element artifacts array, and skip android/freebsd entries (e.g. [[ $artifact == *-android* || $artifact == *freebsd* ]] && continue).

Option A is simpler given the explicit list already exists and matches shouldBuildStandalone.

update_github_release "$tag"
create_sentry_release "$tag"
send_discord_announcement "$tag"
Expand Down
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ warnings = { level = "deny", priority = -1 }
# `bun_asan` is set via RUSTFLAGS (`--cfg=bun_asan` + `--check-cfg=cfg(bun_asan)`)
# by scripts/build/rust.ts for asan builds; register it here so a plain
# `cargo build` / `cargo check` (without those flags) doesn't warn.
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bun_asan)'] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bun_asan)', 'cfg(bun_standalone)'] }
# link.exe unconditionally prints "Creating library X.dll.lib and object
# X.dll.exp" to stdout when linking each proc-macro DLL on Windows hosts;
# there is no linker flag to suppress it. The lint already exempts itself
Expand Down
Loading
Loading