diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 46c979b28c4..934901512c0 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -126,53 +126,51 @@ function getAzureVmSize(os, arch, tier = "build") { return azureVmSizes[`${os}-${arch}`]?.[tier]; } +/** + * The single host image every build lane runs on. All targets below — + * linux x64/aarch64 × gnu/musl, darwin, windows, freebsd, android — are + * cross-compiled from this debian-13 aarch64 box via --target/--sysroot + * (scripts/build/config.ts + flags.ts) so one AMI serves every build. + * @type {Platform} + */ +const buildHostPlatform = { os: "linux", arch: "aarch64", distro: "debian", release: "13" }; + /** * @type {Platform[]} */ const buildPlatforms = [ - // macOS is cross-compiled from glibc amazonlinux (clang --target + the - // Apple SDK fetched by xmac + ld64.lld — see scripts/build/macos-sdk.ts and - // scripts/build/flags.ts). There is no native macOS build lane: the mac + // macOS is cross-compiled from the debian-13 aarch64 host (clang --target + + // the Apple SDK fetched by xmac + ld64.lld — see scripts/build/macos-sdk.ts + // and scripts/build/flags.ts). There is no native macOS build lane: the mac // fleet only runs tests, against these artifacts (see testPlatforms), and // these are the darwin artifacts the release ships. - { os: "darwin", arch: "aarch64", crossCompile: true, distro: "amazonlinux", release: "2023", features: ["docker"] }, - { os: "darwin", arch: "x64", crossCompile: true, distro: "amazonlinux", release: "2023", features: ["docker"] }, - { os: "linux", arch: "aarch64", distro: "amazonlinux", release: "2023", features: ["docker"] }, - { os: "linux", arch: "x64", distro: "amazonlinux", release: "2023", features: ["docker"] }, - { os: "linux", arch: "x64", baseline: true, distro: "amazonlinux", release: "2023", features: ["docker"] }, - { os: "linux", arch: "x64", profile: "asan", distro: "amazonlinux", release: "2023", features: ["docker"] }, - { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.23" }, - { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.23" }, - { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.23" }, - // Android: cross-compiled from glibc amazonlinux via NDK sysroot. Host arch - // matches target arch so only --abi/--target/--sysroot are cross. - { os: "linux", arch: "aarch64", abi: "android", distro: "amazonlinux", release: "2023", features: ["docker"] }, - { os: "linux", arch: "x64", abi: "android", distro: "amazonlinux", release: "2023", features: ["docker"] }, - // FreeBSD: cross-compiled from glibc amazonlinux via base.txz sysroot, - // same model as Android. Target os/arch are explicit. - { os: "freebsd", arch: "x64", distro: "amazonlinux", release: "2023", features: ["docker"] }, - { os: "freebsd", arch: "aarch64", distro: "amazonlinux", release: "2023", features: ["docker"] }, - // Windows is cross-compiled from glibc amazonlinux (clang-cl --target + - // the xwin MSVC/SDK sysroot + lld-link — see scripts/build/winsysroot.ts - // and scripts/build/flags.ts), the same model as macOS above. There is no - // native Windows build lane: the Windows fleet only runs tests, signing, - // and baseline verification, against these artifacts (see testPlatforms), - // and these are the Windows artifacts the release ships. All three lanes - // build without LTO for now: the windows x64 cross toolchain supports - // ThinLTO + cross-language LTO (--lto=on), but LLVM's thin backends - // miscompile JSC on x86-64 at -O1+, so it is not the default — see the - // ltoDefault comment in scripts/build/config.ts. - { os: "windows", arch: "x64", crossCompile: true, distro: "amazonlinux", release: "2023", features: ["docker"] }, - { - os: "windows", - arch: "x64", - baseline: true, - crossCompile: true, - distro: "amazonlinux", - release: "2023", - features: ["docker"], - }, - { os: "windows", arch: "aarch64", crossCompile: true, distro: "amazonlinux", release: "2023", features: ["docker"] }, + { os: "darwin", arch: "aarch64", crossCompile: true, distro: "debian", release: "13" }, + { os: "darwin", arch: "x64", crossCompile: true, distro: "debian", release: "13" }, + { os: "linux", arch: "aarch64", distro: "debian", release: "13" }, + { os: "linux", arch: "x64", distro: "debian", release: "13" }, + // asan x64 cross-builds from the arm64 host too; if install_cross_compiler_rt() + // can't fetch amd64 libclang-rt on arm64, this lane may need an x64 host as + // the one exception — see scripts/bootstrap.sh. + { os: "linux", arch: "x64", profile: "asan", distro: "debian", release: "13" }, + { os: "linux", arch: "aarch64", abi: "musl", distro: "debian", release: "13" }, + { os: "linux", arch: "x64", abi: "musl", distro: "debian", release: "13" }, + // Android: cross-compiled from the debian-13 aarch64 host via NDK sysroot. + { os: "linux", arch: "aarch64", abi: "android", distro: "debian", release: "13" }, + { os: "linux", arch: "x64", abi: "android", distro: "debian", release: "13" }, + // FreeBSD: cross-compiled from the debian-13 aarch64 host via base.txz + // sysroot, same model as Android. Target os/arch are explicit. + { os: "freebsd", arch: "x64", distro: "debian", release: "13" }, + { os: "freebsd", arch: "aarch64", distro: "debian", release: "13" }, + // Windows is cross-compiled from the debian-13 aarch64 host (clang-cl + // --target + the xwin MSVC/SDK sysroot + lld-link — see + // scripts/build/winsysroot.ts and scripts/build/flags.ts), the same model + // as macOS above. There is no native Windows build lane: the Windows fleet + // only runs tests, signing, and baseline verification, against these + // artifacts (see testPlatforms), and these are the Windows artifacts the + // release ships. x64 uses ThinLTO + cross-language LTO by default; arm64 + // stays non-LTO (no windows-arm64-lto WebKit prebuilt, see config.ts). + { os: "windows", arch: "x64", crossCompile: true, distro: "debian", release: "13" }, + { os: "windows", arch: "aarch64", crossCompile: true, distro: "debian", release: "13" }, ]; /** @@ -193,16 +191,12 @@ const testPlatforms = [ { os: "darwin", arch: "x64", release: "14", tier: "latest" }, { os: "linux", arch: "aarch64", distro: "debian", release: "13", tier: "latest" }, { os: "linux", arch: "x64", distro: "debian", release: "13", tier: "latest" }, - { os: "linux", arch: "x64", baseline: true, distro: "debian", release: "13", tier: "latest" }, { os: "linux", arch: "x64", profile: "asan", distro: "debian", release: "13", tier: "latest" }, { os: "linux", arch: "aarch64", distro: "ubuntu", release: "25.04", tier: "latest" }, { os: "linux", arch: "x64", distro: "ubuntu", release: "25.04", tier: "latest" }, - { os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "25.04", tier: "latest" }, { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.23", tier: "latest" }, { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.23", tier: "latest" }, - { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.23", tier: "latest" }, { os: "windows", arch: "x64", release: "2019", tier: "oldest" }, - { os: "windows", arch: "x64", release: "2019", baseline: true, tier: "oldest" }, { os: "windows", arch: "aarch64", release: "11", tier: "latest" }, ]; @@ -386,13 +380,11 @@ function getEc2Agent(platform, options, ec2Options) { * @returns {string} */ function getCppAgent(platform, options) { - const { arch } = platform; - - // Every build lane runs on the Linux EC2 fleet — darwin and windows are - // cross-compiled (the mac/windows fleets only run tests, signing, and - // baseline verification). - return getEc2Agent(platform, options, { - instanceType: arch === "aarch64" ? "c8g.4xlarge" : "c7i.4xlarge", + // Every build lane runs on the single debian-13 aarch64 host image + // (buildHostPlatform) and cross-compiles to its target; the target's + // os/arch only affect build args, not agent tags or image-name. + return getEc2Agent(buildHostPlatform, options, { + instanceType: "c8g.4xlarge", }); } @@ -402,56 +394,10 @@ function getCppAgent(platform, options) { * @returns {string} */ function getLinkBunAgent(platform, options) { - const { arch } = platform; - - return getEc2Agent(platform, options, { - // Full LTO with libbun_rust.a as bitcode peaks >31 GiB on aarch64; xlarge OOMs. - instanceType: arch === "aarch64" ? "r8g.2xlarge" : "r7i.2xlarge", - }); -} - -/** - * Linux box that cross-compiles libbun_rust.a for every linux/freebsd - * target. The image must have rustup + the pinned `rust-toolchain.toml` - * nightly preinstalled (with the `rust-src` component and every target - * triple in `rustup target add` form — bootstrap.sh handles this). - * @returns {Platform} - */ -function getRustPlatform() { - return { - os: "linux", - arch: "aarch64", - abi: "musl", - distro: "alpine", - release: "3.23", - }; -} - -/** - * @param {Platform} platform - * @param {PipelineOptions} options - * @returns {Agent} - */ -function getRustAgent(platform, options) { - const { os } = platform; - - // Windows: cargo's `*-pc-windows-msvc` build needs the xwin MSVC/SDK - // sysroot and clang-cl env that configure sets up on the amazonlinux - // image (cc-crate build scripts compile target C with it), so the rust - // step runs on the same image/fleet as the windows cpp/link steps rather - // than the shared alpine rust box. cargo build is wide — size for cores. - if (os === "windows") { - return getEc2Agent(platform, options, { - instanceType: platform.arch === "aarch64" ? "c8g.4xlarge" : "c7i.4xlarge", - }); - } - - // Linux (gnu/musl/android), FreeBSD, and macOS: cross-compile from one - // Linux aarch64 box. `aarch64/x86_64-apple-darwin` are Tier 2 targets with - // prebuilt std and a staticlib needs no Mach-O link (see - // rustCanCrossFromLinux()). cargo build is wide (1 codegen unit per crate × - // ~80 crates), so size for cores. ASAN doubles the IR — bigger box. - return getEc2Agent(getRustPlatform(), options, { + return getEc2Agent(buildHostPlatform, options, { + // rust-and-link runs cargo (~200 crates) then ThinLTO-links the full graph + // on one box; r8g.xlarge is too tight. ASAN's -Zbuild-std cargo pass + // doubles the IR, so size that lane for cores. instanceType: platform.profile === "asan" ? "r8g.4xlarge" : "r8g.2xlarge", }); } @@ -537,36 +483,20 @@ function getTestAgent(platform, options) { * * @param {Target} target * @param {PipelineOptions} options - * @param {"cpp-only" | "rust-only" | "link-only"} mode + * @param {"cpp-only" | "rust-only" | "link-only" | "rust-and-link"} mode * @returns {string} */ function getBuildArgs(target, options, mode) { - const { os, arch, abi, baseline, profile, crossCompile } = target; + const { os, arch, abi, baseline, profile } = target; const { canary } = options; const args = [`--profile=ci-${mode}`]; - // rust-only cross-compiles (linux host → linux/freebsd targets); os/arch/abi - // must all be explicit — host detection (detectLinuxAbi checks - // /etc/alpine-release) would report the build box's abi (Alpine→musl), not - // the target's. cpp-only/link-only: native build. - if (crossCompile) { - // macOS/Windows cross: every step (cpp/rust/link) runs on a Linux host, - // so the target os/arch are always explicit. - args.push(`--os=${os}`, `--arch=${arch}`); - } else if (mode === "rust-only" && os !== "darwin" && os !== "windows") { - args.push(`--os=${os}`, `--arch=${arch}`); - if (os === "linux") args.push(`--abi=${abi ?? "gnu"}`); - } else if (abi === "musl") { - args.push("--abi=musl"); - } else if (abi === "android") { - // Android cross-compiles C++ from a glibc host: arch/abi must be explicit - // (host detection would report the build box's gnu/x64, not the target). - args.push(`--os=${os}`, `--arch=${arch}`, "--abi=android"); - } else if (os === "freebsd") { - // FreeBSD cross-compiles C++ from a Linux host: os/arch must be explicit. - args.push(`--os=${os}`, `--arch=${arch}`); - } + // All build lanes share a debian-13 arm64 host, so host detection cannot + // infer the target triple — always pass os/arch (and abi on linux). + args.push(`--os=${os}`, `--arch=${arch}`); + if (os === "linux") args.push(`--abi=${abi ?? "gnu"}`); + if (baseline) args.push("--baseline=on"); if (profile === "asan") args.push("--asan=on"); @@ -582,7 +512,7 @@ function getBuildArgs(target, options, mode) { /** * @param {Target} target * @param {PipelineOptions} options - * @param {"cpp-only" | "rust-only" | "link-only"} mode + * @param {"cpp-only" | "rust-only" | "link-only" | "rust-and-link"} mode * @returns {string} */ function getBuildCommand(target, options, mode) { @@ -629,46 +559,30 @@ function getBuildCppStep(platform, options) { } /** + * cargo build + link on one agent. Runs in parallel with build-cpp (no + * depends_on); the build script runs `ninja bun-rust` first, then polls + * `buildkite-agent step get outcome` for `-build-cpp`, downloads + * its archive, and links. Key is `-build-bun` (it produces the final zip) + * so test/release/verify-baseline/binary-size depends_on stay unchanged. + * * @param {Platform} platform * @param {PipelineOptions} options * @returns {Step} */ -function getBuildRustStep(platform, options) { - return { - key: `${getTargetKey(platform)}-build-rust`, - retry: getRetry(), - label: `${getTargetLabel(platform)} - build-rust`, - agents: getRustAgent(platform, options), - cancel_on_build_failing: isMergeQueue(), - // cargo cross-compiles via --os/--arch (mapped to `--target ` - // in build args). The agent image has the pinned nightly + `rustup - // target add` for every triple preinstalled. - command: getBuildCommand(platform, options, "rust-only"), - timeout_in_minutes: 35, - }; -} - -/** - * @param {Platform} platform - * @param {PipelineOptions} options - * @returns {Step} - */ -function getLinkBunStep(platform, options) { +function getBuildBunStep(platform, options) { return { key: `${getTargetKey(platform)}-build-bun`, label: `${getTargetLabel(platform)} - build-bun`, - depends_on: [`${getTargetKey(platform)}-build-cpp`, `${getTargetKey(platform)}-build-rust`], agents: getLinkBunAgent(platform, options), retry: getRetry(), cancel_on_build_failing: isMergeQueue(), + timeout_in_minutes: 60, env: { // ASAN runtime settings — unrelated to build config, affects the // linked binary's startup during the smoke test. ASAN_OPTIONS: "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=0", }, - // link-only downloads artifacts from the sibling build-cpp and - // build-rust steps (derived from BUILDKITE_STEP_KEY) before ninja runs. - command: getBuildCommand(platform, options, "link-only"), + command: getBuildCommand(platform, options, "rust-and-link"), }; } @@ -701,9 +615,12 @@ function getTargetTriplet(platform) { * @returns {boolean} */ function needsBaselineVerification(platform) { - const { os, arch, baseline } = platform; - if (os === "linux") return (arch === "x64" && baseline) || arch === "aarch64"; - if (os === "windows") return arch === "x64" && baseline; + const { os, arch, abi, profile } = platform; + // asan never ships. x64-android is emulator-only; aarch64-android keeps its + // static LSE/SVE scan via --skip-emulation in getVerifyBaselineStep(). + if (profile === "asan") return false; + if (os === "linux") return (arch === "x64" && abi !== "android") || arch === "aarch64"; + if (os === "windows") return arch === "x64"; return false; } @@ -746,9 +663,28 @@ function getEmulatorBinary(platform) { */ function hasWebKitChanges(options) { const { changedFiles = [] } = options; + // Kept pointing at the removed SetupWebKit.cmake (always false) until + // verify-baseline.ts's --jit-stress path is fixed: it runs wasm fixtures + // without BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING / parsed //@ flags, so + // fixtures using wasm-GC types (bbq-osr-with-exceptions, + // omg-tail-call-clobber-scratch-register) fail to parse under it. return changedFiles.some(file => file.includes("SetupWebKit.cmake")); } +/** + * Host platform the verify-baseline step runs on — per-TARGET-arch, not the + * shared arm64 build host. Reuses test-fleet images (debian-13 / win-2019) so + * no extra bake is needed; getPipeline() keys its build-image depends_on on this. + * @param {Platform} platform + * @returns {Platform} + */ +function getVerifyBaselineHost(platform) { + const { os, arch, abi } = platform; + if (os === "windows") return { os: "windows", arch, release: "2019" }; + if (abi === "musl") return { os: "linux", arch, abi: "musl", distro: "alpine", release: "3.23" }; + return { os: "linux", arch, distro: "debian", release: "13" }; +} + /** * @param {Platform} platform * @param {PipelineOptions} options @@ -797,15 +733,16 @@ function getVerifyBaselineStep(platform, options) { ]), ]; - // Windows: the emulator phase runs bun-profile.exe under Intel SDE, so the - // step needs a real Windows machine even though the artifact is built on - // Linux. Everything else verifies on the same Linux fleet that linked it. + // verify-baseline is not a build lane: it stays on a host whose arch matches + // the TARGET so PINNED_QEMU's host-arch-specific static binaries keep working + // (the link agent is now always arm64 and can't run the x86_64-host qemu). + const host = getVerifyBaselineHost(platform); const agents = os === "windows" - ? getEc2Agent({ os: "windows", arch: platform.arch, release: "2019" }, options, { - instanceType: getAzureVmSize("windows", platform.arch), - }) - : getLinkBunAgent(platform, options); + ? getEc2Agent(host, options, { instanceType: getAzureVmSize("windows", platform.arch) }) + : getEc2Agent(host, options, { + instanceType: platform.arch === "aarch64" ? "r8g.2xlarge" : "r7i.2xlarge", + }); return { key: `${targetKey}-verify-baseline`, @@ -818,7 +755,7 @@ function getVerifyBaselineStep(platform, options) { command: [ ...setupCommands, `cargo build --release --manifest-path scripts/verify-baseline-static/Cargo.toml${os === "windows" ? " || exit /b 1" : ""}`, - `bun scripts/verify-baseline.ts --binary ${profileDir}/${profileExe} --emulator ${emulator}${skipEmulationFlag}${jitStressFlag}`, + `bun scripts/verify-baseline.ts --binary ${profileDir}/${profileExe} --arch ${platform.arch} --emulator ${emulator}${skipEmulationFlag}${jitStressFlag}`, ], }; } @@ -1026,11 +963,7 @@ function getBinarySizeStep(releasePlatforms, options, { recordOnly = false } = { return { key: "binary-size", label: `${getBuildkiteEmoji("package")} binary-size`, - agents: getEc2Agent( - buildPlatforms.find(p => p.os === "linux" && p.arch === "aarch64" && p.distro === "amazonlinux"), - options, - { instanceType: "c8g.large" }, - ), + agents: getEc2Agent(buildHostPlatform, options, { instanceType: "c8g.large" }), depends_on: releasePlatforms.map(p => `${getTargetKey(p)}-build-bun`), allow_dependency_failure: true, soft_fail: !!options.skipSizeCheck, @@ -1048,10 +981,10 @@ const BINARY_SIZE_THRESHOLD_MB = 0.5; /** * @param {Platform[]} releasePlatforms * @param {PipelineOptions} options - * @param {{ signed: boolean }} [extra] + * @param {{ signed?: boolean, testStepKeys?: string[] }} [extra] * @returns {Step} */ -function getReleaseStep(releasePlatforms, options, { signed = false } = {}) { +function getReleaseStep(releasePlatforms, options, { signed = false, testStepKeys = [] } = {}) { const { canary } = options; const revision = typeof canary === "number" ? canary : 1; @@ -1061,14 +994,15 @@ function getReleaseStep(releasePlatforms, options, { signed = false } = {}) { ? [...releasePlatforms.filter(p => p.os !== "windows").map(p => `${getTargetKey(p)}-build-bun`), "windows-sign"] : releasePlatforms.map(platform => `${getTargetKey(platform)}-build-bun`); + // Gate canary upload on green tests. A red test lane leaves the artifacts in + // Buildkite but skips the GitHub/S3 upload; the next green main push ships. + // [skip tests] on main still lets the release run (testStepKeys is empty). + depends_on.push(...testStepKeys); + return { key: "release", label: getBuildkiteEmoji("rocket"), - agents: getEc2Agent( - buildPlatforms.find(p => p.os === "linux" && p.arch === "aarch64" && p.distro === "amazonlinux"), - options, - { instanceType: "c8g.large" }, - ), + agents: getEc2Agent(buildHostPlatform, options, { instanceType: "c8g.large" }), depends_on, env: { CANARY: revision, @@ -1496,16 +1430,14 @@ async function getPipeline(options = {}) { } const { buildPlatforms = [], testPlatforms = [], buildImages, publishImages, imageFilter } = options; + // Every build lane runs on buildHostPlatform (see getCppAgent/getLinkBunAgent), + // so the build-image set is exactly {buildHostPlatform} ∪ testPlatforms' native + // images — buildPlatforms entries encode TARGET os/arch/abi, not a host image. const imagePlatforms = new Map( buildImages || publishImages - ? [...buildPlatforms, ...testPlatforms] - // darwin: no cloud images. freebsd and crossCompile platforms: - // they build on a linux host image (getImageKey maps them to the - // matching linux key, which the real linux entries already cover), - // so no separate image is baked for them — and letting them through - // would overwrite the linux entry with a windows/freebsd-flavored - // platform under the same key. - .filter(({ os, crossCompile }) => os !== "darwin" && os !== "freebsd" && !crossCompile) + ? [buildHostPlatform, ...testPlatforms] + // darwin: no cloud images (bare-metal test fleet only). + .filter(({ os }) => os !== "darwin") .filter(({ os, distro }) => !imageFilter || os === imageFilter || distro === imageFilter) .map(platform => [getImageKey(platform), platform]) : [], @@ -1546,32 +1478,26 @@ async function getPipeline(options = {}) { steps.push( ...relevantBuildPlatforms.map(target => { - const imageKey = getImageKey(target); - const rustImageKey = getImageKey(getRustPlatform()); - const dependsOn = imagePlatforms.has(rustImageKey) ? [`${rustImageKey}-build-image`] : []; + // build-cpp/build-bun always run on buildHostPlatform regardless of + // target, so the only build-image dependency is the host's. + const imageKey = getImageKey(buildHostPlatform); + const dependsOn = []; if (imagePlatforms.has(imageKey)) { dependsOn.push(`${imageKey}-build-image`); } - // Windows builds are cross-compiled on Linux, but steps that end up in - // this group still run on native Windows machines: the test shards - // (merged in below by group label) and x64-baseline's verify-baseline - // emulator phase. On [build images] runs they request the freshly - // baked native Windows image, so wait for that bake too. - if (target.os === "windows") { - const nativeWindowsPlatform = testPlatforms.find(p => p.os === "windows" && p.arch === target.arch); - const nativeImageKey = nativeWindowsPlatform && getImageKey(nativeWindowsPlatform); - if (nativeImageKey && imagePlatforms.has(nativeImageKey)) { - dependsOn.push(`${nativeImageKey}-build-image`); - } - } const steps = []; steps.push(getBuildCppStep(target, options)); - steps.push(getBuildRustStep(target, options)); - steps.push(getLinkBunStep(target, options)); + steps.push(getBuildBunStep(target, options)); if (needsBaselineVerification(target)) { - steps.push(getVerifyBaselineStep(target, options)); + // verify-baseline runs on a per-target-arch native host (see + // getVerifyBaselineHost), not buildHostPlatform; its image dep goes + // on the step itself so build-cpp/build-bun don't wait for it. + const verifyImageKey = getImageKey(getVerifyBaselineHost(target)); + const verifyDeps = + verifyImageKey !== imageKey && imagePlatforms.has(verifyImageKey) ? [`${verifyImageKey}-build-image`] : []; + steps.push(getStepWithDependsOn(getVerifyBaselineStep(target, options), ...verifyDeps)); } return getStepWithDependsOn( @@ -1586,20 +1512,37 @@ async function getPipeline(options = {}) { ); } - if (!isMainBranch()) { + // Tests run on main too so the canary release step below can gate on them. + // ASAN is PR-only (see includeASAN above), so the asan test lane is dropped + // on main along with its build. + const relevantTestPlatforms = includeASAN ? testPlatforms : testPlatforms.filter(({ profile }) => profile !== "asan"); + /** @type {string[]} */ + const testStepKeys = []; + { const { skipTests, forceTests, testFiles } = options; if (!skipTests || forceTests) { steps.push( - ...testPlatforms.map(target => ({ - key: getTargetKey(target), - group: getTargetLabel(target), - steps: [getTestBunStep(target, options, { testFiles, buildId })], - })), + ...relevantTestPlatforms.map(target => { + const step = getTestBunStep(target, options, { testFiles, buildId }); + testStepKeys.push(step.key); + // Test shards run on their native platform image; on [build images] + // runs they must wait for that freshly-baked image before starting. + const imageKey = getImageKey(target); + const dependsOn = imagePlatforms.has(imageKey) ? [`${imageKey}-build-image`] : []; + return getStepWithDependsOn( + { + key: getPlatformKey(target), + group: getPlatformLabel(target), + steps: [step], + }, + ...dependsOn, + ); + }), ); } } - // Binary-size tracking covers the artifacts that ship. + // Binary-size tracking: main records the baseline, PRs enforce the threshold. const strippedPlatforms = buildPlatforms.filter(p => (p.profile ?? "release") === "release"); if (!buildId && strippedPlatforms.length) { steps.push(getBinarySizeStep(strippedPlatforms, options, { recordOnly: isMainBranch() })); @@ -1624,7 +1567,7 @@ async function getPipeline(options = {}) { } if (isMainBranch()) { - steps.push(getReleaseStep(buildPlatforms, options, { signed: shouldSignWindows })); + steps.push(getReleaseStep(buildPlatforms, options, { signed: shouldSignWindows, testStepKeys })); } /** @type {Map} */ diff --git a/.buildkite/scripts/upload-release.sh b/.buildkite/scripts/upload-release.sh index 62999d8d449..badaf05ea01 100755 --- a/.buildkite/scripts/upload-release.sh +++ b/.buildkite/scripts/upload-release.sh @@ -296,14 +296,10 @@ function create_release() { bun-linux-aarch64-profile.zip bun-linux-x64.zip bun-linux-x64-profile.zip - bun-linux-x64-baseline.zip - bun-linux-x64-baseline-profile.zip bun-linux-aarch64-musl.zip bun-linux-aarch64-musl-profile.zip bun-linux-x64-musl.zip bun-linux-x64-musl-profile.zip - bun-linux-x64-musl-baseline.zip - bun-linux-x64-musl-baseline-profile.zip bun-linux-aarch64-android.zip bun-linux-aarch64-android-profile.zip bun-linux-x64-android.zip @@ -314,15 +310,48 @@ function create_release() { bun-freebsd-x64-profile.zip bun-windows-x64.zip bun-windows-x64-profile.zip - bun-windows-x64-baseline.zip - bun-windows-x64-baseline-profile.zip bun-windows-aarch64.zip bun-windows-aarch64-profile.zip ) - function upload_artifact() { + # x64 ships one nehalem binary under the plain name. Re-zip it under the + # historical `-baseline` name (inner dir renamed) so older `bun upgrade` + # clients that still request `-baseline` extract correctly. + function alias_baseline_artifact() { + local artifact="$1" + case "$artifact" in + bun-darwin-x64.zip) echo "bun-darwin-x64-baseline.zip" ;; + bun-darwin-x64-profile.zip) echo "bun-darwin-x64-baseline-profile.zip" ;; + bun-linux-x64.zip) echo "bun-linux-x64-baseline.zip" ;; + bun-linux-x64-profile.zip) echo "bun-linux-x64-baseline-profile.zip" ;; + bun-linux-x64-musl.zip) echo "bun-linux-x64-musl-baseline.zip" ;; + bun-linux-x64-musl-profile.zip) echo "bun-linux-x64-musl-baseline-profile.zip" ;; + bun-windows-x64.zip) echo "bun-windows-x64-baseline.zip" ;; + bun-windows-x64-profile.zip) echo "bun-windows-x64-baseline-profile.zip" ;; + *) echo "" ;; + esac + } + + command -v unzip &> /dev/null || package_manager_install unzip + command -v zip &> /dev/null || package_manager_install zip + + # Repack `$src_zip` (inner dir = basename of $src_zip) as `$dst_zip` with the + # inner dir renamed to match `$dst_zip`'s basename. Works in a fresh mktemp + # dir so a caller-CWD change can't collide with the extracted names. + function rezip_as() { + local src_zip="$1" dst_zip="$2" + local src_dir="${src_zip%.zip}" dst_dir="${dst_zip%.zip}" + local abs_src="$PWD/$src_zip" abs_dst="$PWD/$dst_zip" + local work; work="$(mktemp -d)" + run_command unzip -q -d "$work" "$abs_src" + run_command mv "$work/$src_dir" "$work/$dst_dir" + run_command rm -f "$abs_dst" + (cd "$work" && run_command zip -rq "$abs_dst" "$dst_dir") + run_command rm -rf "$work" + } + + function upload_one() { local artifact="$1" - download_buildkite_artifact "$artifact" if [ "$tag" == "canary" ]; then upload_s3_file "releases/$BUILDKITE_COMMIT-canary" "$artifact" & else @@ -333,6 +362,17 @@ function create_release() { wait } + function upload_artifact() { + local artifact="$1" + download_buildkite_artifact "$artifact" + upload_one "$artifact" + local alias="$(alias_baseline_artifact "$artifact")" + if [ -n "$alias" ]; then + rezip_as "$artifact" "$alias" + upload_one "$alias" + fi + } + for artifact in "${artifacts[@]}"; do upload_artifact "$artifact" done diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index ffcfea31e0c..bd4d9f672f6 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -28,7 +28,7 @@ env: # Pin the toolchain explicitly so rustup ignores rust-toolchain.toml's # `targets` list (11 cross triples ≈ 450 MB of prebuilt std we don't need # to lint the host). Keep in sync with `channel` in rust-toolchain.toml. - RUSTUP_TOOLCHAIN: nightly-2026-05-06 + RUSTUP_TOOLCHAIN: nightly-2026-07-20 jobs: clippy: diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 5e96c0d3c9a..9dfbb49cb8f 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -40,7 +40,7 @@ jobs: # Pin the toolchain explicitly so rustup ignores rust-toolchain.toml's # `targets` list (11 cross triples ≈ 450 MB of prebuilt std we don't # need just to run rustfmt). Keep this in sync with `channel` there. - RUSTUP_TOOLCHAIN: nightly-2026-05-06 + RUSTUP_TOOLCHAIN: nightly-2026-07-20 run: | # Without pipefail, `cmd | sed` always reports sed's exit status, so a # failing formatter is invisible to the `wait $PID` checks below. diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index 76bf543731f..6a71a0e306e 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -37,7 +37,7 @@ env: LLVM_VERSION_MAJOR: "21" # Pin so rustup ignores rust-toolchain.toml's `targets` list (11 cross # triples ≈ 450 MB we don't need). Keep in sync with `channel` there. - RUSTUP_TOOLCHAIN: nightly-2026-05-06 + RUSTUP_TOOLCHAIN: nightly-2026-07-20 jobs: miri: diff --git a/packages/bun-release/scripts/upload-npm.ts b/packages/bun-release/scripts/upload-npm.ts index dfb76fe39fe..8d74fe14fbe 100644 --- a/packages/bun-release/scripts/upload-npm.ts +++ b/packages/bun-release/scripts/upload-npm.ts @@ -116,10 +116,12 @@ without *requiring* a postinstall script. postinstall: "node install.js", }, optionalDependencies: Object.fromEntries( - platforms.map(({ bin }) => [ - `${owner}/${bin}`, - dryRun ? `file:./oven-${bin.replaceAll("/", "-") + "-" + version + ".tgz"}` : version, - ]), + platforms + .filter(p => !p.alias) + .map(({ bin }) => [ + `${owner}/${bin}`, + dryRun ? `file:./oven-${bin.replaceAll("/", "-") + "-" + version + ".tgz"}` : version, + ]), ), bin: { bun: "bin/bun.exe", diff --git a/packages/bun-release/src/platform.ts b/packages/bun-release/src/platform.ts index cdd27b0d073..f18198f6fb3 100644 --- a/packages/bun-release/src/platform.ts +++ b/packages/bun-release/src/platform.ts @@ -1,22 +1,19 @@ import { debug } from "./console"; -import { exists, read } from "./fs"; +import { exists } from "./fs"; import { spawn } from "./spawn"; export const os = process.platform; export const arch = os === "darwin" && process.arch === "x64" && isRosetta2() ? "arm64" : process.arch; -export const avx2 = - arch === "x64" && - ((os === "linux" && isLinuxAVX2()) || (os === "darwin" && isDarwinAVX2()) || (os === "win32" && isWindowsAVX2())); - export const abi = os === "android" ? "android" : os === "linux" && isLinuxMusl() ? "musl" : undefined; export type Platform = { os: string; arch: string; abi?: "musl" | "android"; - avx2?: boolean; + /** Back-compat alias package; same binary as the plain entry. Skipped by the postinstall picker. */ + alias?: boolean; bin: string; exe: string; }; @@ -31,13 +28,13 @@ export const platforms: Platform[] = [ { os: "darwin", arch: "x64", - avx2: true, bin: "bun-darwin-x64", exe: "bin/bun", }, { os: "darwin", arch: "x64", + alias: true, bin: "bun-darwin-x64-baseline", exe: "bin/bun", }, @@ -50,13 +47,13 @@ export const platforms: Platform[] = [ { os: "linux", arch: "x64", - avx2: true, bin: "bun-linux-x64", exe: "bin/bun", }, { os: "linux", arch: "x64", + alias: true, bin: "bun-linux-x64-baseline", exe: "bin/bun", }, @@ -71,7 +68,6 @@ export const platforms: Platform[] = [ os: "linux", arch: "x64", abi: "musl", - avx2: true, bin: "bun-linux-x64-musl", exe: "bin/bun", }, @@ -79,6 +75,7 @@ export const platforms: Platform[] = [ os: "linux", arch: "x64", abi: "musl", + alias: true, bin: "bun-linux-x64-musl-baseline", exe: "bin/bun", }, @@ -115,13 +112,13 @@ export const platforms: Platform[] = [ { os: "win32", arch: "x64", - avx2: true, bin: "bun-windows-x64", exe: "bin/bun.exe", }, { os: "win32", arch: "x64", + alias: true, bin: "bun-windows-x64-baseline", exe: "bin/bun.exe", }, @@ -133,15 +130,10 @@ export const platforms: Platform[] = [ }, ]; -export const supportedPlatforms: Platform[] = platforms - .filter( - platform => - platform.os === os && - platform.arch === arch && - (!platform.avx2 || avx2) && - (!platform.abi || abi === platform.abi), - ) - .sort((a, b) => (a.avx2 === b.avx2 ? 0 : a.avx2 ? -1 : 1)); +export const supportedPlatforms: Platform[] = platforms.filter( + platform => + platform.os === os && platform.arch === arch && !platform.alias && (!platform.abi || abi === platform.abi), +); function isLinuxMusl(): boolean { try { @@ -152,25 +144,6 @@ function isLinuxMusl(): boolean { } } -function isLinuxAVX2(): boolean { - try { - return read("/proc/cpuinfo").includes("avx2"); - } catch (error) { - debug("isLinuxAVX2 failed", error); - return false; - } -} - -function isDarwinAVX2(): boolean { - try { - const { exitCode, stdout } = spawn("sysctl", ["-n", "machdep.cpu"]); - return exitCode === 0 && stdout.includes("AVX2"); - } catch (error) { - debug("isDarwinAVX2 failed", error); - return false; - } -} - function isRosetta2(): boolean { try { const { exitCode, stdout } = spawn("sysctl", ["-n", "sysctl.proc_translated"]); @@ -180,17 +153,3 @@ function isRosetta2(): boolean { return false; } } - -function isWindowsAVX2(): boolean { - try { - return ( - spawn("powershell", [ - "-c", - `(Add-Type -MemberDefinition '[DllImport("kernel32.dll")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);' -Name 'Kernel32' -Namespace 'Win32' -PassThru)::IsProcessorFeaturePresent(40);`, - ]).stdout.trim() === "True" - ); - } catch (error) { - debug("isWindowsAVX2 failed", error); - return false; - } -} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 187dac114b2..e48bb2731da 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "nightly-2026-05-06" +channel = "nightly-2026-07-20" # rust-src is needed for -Zbuild-std (Tier 3 targets like # aarch64-unknown-freebsd have no prebuilt std). miri is for # `bun run rust:miri`. targets ensures the diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index b1c465312f1..5509db83838 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Version: 39 +# Version: 41 # A script that installs the dependencies needed to build and test Bun. # This should work on macOS and Linux with a POSIX shell. @@ -263,10 +263,6 @@ check_features() { ci=1 print "CI: enabled" ;; - *--osxcross*) - osxcross=1 - print "Cross-compiling to macOS: enabled" - ;; *--gcc-13*) gcc_version="13" print "GCC 13: enabled" @@ -705,6 +701,8 @@ install_common_software() { fi # https://packages.debian.org # https://packages.ubuntu.com + # lsb-release: apt.llvm.org/llvm.sh greps `lsb_release -cs`; debian + # slim images don't ship it (ubuntu cloud images do). install_packages \ bash \ ca-certificates \ @@ -712,6 +710,7 @@ install_common_software() { htop \ gnupg \ git \ + lsb-release \ unzip \ wget \ libc6-dbg @@ -1092,6 +1091,9 @@ install_build_essentials() { pkg-config \ golang install_packages apache2-utils + # QEMU user-mode for baseline CPU verification in CI (parity with + # the apk arm below; debian ships all target arches in one package). + install_packages qemu-user ;; dnf | yum) install_packages \ @@ -1148,16 +1150,29 @@ install_build_essentials() { install_cmake install_llvm - install_osxcross install_gcc install_rust - install_android_ndk - install_freebsd_sysroot - install_windows_sysroot + # Cross-compile sysroots + runtimes are only needed on the single build + # host (buildHostPlatform in .buildkite/ci.mjs); test images never + # cross-compile, so skip the ~3GB of NDK/SDK/sysroot downloads there. + if is_ci_build_host; then + install_cross_compiler_rt + install_android_ndk + install_freebsd_sysroot + install_linux_glibc_sysroot + install_linux_musl_sysroot + install_windows_sysroot + install_macos_sdk + fi install_ccache install_docker } +is_ci_build_host() { + # Must match buildHostPlatform in .buildkite/ci.mjs. + [ "$os-$distro-$arch-$ci" = "linux-debian-aarch64-1" ] +} + llvm_version_exact() { print "21.1.8" } @@ -1183,6 +1198,9 @@ install_llvm() { # Install llvm-symbolizer explicitly to ensure it's available for ASAN install_packages "llvm-$(llvm_version)-tools" + # Put the full LLVM bin dir on PATH so unversioned llvm-objcopy, + # llvm-strip, llvm-ar etc. resolve (debian only symlinks a subset). + append_to_path "/usr/lib/llvm-$(llvm_version)/bin" ;; brew) install_packages "llvm@$(llvm_version)" @@ -1198,6 +1216,16 @@ install_llvm() { esac } +install_cross_compiler_rt() { + # x64 asan cross needs amd64 compiler-rt. Best-effort: raw calls, never + # abort (apt.llvm.org may not carry amd64 on every arm64 repo). + if [ "$sudo" = "1" ] || [ -z "$can_sudo" ]; then _s=""; else _s="sudo -n"; fi + $_s dpkg --add-architecture amd64 || return + $_s apt-get update -qq || return + $_s apt-get install --yes --no-install-recommends \ + "libclang-rt-$(llvm_version)-dev:amd64" || true +} + install_gcc() { if ! [ "$os" = "linux" ] || ! [ "$distro" = "ubuntu" ] || [ -z "$gcc_version" ]; then return @@ -1305,14 +1333,6 @@ install_rust() { # Ensure all rustup files are accessible (for CI builds where different users run builds) grant_to_user "$rust_home" - case "$osxcross" in - 1) - rustup="$(require rustup)" - execute_as_user "$rustup" target add aarch64-apple-darwin - execute_as_user "$rustup" target add x86_64-apple-darwin - ;; - esac - case "$os" in linux) rustup="$rust_home/bin/rustup" @@ -1333,6 +1353,12 @@ install_rust() { # Windows cross-compile targets (--os=windows from a linux host). execute_as_user "$rustup" target add x86_64-pc-windows-msvc execute_as_user "$rustup" target add aarch64-pc-windows-msvc + # Linux cross-arch/cross-abi targets so an arm64 glibc host can + # cargo-build all four linux triples (x64/aarch64 × gnu/musl). + execute_as_user "$rustup" target add x86_64-unknown-linux-gnu + execute_as_user "$rustup" target add aarch64-unknown-linux-gnu + execute_as_user "$rustup" target add x86_64-unknown-linux-musl + execute_as_user "$rustup" target add aarch64-unknown-linux-musl # rust-src for -Zbuild-std (Tier 3 targets without prebuilt std). execute_as_user "$rustup" component add rust-src ;; @@ -1416,6 +1442,163 @@ install_freebsd_sysroot() { # arch-appropriate /opt/freebsd-sysroot{,-arm64} by well-known path. } +install_linux_glibc_sysroot() { + # ubuntu:20.04 (glibc 2.31) + gcc-13 libstdc++, matching the environment + # the prebuilt WebKit is compiled in (see oven-sh/WebKit Dockerfile). All + # linux-gnu lanes pass --sysroot pointing here so symbol versions never + # exceed 2.31; the --wrap list in flags.ts covers the 2.31 -> 2.17 tail. + case "$os-$ci" in + linux-1) ;; + *) return ;; + esac + if [ "$abi" = "musl" ]; then + return + fi + + if ! [ -f "$(which skopeo)" ]; then install_packages skopeo; fi + if ! [ -f "$(which jq)" ]; then install_packages jq; fi + # Cross-arch GNU strip for -R .eh_frame (host strip rejects foreign-arch ELF). + case "$arch" in + aarch64) install_packages binutils-x86-64-linux-gnu ;; + x64) install_packages binutils-aarch64-linux-gnu ;; + esac + skopeo="$(require skopeo)" + jq_bin="$(require jq)" + if [ "$sudo" = "1" ] || [ -z "$can_sudo" ]; then _s=""; else _s="sudo -n"; fi + + for sr_arch in x86_64 aarch64; do + case "$sr_arch" in + x86_64) + sysroot="/opt/linux-sysroot-glibc" + deb_arch="amd64" + apt_base="http://archive.ubuntu.com/ubuntu" + ;; + aarch64) + sysroot="/opt/linux-sysroot-glibc-arm64" + deb_arch="arm64" + apt_base="http://ports.ubuntu.com/ubuntu-ports" + ;; + esac + if [ -f "$sysroot/usr/include/features.h" ] && [ -d "$sysroot/usr/include/c++/13" ]; then + continue + fi + execute_sudo rm -rf "$sysroot" + execute_sudo mkdir -p "$sysroot" + tmp="$(create_tmp_directory)" + mkdir -p "$tmp/img" + + # 1. ubuntu:20.04 rootfs (glibc 2.31 runtime libs). + execute "$skopeo" copy --override-arch "$deb_arch" \ + "docker://docker.io/library/ubuntu:20.04" "dir:$tmp/img" + for d in $("$jq_bin" -r '.layers[].digest' "$tmp/img/manifest.json" | sed 's/^sha256://'); do + # Raw tar (not execute_sudo) so mknod failures on /dev nodes don't + # abort; the libc6 deb below provides the files we actually need. + $_s tar -xzf "$tmp/img/$d" -C "$sysroot" 2>/dev/null || true + done + + # 2. focal runtime + dev headers (the minimal base image has runtime + # libc but tar may not preserve all symlinks; dev headers are absent). + pkgz1=$(download_file "$apt_base/dists/focal-updates/main/binary-${deb_arch}/Packages.gz") + pkgz2=$(download_file "$apt_base/dists/focal/main/binary-${deb_arch}/Packages.gz") + # focal-updates first so awk's first-match picks the patched version. + execute sh -c "gzip -dc '$pkgz1' '$pkgz2' > '$tmp/Packages'" + for pkg in libc6 libc6-dev linux-libc-dev libcrypt1 libcrypt-dev; do + path=$(awk -v p="$pkg" '$1=="Package:"&&$2==p{f=1} f&&$1=="Filename:"{print $2; exit}' "$tmp/Packages") + if [ -n "$path" ]; then + deb=$(download_file "$apt_base/$path") + execute_sudo dpkg-deb -x "$deb" "$sysroot" + fi + done + # Absolute symlinks from the .debs point at host paths; rewrite them + # to stay inside the sysroot so -lpthread/-ldl resolve to target libs. + find "$sysroot" -type l 2>/dev/null | while read -r l; do + t="$(readlink "$l")" + case "$t" in /*) $_s ln -sfn "$sysroot$t" "$l" ;; esac + done + # libc.so linker script uses absolute /lib// paths; make sure + # those resolve inside the sysroot (usrmerge-style symlink if missing). + triple="${sr_arch}-linux-gnu" + if ! [ -e "$sysroot/lib/$triple/libc.so.6" ]; then + execute_sudo mkdir -p "$sysroot/lib" + execute_sudo ln -sfn "../usr/lib/$triple" "$sysroot/lib/$triple" + fi + if [ "$sr_arch" = "x86_64" ] && ! [ -e "$sysroot/lib64" ]; then + execute_sudo ln -sfn "usr/lib/$triple" "$sysroot/lib64" + fi + + # 3. gcc-13 (libstdc++-13-dev, libgcc-13-dev) from the same mirrored + # release the WebKit Dockerfile uses. + gcc13=$(download_file "https://github.com/oven-sh/WebKit/releases/download/gcc-13-focal-debs/gcc-13-focal-${deb_arch}.tar.gz") + mkdir -p "$tmp/gcc13" + execute tar -xzf "$gcc13" -C "$tmp/gcc13" + for deb in "$tmp/gcc13"/*.deb; do + execute_sudo dpkg-deb -x "$deb" "$sysroot" + done + + execute_sudo rm -rf "$tmp" + if ! [ -d "$sysroot/usr/include/c++/13" ]; then + error "$sysroot missing usr/include/c++/13 after gcc-13 overlay" + fi + print "installed: $sysroot (ubuntu:20.04 glibc 2.31 + gcc-13 libstdc++)" + done +} + +alpine_sysroot_version() { + # Keep in sync with the alpine release testPlatforms runs on. + print "3.23" +} + +install_linux_musl_sysroot() { + # musl + modern libstdc++ for --abi=musl cross-compiles from a glibc host. + # Populated from alpine's own packages via apk.static so the libstdc++ is + # the same one the native alpine test image uses. CI-only. + case "$os-$ci" in + linux-1) ;; + *) return ;; + esac + if [ "$abi" = "musl" ]; then + return + fi + + alpine_ver="$(alpine_sysroot_version)" + cdn="https://dl-cdn.alpinelinux.org/alpine/v${alpine_ver}" + host_m="$(uname -m)" + + # apk.static (host arch) can install foreign-arch packages into any root. + apk_tmp="$(create_tmp_directory)" + idx=$(download_file "$cdn/main/$host_m/APKINDEX.tar.gz") + apk_ver="$(tar -xzOf "$idx" APKINDEX 2>/dev/null | + awk '/^P:apk-tools-static$/{f=1} f&&/^V:/{print substr($0,3); exit}')" + if [ -z "$apk_ver" ]; then + error "could not resolve apk-tools-static version from $cdn/main/$host_m/APKINDEX.tar.gz" + fi + apk_pkg=$(download_file "$cdn/main/$host_m/apk-tools-static-${apk_ver}.apk") + execute tar -xzf "$apk_pkg" -C "$apk_tmp" sbin/apk.static + apk="$apk_tmp/sbin/apk.static" + + for ml_arch in x86_64 aarch64; do + case "$ml_arch" in + x86_64) sysroot="/opt/linux-sysroot-musl" ;; + aarch64) sysroot="/opt/linux-sysroot-musl-arm64" ;; + esac + # Same sentinel detectLinuxMuslSysroot() uses. + if [ -f "$sysroot/usr/lib/libc.so" ]; then + continue + fi + execute_sudo rm -rf "$sysroot" + execute_sudo mkdir -p "$sysroot" + execute_sudo "$apk" --arch "$ml_arch" --root "$sysroot" \ + --repository "$cdn/main" --allow-untrusted --no-cache --initdb \ + add musl-dev libc-dev linux-headers g++ libstdc++-dev + if ! [ -f "$sysroot/usr/lib/libc.so" ]; then + error "$sysroot not populated (required for linux-musl cross-arch builds)" + fi + done + execute_sudo rm -rf "$apk_tmp" + # No LINUX_MUSL_SYSROOT export: detectLinuxMuslSysroot() picks the + # arch-appropriate /opt/linux-sysroot-musl{,-arm64} by well-known path. +} + xwin_version() { # Keep in sync with XWIN_VERSION in scripts/build/winsysroot.ts and # .buildkite/Dockerfile. @@ -1535,44 +1718,57 @@ install_docker() { fi } -macos_sdk_version() { - # https://github.com/alexey-lysiuk/macos-sdk/releases - print "13.3" +macos_sdk_pinned_version() { + # Keep in sync with MACOS_SDK_VERSION in scripts/build/macos-sdk.ts. + print "26.5" +} + +macos_sdk_clt_release() { + # Keep in sync with MACOS_SDK_CLT_RELEASE in scripts/build/macos-sdk.ts. + print "26.5" } -install_osxcross() { - if ! [ "$os" = "linux" ] || ! [ "$osxcross" = "1" ]; then +install_macos_sdk() { + # macOS SDK for cross-compiling darwin from this Linux host. Fetched via the + # repo's vendored xmac.mjs (pulled at BUN_BOOTSTRAP_REPO_REF) so the same + # Apple-CDN download path is used here and at build-time. resolveMacosSdkPath() + # in scripts/build/macos-sdk.ts checks /opt/macos-sdk before falling back to + # a per-job download. + case "$os-$ci" in + linux-1) ;; + *) return ;; + esac + # darwin cross lanes never run on a musl host; alpine test images skip it. + if [ "$abi" = "musl" ]; then return fi - install_packages \ - libssl-dev \ - lzma-dev \ - libxml2-dev \ - zlib1g-dev \ - bzip2 \ - cpio - - osxcross_path="/opt/osxcross" - create_directory "$osxcross_path" - - osxcross_commit="29fe6dd35522073c9df5800f8cd1feb4b9a993a8" - osxcross_tar="$(download_file "https://github.com/tpoechtrager/osxcross/archive/$osxcross_commit.tar.gz")" - execute tar -xzf "$osxcross_tar" -C "$osxcross_path" - - osxcross_build_path="$osxcross_path/build" - execute mv "$osxcross_path/osxcross-$osxcross_commit" "$osxcross_build_path" - - osxcross_sdk_tar="$(download_file "https://github.com/alexey-lysiuk/macos-sdk/releases/download/$(macos_sdk_version)/MacOSX$(macos_sdk_version).tar.xz")" - execute mv "$osxcross_sdk_tar" "$osxcross_build_path/tarballs/MacOSX$(macos_sdk_version).sdk.tar.xz" + sdk_ver="$(macos_sdk_pinned_version)" + sysroot="/opt/macos-sdk" + # Same completeness sentinel isMacosSdk() uses. + if [ -f "$sysroot/MacOSX${sdk_ver}.sdk/usr/include/sys/syscall.h" ]; then + return + fi - bash="$(require bash)" - execute_sudo ln -sf "$(which clang-$(llvm_version))" /usr/bin/clang - execute_sudo ln -sf "$(which clang++-$(llvm_version))" /usr/bin/clang++ - execute_sudo "$bash" -lc "UNATTENDED=1 TARGET_DIR='$osxcross_path' $osxcross_build_path/build.sh" + bun_path="$(require bun)" + # xmac calls out to `xz` for the pbzx/xip payload. + case "$pm" in + apt) install_packages xz-utils ;; + *) install_packages xz ;; + esac - execute_sudo rm -rf "$osxcross_build_path" - grant_to_user "$osxcross_path" + repo_ref="${BUN_BOOTSTRAP_REPO_REF:-main}" + xmac_mjs=$(download_file "https://raw.githubusercontent.com/oven-sh/bun/${repo_ref}/scripts/build/xmac.mjs") + staging="$(create_tmp_directory)" + execute_sudo rm -rf "$sysroot" + execute_sudo mkdir -p "$sysroot" + # stdout is dropped: xmac draws progress bars there even without a TTY. + execute "$bun_path" "$xmac_mjs" splat --accept-license --sdk-only \ + --release "$(macos_sdk_clt_release)" --sdk "$sdk_ver" \ + --output "$staging" --cache-dir "$staging/cache" >/dev/null + execute_sudo mv "$staging/SDKs/MacOSX${sdk_ver}.sdk" "$sysroot/" + execute_sudo rm -rf "$staging" + grant_to_user "$sysroot" } install_tailscale() { @@ -1906,6 +2102,27 @@ clean_system() { for path in $tmp_paths; do execute_sudo rm -rf "$path"/* done + + case "$pm" in + apt) + execute_sudo apt-get clean + execute_sudo rm -rf /var/lib/apt/lists/* + ;; + dnf | yum) + execute_sudo "$pm" clean all + ;; + apk) + execute_sudo rm -rf /var/cache/apk/* + ;; + esac + + if command -v fstrim >/dev/null 2>&1; then + if [ "$sudo" = "1" ] || [ -z "$can_sudo" ]; then + fstrim -av || true + else + sudo -n fstrim -av || true + fi + fi } ensure_no_tmpfs() { diff --git a/scripts/build.ts b/scripts/build.ts index b5ecf135184..7dc5e8acda0 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -24,6 +24,7 @@ import { spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { + canTraceOrderFile, downloadArtifacts, inheritOrderFile, isCI, @@ -34,6 +35,7 @@ import { printEnvironment, regenerateOrderFile, reportOrderFileBootstrap, + reportOrderFileCannotTrace, reportOrderFileFailure, shouldGenerateOrderFile, spawnWithAnnotations, @@ -123,19 +125,39 @@ async function main(): Promise { await startGroup("Download artifacts", () => downloadArtifacts(result.cfg)); } - // The order file is a link input, so it must land before ninja. + // The order file is a link input, so it must land before the linking ninja + // pass. In rust-and-link mode it runs between cargo and the build-cpp + // poll (whose sleep loop yields cleanly) so it doesn't stall cargo. const orderCtx = orderFileContext(); + const runInherit = () => + (orderFileEligible(result.cfg, orderCtx) && !shouldGenerateOrderFile(result.cfg, orderCtx) + ? inheritOrderFile(result.cfg, orderCtx) + : Promise.resolve(false) + ).catch(e => { + console.log(`~ symbol order: inherit failed (${(e as Error)?.message ?? e}); linking unordered`); + return false; + }); let inherited = false; - if (orderFileEligible(result.cfg, orderCtx) && !shouldGenerateOrderFile(result.cfg, orderCtx)) { - inherited = (await startGroup("Inherit symbol order file", () => - inheritOrderFile(result.cfg, orderCtx), - )) as boolean; - } - const runNinja = () => - spawnWithAnnotations("ninja", ninjaArgv(result.cfg), { label: "ninja", env: ninjaEnv(result.cfg, result.env) }); + const runNinja = (targets: string[] = args.ninjaTargets) => + spawnWithAnnotations("ninja", ["-C", result.cfg.buildDir, ...args.ninjaArgs, ...targets], { + label: "ninja", + env: ninjaEnv(result.cfg, result.env), + }); + + // rust-and-link: build libbun_rust.a first so cargo overlaps with the + // sibling build-cpp job, THEN poll for build-cpp's outcome + download + // its archive, THEN link. link-only skips straight to the full build + // (its artifacts were downloaded above). + if (result.cfg.buildkite && result.cfg.mode === "rust-and-link") { + await startGroup("Build Rust", () => runNinja(["bun-rust"])); + inherited = (await startGroup("Inherit symbol order file", runInherit)) as boolean; + await startGroup("Wait for build-cpp & download artifacts", () => downloadArtifacts(result.cfg)); + } else { + inherited = (await startGroup("Inherit symbol order file", runInherit)) as boolean; + } - await startGroup("Build", runNinja); + await startGroup("Build", () => runNinja()); // Trace and relink when we are a release, when a commit asked for it, or when // there was nothing to inherit. A failed trace is not fatal: the order file is @@ -158,16 +180,17 @@ async function main(): Promise { } } else if (orderFileEligible(result.cfg, orderCtx) && result.output.exe) { // Inherited: a stale file is a slower binary, not a broken one. + if (!inherited && !canTraceOrderFile(result.cfg)) reportOrderFileCannotTrace(result.cfg); verifyOrderFileApplied(result.cfg, orderCtx, result.output.exe, { strict: false }); } // cpp-only/rust-only: upload build outputs for downstream link-only. - // link-only: package + upload zips for downstream test steps. + // link-only/rust-and-link: package + upload zips for downstream test steps. if (result.cfg.buildkite) { if (result.cfg.mode === "cpp-only" || result.cfg.mode === "rust-only") { await startGroup("Upload artifacts", () => uploadArtifacts(result.cfg, result.output)); } - if (result.cfg.mode === "link-only") { + if (result.cfg.mode === "link-only" || result.cfg.mode === "rust-and-link") { await startGroup("Package and upload", () => packageAndUpload(result.cfg, result.output)); } } @@ -443,6 +466,8 @@ function parseArgs(argv: string[]): CliArgs { "macosSdk", "osxDeploymentTarget", "winsysroot", + "linuxSysroot", + "freebsdSysroot", ]); for (let i = 0; i < argv.length; i++) { diff --git a/scripts/build/CLAUDE.md b/scripts/build/CLAUDE.md index d387f153bdd..aa9015b38a9 100644 --- a/scripts/build/CLAUDE.md +++ b/scripts/build/CLAUDE.md @@ -164,7 +164,7 @@ For `mode: "full"` (the normal case): 8. **Post-link** — strip (release only), dsymutil (darwin release only). 9. **Smoke test** — ` --revision` catches load-time failures. -Split CI modes: `rust-only` (lolhtml+codegen+cargo → libbun_rust.a), `cpp-only` (deps+codegen+compile → archive), `link-only` (download artifacts → link). +Split CI modes: `rust-only` (lolhtml+codegen+cargo → libbun_rust.a), `cpp-only` (deps+codegen+compile → archive), `link-only` (download artifacts → link), `rust-and-link` (cargo + poll build-cpp + download archive → link). ### Phase 3 — Execute diff --git a/scripts/build/buildOptionsRs.ts b/scripts/build/buildOptionsRs.ts index 70ab863a410..1137e762ab6 100644 --- a/scripts/build/buildOptionsRs.ts +++ b/scripts/build/buildOptionsRs.ts @@ -47,7 +47,6 @@ export function generateBuildOptionsRs(cfg: Config): string { `pub const SHA: &str = ${rstr(cfg.revision)};`, `pub const REPORTED_NODEJS_VERSION: &str = ${rstr(cfg.nodejsVersion)};`, `pub const RELEASE_SAFE: bool = ${cfg.assertions};`, - `pub const BASELINE: bool = ${cfg.baseline};`, `pub const IS_CANARY: bool = ${cfg.canary};`, `pub const CANARY_REVISION: &str = ${rstr(cfg.canaryRevision)};`, `pub const ENABLE_FUZZILLI: bool = ${cfg.fuzzilli};`, diff --git a/scripts/build/bun.ts b/scripts/build/bun.ts index 7efdbe5cc16..afbb4932e13 100644 --- a/scripts/build/bun.ts +++ b/scripts/build/bun.ts @@ -17,9 +17,12 @@ * - "cpp-only": compile to libbun.a, skip rust/link (CI upstream) * - "rust-only": codegen + cargo → libbun_rust.a (CI upstream) * - "link-only": link pre-built artifacts (CI downstream) + * - "rust-and-link": cargo + link; downloads cpp-only's archive (CI) * - * cpp-only/rust-only/link-only are for the CI split where C++ and Rust - * build in parallel on separate machines then meet for linking. + * The split modes are for CI where C++ and Rust build in parallel on + * separate machines. rust-and-link folds the rust + link steps onto one + * agent (cargo runs while cpp-only is still compiling elsewhere; the + * cpp archive is polled for and downloaded before ninja links). */ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs"; @@ -162,6 +165,9 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput { if (cfg.mode === "link-only") { return emitLinkOnly(n, cfg); } + if (cfg.mode === "rust-and-link") { + return emitRustAndLink(n, cfg, sources); + } const exeName = bunExeName(cfg); @@ -541,9 +547,6 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput { * Does NOT need: any C dep built, any cxx, PCH, link. ninja only pulls * what's depended on — lolhtml's configure/build rules are emitted but * unused (only its `.ref` fetch stamp is depended on by emitRust). - * - * Cross-compilation: see `rustCanCrossFromLinux()` in rust.ts for which - * targets share a linux runner vs need a native agent. */ function emitRustOnly(n: Ninja, cfg: Config, sources: Sources): BunOutput { n.comment("════════════════════════════════════════════════════════════════"); @@ -655,6 +658,88 @@ function emitLinkOnly(n: Ninja, cfg: Config): BunOutput { }; } +/** + * rust-and-link mode: cargo build + link on one CI agent. The cpp archive + * and dep libs are downloaded from the sibling build-cpp step (ci.ts polls + * for its outcome and downloads before ninja runs); libbun_rust.a is built + * locally. Graph = emitRustOnly's cargo edge + emitLinkOnly's link edge. + * + * Expected downloaded artifacts (same paths cpp-only produced): + * - libbun-profile.a — from cpp-only's ar() + * - deps//lib.a — from cpp-only's dep builds + * - cache/webkit-/lib/... — WebKit prebuilt (same cache path) + */ +function emitRustAndLink(n: Ninja, cfg: Config, sources: Sources): BunOutput { + const exeName = bunExeName(cfg); + + n.comment("════════════════════════════════════════════════════════════════"); + n.comment(` Building ${exeName} (rust-and-link — cpp archive from buildkite)`); + n.comment("════════════════════════════════════════════════════════════════"); + n.blank(); + + // ─── Rust (built here) ─── + // lolhtml fetch + codegen + cargo — same as emitRustOnly. The cargo edge + // runs while build-cpp is still compiling on its own agent; by the time + // ninja reaches the link edge, ci.ts has already downloaded the archive. + const lolhtmlDep = resolveDep(n, cfg, lolhtml, new Map()); + assert(lolhtmlDep !== null, "lolhtml resolveDep returned null — should never be skipped"); + + const codegen = emitCodegen(n, cfg, sources); + + const rustObjects = emitRust(n, cfg, { + codegenInputs: codegen.rustInputs, + codegenOrderOnly: codegen.rustOrderOnly, + rustSources: sources.rust, + vendorStamps: lolhtmlDep.outputs, + }); + + // ─── C++ archive + dep libs (downloaded, not built) ─── + // Paths computed exactly as emitLinkOnly does — must match cpp-only's + // output layout. ninja sees them as source inputs (no build rule). + const depLibs: string[] = []; + for (const dep of allDeps) { + depLibs.push(...computeDepLibs(cfg, dep)); + } + const archive = resolve(cfg.buildDir, `${cfg.libPrefix}${exeName}${cfg.libSuffix}`); + + // ─── Link ─── + const flags = computeFlags(cfg); + + n.comment("─── Link ───"); + n.blank(); + + const windowsRes = cfg.windows ? [emitWindowsResources(n, cfg)] : []; + + const shims = emitShims(n, cfg); + const linkObjects = [archive, ...rustLtoLinkInputs(n, cfg, rustObjects), ...windowsRes]; + const ldflags = [...flags.ldflags, ...systemLibs(cfg), ...shims.ldflags]; + const exe = link(n, cfg, exeName, linkObjects, { + libs: depLibs, + flags: ldflags, + implicitInputs: [...linkImplicitInputs(cfg), ...shims.implicitInputs], + linkerMapOutput: cfg.linux && cfg.release && !cfg.asan && !cfg.valgrind ? linkerMapPath(cfg) : undefined, + }); + + let strippedExe: string | undefined; + let dsym: string | undefined; + if (shouldStrip(cfg)) { + strippedExe = emitStrip(n, cfg, exe, flags.stripflags); + if (cfg.darwin) dsym = emitDsymutil(n, cfg, exe, exeName); + } + if (strippedExe === undefined) n.phony("bun", [exe]); + emitSmokeTest(n, cfg, exe, exeName); + + return { + exe, + strippedExe, + dsym, + deps: [lolhtmlDep], + codegen, + rustObjects, + objects: [], + }; +} + /** * Smoke test: run the built executable with --revision. If it crashes or * errors, the build failed — typically means a link-time issue that the @@ -662,9 +747,9 @@ function emitLinkOnly(n: Ninja, cfg: Config): BunOutput { * mismatch, etc.). */ function emitSmokeTest(n: Ninja, cfg: Config, exe: string, exeName: string): void { - // Cross-compiled binaries can't run on the build host. Skip the smoke - // test entirely — `ninja check` becomes a no-op alias for the exe. - if (cfg.crossTarget !== undefined) { + // Skip when the binary can't run on this host (different os/arch/abi) — + // `ninja check` becomes a no-op alias for the exe. + if (!cfg.canRunOnHost) { n.phony("check", [exe]); return; } diff --git a/scripts/build/ci.ts b/scripts/build/ci.ts index 006791e7ab8..4a1bd78b299 100644 --- a/scripts/build/ci.ts +++ b/scripts/build/ci.ts @@ -353,23 +353,24 @@ export function computeBunTriplet(cfg: Config): string { let t = `bun-${cfg.os}-${cfg.arch}`; if (cfg.abi === "musl") t += "-musl"; if (cfg.abi === "android") t += "-android"; - if (cfg.baseline) t += "-baseline"; + // No `-baseline` suffix: x64 is always baseline and the historical + // `-baseline` names are published as release-side aliases. return t; } /** - * Post-link packaging and upload for link-only mode. Runs AFTER ninja - * succeeds — at that point bun-profile (and stripped bun) exist. + * Post-link packaging and upload for link-only / rust-and-link mode. Runs + * AFTER ninja succeeds — at that point bun-profile (and stripped bun) exist. * * Generates features.json, packages into zips, * uploads. Contract with test steps: see block comment above. */ export function packageAndUpload(cfg: Config, output: BunOutput): void { - if (!isBuildkite || cfg.mode !== "link-only") return; + if (!isBuildkite || (cfg.mode !== "link-only" && cfg.mode !== "rust-and-link")) return; const exe = output.exe; if (exe === undefined) { - throw new BuildError("link-only packaging: output.exe unset"); + throw new BuildError(`${cfg.mode} packaging: output.exe unset`); } const buildDir = cfg.buildDir; @@ -381,11 +382,10 @@ export function packageAndUpload(cfg: Config, output: BunOutput): void { // Env vars match cmake's (BuildBun.cmake ~1462). // No setarch wrapper — cmake doesn't use one for features.mjs either // (only for the --revision smoke test). - // Cross-compiled binaries can't run on the build host — every field is a - // build-time constant, so generate the same payload host-side instead - // (the feature list is parsed out of src/analytics/lib.rs; see - // features-json.ts). - if (cfg.crossTarget !== undefined) { + // Binaries that can't run on this host: every field is a build-time + // constant, so generate the same payload host-side instead (the feature + // list is parsed out of src/analytics/lib.rs; see features-json.ts). + if (!cfg.canRunOnHost) { console.log("Generating features.json (host-side; cross-compiled binary cannot run here)..."); writeFileSync(resolve(buildDir, "features.json"), crossFeaturesJson(cfg)); } else { @@ -495,19 +495,25 @@ function makeZip(cfg: Config, name: string, files: string[]): string { } /** - * Download artifacts from sibling buildkite steps before a link-only build. - * Derives sibling step keys from BUILDKITE_STEP_KEY (swap `-build-bun` → - * `-build-cpp` / `-build-rust`). Gunzips any .gz files after download. + * Download artifacts from sibling buildkite steps before a link-only / + * rust-and-link build. Derives sibling step keys from BUILDKITE_STEP_KEY + * (swap `-build-bun` → `-build-cpp` / `-build-rust`). Gunzips any .gz files + * after download. + * + * rust-and-link runs in parallel with build-cpp (no depends_on), so it + * POLLS `buildkite-agent step get outcome` for the cpp step until it passes + * before attempting the download. link-only has depends_on and skips the + * poll. * * Call BEFORE ninja — the downloaded files are ninja's link inputs. */ export async function downloadArtifacts(cfg: Config): Promise { - if (cfg.mode !== "link-only") return; + if (cfg.mode !== "link-only" && cfg.mode !== "rust-and-link") return; const stepKey = process.env.BUILDKITE_STEP_KEY; if (stepKey === undefined) { throw new BuildError("BUILDKITE_STEP_KEY unset", { - hint: "link-only mode requires running inside a Buildkite job", + hint: `${cfg.mode} mode requires running inside a Buildkite job`, }); } @@ -519,17 +525,28 @@ export async function downloadArtifacts(cfg: Config): Promise { }); } const targetKey = m[1]!; + const cppStep = `${targetKey}-build-cpp`; + + // rust-and-link: no depends_on on build-cpp (it started alongside us so + // cargo could overlap). Poll its outcome; "passed" → download, any + // terminal failure → exit 1 with a clear message so the annotation points + // at build-cpp rather than a confusing "artifact not found" here. + if (cfg.mode === "rust-and-link") { + await waitForStepOutcome(cppStep); + } - // Both downloads at once (buildkite-agent already parallelizes within a - // step's artifact set; this overlaps the two STEPS). Gunzip after BOTH - // complete — the rust .a is gzipped too on posix, and the .gz scan is a - // recursive walk so we want every artifact on disk first. - const dl = (suffix: "cpp" | "rust") => { - const step = `${targetKey}-build-${suffix}`; + const dl = (step: string) => { console.log(`Downloading artifacts from ${step}...`); return runAsync(["buildkite-agent", "artifact", "download", "*", ".", "--step", step], cfg.buildDir); }; - await Promise.all([dl("cpp"), dl("rust")]); + if (cfg.mode === "rust-and-link") { + // rust built locally — only the cpp archive + dep libs are fetched. + await dl(cppStep); + } else { + // link-only: both siblings. Overlap the two downloads; gunzip after both + // complete (the .gz scan is a recursive walk, so everything on disk first). + await Promise.all([dl(cppStep), dl(`${targetKey}-build-rust`)]); + } // Recursive: rust artifact lands under rust-target///. const gzFiles: string[] = []; @@ -537,8 +554,11 @@ export async function downloadArtifacts(cfg: Config): Promise { if (!existsSync(dir)) return; for (const e of readdirSync(dir, { withFileTypes: true })) { const p = resolve(dir, e.name); - if (e.isDirectory()) walk(p); - else if (e.isFile() && e.name.endsWith(".gz")) gzFiles.push(relative(cfg.buildDir, p)); + if (e.isDirectory()) { + // rust-and-link built rust locally; skip cargo's huge output tree. + if (cfg.mode === "rust-and-link" && e.name === "rust-target") continue; + walk(p); + } else if (e.isFile() && e.name.endsWith(".gz")) gzFiles.push(relative(cfg.buildDir, p)); } }; walk(cfg.buildDir); @@ -579,6 +599,57 @@ function runAsync(argv: string[], cwd: string): Promise { }); } +/** + * Poll `buildkite-agent step get outcome --step ` until the step + * reaches a terminal state. Returns on "passed"; throws on any failure + * outcome so the caller exits 1 with a message that points at the real + * failing step (rather than a downstream "artifact not found"). + */ +async function waitForStepOutcome(stepKey: string): Promise { + const failed = new Set(["hard_failed", "soft_failed", "errored", "canceled", "cancelled"]); + const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + const start = Date.now(); + const deadlineMs = 60 * 60 * 1000; + let last = ""; + console.log(`Waiting for ${stepKey} to finish...`); + for (;;) { + const result = spawnSync("buildkite-agent", ["step", "get", "outcome", "--step", stepKey], { encoding: "utf8" }); + if (result.error) { + throw new BuildError(`Failed to spawn buildkite-agent`, { cause: result.error }); + } + if (result.status !== 0) { + const err = (result.stderr ?? "").trim(); + if (err !== last) { + console.log(` buildkite-agent step get exited ${result.status}: ${err}`); + last = err; + } + if (Date.now() - start > deadlineMs) { + throw new BuildError(`buildkite-agent step get kept failing for ${stepKey}`, { hint: err }); + } + await sleep(3000); + continue; + } + const outcome = (result.stdout ?? "").trim(); + if (outcome !== last) { + const elapsed = Math.round((Date.now() - start) / 1000); + console.log(` ${stepKey} outcome: ${outcome || "(running)"} [${elapsed}s]`); + last = outcome; + } + if (outcome === "passed") return; + if (failed.has(outcome)) { + throw new BuildError(`Sibling step ${stepKey} ${outcome} — nothing to link`, { + hint: `See the ${stepKey} job for the real error; this step only downloads its artifacts.`, + }); + } + if (Date.now() - start > deadlineMs) { + throw new BuildError(`Timed out after 60m waiting for ${stepKey}`, { + hint: `${stepKey} never reached a terminal outcome; check that job for a hang.`, + }); + } + await sleep(3000); + } +} + // ═══════════════════════════════════════════════════════════════════════════ // Symbol ordering file // @@ -628,12 +699,37 @@ export function orderFileContext(): OrderFileContext { /** Only builds that link, on targets that use an order file, outside PRs. */ export function orderFileEligible(cfg: Config, ctx: OrderFileContext): boolean { if (!usesOrderFile(cfg) || !ctx.buildkite || ctx.pullRequest) return false; - return cfg.mode === "full" || cfg.mode === "link-only"; + return cfg.mode === "full" || cfg.mode === "link-only" || cfg.mode === "rust-and-link"; } -/** Tracing runs the binary we just linked, which a cross build cannot execute. */ -function canTraceOrderFile(cfg: Config): boolean { - return cfg.crossTarget === undefined; +/** Tracing runs the binary we just linked, so the host must be able to execute it. */ +export function canTraceOrderFile(cfg: Config): boolean { + return cfg.canRunOnHost; +} + +/** + * An eligible lane that cannot trace (cross-compiled) and inherited nothing is + * shipping unordered with no way to recover on this host. Annotate so it is + * visible; the fix is tracing under qemu-user or on a native-arch step. + */ +export function reportOrderFileCannotTrace(cfg: Config): void { + const msg = + `${orderFileArtifact(cfg)}: nothing to inherit and this lane cross-compiles ` + + `(target ${cfg.crossTarget}), so the binary cannot be traced here. Shipping unordered.`; + console.log(`~ symbol order: ${msg}`); + if (!isBuildkite) return; + utils.reportAnnotationToBuildKite({ + style: "warning", + priority: 5, + label: "symbol order file", + content: utils.formatAnnotationToHtml({ + filename: "scripts/build/ci.ts", + title: "symbol order file: cross-compiled lane cannot trace, shipping unordered", + content: msg, + source: "build", + level: "warning", + }), + }); } /** Artifact name for the standalone order file: `bun-linux-x64.order`. */ diff --git a/scripts/build/compile.ts b/scripts/build/compile.ts index 57df6cc9a2a..66cadf18490 100644 --- a/scripts/build/compile.ts +++ b/scripts/build/compile.ts @@ -13,7 +13,7 @@ import { assert } from "./error.ts"; import { writeIfChanged } from "./fs.ts"; import type { BuildNode, Ninja, Rule } from "./ninja.ts"; import { quote } from "./shell.ts"; -import { machoPostlinkCommand } from "./shims.ts"; +import { elfDebugCompressPostlinkCommand, machoPostlinkCommand } from "./shims.ts"; import { streamPath } from "./stream.ts"; // --------------------------------------------------------------------------- @@ -170,7 +170,7 @@ export function registerCompileRules(n: Ninja, cfg: Config): void { n.rule("link", { command: cfg.windows ? `${wrap} ${cxx} /nologo -fuse-ld=lld ${q(`/clang:-B${dirname(cfg.ld)}`)} @$out.rsp /Fe$out /link $ldflags` - : `${wrap} ${cxx} @$out.rsp $ldflags -o $out${machoPostlinkCommand(cfg)}`, + : `${wrap} ${cxx} @$out.rsp $ldflags -o $out${elfDebugCompressPostlinkCommand(cfg)}${machoPostlinkCommand(cfg)}`, description: "link $out", rspfile: "$out.rsp", rspfile_content: "$in_newline", diff --git a/scripts/build/config.ts b/scripts/build/config.ts index 0da4cc90dad..62a91376f6e 100644 --- a/scripts/build/config.ts +++ b/scripts/build/config.ts @@ -21,7 +21,7 @@ export type OS = "linux" | "darwin" | "windows" | "freebsd"; export type Arch = "x64" | "aarch64"; export type Abi = "gnu" | "musl" | "android"; export type BuildType = "Debug" | "Release" | "RelWithDebInfo" | "MinSizeRel"; -export type BuildMode = "full" | "cpp-only" | "rust-only" | "link-only"; +export type BuildMode = "full" | "cpp-only" | "rust-only" | "link-only" | "rust-and-link"; export type WebKitMode = "prebuilt" | "local"; /** @@ -95,6 +95,13 @@ export interface Config { * quoteArgs(), tool executable suffixes. See Host type docs. */ host: Host; + /** + * True when the linked binary can execute on this host (same os+arch, and on + * linux same abi). Distinct from `crossTarget === undefined`: a native-arch + * linux-gnu build still passes --target/--sysroot for glibc pinning but the + * output runs fine here. + */ + canRunOnHost: boolean; // ─── Platform file conventions ─── // Centralized so a new target (or a forgotten .exe) is one edit away. @@ -134,7 +141,7 @@ export interface Config { asan: boolean; assertions: boolean; logs: boolean; - /** x64-only: target nehalem (no AVX) instead of haswell. */ + /** x64-only: target nehalem (no AVX). Default true on x64 — the only x64 build we ship. */ baseline: boolean; canary: boolean; /** MinSizeRel → optimize for size. */ @@ -359,6 +366,8 @@ export interface PartialConfig { freebsdSysroot?: string; /** FreeBSD release version (default: FREEBSD_VERSION_DEFAULT). Only used when os=freebsd. */ freebsdVersion?: string; + /** Linux glibc sysroot (pinned old glibc/libstdc++). Only used when linux && abi=gnu. */ + linuxSysroot?: string; /** * macOS SDK path (a MacOSX*.sdk directory). Only used when cross-compiling * for darwin from a non-darwin host; native darwin builds use xcrun. @@ -551,6 +560,32 @@ export function detectFreebsdSysroot(arch: Arch): string | undefined { return undefined; } +/** + * Locate the linux-gnu sysroot: ubuntu:20.04 (glibc 2.31) + gcc-13 libstdc++, + * matching the WebKit prebuilt's build environment. Arch-specific. See + * install_linux_glibc_sysroot() in scripts/bootstrap.sh. + */ +export function detectLinuxGlibcSysroot(arch: Arch): string | undefined { + const looksValid = (p: string) => existsSync(join(p, "usr", "include", "c++", "13")); + const env = process.env.LINUX_GLIBC_SYSROOT; + if (env && looksValid(env)) return env; + const candidate = arch === "aarch64" ? "/opt/linux-sysroot-glibc-arm64" : "/opt/linux-sysroot-glibc"; + return looksValid(candidate) ? candidate : undefined; +} + +/** + * Locate a linux-musl sysroot — alpine rootfs with musl + modern libstdc++; + * see install_linux_musl_sysroot() in scripts/bootstrap.sh. Checks env var then + * well-known install paths. Arch-specific. Returns undefined if none found. + */ +export function detectLinuxMuslSysroot(arch: Arch): string | undefined { + const looksValid = (p: string) => existsSync(join(p, "usr", "lib", "libc.so")); + const env = process.env.LINUX_MUSL_SYSROOT; + if (env && looksValid(env)) return env; + const candidate = arch === "aarch64" ? "/opt/linux-sysroot-musl-arm64" : "/opt/linux-sysroot-musl"; + return looksValid(candidate) ? candidate : undefined; +} + /** * Locate a Windows sysroot (xwin splat of the MSVC CRT/STL + Windows SDK in * Visual Studio layout). Checks the env var then well-known install paths. @@ -753,30 +788,18 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con // build:asan always set ENABLE_ASSERTIONS=ON for this reason. const assertions = partial.assertions ?? (debug || asan); - // Resolved early because the LTO defaults below need it (the windows - // -baseline WebKit prebuilt has no -lto variant). - const baseline = partial.baseline ?? false; - - // LTO: default on for CI release non-asan non-assertions builds on Linux - // and on darwin cross-compiles. Windows is NOT in the default even though - // the windows x64 cross toolchain fully supports ThinLTO + cross-language - // LTO (and `--lto=on` still builds that way): LLVM's ThinLTO backend - // pipeline miscompiles JSC on x86-64 at -O1 and above — JS-visible - // corruption in the bundler tests, the same family as the linux x86-64 - // ThinLTO miscompile that keeps linux on full LTO — and the regular-LTO - // route for COFF (full-LTO WebKit windows artifacts + a COFF rust summary - // fix-up) hasn't been built yet. Re-enable the default once one of those - // lands. The -lto WebKit prebuilts only exist for the cross toolchain, so - // native windows/darwin lanes are non-LTO regardless. - const ltoDefault = release && (linux || darwinCross) && ci && !assertions && !asan; + // LTO: default on for CI release non-asan non-assertions builds across + // linux, darwin-cross, and windows-cross. All three use ThinLTO (the JSC + // ThinLTO miscompile was fixed upstream). The -lto WebKit prebuilts only + // exist for the cross toolchain, so native windows/darwin stay non-LTO. + const windowsCross = windows && host.os !== "windows"; + const ltoDefault = release && (linux || darwinCross || windowsCross) && ci && !assertions && !asan; let lto = partial.lto ?? ltoDefault; // ASAN and LTO don't mix — ASAN wins (silently, no warn — config is explicit). // Android: no LTO prebuilt WebKit exists; force off so the right tarball is fetched. - // Windows arm64 / baseline: same — oven-sh/WebKit ships no - // bun-webkit-windows-arm64-lto (LLVM's CodeView emitter aborts on ARM64 - // NEON tuple registers during LTO codegen), and the pinned WEBKIT_VERSION - // predates the -baseline-lto variant. - if ((asan && lto) || abi === "android" || (windows && (arm64 || baseline))) { + // Windows arm64: oven-sh/WebKit ships no bun-webkit-windows-arm64-lto + // (LLVM's CodeView emitter aborts on ARM64 NEON tuple registers). + if ((asan && lto) || abi === "android" || (windows && arm64)) { lto = false; } @@ -851,7 +874,7 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con // Logs: on by default in debug non-test const logs = partial.logs ?? debug; - // (`baseline` is resolved earlier, next to the LTO defaults.) + const baseline = partial.baseline ?? x64; const canary = partial.canary ?? true; const canaryRevision = canary ? "1" : "0"; @@ -986,6 +1009,45 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con } } + // ─── Linux-gnu/musl sysroot + target ─── + // Every CI linux-gnu build (native AND cross-arch) uses the ubuntu:20.04 + + // gcc-13 sysroot so the glibc verneed matches what the --wrap list covers + // and the libstdc++ ABI matches the WebKit prebuilt. musl uses an + // alpine-derived sysroot. Local dev without a sysroot builds native. + if (linux && abi !== "android" && crossTarget === undefined) { + const llvmArch = x64 ? "x86_64" : "aarch64"; + const hostAbi = host.os === "linux" ? detectLinuxAbi() : undefined; + const isCross = arch !== host.arch || abi !== hostAbi; + if (abi === "musl") { + sysroot = detectLinuxMuslSysroot(arch); + if (sysroot !== undefined || isCross) { + crossTarget = `${llvmArch}-alpine-linux-musl`; + if (sysroot === undefined) { + const p = arch === "aarch64" ? "/opt/linux-sysroot-musl-arm64" : "/opt/linux-sysroot-musl"; + throw new BuildError(`--os=linux --arch=${arch} --abi=musl requires a musl sysroot when cross-compiling`, { + hint: `Set LINUX_MUSL_SYSROOT or provision ${p} (see install_linux_musl_sysroot() in scripts/bootstrap.sh).`, + }); + } + } + } else { + sysroot = + partial.linuxSysroot !== undefined + ? isAbsolute(partial.linuxSysroot) + ? partial.linuxSysroot + : resolve(cwd, partial.linuxSysroot) + : detectLinuxGlibcSysroot(arch); + if (sysroot !== undefined || isCross) { + crossTarget = `${llvmArch}-linux-gnu`; + if (sysroot === undefined) { + const p = arch === "aarch64" ? "/opt/linux-sysroot-glibc-arm64" : "/opt/linux-sysroot-glibc"; + throw new BuildError(`--os=linux --arch=${arch} --abi=gnu cross-compile requires a glibc sysroot`, { + hint: `Set LINUX_GLIBC_SYSROOT or provision ${p} (see install_linux_glibc_sysroot() in scripts/bootstrap.sh).`, + }); + } + } + } + } + // ─── Cross-compilation (Windows) ─── // Same pattern as Android/FreeBSD, with the MSVC spin: the host LLVM's // clang-cl/lld-link/llvm-lib/llvm-rc are used (tools.ts picks them by @@ -1063,7 +1125,7 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con crossTarget = `${arm64 ? "arm64" : "x86_64"}-apple-macosx`; osxDeploymentTarget = partial.osxDeploymentTarget ?? MIN_OSX_DEPLOYMENT_TARGET; // rust-only mode never compiles C/C++ or links, so it doesn't need the - // SDK — skip resolution to keep the shared CI rust box from downloading + // SDK — skip resolution so a rust-only build doesn't download // a ~730 MB sysroot it never reads. if ((partial.mode ?? "full") !== "rust-only") { osxSysroot = resolveMacosSdkPath(partial.macosSdk, cacheDir, cwd); @@ -1117,6 +1179,7 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con x64, arm64, host, + canRunOnHost: os === host.os && arch === host.arch && (!linux || abi === (detectLinuxAbi() ?? abi)), exeSuffix, objSuffix, libPrefix, @@ -1164,7 +1227,16 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con rustLld: toolchain.rustLld, rustLlvmVersion: toolchain.rustLlvmVersion, rustSysroot: toolchain.rustSysroot, - strip: ld64StripSwap?.strip ?? toolchain.strip, + // Cross strips: linux-gnu uses -strip (GNU, handles -R .eh_frame + // fully; host strip rejects foreign-arch ELF); other cross targets use + // llvm-strip. + strip: + ld64StripSwap?.strip ?? + (crossTarget !== undefined + ? linux && abi === "gnu" && existsSync(`/usr/bin/${crossTarget}-strip`) + ? `/usr/bin/${crossTarget}-strip` + : (toolchain.llvmStrip ?? toolchain.strip) + : toolchain.strip), dsymutil: toolchain.dsymutil, bun: toolchain.bun, jsRuntime: toolchain.jsRuntime, diff --git a/scripts/build/deps/brotli.ts b/scripts/build/deps/brotli.ts index 15d55321da3..12eb3514018 100644 --- a/scripts/build/deps/brotli.ts +++ b/scripts/build/deps/brotli.ts @@ -53,16 +53,6 @@ export const brotli: Dependency = { pic: true, }; - // LTO miscompile: on linux-x64 with AVX (non-baseline), BrotliDecompress - // errors out mid-stream. Root cause unknown — likely an alias-analysis - // issue around brotli's ring-buffer copy hoisting. -fno-lto sidesteps it. - // Linux-only: clang's LTO on darwin/windows has a different codepath. - // x64+non-baseline only: the SSE/AVX path is where the miscompile lives; - // baseline (SSE2-only) doesn't hit it. - if (cfg.linux && cfg.x64 && !cfg.baseline) { - spec.cflags = ["-fno-lto"]; - } - return spec; }, diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 94cbe6b261d..e50f02454d2 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -5,9 +5,10 @@ */ // oven-sh/WebKit main: macOS + Windows artifacts cross-compiled on Linux, // -lto variants built with ThinLTO (per-module summaries for cross-language -// importing), and the Windows ICU data table filtered + per-item zstd -// compressed (lazily decompressed via bun_icu_decompress.cpp). -export const WEBKIT_VERSION = "a0e65bf298499d828f0c60d4557899b94a69d7ae"; +// importing), every x64 at the nehalem floor (no separate -baseline variant), +// typed-array constructor ClassInfo kept address-unique under LTO, and the +// Windows ICU data table filtered + per-item zstd compressed. +export const WEBKIT_VERSION = "c9296e353e365ecf0de82f273bb0a88a3df465be"; /** * WebKit (JavaScriptCore) — the JS engine. @@ -56,17 +57,13 @@ import { type Dependency, type NestedCmakeBuild, type Source, depBuildDir, depSo /** * Tarball suffix encoding ABI-affecting flags. MUST match the WebKit - * release workflow naming in oven-sh/WebKit's CI. + * release workflow naming in oven-sh/WebKit's CI. There is no -baseline + * variant: every x64 WebKit is built at the nehalem floor. */ function prebuiltSuffix(cfg: Config): string { let s = ""; if (cfg.linux && cfg.abi === "musl") s += "-musl"; if (cfg.linux && cfg.abi === "android") s += "-android"; - // Baseline WebKit artifacts (-march=nehalem, /arch:SSE2 ICU) exist for - // Linux amd64 (glibc + musl) and Windows amd64. No baseline variant for - // arm64 or macOS. Suffix order matches the release asset names: - // bun-webkit-linux-amd64-musl-baseline-lto.tar.gz - if (cfg.baseline && cfg.x64) s += "-baseline"; if (cfg.debug) s += "-debug"; else if (cfg.lto) s += "-lto"; if (cfg.asan) s += "-asan"; @@ -237,7 +234,7 @@ export const webkit: Dependency = { // forward: // - CPU target (-march/-mcpu): WebKit never sets this — without it, // local builds target generic x86-64 while bun + prebuilt WebKit - // target haswell/nehalem. + // target nehalem. // - LTO/PGO: WebKit's cmake doesn't set those itself. // // Windows: ICU built from source via preBuild before cmake configure. diff --git a/scripts/build/flags.ts b/scripts/build/flags.ts index 8ee233233a9..b17ce9f9a4a 100644 --- a/scripts/build/flags.ts +++ b/scripts/build/flags.ts @@ -69,13 +69,8 @@ export const cpuTargetFlags: Flag[] = [ }, { flag: "-march=nehalem", - when: c => c.x64 && c.baseline, - desc: "x64 baseline: Nehalem (2008) — no AVX, broadest compatibility", - }, - { - flag: "-march=haswell", - when: c => c.x64 && !c.baseline, - desc: "x64 default: Haswell (2013) — AVX2, BMI2 available", + when: c => c.x64, + desc: "x64: Nehalem (2008) — no AVX, broadest compatibility", }, ]; @@ -472,31 +467,12 @@ export const globalFlags: Flag[] = [ // WebKit macos -lto prebuilts and rustc's -Clinker-plugin-lto bitcode are // both ThinLTO-summaried, so this makes the whole link one uniform // ThinLTO graph with cross-module importing across C++/Rust/JSC - // boundaries. Darwin only for now — see the -flto=full entry below. + // boundaries. All platforms now use ThinLTO (the linux JSC ThinLTO + // miscompile was fixed in the WebKit prebuilt). flag: "-flto=thin", - when: c => c.darwin && c.lto, + when: c => c.unix && c.lto, desc: "Thin link-time optimization", }, - { - // Linux stays on full LTO: the LLVM 22 ThinLTO backend pipeline - // (rust-lld) miscompiles JavaScriptCore on linux at every opt level - // above --lto-O0 — a JIT-tier correctness bug plus several bundler hangs - // in the test suite, on both x64 and aarch64, with cross-module - // importing disabled, ICF ruled out, and WPD ruled out. The same - // bitcode through ld64.lld on darwin is fine. Full LTO uses a different - // (regular-LTO) pass pipeline over one merged module and has shipped - // green for months. Cost: the link is serial (~14 min vs ~1.5 min). - // Rust<->C++ cross-language inlining still happens: the Rust side emits - // one fat, summary-less bitcode module (CARGO_PROFILE_RELEASE_LTO=fat - // under -Clinker-plugin-lto — see rust.ts) that joins the same - // regular-LTO partition as the C++, so nothing goes through the - // miscompiling ThinLTO backends. Revisit ThinLTO once the bad pass is - // isolated — the repro is `bun -e 'require("axobject-query")'` failing - // in the DFG tier. - flag: "-flto=full", - when: c => c.unix && !c.darwin && c.lto, - desc: "Full link-time optimization (linux: ThinLTO miscompiles JSC, see comment)", - }, { // Windows (cross) uses ThinLTO like darwin: clang-cl accepts -flto=thin // directly (core option), the WebKit windows-amd64-lto prebuilt is @@ -532,13 +508,9 @@ export const globalFlags: Flag[] = [ // (typeidCompatibleVTable entries) and whole-program devirtualization // runs in index-based mode via --lto-whole-program-visibility at link // time. 0 is also the default for rustc, for Apple targets, and for the - // WebKit macos/windows -lto prebuilts, so this is the configuration that - // can't drift. Windows: -fwhole-program-vtables is never passed there - // (see above) so 0 is already the default — kept explicit so the - // ThinLTO graph can't drift if that ever changes. Not linux: full LTO - // (no per-module summaries, so the flag is meaningless there). + // WebKit -lto prebuilts, so this is the configuration that can't drift. flag: "-fno-split-lto-unit", - when: c => (c.darwin || c.windows) && c.lto, + when: c => c.lto, desc: "Index-based WPD: keep type metadata in the ThinLTO summaries, no regular-LTO half", }, @@ -868,21 +840,24 @@ export const linkerFlags: Flag[] = [ // only fires for classes explicitly annotated [[clang::lto_visibility]], // i.e. never. A static executable that only dlopens C-ABI addons (NAPI) // satisfies the whole-program assumption. ld64.lld has no named option - // for this; -mllvm reaches the underlying cl::opt directly. Darwin only: - // linux is on full LTO where this was never enabled. + // for this; -mllvm reaches the underlying cl::opt directly. flag: ["-Wl,-mllvm,-whole-program-visibility"], when: c => c.darwin && c.lto, desc: "Enable index-based whole-program devirtualization at link time", }, { - flag: ["-flto=thin", "-fwhole-program-vtables", "-fforce-emit-vtables"], - when: c => c.darwin && c.lto, - desc: "LTO at link time (matches compile-side -flto=thin)", + // ELF spelling of the entry above. The WebKit -lto prebuilts carry the + // !type/!vcall_visibility metadata (built with -fwhole-program-vtables), + // so this upgrades JSC/WTF's exported classes to hidden LTO visibility + // and lets WPD fire on them, not just on our -fvisibility=hidden classes. + flag: ["-Wl,--lto-whole-program-visibility"], + when: c => c.unix && !c.darwin && c.lto, + desc: "Enable index-based whole-program devirtualization at link time (lld ELF)", }, { - flag: ["-flto=full", "-fwhole-program-vtables", "-fforce-emit-vtables"], - when: c => c.unix && !c.darwin && c.lto, - desc: "LTO at link time (matches compile-side -flto=full)", + flag: ["-flto=thin", "-fwhole-program-vtables", "-fforce-emit-vtables"], + when: c => c.unix && c.lto, + desc: "LTO at link time (matches compile-side -flto=thin)", }, { // Without -O at link time, clang's driver defaults LTO codegen to -O2. @@ -1123,10 +1098,15 @@ export const linkerFlags: Flag[] = [ // ─── Linux ─── { - // Wrap glibc symbols whose default version on a modern build host is - // > 2.17. Each __wrap_X in workaround-missing-symbols.cpp pins to the + flag: c => [`--target=${c.crossTarget!}`, `--sysroot=${c.sysroot!}`], + when: c => c.linux && c.abi !== "android" && c.crossTarget !== undefined && c.sysroot !== undefined, + desc: "linux sysroot link (gnu: ubuntu:20.04+gcc-13; musl: alpine)", + }, + { + // Wrap glibc symbols whose default version on the sysroot's glibc (2.31) + // is > 2.17. Each __wrap_X in workaround-missing-symbols.cpp pins to the // 2.2.5/2.17 compat version (or a raw syscall) so the binary's verneed - // never exceeds the floor regardless of the host's glibc. + // never exceeds the floor. flag: [ "exp", "exp2", @@ -1378,13 +1358,13 @@ export const linkerFlags: Flag[] = [ // LLVM_ENABLE_ZLIB or did not find zlib at build time`. // We only fall onto rust-lld for cross-language LTO when rustc's LLVM is // newer than the system clang/lld (see config.ts `cfg.ld` selection); in - // that case, drop the flag rather than fail the link. Larger debug - // sections in `bun-profile` is a build-size cost, not a correctness one — - // and only on agents where the LLVM versions diverge. The system lld path - // (linux/freebsd llvm-* packages) keeps compressing. + // that case the link-time flag is dropped and llvm-objcopy compresses + // post-link instead (shims.ts elfDebugCompressPostlinkCommand) — an + // uncompressed bun-profile is ~2x larger and every `--compile` test + // copies it, so leaving it uncompressed times CI out. flag: "-Wl,--compress-debug-sections=zlib", when: c => (c.linux || c.freebsd) && c.ld !== c.rustLld, - desc: "Compress ELF debug sections (skipped with rust-lld — built without zlib)", + desc: "Compress ELF debug sections (post-link via llvm-objcopy with rust-lld — built without zlib)", }, { flag: "-Wl,--gc-sections", @@ -1670,7 +1650,7 @@ export function computeDepFlags(cfg: Config): { cflags: string[]; cxxflags: stri /** * Just the -march/-mcpu/-mtune flags. For deps (WebKit) whose own build system * sets -O/-g/sanitizer flags but never sets a CPU target, so without this they - * end up targeting generic x86-64 while the rest of bun targets haswell. + * end up targeting generic x86-64 while the rest of bun targets nehalem. */ export function computeCpuTargetFlags(cfg: Config): string[] { const out: string[] = []; diff --git a/scripts/build/profiles.ts b/scripts/build/profiles.ts index 22de2b7a31c..ffcb9885a7d 100644 --- a/scripts/build/profiles.ts +++ b/scripts/build/profiles.ts @@ -198,9 +198,8 @@ export const profiles = { /** * CI: compile libbun_rust.a only. Target platform via --os/--arch - * overrides (cargo `--target `; linux/freebsd/darwin targets - * cross-compile from the shared linux box, windows from the amazonlinux - * fleet with a fetched winsysroot — see `rustCanCrossFromLinux()`). + * overrides (cargo `--target `). Superseded in CI by + * `ci-rust-and-link`; kept for ad-hoc rust-only builds. */ "ci-rust-only": { buildType: "Release", @@ -219,6 +218,20 @@ export const profiles = { webkit: "prebuilt", }, + /** + * CI: cargo build + link on one machine. Polls the sibling build-cpp step + * for its archive/dep-lib artifacts, then links and packages. Saves an + * agent spawn vs rust-only → link-only. Resolves the full toolchain (link + * needs ld/strip/rc), unlike rust-only. + */ + "ci-rust-and-link": { + buildType: "Release", + mode: "rust-and-link", + ci: true, + buildkite: true, + webkit: "prebuilt", + }, + /** CI full build with LTO. */ "ci-release": { buildType: "Release", diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index 73ef1993238..7ceb5cc2065 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -70,45 +70,6 @@ function cargoProfile(cfg: Config): { name: string; subdir: string } { return cfg.buildType === "Debug" ? { name: "dev", subdir: "debug" } : { name: "release", subdir: "release" }; } -/** - * Can a linux host cross-compile the Rust staticlib for `cfg`'s target - * without a native runner? - * - * Used by CI's `build-rust` step to decide fan-out: targets that return - * `true` here share one fast linux box (one `cargo build --target ` - * each); targets that return `false` get a native agent. - * - * linux-{gnu,musl,android} × {x64,aarch64}: yes — `rustup target add` - * installs prebuilt std, no foreign linker needed for a staticlib. - * freebsd × {x64,aarch64}: yes — Tier 2/3 (`-Zbuild-std` for aarch64), - * staticlib needs no FreeBSD libc to produce. - * darwin × {x64,aarch64}: yes — Tier 2 prebuilt std, and a staticlib - * needs no Mach-O link. No build script in the current dep graph - * compiles C for the target; if one ever does, emitRust's - * CFLAGS_/SDKROOT forwarding (set when the SDK is resolved) - * points cc-rs at the macOS SDK. - * windows-msvc × {x64,aarch64}: yes *when a Windows sysroot (xwin splat) - * is present* — the staticlib itself needs no SDK, but the bun_shim_impl - * PE that emitRust() also builds links against kernel32/ntdll import - * libs via lld-link + /winsysroot (see config.ts `winsysroot`). The - * shared CI rust box doesn't carry a splat, so CI runs these on the - * amazonlinux fleet instead, where configure fetches one per build. - * - * Unlike zig (which bundled its own libc/SDK for every target), cargo - * delegates to a system C toolchain for any `cc`/`bindgen`/link step, so - * the cross-compile boundary is "does the host have a C cross-toolchain - * for the target", not "does rustc support the triple". - */ -export function rustCanCrossFromLinux(cfg: Config): boolean { - if (cfg.linux) return true; // gnu, musl, android — all archs - if (cfg.freebsd) return true; - if (cfg.darwin) return true; - // windows: possible with a winsysroot (see above), but the shared rust - // box isn't provisioned with one — CI routes windows rust-only to the - // amazonlinux fleet (which fetches a splat at configure time) instead. - return false; -} - /** * All target triples CI builds. Exposed so `rust:check-all` can iterate * `cargo check --target ` without re-deriving the list. @@ -297,8 +258,10 @@ export function registerRustRules(n: Ninja, cfg: Config): void { const rustup = findRustup(cfg); if (rustup !== undefined && cfg.rustToolchain !== undefined) { + // `-q` + `--no-self-update` silence the five `info:` lines rustup prints + // on every no-op reinstall; warnings/errors still show. const chain = - `${stream} --console $env ${q(rustup)} toolchain install ${cfg.rustToolchain} --force --component rust-src $rust_target_arg && ` + + `${stream} --console $env ${q(rustup)} -q toolchain install ${cfg.rustToolchain} --force --no-self-update --component rust-src $rust_target_arg && ` + `${stream} --console --cwd=$cwd $env ${q(cfg.cargo)} build $args`; n.rule("rust_build_cross", { command: hostWin ? `cmd /c "${chain}"` : chain, @@ -449,16 +412,22 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string // `-Cllvm-args=-addrsig` sets the same LLVM module flag clang's `-faddrsig` // does. Harmless on Apple ld64 (ignores the section). rustflags.push("-Cllvm-args=-addrsig"); + // Reuse an upstream crate's monomorphization instead of re-instantiating + // it locally. rustc defaults this on only at opt-level 0/1/s/z: at O2/O3 a + // shared generic is an out-of-line upstream symbol the caller can't + // inline. Cross-language ThinLTO re-imports and inlines any callee under + // the import threshold at link time, so here it only dedups the large + // bodies nobody inlines. Nightly-only; the pinned toolchain is nightly. + // Not under ASAN: routing Box/Vec allocs through the shared alloc-crate + // instantiation moves their frames and LSAN's conservative reachability + // loses some at-exit allocations it previously found (bun-info, bun-audit, + // issue 30205), turning benign at-exit state into reported leaks. + if (!cfg.asan) rustflags.push("-Zshare-generics=y"); // Match the C++ side's CPU target (`cpuTargetFlags` in flags.ts) so Rust - // codegen sees the same ISA. Without this, C++ is built with - // `-march=haswell` while Rust defaults to generic x86-64 (SSE2 only), - // leaving auto-vectorization and BMI/LZCNT/POPCNT on the table for the - // entire Rust crate graph. rustc's `-C target-cpu=` takes LLVM CPU names + // codegen sees the same ISA. rustc's `-C target-cpu=` takes LLVM CPU names // (same vocabulary as clang's `-march=`/`-mcpu=`), so the mapping is 1:1. const cpuTarget = cfg.x64 - ? cfg.baseline - ? "nehalem" - : "haswell" + ? "nehalem" : cfg.darwin ? "apple-m1" : // armv8-a+crc isn't a CPU name — closest LLVM model with CRC baseline: @@ -606,35 +575,14 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string rustflags.push("-Cembed-bitcode=yes"); // EnableSplitLTOUnit consistency: lld errors with "inconsistent LTO Unit // splitting" if any bitcode module in the link disagrees with the others. - // The Rust value must match whatever the C++ side produces, and that - // differs per platform: - // - // - darwin (ThinLTO): the C/C++ side passes -fno-split-lto-unit - // everywhere (index-based WPD, no hybrid split mode) and Apple - // targets default to 0 anyway, so every C/C++ module is 0. rustc's - // default is also 0 — pass nothing. Adding -Zsplit-lto-unit here - // would make the Rust modules the inconsistent ones and abort the - // link. - // - windows cross (ThinLTO): same as darwin — clang-cl never gets - // -fwhole-program-vtables (COFF associative-COMDAT abort) and - // -fno-split-lto-unit is passed explicitly, so every C/C++ module is - // 0 and rustc's default 0 matches — pass nothing. - // - linux (full LTO): the regular-LTO summary clang writes on ELF - // always says EnableSplitLTOUnit=1, so every C++ module (ours and - // the WebKit -lto prebuilts) carries 1. The Rust modules' - // EnableSplitLTOUnit module flag must say 1 to match → - // -Zsplit-lto-unit. (The flag is stamped per-CGU at module - // creation, survives rustc's fat-LTO pre-merge, and is what the - // rust_lto_fix step's `opt --module-summary` copies into the - // regular-LTO summary it bolts onto the merged module — see - // rust-lto-fix-cli.ts.) - // - // (`-Clink-arg=-fuse-ld=lld` is pushed unconditionally above — under LTO - // it doubles as making rustc's bitcode link go through the LTO-aware - // linker our final link uses, not BFD `/usr/bin/ld`.) + // Every LTO platform now links ThinLTO with the C/C++ side passing + // -fno-split-lto-unit (index-based WPD, no hybrid split), so every C/C++ + // module (ours and the WebKit -lto prebuilts) says 0. rustc's default is + // also 0, so pass nothing. (`-Clink-arg=-fuse-ld=lld` is pushed + // unconditionally above — under LTO it doubles as making rustc's bitcode + // link go through the LTO-aware linker our final link uses, not BFD + // `/usr/bin/ld`.) if (!cfg.darwin && !cfg.windows) { - rustflags.push("-Zsplit-lto-unit"); - // Rust functions default to carrying the `uwtable(async)` attribute. // When the LTO inliner inlines such a callee into one of our C++ // callers (compiled without unwind tables), the caller inherits the @@ -714,34 +662,14 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string } } if (cfg.crossLangLto) { - // The Rust bitcode's shape must match the platform's C++ LTO mode so both - // sides land in the same LTO partition at link time and can exchange - // function bodies. (The workspace `[profile.release] lto = "fat"` exists - // for non-LTO release builds, where the rust .a is linked as + // Every crossLangLto platform links ThinLTO, so leave each crate's per-CGU + // bitcode with its ThinLTO summary intact: the whole link is one uniform + // ThinLTO graph and cross-module importing works across Rust↔C++/JSC. + // `fat` would pre-merge the crates into one summary-less blob the thin + // link can't import from. (The workspace `[profile.release] lto = "fat"` + // exists for non-LTO release builds, where the rust .a is linked as // already-codegen'd machine code and still wants intra-Rust inlining.) - // - // - darwin / windows cross (ThinLTO links): `off`. Each crate's - // per-CGU bitcode keeps its ThinLTO summary, so the whole link is - // one uniform ThinLTO graph and cross-module importing works across - // Rust↔C++/JSC. `fat` would pre-merge the crates into one - // summary-less blob the thin link can't import from (and rustc's - // serial pre-merge is wasted work — the linker schedules the - // backends itself). - // - ELF (full-LTO link — see the -flto=full entry in flags.ts): `fat`. - // rustc pre-merges every crate (including the prebuilt std's - // embedded bitcode) into ONE summary-less regular-LTO module, which - // lld then merges into the same regular-LTO partition as the C++ - // `-flto=full` objects — that merge is what gives Rust↔C++ - // cross-language inlining under full LTO. (The merged module first - // gets a regular-LTO summary bolted on by the rust_lto_fix edge — - // rustLtoLinkInputs() below — because lld's EnableSplitLTOUnit - // consistency check requires one.) With `off`, the per-CGU - // ThinLTO-summaried modules are processed as ThinLTO partitions - // instead, which (a) never exchange function bodies with the C++ - // regular partition (no cross-language inlining at all), and - // (b) go through the LLVM 22 ThinLTO backend pipeline that - // miscompiles JSC on linux. - env.CARGO_PROFILE_RELEASE_LTO = cfg.darwin || cfg.windows ? "off" : "fat"; + env.CARGO_PROFILE_RELEASE_LTO = "off"; } else if (cfg.asan) { // release-asan has `cfg.lto` forced off (config.ts), but without this // override Cargo.toml's `[profile.release] lto = "fat"` still applies — @@ -936,7 +864,10 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string */ export function rustLtoLinkInputs(n: Ninja, cfg: Config, rustObjects: string[]): string[] { const rustLib = rustObjects[0]; - if (!cfg.crossLangLto || cfg.darwin || cfg.windows || rustLib === undefined) return rustObjects; + // All LTO platforms now use ThinLTO with -fno-split-lto-unit and per-CGU + // rust bitcode (CARGO_PROFILE_RELEASE_LTO=off), so the regular-LTO summary + // fix-up below is never needed. Delete this function once confirmed. + if (cfg.lto || !cfg.crossLangLto || cfg.darwin || cfg.windows || rustLib === undefined) return rustObjects; assert( cfg.rustSysroot !== undefined && cfg.host.rustTriple !== undefined, "ELF cross-language LTO needs rustc's sysroot to locate its LLVM tools (llvm-link/opt) for the regular-LTO summary fix-up, but rustc wasn't found", diff --git a/scripts/build/shims.ts b/scripts/build/shims.ts index 4aaa6a1118c..f446d97f4d9 100644 --- a/scripts/build/shims.ts +++ b/scripts/build/shims.ts @@ -11,8 +11,8 @@ */ import { spawnSync } from "node:child_process"; -import { mkdirSync } from "node:fs"; -import { resolve } from "node:path"; +import { existsSync, mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; import type { Config } from "./config.ts"; import { DARWIN_STACK_SIZE } from "./flags.ts"; import type { Ninja } from "./ninja.ts"; @@ -95,6 +95,30 @@ export function machoPostlinkImplicitInputs(cfg: Config): string[] { return [machoPostlinkToolPath(cfg), machoEntitlementsPlist(cfg)]; } +/** + * ELF + rust-lld: rust-lang/llvm-project builds lld without LLVM_ENABLE_ZLIB, + * so `-Wl,--compress-debug-sections=zlib` is dropped when the crosslang-LTO + * rust-lld swap is active (flags.ts). Uncompressed DWARF makes bun-profile + * ~2x larger (~900MB), and every `bun build --compile` in the test suite + * copies the running binary, so the size shows up as CI test timeouts, not + * just artifact bloat. Compress after the link with llvm-objcopy instead — + * same tool the musl CRT decompress shim already relies on. + */ +export function needsElfDebugCompressPostlink(cfg: Config): boolean { + return (cfg.linux || cfg.freebsd) && cfg.rustLld !== undefined && cfg.ld === cfg.rustLld; +} + +/** + * Command suffix for the link rule: `... -o $out && llvm-objcopy + * --compress-debug-sections=zlib $out`. Empty when not needed so callers + * can append unconditionally (mutually exclusive with machoPostlinkCommand). + */ +export function elfDebugCompressPostlinkCommand(cfg: Config): string { + if (!needsElfDebugCompressPostlink(cfg)) return ""; + const llvmObjcopy = resolve(dirname(cfg.cc), "llvm-objcopy"); + return ` && ${quote(existsSync(llvmObjcopy) ? llvmObjcopy : "llvm-objcopy", false)} --compress-debug-sections=zlib $out`; +} + /** * macOS-from-Linux cross links resolve compiler-rt builtins from the SDK's * libSystem reexport (libcompiler_rt.tbd), which covers the generic builtins @@ -166,11 +190,12 @@ export function registerShimRules(n: Ninja, cfg: Config): void { } if (needsMuslCrtDecompress(cfg)) { - // binutils objcopy (same package as `strip`, already required on linux — - // see tools.ts). restat=1: a no-op decompress keeps the mtime so the - // link doesn't re-run. + // llvm-objcopy (multi-target; host GNU objcopy rejects foreign-arch ELF). + // Resolve it next to clang (debian has no unversioned symlink on PATH). + // restat=1: a no-op decompress keeps the mtime so the link doesn't re-run. + const llvmObjcopy = resolve(dirname(cfg.cc), "llvm-objcopy"); n.rule("shim_crt_decompress", { - command: `objcopy --decompress-debug-sections $in $out`, + command: `${q(existsSync(llvmObjcopy) ? llvmObjcopy : "llvm-objcopy")} --decompress-debug-sections $in $out`, description: "decompress-crt $out", restat: true, }); @@ -242,12 +267,19 @@ export function emitShims(n: Ninja, cfg: Config): ShimLinkOpts { // tiny dir, no point routing through the obj-dir set). mkdirSync(crtDir, { recursive: true }); + // Cross-compiling musl from a glibc host: point the probe at the musl + // sysroot so clang resolves the CRT there instead of the host /usr/lib. + // Native musl (sysroot undefined) keeps the bare probe. + const probeArgs = cfg.sysroot !== undefined ? [`--target=${cfg.crossTarget!}`, `--sysroot=${cfg.sysroot}`] : []; + for (const name of MUSL_CRT_OBJECTS) { // Ask clang where it would find this startfile. Legitimate // configure-time spawn (environment probe, not a build artifact). // If the file isn't installed clang echoes the bare name back — // skip those rather than emit a broken edge. - const found = spawnSync(cfg.cc, [`-print-file-name=${name}`], { encoding: "utf8" }).stdout.trim(); + const found = spawnSync(cfg.cc, [...probeArgs, `-print-file-name=${name}`], { + encoding: "utf8", + }).stdout.trim(); if (!found || found === name) continue; const out = resolve(crtDir, name); n.build({ outputs: [out], rule: "shim_crt_decompress", inputs: [found] }); diff --git a/scripts/build/source.ts b/scripts/build/source.ts index cbad9e8a56b..1696175dd00 100644 --- a/scripts/build/source.ts +++ b/scripts/build/source.ts @@ -601,8 +601,8 @@ export function registerDepRules(n: Ninja, cfg: Config): void { const rustup = q(join(dirname(cfg.cargo), `rustup${cfg.host.exeSuffix}`)); const cargoCrossEnsure = cfg.rustToolchain !== undefined - ? `${stream} $env ${rustup} toolchain install ${cfg.rustToolchain} --force --component rust-src --target $rust_target` - : `${stream} $env ${rustup} target add $rust_target`; + ? `${stream} $env ${rustup} -q toolchain install ${cfg.rustToolchain} --force --no-self-update --component rust-src --target $rust_target` + : `${stream} $env ${rustup} -q target add $rust_target`; // Windows: ninja runs commands via CreateProcess (no shell) — wrap in // `cmd /c "..."` so `&&` is interpreted as a chain operator instead of // being passed as a literal arg. See rust.ts `rust_build_cross`. diff --git a/scripts/build/tools.ts b/scripts/build/tools.ts index ff216313129..5bd38265c25 100644 --- a/scripts/build/tools.ts +++ b/scripts/build/tools.ts @@ -664,11 +664,15 @@ export function findRustLld(os: OS): { const rustup = findTool({ names: ["rustup"], paths: [join(cargoHome, "bin")], required: false })?.path; const channel = readRustToolchainChannel(); if (rustup !== undefined && channel !== undefined) { - spawnSync(rustup, ["toolchain", "install", channel, "--force", "--profile", "minimal", "--component", "rust-src"], { - encoding: "utf8", - timeout: 300_000, - stdio: ["ignore", "ignore", "inherit"], // surface download/error output - }); + spawnSync( + rustup, + ["-q", "toolchain", "install", channel, "--no-self-update", "--profile", "minimal", "--component", "rust-src"], + { + encoding: "utf8", + timeout: 300_000, + stdio: ["ignore", "ignore", "inherit"], // surface download/error output; `-q` hides `info:` noise + }, + ); } // One spawn for both sysroot and host triple / LLVM version. `-vV` prints diff --git a/scripts/build/workarounds.ts b/scripts/build/workarounds.ts index 1747bbaf906..b97b59fb232 100644 --- a/scripts/build/workarounds.ts +++ b/scripts/build/workarounds.ts @@ -105,7 +105,7 @@ export const workarounds: Workaround[] = [ }, cleanup: `Delete scripts/build/rust-lto-fix-cli.ts, the rust_lto_fix rule and rustLtoLinkInputs() in ` + - `rust.ts, unwrap its two call sites in bun.ts, drop "llvm-tools" from rust-toolchain.toml's ` + + `rust.ts, unwrap its call sites in bun.ts, drop "llvm-tools" from rust-toolchain.toml's ` + `components, and delete this entry.`, }, { @@ -113,7 +113,8 @@ export const workarounds: Workaround[] = [ issue: "https://rustc-dev-guide.rust-lang.org/backend/updating-llvm.html", description: "rustc's bundled LLVM is newer than clang's, so clang's ld.lld can't read " + - "-Clinker-plugin-lto bitcode (forward-compatible only). Link with rust-lld instead.", + "-Clinker-plugin-lto bitcode (forward-compatible only). Link with rust-lld instead " + + "(and compress ELF debug sections post-link via llvm-objcopy, since rust-lld lacks zlib).", applies: cfg => cfg.crossLangLto && cfg.rustLlvmVersion !== undefined && cfg.clangVersion !== undefined, expectedToBeFixed: cfg => { // Obsolete once clang's LLVM major catches up to (or passes) rustc's — diff --git a/scripts/packer/build-image.pkr.hcl b/scripts/packer/build-image.pkr.hcl new file mode 100644 index 00000000000..c1b3f444434 --- /dev/null +++ b/scripts/packer/build-image.pkr.hcl @@ -0,0 +1,224 @@ +// Linux (debian-13) build image for local dev (docker) and CI (AWS AMI). +// Both builders run the same scripts/bootstrap.sh provisioner so the two +// environments stay in lock-step. +// +// Shared variables (repo_ref, bootstrap_script, agent_script, image_name, +// build_number) live in variables.pkr.hcl — `packer init`/`build` is invoked +// on the whole directory (see scripts/machine.mjs), so redeclaring them here +// would be a duplicate-variable error. + +packer { + required_plugins { + docker = { + source = "github.com/hashicorp/docker" + version = ">= 1.0.0" + } + amazon = { + source = "github.com/hashicorp/amazon" + version = ">= 1.3.0" + } + } +} + +variable "arch" { + type = string + default = "x64" + description = "Target architecture: x64 or aarch64." + validation { + condition = contains(["x64", "aarch64"], var.arch) + error_message = "The arch variable must be one of: x64, aarch64." + } +} + +variable "ci" { + type = bool + default = true + description = "Pass --ci to bootstrap.sh (installs buildkite-agent, sysroots, prefetch cache)." +} + +variable "version" { + type = string + default = "0" + description = "Bootstrap version (the `# Version:` comment in scripts/bootstrap.sh). Used in the AMI name / docker tag." +} + +variable "region" { + type = string + default = env("AWS_REGION") != "" ? env("AWS_REGION") : "us-east-1" + description = "AWS region for the amazon-ebs builder." +} + +variable "instance_type" { + type = string + default = "" + description = "EC2 instance type for the bake VM. Empty => derived from arch." +} + +variable "root_volume_size" { + type = number + default = 64 + description = "Root EBS volume size in GiB (xfs, gp3)." +} + +locals { + // Debian Cloud Team AMIs (owner 136693071363) use amd64/arm64 in the Name; + // EC2's architecture filter wants x86_64/arm64. + debian_name_arch = var.arch == "aarch64" ? "arm64" : "amd64" + ec2_arch = var.arch == "aarch64" ? "arm64" : "x86_64" + // c7i for x64, c7g for Graviton — build-only VM, not the CI runner size. + instance_type = var.instance_type != "" ? var.instance_type : (var.arch == "aarch64" ? "c7g.2xlarge" : "c7i.2xlarge") + // Matches getImageKey()+getImageName() in .buildkite/ci.mjs when called + // with {os:"linux", arch, distro:"debian", release:"13"} under publish: + // "linux--13-debian-v" + // image_name (from variables.pkr.hcl) overrides for [build images] runs. + ami_name = var.image_name != "" ? var.image_name : "linux-${var.arch}-13-debian-v${var.version}" + ci_flag = var.ci ? "--ci" : "" +} + +// --------------------------------------------------------------------------- +// Docker: local-dev image. Same bootstrap as the AMI so `docker run` matches +// what CI sees. `commit=true` => the provisioned container is committed to an +// image; the docker-tag post-processor names it. +// --------------------------------------------------------------------------- +source "docker" "debian" { + image = "debian:13-slim" + platform = "linux/${local.debian_name_arch}" + commit = true + changes = [ + "LABEL org.opencontainers.image.source=https://github.com/oven-sh/bun", + "LABEL sh.bun.bootstrap.version=${var.version}", + "ENV CI=true", + "ENV DEBIAN_FRONTEND=noninteractive", + ] +} + +// --------------------------------------------------------------------------- +// AWS: CI AMI. Filters the latest official debian-13 cloud image for the +// requested arch, bakes on a gp3/xfs root volume. +// --------------------------------------------------------------------------- +source "amazon-ebs" "debian" { + region = var.region + instance_type = local.instance_type + ssh_username = "admin" + + ami_name = local.ami_name + ami_description = "Bun CI build image (debian-13, ${var.arch}, bootstrap v${var.version})" + force_deregister = true + force_delete_snapshot = true + + source_ami_filter { + filters = { + name = "debian-13-${local.debian_name_arch}-*" + architecture = local.ec2_arch + root-device-type = "ebs" + virtualization-type = "hvm" + } + owners = ["136693071363"] // Debian Cloud Team + most_recent = true + } + + // Root volume on gp3. Debian cloud AMIs expose root as /dev/xvda. + // NOTE: the Debian base AMI ships an ext4 root; an xfs ROOT needs a + // rebased AMI or a separate data volume. For now we attach a second gp3 + // volume that bootstrap can mkfs.xfs and mount for /var/lib/buildkite — + // revisit once a debian-13-xfs base exists. + launch_block_device_mappings { + device_name = "/dev/xvda" + volume_size = var.root_volume_size + volume_type = "gp3" + delete_on_termination = true + } + launch_block_device_mappings { + device_name = "/dev/xvdb" + volume_size = var.root_volume_size + volume_type = "gp3" + delete_on_termination = true + } + user_data = <<-EOF + #cloud-config + fs_setup: + - device: /dev/xvdb + filesystem: xfs + label: build + mounts: + - ["/dev/xvdb", "/var/lib/buildkite-agent", "xfs", "defaults,nofail", "0", "2"] + EOF + + tags = { + Name = local.ami_name + os = "linux" + arch = var.arch + distro = "debian" + build = var.build_number + } +} + +build { + name = "linux-debian-13" + sources = [ + "source.docker.debian", + "source.amazon-ebs.debian", + ] + + // debian:13-slim has no curl/sudo/ca-certs; bootstrap.sh's fetch() needs + // curl-or-wget before install_common_software runs. The Debian AMI already + // has these, but a second `apt-get install` is a no-op there. + provisioner "shell" { + environment_vars = ["DEBIAN_FRONTEND=noninteractive"] + inline = [ + "set -eu", + "if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi", + "$SUDO apt-get update -y", + "$SUDO apt-get install -y --no-install-recommends ca-certificates curl sudo", + ] + } + + // Upload bootstrap.sh (path comes from -var bootstrap_script=..., see + // machine.mjs; default in variables.pkr.hcl is the .ps1 — caller must + // override for this template). + provisioner "file" { + source = var.bootstrap_script + destination = "/tmp/bootstrap.sh" + } + + // Run bootstrap. docker runs as root (no sudo); amazon-ebs runs as `admin` + // with passwordless sudo. `-E` preserves BUN_BOOTSTRAP_REPO_REF across the + // sudo boundary so prefetch_build_deps() clones the right ref. + provisioner "shell" { + environment_vars = [ + "BUN_BOOTSTRAP_REPO_REF=${var.repo_ref}", + "DEBIAN_FRONTEND=noninteractive", + ] + execute_command = "chmod +x {{ .Path }}; if command -v sudo >/dev/null 2>&1 && [ \"$(id -u)\" -ne 0 ]; then sudo -E sh -c '{{ .Vars }} {{ .Path }}'; else {{ .Vars }} sh '{{ .Path }}'; fi" + inline = [ + "set -eu", + "sh /tmp/bootstrap.sh ${local.ci_flag}", + ] + } + + // Optional: install agent.mjs as a service. Skipped when agent_script is + // empty (local docker dev image). Mirrors the Windows templates' step 2/3. + provisioner "file" { + only = ["amazon-ebs.debian"] + source = var.agent_script + destination = "/tmp/agent.mjs" + } + provisioner "shell" { + only = ["amazon-ebs.debian"] + inline = [ + "set -eu", + "if [ -s /tmp/agent.mjs ]; then", + " sudo mkdir -p /var/lib/buildkite-agent", + " sudo cp /tmp/agent.mjs /var/lib/buildkite-agent/agent.mjs", + " sudo $(command -v node || command -v bun) /var/lib/buildkite-agent/agent.mjs install", + "fi", + ] + } + + // Tag the committed docker image so `docker run oven/bun-build:` works. + post-processor "docker-tag" { + only = ["docker.debian"] + repository = "oven/bun-build" + tags = [local.ami_name, "debian-13-${var.arch}"] + } +} diff --git a/scripts/verify-baseline-static/CLAUDE.md b/scripts/verify-baseline-static/CLAUDE.md index 658dc893061..da16f6115b5 100644 --- a/scripts/verify-baseline-static/CLAUDE.md +++ b/scripts/verify-baseline-static/CLAUDE.md @@ -51,17 +51,16 @@ false positive. ## Which builds run this -`.buildkite/ci.mjs:592` — `needsBaselineVerification()`: +See `needsBaselineVerification()` in `.buildkite/ci.mjs`: -| Target | Allowlist file | -| ----------------------------------------------- | --------------------------- | -| `linux-x64-baseline`, `linux-x64-musl-baseline` | `allowlist-x64.txt` | -| `windows-x64-baseline` | `allowlist-x64-windows.txt` | -| `linux-aarch64`, `linux-aarch64-musl` | `allowlist-aarch64.txt` | +| Target | Allowlist file | +| -------------------------------------------------------------- | --------------------------- | +| `linux-x64`, `linux-x64-musl` | `allowlist-x64.txt` | +| `windows-x64` | `allowlist-x64-windows.txt` | +| `linux-aarch64`, `linux-aarch64-musl`, `linux-aarch64-android` | `allowlist-aarch64.txt` | -x64 baseline = Nehalem (`-march=nehalem`, `cmake/CompilerFlags.cmake:33`). -aarch64 baseline = `armv8-a+crc` (`cmake/CompilerFlags.cmake:27-29`). aarch64 -has no separate "baseline" build — the regular build _is_ the baseline. +x64 baseline = Nehalem (`-march=nehalem`). aarch64 baseline = `armv8-a+crc`. +Every x64 build is baseline (there is no separate `-baseline` variant). ## Reproduce a CI failure locally @@ -70,8 +69,8 @@ building locally unless you build with the exact baseline toolchain. Download the artifact instead. 1. Get `-profile.zip` from the failing build's `build-bun` step - (Artifacts tab in Buildkite). Triplets look like `bun-linux-x64-baseline`, - `bun-linux-aarch64-musl`, `bun-windows-x64-baseline`. + (Artifacts tab in Buildkite). Triplets look like `bun-linux-x64`, + `bun-linux-aarch64-musl`, `bun-windows-x64`. 2. Build and run the scanner (host arch is irrelevant — the scanner reads the binary's headers, it doesn't execute it): @@ -81,7 +80,7 @@ the artifact instead. # Linux x64 baseline ./scripts/verify-baseline-static/target/release/verify-baseline-static \ - --binary bun-linux-x64-baseline-profile/bun-profile \ + --binary bun-linux-x64-profile/bun-profile \ --allowlist scripts/verify-baseline-static/allowlist-x64.txt # Linux aarch64 @@ -91,7 +90,7 @@ the artifact instead. # Windows x64 baseline (PDB auto-discovered at .pdb) ./scripts/verify-baseline-static/target/release/verify-baseline-static \ - --binary bun-windows-x64-baseline-profile/bun-profile.exe \ + --binary bun-windows-x64-profile/bun-profile.exe \ --allowlist scripts/verify-baseline-static/allowlist-x64-windows.txt ``` @@ -164,10 +163,10 @@ table, an HWCAP test. Known patterns: | Rust `memchr` | `is_x86_feature_detected!()` | (via lolhtml dep) | | compiler-rt outline-atomics (aarch64) | `__aarch64_have_lse_atomics` (= `AT_HWCAP & HWCAP_ATOMICS`) | compiler-rt builtin | -**If no gate exists:** (B). Usually a subbuild that ignored -`ENABLE_BASELINE` and picked up host `-march=native`. Fix the -`cmake/targets/Build*.cmake` for that dep. Confirm with the emulator (the -ground-truth check): +**If no gate exists:** (B). Usually a subbuild that picked up host +`-march=native` instead of the pinned `-march=nehalem` / `-mcpu=cortex-a53`. +Fix that dep's compile flags in `scripts/build/deps/`. Confirm with the +emulator (the ground-truth check): ```sh qemu-x86_64 -cpu Nehalem ./bun-profile # x64 → SIGILL = bug diff --git a/scripts/verify-baseline-static/allowlist-aarch64.txt b/scripts/verify-baseline-static/allowlist-aarch64.txt index 73eedd9abc1..38f503017e9 100644 --- a/scripts/verify-baseline-static/allowlist-aarch64.txt +++ b/scripts/verify-baseline-static/allowlist-aarch64.txt @@ -235,6 +235,15 @@ uw_update_context [SVE] uw_update_context_1 [SVE] +# ---------------------------------------------------------------------------- +# libgcc SME lazy-save ABI routines (gcc 14+). Gate: only called from code +# compiled with __arm_streaming/__arm_new_za (Bun has none); the routines +# themselves early-return when TPIDR2_EL0 is null on non-SME hardware. +# (1 symbols) +# ---------------------------------------------------------------------------- +__arm_tpidr2_save [SVE] + + # ---------------------------------------------------------------------------- # libdeflate Adler-32 NEON DotProd. Gate: HWCAP_ASIMDDP. # (1 symbols) diff --git a/scripts/verify-baseline-static/allowlist-x64-windows.txt b/scripts/verify-baseline-static/allowlist-x64-windows.txt index 6c2845898b8..32c5e8ecedb 100644 --- a/scripts/verify-baseline-static/allowlist-x64-windows.txt +++ b/scripts/verify-baseline-static/allowlist-x64-windows.txt @@ -734,7 +734,11 @@ ecp_nistz256_ord_sqr_mont_adx [ADX, BMI2] ecp_nistz256_select_w5_avx2 [AVX, AVX2] ecp_nistz256_select_w7_avx2 [AVX, AVX2] ecp_nistz256_sqr_mont_adx [ADX, BMI2] -rsaz_1024_gather5_avx2 [AVX, AVX2] +# rsaz_1024_gather5_avx2 is the last global in rsaz-avx2-win.asm; when lld-link +# lays aesni-gcm-x86_64-win.asm right after it, the file-initial local label +# _aesni_ctr32_ghash_6x (no PUBLIC record) bleeds into this symbol's PDB range. +# Both are OPENSSL_ia32cap_P-gated; ceiling widened to cover the bleed. +rsaz_1024_gather5_avx2 [AES, AVX, AVX2, MOVBE, PCLMULQDQ] rsaz_1024_mul_avx2 [AVX, AVX2] rsaz_1024_scatter5_avx2 [AVX, AVX2] rsaz_1024_sqr_avx2 [AVX, AVX2] diff --git a/scripts/verify-baseline-static/allowlist-x64.txt b/scripts/verify-baseline-static/allowlist-x64.txt index 99dde9b5e3b..76eebb1fdb4 100644 --- a/scripts/verify-baseline-static/allowlist-x64.txt +++ b/scripts/verify-baseline-static/allowlist-x64.txt @@ -811,14 +811,14 @@ CRYPTO_rdrand_multiple8_buf [RDRAND] FSE_decompress_wksp_body_bmi2 [BMI2, LZCNT] FSE_readNCount_body_bmi2 [BMI2, LZCNT] HUF_compress1X_usingCTable_internal_bmi2 [BMI2] -HUF_decompress1X1_usingDTable_internal_bmi2 [BMI2, LZCNT] -HUF_decompress1X2_usingDTable_internal_bmi2 [BMI2, LZCNT] -HUF_decompress4X1_usingDTable_internal_bmi2 [BMI2, LZCNT] -HUF_decompress4X1_usingDTable_internal_fast [BMI2] +HUF_decompress1X1_usingDTable_internal_bmi2 [BMI1, BMI2, LZCNT] +HUF_decompress1X2_usingDTable_internal_bmi2 [BMI1, BMI2, LZCNT] +HUF_decompress4X1_usingDTable_internal_bmi2 [BMI1, BMI2, LZCNT] +HUF_decompress4X1_usingDTable_internal_fast [BMI1, BMI2] HUF_decompress4X1_usingDTable_internal_fast_asm_loop [BMI2] HUF_decompress4X1_usingDTable_internal_fast_c_loop [BMI2] -HUF_decompress4X2_usingDTable_internal_bmi2 [BMI2, LZCNT] -HUF_decompress4X2_usingDTable_internal_fast [BMI2] +HUF_decompress4X2_usingDTable_internal_bmi2 [BMI1, BMI2, LZCNT] +HUF_decompress4X2_usingDTable_internal_fast [BMI1, BMI2] HUF_decompress4X2_usingDTable_internal_fast_asm_loop [BMI2] HUF_decompress4X2_usingDTable_internal_fast_c_loop [BMI2] HUF_readStats_body_bmi2 [BMI2, LZCNT] diff --git a/scripts/verify-baseline.ts b/scripts/verify-baseline.ts index 6093b710a05..70b7a028a2c 100644 --- a/scripts/verify-baseline.ts +++ b/scripts/verify-baseline.ts @@ -6,8 +6,8 @@ // Windows x64: Intel SDE with -nhm (no AVX) // // Usage: -// bun scripts/verify-baseline.ts --binary ./bun --emulator /usr/bin/qemu-x86_64 -// bun scripts/verify-baseline.ts --binary ./bun.exe --emulator ./sde.exe +// bun scripts/verify-baseline.ts --binary ./bun --arch x64 --emulator /usr/bin/qemu-x86_64 +// bun scripts/verify-baseline.ts --binary ./bun.exe --arch x64 --emulator ./sde.exe import { readdirSync } from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; @@ -20,6 +20,10 @@ const { values } = parseArgs({ args: process.argv.slice(2), options: { binary: { type: "string" }, + // Target arch of --binary ("x64" | "aarch64"). The host may differ (e.g. + // verifying an x64 build from an arm64 CI host), so process.arch is only a + // fallback for local dev. + arch: { type: "string" }, emulator: { type: "string" }, "jit-stress": { type: "boolean", default: false }, "skip-emulation": { type: "boolean", default: false }, @@ -50,9 +54,14 @@ const fixturesDir = join(repoRoot, "test", "js", "bun", "jsc-stress", "fixtures" const wasmFixturesDir = join(fixturesDir, "wasm"); const preloadPath = join(repoRoot, "test", "js", "bun", "jsc-stress", "preload.js"); -// Platform detection +// Host OS (getVerifyBaselineHost() guarantees host OS == target OS here). const isWindows = process.platform === "win32"; -const isAarch64 = process.arch === "arm64"; +// Target arch (host arch may differ; --arch is explicit). +const targetArch = values.arch ?? (process.arch === "arm64" ? "aarch64" : "x64"); +if (targetArch !== "x64" && targetArch !== "aarch64") { + throw new Error(`--arch must be "x64" or "aarch64", got: ${targetArch}`); +} +const isAarch64 = targetArch === "aarch64"; // SDE outputs this when a chip-check violation occurs const SDE_VIOLATION_PATTERN = /SDE-ERROR:.*not valid for specified chip/i; diff --git a/src/ast/char_freq.rs b/src/ast/char_freq.rs index dca51831233..78e28bd8c88 100644 --- a/src/ast/char_freq.rs +++ b/src/ast/char_freq.rs @@ -117,7 +117,7 @@ fn scan_big(out: &mut Buffer, text: &[u8], delta: i32) { let unrolled = text.len() - (text.len() % SCAN_BIG_CHUNK_SIZE); let (chunks, remain) = text.split_at(unrolled); - for chunk in chunks.chunks_exact(SCAN_BIG_CHUNK_SIZE) { + for chunk in chunks.as_chunks::().0 { // PERF: candidate for unrolling — profile for i in 0..SCAN_BIG_CHUNK_SIZE { deltas[chunk[i] as usize] += delta; diff --git a/src/boringssl/lib.rs b/src/boringssl/lib.rs index a7f6f01126a..1dbc14ef5b5 100644 --- a/src/boringssl/lib.rs +++ b/src/boringssl/lib.rs @@ -392,8 +392,8 @@ impl core::fmt::Display for AltNameIp<'_> { match self.0 { [a, b, c, d] => write!(f, "{a}.{b}.{c}.{d}"), octets if octets.len() == 16 => { - for (i, pair) in octets.chunks_exact(2).enumerate() { - let group = u16::from_be_bytes([pair[0], pair[1]]); + for (i, pair) in octets.as_chunks::<2>().0.iter().enumerate() { + let group = u16::from_be_bytes(*pair); if i > 0 { f.write_str(":")?; } diff --git a/src/bun_alloc/lib.rs b/src/bun_alloc/lib.rs index 633158a33ea..9471e64fa35 100644 --- a/src/bun_alloc/lib.rs +++ b/src/bun_alloc/lib.rs @@ -3289,12 +3289,10 @@ impl< ) -> core::result::Result<(), AllocError> { let _guard = self.map().mutex.lock(); - let slice: &'static [u8]; - // Is this actually a slice into the map? Don't free it. - if self.is_key_statically_allocated(key) { + let slice: &'static [u8] = if self.is_key_statically_allocated(key) { // SAFETY: key points into self.key_list_buffer which lives for the singleton's life. - slice = unsafe { core::slice::from_raw_parts(key.as_ptr(), key.len()) }; + unsafe { core::slice::from_raw_parts(key.as_ptr(), key.len()) } } else if self.key_list_buffer_used + key.len() < self.key_list_buffer.len() { let start = self.key_list_buffer_used; self.key_list_buffer_used += key.len(); @@ -3310,7 +3308,7 @@ impl< }; dst.copy_from_slice(key); // SAFETY: points into self.key_list_buffer (singleton-static lifetime). - slice = unsafe { core::slice::from_raw_parts(dst.as_ptr(), dst.len()) }; + unsafe { core::slice::from_raw_parts(dst.as_ptr(), dst.len()) } } else { // Propagate OOM. Route // through mimalloc directly (PORTING.md forbids `Box::leak`) so the @@ -3324,8 +3322,8 @@ impl< unsafe { core::ptr::copy_nonoverlapping(key.as_ptr(), ptr, key.len()) }; // SAFETY: allocation is owned by this singleton for process lifetime (or until // freed below on overwrite). - slice = unsafe { core::slice::from_raw_parts(ptr, key.len()) }; - } + unsafe { core::slice::from_raw_parts(ptr, key.len()) } + }; let slice = if REMOVE_TRAILING_SLASHES { trim_right(slice, b"/") diff --git a/src/bun_bin/lib.rs b/src/bun_bin/lib.rs index 751ee356fb2..afa92720f6a 100644 --- a/src/bun_bin/lib.rs +++ b/src/bun_bin/lib.rs @@ -16,7 +16,7 @@ //! ## Layout //! //! `main()`'s callee chain — the C-ABI leaves (`bun_initialize_process`, -//! `bun_warn_avx_missing`, the `__wrap_*` shims, …) plus `cli::Cli::start` +//! the `__wrap_*` shims, …) plus `cli::Cli::start` //! and everything `bun run` reaches under it — sits on the cold-start //! critical path: each call can fault a fresh page run. A //! `--symbol-ordering-file` 2-pass relink that clustered these onto shared @@ -200,22 +200,6 @@ pub unsafe extern "C" fn main(argc: c_int, argv: *const *const c_char) -> c_int output::stdio::init(); let _flush = output::flush_guard(); - // `bun_warn_avx_missing(...)` — x86_64 + SIMD + posix only. - #[cfg(all(target_arch = "x86_64", unix))] - if bun_core::Environment::ENABLE_SIMD { - unsafe extern "C" { - fn bun_warn_avx_missing(url: *const core::ffi::c_char); - } - // SAFETY: BUN__GITHUB_BASELINE_URL is a NUL-terminated static; the C - // side only reads it to print the suggested download URL. - unsafe { - bun_warn_avx_missing( - bun_runtime::cli::upgrade_command::UpgradeCommand::BUN__GITHUB_BASELINE_URL - .as_ptr(), - ); - } - } - // 5. Per-thread stack-limit cache for the JS recursion guard. StackCheck::configure_thread(); bun_io::ParentDeathWatchdog::install(); diff --git a/src/bun_core/Global.rs b/src/bun_core/Global.rs index dd59d7770ba..6d5647465e6 100644 --- a/src/bun_core/Global.rs +++ b/src/bun_core/Global.rs @@ -497,7 +497,7 @@ pub const os_display: &str = if cfg!(target_os = "android") { env::OS.display_string() }; -// Bun v1.0.0 (Linux x64 baseline) +// Bun v1.0.0 (Linux x64) // Bun v1.0.0-debug (Linux x64) // Bun v1.0.0-canary.0+44e09bb7f (Linux x64) pub const unhandled_error_bun_version_string: &str = concatcp!( @@ -511,7 +511,7 @@ pub const unhandled_error_bun_version_string: &str = concatcp!( os_display, " ", arch_name, - if env::BASELINE { " baseline)" } else { ")" }, + ")", ); pub const arch_name: &str = if cfg!(target_arch = "x86_64") { @@ -624,6 +624,7 @@ pub(crate) fn is_exiting() -> bool { // args and are `noreturn`/kernel-validated — no memory-safety preconditions, // so `safe fn` discharges the link-time proof and the call sites are plain // calls. `#[link_name]` avoids colliding with this module's own `pub fn exit`. +#[allow(suspicious_runtime_symbol_definitions)] // signatures are ABI-identical; `safe fn` is intentional (above) unsafe extern "C" { #[link_name = "abort"] safe fn libc_abort() -> !; diff --git a/src/bun_core/build.rs b/src/bun_core/build.rs index 5ef9d8badf9..3e1947b9a80 100644 --- a/src/bun_core/build.rs +++ b/src/bun_core/build.rs @@ -9,7 +9,7 @@ //! //! `build_options.rs` is written at configure time by //! `scripts/build/buildOptionsRs.ts` from the resolved `Config` (sha, -//! version, baseline, …). This script does NOT run the generator — it just +//! version, …). This script does NOT run the generator — it just //! resolves the path and tells cargo to track the file so a sha/version //! change recompiles `bun_core`. Mirrors `src/{jsc,runtime}/build.rs`. diff --git a/src/bun_core/env.rs b/src/bun_core/env.rs index 3328cb84e04..673122094b2 100644 --- a/src/bun_core/env.rs +++ b/src/bun_core/env.rs @@ -53,9 +53,6 @@ pub const CI_ASSERT: bool = pub const SHOW_CRASH_TRACE: bool = IS_DEBUG || IS_TEST || ENABLE_ASAN; pub const REPORTED_NODEJS_VERSION: &str = build_options::REPORTED_NODEJS_VERSION; -pub const BASELINE: bool = build_options::BASELINE; -/// Only `BASELINE` gates SIMD. -pub const ENABLE_SIMD: bool = !BASELINE; pub const GIT_SHA: &str = build_options::SHA; pub const GIT_SHA_SHORT: &str = if !build_options::SHA.is_empty() { const_str_slice(build_options::SHA, 0, 9) diff --git a/src/bun_core/lib.rs b/src/bun_core/lib.rs index 6b3abe56cb6..b7c6e640cc1 100644 --- a/src/bun_core/lib.rs +++ b/src/bun_core/lib.rs @@ -1846,15 +1846,17 @@ pub(crate) mod strings_impl { const HIGH_BITS: u64 = 0x8080_8080_8080_8080; let mut copied = 0usize; - for (d, s) in dst.chunks_exact_mut(8).zip(src.chunks_exact(8)) { - let word = u64::from_ne_bytes(s.try_into().expect("infallible: size matches")); + let (dst_chunks, _) = dst.as_chunks_mut::<8>(); + let (src_chunks, _) = src.as_chunks::<8>(); + for (d, s) in dst_chunks.iter_mut().zip(src_chunks) { + let word = u64::from_ne_bytes(*s); let mask = word & HIGH_BITS; if mask != 0 { let ascii = (mask.trailing_zeros() / 8) as usize; d[..ascii].copy_from_slice(&s[..ascii]); return copied + ascii; } - d.copy_from_slice(&word.to_ne_bytes()); + *d = word.to_ne_bytes(); copied += 8; } for (d, &s) in dst[copied..].iter_mut().zip(&src[copied..]) { diff --git a/src/bun_core/string/immutable/unicode.rs b/src/bun_core/string/immutable/unicode.rs index 3db3b6b67f6..3e48fffd0a4 100644 --- a/src/bun_core/string/immutable/unicode.rs +++ b/src/bun_core/string/immutable/unicode.rs @@ -523,7 +523,7 @@ pub fn copy_latin1_into_ascii(dest: &mut [u8], src: &[u8]) { } } - if to.len() >= 16 && crate::Environment::ENABLE_SIMD { + if to.len() >= 16 { const VECTOR_SIZE: usize = 16; let remain_in_u64_len = remain.len() - (remain.len() % VECTOR_SIZE); let to_in_u64_len = to.len() - (to.len() % VECTOR_SIZE); diff --git a/src/collections/bit_set.rs b/src/collections/bit_set.rs index 6e4ef3cb7d5..8c1f7bc6b05 100644 --- a/src/collections/bit_set.rs +++ b/src/collections/bit_set.rs @@ -79,15 +79,14 @@ fn set_range_value_masks(masks: &mut [usize], range: Range, value: bool) { mask2 = bool_mask_usize(value) >> ((MASK_LEN - 1) - (end_bit - 1)); masks[start_mask_index] |= mask1 & mask2; } else { - let bulk_mask_index: usize; - if start_bit > 0 { + let bulk_mask_index: usize = if start_bit > 0 { masks[start_mask_index] = (masks[start_mask_index] & !(bool_mask_usize(true) << start_bit)) | (bool_mask_usize(value) << start_bit); - bulk_mask_index = start_mask_index + 1; + start_mask_index + 1 } else { - bulk_mask_index = start_mask_index; - } + start_mask_index + }; for mask in &mut masks[bulk_mask_index..end_mask_index] { *mask = bool_mask_usize(value); diff --git a/src/collections/multi_array_list.rs b/src/collections/multi_array_list.rs index 883f3d72888..8cd3e767879 100644 --- a/src/collections/multi_array_list.rs +++ b/src/collections/multi_array_list.rs @@ -255,7 +255,7 @@ use crate::const_str_eq; /// callers routinely use lifetime-carrying field types (`&'a [u8]`, `Ref<'a>`). #[inline(always)] const fn type_id_of() -> TypeId { - core::intrinsics::type_id::() + const { core::intrinsics::type_id::() } } /// Reflected fields of `T` (struct only). Panics at const-eval for non-structs. @@ -295,7 +295,7 @@ const fn align_sort_key(size: usize, struct_align: usize) -> usize { return 1; } // Largest power of two dividing `size`. - let pow2 = size & size.wrapping_neg(); + let pow2 = size.isolate_lowest_one(); if pow2 < struct_align { pow2 } else { @@ -344,7 +344,7 @@ impl Reflected { let mut i = 0; while i < n { let f = &fields[i]; - let size = match f.ty.info().size { + let size = match f.ty.size() { Some(s) => s, None => panic!("MultiArrayList: field type must be Sized"), }; diff --git a/src/crash_handler/lib.rs b/src/crash_handler/lib.rs index e245ca4716a..fbc1749f083 100644 --- a/src/crash_handler/lib.rs +++ b/src/crash_handler/lib.rs @@ -1595,7 +1595,7 @@ mod draft { }; const METADATA_VERSION_LINE: &str = const_format::formatcp!( - "Bun {}v{} {} {}{}\n", + "Bun {}v{} {} {}\n", if Environment::IS_DEBUG { "Debug " } else if Environment::IS_CANARY { @@ -1606,11 +1606,6 @@ mod draft { bun_core::package_json_version_with_sha, bun_core::os_display, ARCH_DISPLAY_STRING, - if Environment::BASELINE { - " (baseline)" - } else { - "" - }, ); /// Extract `(pc, fp)` from the `ucontext_t` the kernel hands the signal @@ -2312,14 +2307,12 @@ mod draft { /// eg: 'https://bun.report/1.1.3/we04c... /// ^ this tells you it is windows x86_64 /// - /// Baseline gets a weirder encoding of a mix of b and e. + /// x64 ships one nehalem build; the old baseline codes ('B','b','e','g') + /// are no longer emitted but the backend still accepts them from old bins. struct Platform; impl Platform { // Rust cannot concat ident names at const time without a proc-macro; spell out the cfg matrix. - // Baseline is `Environment::BASELINE`, not a Cargo feature — `cfg(feature = "baseline")` - // was always false because no such feature exists, so baseline builds emitted the - // non-baseline char and bun.report symbolicated them against the wrong artifact. const CURRENT: u8 = { // Android folds into the Linux variants. bun.report decodes the same // single-char codes; introducing new ones would break older decoders. @@ -2328,7 +2321,7 @@ mod draft { target_arch = "x86_64" ))] { - if Environment::BASELINE { b'B' } else { b'l' } + b'l' } #[cfg(all( any(target_os = "linux", target_os = "android"), @@ -2339,7 +2332,7 @@ mod draft { } #[cfg(all(target_os = "macos", target_arch = "x86_64"))] { - if Environment::BASELINE { b'b' } else { b'm' } + b'm' } #[cfg(all(target_os = "macos", target_arch = "aarch64"))] { @@ -2347,7 +2340,7 @@ mod draft { } #[cfg(all(windows, target_arch = "x86_64"))] { - if Environment::BASELINE { b'e' } else { b'w' } + b'w' } #[cfg(all(windows, target_arch = "aarch64"))] { @@ -2355,7 +2348,7 @@ mod draft { } #[cfg(all(target_os = "freebsd", target_arch = "x86_64"))] { - if Environment::BASELINE { b'g' } else { b'f' } + b'f' } #[cfg(all(target_os = "freebsd", target_arch = "aarch64"))] { diff --git a/src/css/values/color.rs b/src/css/values/color.rs index c714d993f54..80336dbc272 100644 --- a/src/css/values/color.rs +++ b/src/css/values/color.rs @@ -954,7 +954,7 @@ bitflags::bitflags! { impl ColorFallbackKind { pub fn lowest(self) -> ColorFallbackKind { - ColorFallbackKind::from_bits_truncate(self.bits() & self.bits().wrapping_neg()) + ColorFallbackKind::from_bits_truncate(self.bits().isolate_lowest_one()) } pub fn highest(self) -> ColorFallbackKind { diff --git a/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs b/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs index 17b4ba3aefe..bdb85a240d4 100644 --- a/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs +++ b/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs @@ -1842,7 +1842,9 @@ impl<'a> Headers8Bit<'a> { fn iter(&self) -> impl Iterator + '_ { self.slices - .chunks_exact(2) + .as_chunks::<2>() + .0 + .iter() .map(|pair| (pair[0].slice(), pair[1].slice())) } diff --git a/src/install/PackageManager/CommandLineArguments.rs b/src/install/PackageManager/CommandLineArguments.rs index 498787b95e2..a430696dfd5 100644 --- a/src/install/PackageManager/CommandLineArguments.rs +++ b/src/install/PackageManager/CommandLineArguments.rs @@ -1320,8 +1320,8 @@ Full documentation is available at https://bun.com/docs/cli/pm#scan. if let Some(cwd_) = args.option(b"--cwd") { let mut buf = PathBuffer::uninit(); let mut buf2 = PathBuffer::uninit(); - let final_path: &mut bun_core::ZStr; - if !cwd_.is_empty() && cwd_[0] == b'.' { + + let final_path: &mut bun_core::ZStr = if !cwd_.is_empty() && cwd_[0] == b'.' { let cwd_len = bun_sys::getcwd(&mut buf[..])?; let cwd = &buf[..cwd_len]; let parts: [&[u8]; 1] = [cwd_]; @@ -1332,12 +1332,12 @@ Full documentation is available at https://bun.com/docs/cli/pm#scan. ) .len(); buf2[len] = 0; - final_path = bun_core::ZStr::from_buf_mut(&mut buf2[..], len); + bun_core::ZStr::from_buf_mut(&mut buf2[..], len) } else { buf[..cwd_.len()].copy_from_slice(cwd_); buf[cwd_.len()] = 0; - final_path = bun_core::ZStr::from_buf_mut(&mut buf[..], cwd_.len()); - } + bun_core::ZStr::from_buf_mut(&mut buf[..], cwd_.len()) + }; if let Err(err) = bun_sys::chdir(final_path) { Output::err_generic( "failed to change directory to \"{}\": {}\n", diff --git a/src/install/resolvers/folder_resolver.rs b/src/install/resolvers/folder_resolver.rs index f967e3a93b6..14f8b834f92 100644 --- a/src/install/resolvers/folder_resolver.rs +++ b/src/install/resolvers/folder_resolver.rs @@ -186,7 +186,7 @@ fn normalize_package_json_path<'a>( non_normalized_path: &[u8], ) -> Paths<'a> { let abs: &[u8]; - let rel: &[u8]; + // We consider it valid if there is a package.json in the folder let normalized: &[u8] = if non_normalized_path.len() == 1 && non_normalized_path[0] == b'.' { non_normalized_path @@ -198,7 +198,7 @@ fn normalize_package_json_path<'a>( const PACKAGE_JSON_LEN: usize = "/package.json".len(); - if strings::starts_with_char(normalized, b'.') { + let rel: &[u8] = if strings::starts_with_char(normalized, b'.') { let mut tempcat = PathBuffer::uninit(); tempcat[..normalized.len()].copy_from_slice(normalized); @@ -210,10 +210,10 @@ fn normalize_package_json_path<'a>( &tempcat[0..normalized.len() + PACKAGE_JSON_LEN], ]; abs = FileSystem::instance().abs_buf(&parts, joined); - rel = FileSystem::instance().relative( + FileSystem::instance().relative( FileSystem::instance().top_level_dir(), &abs[0..abs.len() - PACKAGE_JSON_LEN], - ); + ) } else { let joined_len = joined.len(); let mut remain: &mut [u8] = &mut joined[..]; @@ -246,11 +246,11 @@ fn normalize_package_json_path<'a>( let abs_len = joined_len - remain_after; abs = &joined[0..abs_len]; // We store the folder name without package.json - rel = FileSystem::instance().relative( + FileSystem::instance().relative( FileSystem::instance().top_level_dir(), &abs[0..abs.len() - PACKAGE_JSON_LEN], - ); - } + ) + }; let abs_len = abs.len(); joined[abs_len] = 0; diff --git a/src/js_parser/parse/parse_typescript.rs b/src/js_parser/parse/parse_typescript.rs index 21d031d1691..7e13e9a5445 100644 --- a/src/js_parser/parse/parse_typescript.rs +++ b/src/js_parser/parse/parse_typescript.rs @@ -628,25 +628,22 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O name: js_ast::StoreStr::new(b"" as &[u8]), value: None, }; - // Assigned in both live arms below; the third arm returns. - let needs_symbol: bool; - // Parse the name - if p.lexer.token == T::TStringLiteral { + let needs_symbol: bool = if p.lexer.token == T::TStringLiteral { // `slice8()` is currently duplicated in E.rs (two impl blocks); // read `.data` directly — `to_utf8_e_string` guarantees `is_utf16 == false`. let estr = p.lexer.to_utf8_e_string()?; debug_assert!(!estr.is_utf16); value.name = estr.data; - needs_symbol = js_lexer::is_identifier(value.name.slice()); + js_lexer::is_identifier(value.name.slice()) } else if p.lexer.is_identifier_or_keyword() { value.name = js_ast::StoreStr::new(p.lexer.identifier); - needs_symbol = true; + true } else { p.lexer.expect(T::TIdentifier)?; // error early, name is still `undefined` return Err(crate::Error::SyntaxError); - } + }; p.lexer.next()?; // Identifiers can be referenced by other values diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index aed62821fd9..109d64bcebb 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -7900,14 +7900,13 @@ pub fn print_ast<'a, W: WriterTrait, const ASCII_ONLY: bool, const GENERATE_SOUR // hoisted out of the `minify_identifiers` arm so the // `&'r mut MinifyRenamer` borrow stored in `renamer` outlives the branch. let mut minify_renamer; - let renamer: rename::Renamer<'_, '_>; // `Scope` isn't `Copy` here and the only // consumer (`compute_reserved_names_for_scope`) walks `members`/`generated`/ // `children` — never `parent` — so we re-point at the in-place // `tree.module_scope` instead (lives for `'a`). let module_scope = &tree.module_scope; let stable_source_indices = [source.index.0]; - if opts.minify_identifiers { + let renamer: rename::Renamer<'_, '_> = if opts.minify_identifiers { let mut reserved_names = rename::compute_initial_reserved_names(opts.module_type)?; for child in module_scope.children.slice() { // `StoreRef` has safe `DerefMut`; copy the handle to a mut @@ -7985,11 +7984,11 @@ pub fn print_ast<'a, W: WriterTrait, const ASCII_ONLY: bool, const GENERATE_SOUR let minifier = tree.char_freq.as_ref().unwrap().compile(); minify_renamer.assign_names_by_frequency(&minifier)?; - renamer = rename::Renamer::MinifyRenamer(&mut *minify_renamer); + rename::Renamer::MinifyRenamer(&mut *minify_renamer) } else { no_op_renamer = rename::NoOpRenamer::init(symbols, source); - renamer = no_op_renamer.to_renamer(); - } + no_op_renamer.to_renamer() + }; // defer: if minify_identifiers { renamer.deinit() } — Drop handles. diff --git a/src/jsc/bindings/ANSIHelpers.h b/src/jsc/bindings/ANSIHelpers.h index 65ff652d420..4027b6c9f0f 100644 --- a/src/jsc/bindings/ANSIHelpers.h +++ b/src/jsc/bindings/ANSIHelpers.h @@ -7,8 +7,8 @@ // Runtime-dispatched (HWY_DYNAMIC_DISPATCH) escape scan, defined in // highway_strings.cpp. Picks AVX2/AVX-512/SVE at runtime, so the no-escape fast -// path keeps wide vectors even on the -march=nehalem baseline build where the -// WTF SIMD helpers below would otherwise be pinned to SSE width. +// path keeps wide vectors where the WTF SIMD helpers below (compiled for the +// -march=nehalem target) would otherwise be pinned to SSE width. extern "C" size_t highway_index_of_escape_char8(const uint8_t* input, size_t len); extern "C" size_t highway_index_of_escape_char16(const uint16_t* input, size_t len); @@ -49,9 +49,8 @@ static auto exactEscapeMatch(std::conditional_t(end - start); // Long scans delegate to the runtime-dispatched Highway kernel so the - // baseline build isn't pinned to SSE width — but only once the first chunk + // nehalem target isn't pinned to SSE width — but only once the first chunk // is confirmed clean. When an escape sits at/near the start (e.g. dense SGR // input, where stripANSI re-scans the still-large remainder after each // sequence) the inlined path finds it in this one cheap chunk and never diff --git a/src/jsc/bindings/NodeTLS.cpp b/src/jsc/bindings/NodeTLS.cpp index 218c78cd993..3147ea7a685 100644 --- a/src/jsc/bindings/NodeTLS.cpp +++ b/src/jsc/bindings/NodeTLS.cpp @@ -131,8 +131,10 @@ JSC_DEFINE_HOST_FUNCTION(getSystemCACertificates, (JSC::JSGlobalObject * globalO RELEASE_AND_RETURN(scope, JSValue::encode(JSC::objectConstructorFreeze(globalObject, rootCertificates))); } -extern "C" JSC::EncodedJSValue Bun__getTLSDefaultCiphers(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame); -extern "C" JSC::EncodedJSValue Bun__setTLSDefaultCiphers(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame); +// Rust side is #[host_fn(export = ...)] which emits extern "sysv64" on +// win-x64; BUN_DECLARE_HOST_FUNCTION carries SYSV_ABI so both sides agree. +BUN_DECLARE_HOST_FUNCTION(Bun__getTLSDefaultCiphers); +BUN_DECLARE_HOST_FUNCTION(Bun__setTLSDefaultCiphers); JSC_DEFINE_HOST_FUNCTION(getDefaultCiphers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { diff --git a/src/jsc/bindings/c-bindings.cpp b/src/jsc/bindings/c-bindings.cpp index 5dac8137d34..c82a4e6bfa3 100644 --- a/src/jsc/bindings/c-bindings.cpp +++ b/src/jsc/bindings/c-bindings.cpp @@ -22,25 +22,6 @@ #endif // !OS(WINDOWS) #include -#if CPU(X86_64) && !OS(WINDOWS) -extern "C" void bun_warn_avx_missing(const char* url) -{ - __builtin_cpu_init(); - if (__builtin_cpu_supports("avx")) { - return; - } - - static constexpr const char* str = "warn: CPU lacks AVX support, strange crashes may occur. Reinstall Bun or use *-baseline build:\n "; - const size_t len = strlen(str); - - char buf[512]; - strcpy(buf, str); - strcpy(buf + len, url); - strcpy(buf + len + strlen(url), "\n\0"); - [[maybe_unused]] auto _ = write(STDERR_FILENO, buf, strlen(buf)); -} -#endif - // Error condition is encoded as max int32_t. // The only error in this function is ESRCH (no process found) extern "C" int32_t get_process_priority(int32_t pid) diff --git a/src/jsc/bindings/xxhash3.cpp b/src/jsc/bindings/xxhash3.cpp index b77dd94438c..2b479d0d7d3 100644 --- a/src/jsc/bindings/xxhash3.cpp +++ b/src/jsc/bindings/xxhash3.cpp @@ -1,11 +1,11 @@ // Runtime-dispatched SIMD xxHash3 (XXH3_64bits) via Google Highway. // // Bun.hash.xxHash3 used the twox-hash Rust crate, which selects its SIMD -// backend at compile time. On the linux-x64-baseline build (nehalem / SSE2) -// that meant the long-input stripe loop never reached AVX2, costing ~19% on -// 128 KB inputs versus the haswell build. This file moves the hot path to -// Highway's HWY_DYNAMIC_DISPATCH (the same mechanism as highway_strings.cpp), -// so a single binary picks the widest ISA the CPU actually supports. +// backend at compile time. On a nehalem (SSE2) target that meant the +// long-input stripe loop never reached AVX2, costing ~19% on 128 KB inputs. +// This file moves the hot path to Highway's HWY_DYNAMIC_DISPATCH (the same +// mechanism as highway_strings.cpp), so a single binary picks the widest ISA +// the CPU actually supports. // // Output is bit-identical to the reference XXH3_64bits for every input: only // the long-keys stripe loop (accumulate_512 + scrambleAcc) is vectorized, and diff --git a/src/options_types/compile_target.rs b/src/options_types/compile_target.rs index 437a894979f..0451dd8ab43 100644 --- a/src/options_types/compile_target.rs +++ b/src/options_types/compile_target.rs @@ -29,7 +29,7 @@ impl Default for CompileTarget { Self { os: Environment::OS, arch: Environment::ARCH, - baseline: !Environment::ENABLE_SIMD, + baseline: false, version: Version { major: Environment::VERSION.major as _, // @truncate minor: Environment::VERSION.minor as _, // @truncate diff --git a/src/react_compiler/ssa/enter_ssa.rs b/src/react_compiler/ssa/enter_ssa.rs index 79eba68e0b3..7104e4716e3 100644 --- a/src/react_compiler/ssa/enter_ssa.rs +++ b/src/react_compiler/ssa/enter_ssa.rs @@ -240,12 +240,12 @@ impl SSABuilder { } fn fix_incomplete_phis(&mut self, block_id: BlockId, env: &mut Environment) { - let incomplete_phis: Vec = self.states[block_id.0 as usize] - .as_mut() - .unwrap() - .incomplete_phis - .drain(..) - .collect(); + let incomplete_phis: Vec = core::mem::take( + &mut self.states[block_id.0 as usize] + .as_mut() + .unwrap() + .incomplete_phis, + ); for phi in &incomplete_phis { self.add_phi(block_id, &phi.old_place, &phi.new_place, env); } diff --git a/src/router/lib.rs b/src/router/lib.rs index bb1bbabf251..8e0b379cce1 100644 --- a/src/router/lib.rs +++ b/src/router/lib.rs @@ -1568,7 +1568,7 @@ pub mod pattern { let segment = &path_[0..path_.iter().position(|&b| b == b'/').unwrap_or(path_.len())]; if !str_.eql_bytes(segment) { - params.truncate(0); // shrinkRetainingCapacity(0) + params.clear(); // shrinkRetainingCapacity(0) return false; } @@ -1591,7 +1591,7 @@ pub mod pattern { path_ = &path_[i + 1..]; if pattern.is_end(name) { - params.truncate(0); // shrinkRetainingCapacity(0) + params.clear(); // shrinkRetainingCapacity(0) return false; } @@ -2393,7 +2393,7 @@ mod tests { let mut parameters = route_param::List::default(); let mut failures: usize = 0; for (pattern, pathname, entries) in list.iter() { - parameters.truncate(0); + parameters.clear(); 'fail: { if !Pattern::match_::(pathname, pattern, pattern, &mut parameters) { diff --git a/src/runtime/cli/install.ps1 b/src/runtime/cli/install.ps1 index d33984df0df..a03faa1585c 100644 --- a/src/runtime/cli/install.ps1 +++ b/src/runtime/cli/install.ps1 @@ -89,11 +89,11 @@ function Get-Env { $EnvRegisterKey.GetValue($Key, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } -# The installation of bun is it's own function so that in the unlikely case the $IsBaseline check fails, we can do a recursive call. -# There are also lots of sanity checks out of fear of anti-virus software or other weird Windows things happening. +# There are lots of sanity checks out of fear of anti-virus software or other weird Windows things happening. function Install-Bun { param( [string]$Version, + # Kept for back-compat with callers; x64 ships one (nehalem) binary now. [bool]$ForceBaseline = $False ); @@ -107,6 +107,8 @@ function Install-Bun { $IsARM64 = $Arch -eq "ARM64" $BunArch = if ($IsARM64) { "aarch64" } else { "x64" } + # Current x64 releases ship one nehalem binary, but a pinned $Version may + # predate that. Probe AVX2 on x64; new releases alias the -baseline name. $IsBaseline = $false if (-not $IsARM64) { $IsBaseline = $ForceBaseline @@ -205,7 +207,7 @@ function Install-Bun { $BunRevision = "$(& "${BunBin}\bun.exe" --revision)" if ($LASTEXITCODE -eq 1073741795) { # STATUS_ILLEGAL_INSTRUCTION if ($IsBaseline) { - Write-Output "Install Failed - bun.exe (baseline) is not compatible with your CPU.`n" + Write-Output "Install Failed - bun.exe is not compatible with your CPU (requires SSE4.2 / Nehalem or later).`n" Write-Output "Please open a GitHub issue with your CPU model:`nhttps://github.com/oven-sh/bun/issues/new/choose`n" return 1 } diff --git a/src/runtime/cli/install.sh b/src/runtime/cli/install.sh index 59c680adad7..b338bd03cde 100644 --- a/src/runtime/cli/install.sh +++ b/src/runtime/cli/install.sh @@ -105,7 +105,8 @@ GITHUB=${GITHUB-"https://github.com"} github_repo="$GITHUB/oven-sh/bun" -# If AVX2 isn't supported, use the -baseline build +# A pinned older version ($1) may still split haswell/nehalem; probe AVX2 and +# request `-baseline` on miss. Current releases alias that name. case "$target" in 'darwin-x64'*) if [[ $(sysctl -a | grep machdep.cpu | grep AVX2) == '' ]]; then @@ -113,7 +114,6 @@ case "$target" in fi ;; 'linux-x64'*) - # If AVX2 isn't supported, use the -baseline build if [[ $(cat /proc/cpuinfo | grep avx2) = '' ]]; then target="$target-baseline" fi diff --git a/src/runtime/cli/upgrade_command.rs b/src/runtime/cli/upgrade_command.rs index ebab8377c38..291dbc2cffd 100644 --- a/src/runtime/cli/upgrade_command.rs +++ b/src/runtime/cli/upgrade_command.rs @@ -113,19 +113,10 @@ impl Version { } else { "" }; - const SUFFIX_CPU: &'static str = if Environment::BASELINE { - "-baseline" - } else { - "" - }; - const SUFFIX: &'static str = const_format::concatcp!(Version::SUFFIX_ABI, Version::SUFFIX_CPU); + const SUFFIX: &'static str = Version::SUFFIX_ABI; pub const FOLDER_NAME: &'static str = const_format::concatcp!("bun-", Version::TRIPLET, Version::SUFFIX); - pub const BASELINE_FOLDER_NAME: &'static str = - const_format::concatcp!("bun-", Version::TRIPLET, "-baseline"); pub const ZIP_FILENAME: &'static str = const_format::concatcp!(Version::FOLDER_NAME, ".zip"); - pub const BASELINE_ZIP_FILENAME: &'static str = - const_format::concatcp!(Version::BASELINE_FOLDER_NAME, ".zip"); pub const PROFILE_FOLDER_NAME: &'static str = const_format::concatcp!("bun-", Version::TRIPLET, Version::SUFFIX, "-profile"); @@ -135,17 +126,6 @@ impl Version { const CURRENT_VERSION: &'static str = const_format::concatcp!("bun-v", Global::package_json_version); - pub const BUN__GITHUB_BASELINE_URL: &'static ZStr = { - const S: &str = const_format::concatcp!( - "https://github.com/oven-sh/bun/releases/download/bun-v", - Global::package_json_version, - "/", - Version::BASELINE_ZIP_FILENAME, - "\0" - ); - ZStr::from_static(S.as_bytes()) - }; - pub fn is_current(&self) -> bool { &*self.tag == Self::CURRENT_VERSION.as_bytes() } @@ -161,7 +141,7 @@ impl Version { tag: IntegrityTag::SHA256, ..Default::default() }; - for (i, pair) in buf[PREFIX.len()..].chunks_exact(2).enumerate() { + for (i, pair) in buf[PREFIX.len()..].as_chunks::<2>().0.iter().enumerate() { match bun_fmt::hex_pair_value(pair[0], pair[1]) { Some(byte) => digest.value[i] = byte, None => return Integrity::default(), @@ -199,8 +179,6 @@ pub(crate) static Bun__githubURL: SyncCStr = SyncCStr( pub struct UpgradeCommand; impl UpgradeCommand { - pub const BUN__GITHUB_BASELINE_URL: &'static ZStr = Version::BUN__GITHUB_BASELINE_URL; - const DEFAULT_GITHUB_HEADERS: &'static [u8] = b"Acceptapplication/vnd.github.v3+json"; pub fn get_latest_version( diff --git a/src/runtime/image/codecs.rs b/src/runtime/image/codecs.rs index e6388b129cb..347fb2eec6a 100644 --- a/src/runtime/image/codecs.rs +++ b/src/runtime/image/codecs.rs @@ -301,7 +301,7 @@ pub fn decode(bytes: &[u8], max_pixels: u64, hint: DecodeHint) -> Result().0 { if px[3] == 0 { px[0] = 0; px[1] = 0; diff --git a/src/runtime/node/path.rs b/src/runtime/node/path.rs index cc596ab73b1..4b21ee56b95 100644 --- a/src/runtime/node/path.rs +++ b/src/runtime/node/path.rs @@ -1085,10 +1085,10 @@ fn _format_t<'a, T: PathCharCwd>( // `${pathObject.name || ''}${formatExt(pathObject.ext)}`; let mut base_len = base.len(); // Borrowck: track range into buf instead of slice. - let base_or_name_ext_range: (usize, usize); - if base_len > 0 { + + let base_or_name_ext_range: (usize, usize) = if base_len > 0 { memmove(&mut buf[0..base_len], base); - base_or_name_ext_range = (0, base_len); + (0, base_len) } else { let formatted_ext_len = { // Borrowck: inline format_ext_t to avoid overlapping &mut. @@ -1116,12 +1116,12 @@ fn _format_t<'a, T: PathCharCwd>( if name_len > 0 { memmove(&mut buf[0..name_len], _name); } - base_or_name_ext_range = if buf_size > 0 { + if buf_size > 0 { (0, buf_size) } else { (0, base_len) - }; - } + } + }; // Translated from the following JS code: // if (!dir) { diff --git a/src/runtime/socket/udp_socket.rs b/src/runtime/socket/udp_socket.rs index c0a45d8341d..25da46e3411 100644 --- a/src/runtime/socket/udp_socket.rs +++ b/src/runtime/socket/udp_socket.rs @@ -2289,7 +2289,7 @@ pub fn js_dgram_bind_fd(global: &JSGlobalObject, frame: &CallFrame) -> JsResult< // Numeric literals only — the JS layer resolves names before calling. let mut storage: sockaddr_storage = bun_core::ffi::zeroed(); - let socklen: libc::socklen_t; + // SAFETY: storage is large enough for sockaddr_in; src is NUL-terminated. let addr4 = unsafe { &mut *std::ptr::from_mut(&mut storage).cast::() }; // SAFETY: libc addr-format fn; src is NUL-terminated, dst points to in_addr-sized storage. @@ -2300,10 +2300,10 @@ pub fn js_dgram_bind_fd(global: &JSGlobalObject, frame: &CallFrame) -> JsResult< (&raw mut addr4.addr).cast::(), ) }; - if parsed_v4 == 1 { + let socklen: libc::socklen_t = if parsed_v4 == 1 { addr4.family = inet::AF_INET as inet::sa_family_t; addr4.port = htons(port); - socklen = size_of::() as libc::socklen_t; + size_of::() as libc::socklen_t } else { // SAFETY: storage is large enough for sockaddr_in6. let addr6 = unsafe { &mut *std::ptr::from_mut(&mut storage).cast::() }; @@ -2326,8 +2326,8 @@ pub fn js_dgram_bind_fd(global: &JSGlobalObject, frame: &CallFrame) -> JsResult< } addr6.family = inet::AF_INET6 as inet::sa_family_t; addr6.port = htons(port); - socklen = size_of::() as libc::socklen_t; - } + size_of::() as libc::socklen_t + }; // IPV6_V6ONLY, SO_REUSEADDR/SO_REUSEPORT and bind(2) go through bsd.c // so this doesn't fork its platform gate. diff --git a/src/runtime/valkey_jsc/valkey.rs b/src/runtime/valkey_jsc/valkey.rs index 8af08821c23..b2a61080693 100644 --- a/src/runtime/valkey_jsc/valkey.rs +++ b/src/runtime/valkey_jsc/valkey.rs @@ -1214,9 +1214,8 @@ impl ValkeyClient { // `self.write_buffer` directly (disjoint field) via `WriteBufWriter`. let hello_write_result = { let mut hello_args_buf: [&[u8]; 4] = [b"3", b"AUTH", b"", b""]; - let hello_args: &[&[u8]]; - if !self.username.is_empty() || !self.password.is_empty() { + let hello_args: &[&[u8]] = if !self.username.is_empty() || !self.password.is_empty() { hello_args_buf[0] = b"3"; hello_args_buf[1] = b"AUTH"; @@ -1228,10 +1227,10 @@ impl ValkeyClient { hello_args_buf[3] = &self.password; } - hello_args = &hello_args_buf[0..4]; + &hello_args_buf[0..4] } else { - hello_args = &hello_args_buf[0..1]; - } + &hello_args_buf[0..1] + }; // Format and send the HELLO command without adding to command queue // We'll handle this response specially in handleResponse diff --git a/src/runtime/webcore/encoding.rs b/src/runtime/webcore/encoding.rs index 1af04d964c7..344d8a57b14 100644 --- a/src/runtime/webcore/encoding.rs +++ b/src/runtime/webcore/encoding.rs @@ -811,8 +811,8 @@ pub(crate) unsafe fn construct_from_u8( // directly into a `Vec` so we never depend on an allocator- // layout-dependent `Vec → Vec` header reinterpret. let mut to = vec![0u8; len * 2]; - for (out, &b) in to.chunks_exact_mut(2).zip(input_slice) { - out.copy_from_slice(&u16::from(b).to_ne_bytes()); + for (out, &b) in to.as_chunks_mut::<2>().0.iter_mut().zip(input_slice) { + *out = u16::from(b).to_ne_bytes(); } to } diff --git a/src/sql_jsc/mysql/MySQLValue.rs b/src/sql_jsc/mysql/MySQLValue.rs index d6e4c49424e..7d2f4e999b9 100644 --- a/src/sql_jsc/mysql/MySQLValue.rs +++ b/src/sql_jsc/mysql/MySQLValue.rs @@ -217,51 +217,47 @@ fn validate_bigint( impl Value { pub fn to_data(&self, field_type: FieldType) -> Result { let mut buffer = [0u8; 15]; // Large enough for all fixed-size types - let pos: usize; - match self { + + let pos: usize = match self { Value::Null => return Ok(Data::Empty), Value::Bool(b) => { buffer[0] = if *b { 1 } else { 0 }; - pos = 1; + 1 } Value::Short(s) => { buffer[0..2].copy_from_slice(&s.to_le_bytes()); - pos = 2; + 2 } Value::Ushort(s) => { buffer[0..2].copy_from_slice(&s.to_le_bytes()); - pos = 2; + 2 } Value::Int(i) => { buffer[0..4].copy_from_slice(&i.to_le_bytes()); - pos = 4; + 4 } Value::Uint(i) => { buffer[0..4].copy_from_slice(&i.to_le_bytes()); - pos = 4; + 4 } Value::Long(l) => { buffer[0..8].copy_from_slice(&l.to_le_bytes()); - pos = 8; + 8 } Value::Ulong(l) => { buffer[0..8].copy_from_slice(&l.to_le_bytes()); - pos = 8; + 8 } Value::Float(f) => { buffer[0..4].copy_from_slice(&f.to_bits().to_le_bytes()); - pos = 4; + 4 } Value::Double(d) => { buffer[0..8].copy_from_slice(&d.to_bits().to_le_bytes()); - pos = 8; - } - Value::Date(d) => { - pos = d.to_binary(field_type, &mut buffer) as usize; - } - Value::Time(d) => { - pos = d.to_binary(field_type, &mut buffer) as usize; + 8 } + Value::Date(d) => d.to_binary(field_type, &mut buffer) as usize, + Value::Time(d) => d.to_binary(field_type, &mut buffer) as usize, Value::StringData(data) | Value::BytesData(data) => { // `bun_sql::shared::Data` is not // `Clone`, so return a `Temporary` aliasing the @@ -290,7 +286,7 @@ impl Value { Data::Temporary(bun_ptr::RawSlice::new(s)) }); } - } + }; Data::create(&buffer[0..pos]).map_err(|_| any_mysql_error::Error::OutOfMemory) } diff --git a/src/sys/lib.rs b/src/sys/lib.rs index 8205e65de80..c6e07160886 100644 --- a/src/sys/lib.rs +++ b/src/sys/lib.rs @@ -1596,6 +1596,9 @@ pub const MAX_COUNT: usize = u32::MAX as usize; #[cfg(unix)] pub(crate) mod safe_libc { use core::ffi::c_int; + // `close` is a libc symbol std relies on; this is an FFI import (not a + // competing definition) with the canonical signature. + #[allow(suspicious_runtime_symbol_definitions)] unsafe extern "C" { #[cfg(not(any(target_os = "linux", target_os = "android")))] pub(crate) safe fn close(fd: c_int) -> c_int; diff --git a/test/cli/run/require-cache.test.ts b/test/cli/run/require-cache.test.ts index 027bf515b47..8207cadc374 100644 --- a/test/cli/run/require-cache.test.ts +++ b/test/cli/run/require-cache.test.ts @@ -8,6 +8,7 @@ import { isCI, isIntelMacOS, isMacOS, + isMusl, isWindows, tempDirWithFiles, } from "harness"; @@ -210,8 +211,10 @@ describe.concurrent("require.cache", () => { }, 60000); test.todoIf( - // Flaky specifically on macOS CI. - isBroken && isMacOS && isCI, + // Flaky specifically on macOS CI, and on musl-aarch64 under ThinLTO + + // -Zshare-generics where RSS reports ~280 MB for the same workload + // that measures under 64 MB elsewhere (intermittent). + isBroken && isCI && (isMacOS || (isMusl && isArm64)), )( "via require() with a lot of function calls", async () => { diff --git a/test/internal/macos-cross-config.test.ts b/test/internal/macos-cross-config.test.ts index a7aa7b8124d..f196d634659 100644 --- a/test/internal/macos-cross-config.test.ts +++ b/test/internal/macos-cross-config.test.ts @@ -17,8 +17,12 @@ import { webkit } from "../../scripts/build/deps/webkit.ts"; import { parsePackedFeaturesList } from "../../scripts/build/features-json.ts"; import { computeFlags, DARWIN_STACK_SIZE } from "../../scripts/build/flags.ts"; import { MACOS_SDK_VERSION, macosSdkCachePath, resolveMacosSdkPath } from "../../scripts/build/macos-sdk.ts"; -import { rustCanCrossFromLinux, rustTarget } from "../../scripts/build/rust.ts"; -import { machoEntitlementsPlist, machoPostlinkCommand } from "../../scripts/build/shims.ts"; +import { rustTarget } from "../../scripts/build/rust.ts"; +import { + elfDebugCompressPostlinkCommand, + machoEntitlementsPlist, + machoPostlinkCommand, +} from "../../scripts/build/shims.ts"; /** A fully-populated fake toolchain — resolveConfig never spawns any of these. */ function mockToolchain(overrides: Partial = {}): Toolchain { @@ -181,7 +185,7 @@ describe.skipIf(isMacOS)("macOS cross-compile config (non-darwin host)", () => { test("native links don't get a postlink command", () => { const linux = resolveConfig( - { os: "linux", arch: "x64", abi: "gnu", buildType: "Release" }, + { os: "linux", arch: "x64", abi: "gnu", buildType: "Release", linuxSysroot: "/fake" }, mockToolchain({ ld64Lld: undefined, llvmStrip: undefined, dsymutil: undefined }), ); expect(machoPostlinkCommand(linux)).toBe(""); @@ -197,7 +201,6 @@ describe.skipIf(isMacOS)("macOS cross-compile config (non-darwin host)", () => { test("rust side cross-compiles to apple-darwin triples from linux", () => { const cfg = resolveDarwin(); expect(rustTarget(cfg)).toBe("aarch64-apple-darwin"); - expect(rustCanCrossFromLinux(cfg)).toBe(true); expect(rustTarget(resolveDarwin({ arch: "x64" }))).toBe("x86_64-apple-darwin"); }); @@ -213,15 +216,39 @@ describe.skipIf(isMacOS)("macOS cross-compile config (non-darwin host)", () => { expect(x64.url).toContain("bun-webkit-macos-amd64.tar.gz"); }); - test("native linux configs are unaffected", () => { + test("rust-lld links compress ELF debug sections post-link, not at link time", () => { + // rust-lld (built without LLVM_ENABLE_ZLIB) can't take + // --compress-debug-sections=zlib, so when the crosslang-LTO swap picks it + // the flag is dropped and llvm-objcopy compresses after the link instead. + // Uncompressed DWARF roughly doubles bun-profile, which every `--compile` + // test copies — the size is a CI-timeout regression, not just cosmetic. + const rustLld = "/fake/rust/lib/rustlib/x86_64-unknown-linux-gnu/bin/gcc-ld/ld.lld"; + const linux = { os: "linux", arch: "x64", abi: "gnu", buildType: "Release", linuxSysroot: "/fake" } as const; + const withRustLld = resolveConfig( + { ...linux, lto: true }, + mockToolchain({ ld64Lld: undefined, llvmStrip: undefined, dsymutil: undefined, rustLld }), + ); + expect(withRustLld.ld).toBe(rustLld); + expect(computeFlags(withRustLld).ldflags).not.toContain("-Wl,--compress-debug-sections=zlib"); + expect(elfDebugCompressPostlinkCommand(withRustLld)).toContain("--compress-debug-sections=zlib $out"); + + // System lld (no swap): compress at link time, no postlink pass. + const systemLld = resolveConfig( + linux, + mockToolchain({ ld64Lld: undefined, llvmStrip: undefined, dsymutil: undefined, rustLld }), + ); + expect(systemLld.ld).toBe("/fake/llvm/bin/ld.lld"); + expect(computeFlags(systemLld).ldflags).toContain("-Wl,--compress-debug-sections=zlib"); + expect(elfDebugCompressPostlinkCommand(systemLld)).toBe(""); + }); + + test("linux configs don't pick up darwin cross machinery", () => { const cfg = resolveConfig( - { os: "linux", arch: "x64", abi: "gnu", buildType: "Release" }, + { os: "linux", arch: "x64", abi: "gnu", buildType: "Release", linuxSysroot: "/fake" }, mockToolchain({ ld64Lld: undefined, llvmStrip: undefined, dsymutil: undefined }), ); - expect(cfg.crossTarget).toBeUndefined(); expect(cfg.osxSysroot).toBeUndefined(); expect(cfg.ld).toBe("/fake/llvm/bin/ld.lld"); - expect(cfg.strip).toBe("/fake/bin/strip"); const flags = computeFlags(cfg); expect(flags.cxxflags.some(f => f.includes("apple-macosx"))).toBe(false); diff --git a/test/internal/source-lints/webkit-prebuilt-url.test.ts b/test/internal/source-lints/webkit-prebuilt-url.test.ts index 7be3becb1bc..5a7ac2701e9 100644 --- a/test/internal/source-lints/webkit-prebuilt-url.test.ts +++ b/test/internal/source-lints/webkit-prebuilt-url.test.ts @@ -41,10 +41,20 @@ function mockToolchain(): Toolchain { }; } -/** Shorthand: a Linux glibc x64 release target, abi pinned so host detection never runs. */ +/** Shorthand: a Linux glibc x64 release target. linuxSysroot stubbed so the + * cross-arch block in resolveConfig never throws on a non-x64-glibc host. */ function resolveLinuxRelease(partial: PartialConfig = {}): Config { return resolveConfig( - { os: "linux", arch: "x64", abi: "gnu", buildType: "Release", lto: false, ...partial }, + { + os: "linux", + arch: "x64", + abi: "gnu", + buildType: "Release", + lto: false, + baseline: false, + linuxSysroot: "/fake/linux-sysroot", + ...partial, + }, mockToolchain(), ); } @@ -77,7 +87,7 @@ describe("WebKit prebuilt URL", () => { test("debug picks the -debug artifact from the same release tag", () => { const cfg = resolveConfig( - { os: "linux", arch: "x64", abi: "gnu", buildType: "Debug", asan: false }, + { os: "linux", arch: "x64", abi: "gnu", buildType: "Debug", asan: false, baseline: false, linuxSysroot: "/fake" }, mockToolchain(), ); expect(prebuiltUrlOf(cfg)).toBe( @@ -85,6 +95,15 @@ describe("WebKit prebuilt URL", () => { ); }); + test("baseline does not affect the suffix (every x64 WebKit is built at the nehalem floor)", () => { + expect(prebuiltUrlOf(resolveLinuxRelease({ lto: true, baseline: true }))).toBe( + `https://github.com/oven-sh/WebKit/releases/download/${defaultTag}/bun-webkit-linux-amd64-lto.tar.gz`, + ); + expect(prebuiltUrlOf(resolveLinuxRelease({ asan: true, baseline: true }))).toBe( + `https://github.com/oven-sh/WebKit/releases/download/${defaultTag}/bun-webkit-linux-amd64-asan.tar.gz`, + ); + }); + test("--webkit-version= uses the plain autobuild- tag", () => { const sha = "0123456789abcdef0123456789abcdef01234567"; const cfg = resolveLinuxRelease({ webkitVersion: sha }); diff --git a/test/internal/source-lints/windows-cross-config.test.ts b/test/internal/source-lints/windows-cross-config.test.ts index 2c78df555db..fbd2e375ecb 100644 --- a/test/internal/source-lints/windows-cross-config.test.ts +++ b/test/internal/source-lints/windows-cross-config.test.ts @@ -17,7 +17,7 @@ import { join } from "node:path"; import { resolveConfig, type Config, type PartialConfig, type Toolchain } from "../../../scripts/build/config.ts"; import { webkit } from "../../../scripts/build/deps/webkit.ts"; import { computeFlags } from "../../../scripts/build/flags.ts"; -import { rustCanCrossFromLinux, rustTarget } from "../../../scripts/build/rust.ts"; +import { rustTarget } from "../../../scripts/build/rust.ts"; /** A fully-populated fake toolchain — resolveConfig never spawns any of these. */ function mockToolchain(overrides: Partial = {}): Toolchain { @@ -75,50 +75,37 @@ function resolveWindowsCross(partial: PartialConfig = {}, toolchain = mockToolch } describe.skipIf(isWindows)("Windows cross-compile LTO config (non-windows host)", () => { - test("ci release x64 cross builds default to non-LTO; --lto=on opts into ThinLTO with cross-language LTO", () => { + test("ci release x64 cross builds default to ThinLTO with cross-language LTO", () => { const cfg = resolveWindowsCross(); expect(cfg.windows).toBe(true); expect(cfg.crossTarget).toBe("x86_64-pc-windows-msvc"); - // Not the default: LLVM's ThinLTO backends miscompile JSC on x86-64 at - // -O1+ (see the ltoDefault comment in config.ts). - expect(cfg.lto).toBe(false); - expect(cfg.crossLangLto).toBe(false); - - // The toolchain support is still wired up behind --lto=on. - const opted = resolveWindowsCross({ lto: true }); - expect(opted.lto).toBe(true); + expect(cfg.lto).toBe(true); // Rust↔C++ inlining: rustc emits bitcode (-Clinker-plugin-lto) and the // final lld-link runs one ThinLTO graph across both halves. - expect(opted.crossLangLto).toBe(true); + expect(cfg.crossLangLto).toBe(true); }); - test("no -lto WebKit prebuilt exists for arm64 or baseline — LTO is forced off there", () => { + test("no -lto WebKit prebuilt exists for arm64 — LTO is forced off there", () => { // arm64: LLVM's CodeView emitter aborts on ARM64 NEON tuple registers // during LTO codegen, so oven-sh/WebKit ships no windows-arm64-lto. const arm64 = resolveWindowsCross({ arch: "aarch64" }); expect(arm64.lto).toBe(false); expect(arm64.crossLangLto).toBe(false); - - // baseline: no windows-amd64-baseline-lto variant. - const baseline = resolveWindowsCross({ baseline: true }); - expect(baseline.lto).toBe(false); - // Forced off even when explicitly requested, so the WebKit fetch never // 404s on a tarball that doesn't exist. expect(resolveWindowsCross({ arch: "aarch64", lto: true }).lto).toBe(false); - expect(resolveWindowsCross({ baseline: true, lto: true }).lto).toBe(false); }); test("local (non-ci) release builds stay non-LTO unless asked", () => { const local = resolveWindowsCross({ ci: false }); expect(local.lto).toBe(false); - const explicit = resolveWindowsCross({ ci: false, lto: true }); + const explicit = resolveWindowsCross({ ci: false, lto: true, baseline: false }); expect(explicit.lto).toBe(true); expect(explicit.crossLangLto).toBe(true); }); test("compile flags use clang-cl ThinLTO without whole-program vtables", () => { - const flags = computeFlags(resolveWindowsCross({ lto: true })); + const flags = computeFlags(resolveWindowsCross({ lto: true, baseline: false })); expect(flags.cxxflags).toContain("-flto=thin"); expect(flags.cflags).toContain("-flto=thin"); // Every summaried module must agree on EnableSplitLTOUnit; rustc's @@ -151,7 +138,10 @@ describe.skipIf(isWindows)("Windows cross-compile LTO config (non-windows host)" "gcc-ld/lld-link": "", }); const rustLld = join(String(dir), "gcc-ld", "ld.lld"); - const cfg = resolveWindowsCross({ lto: true }, mockToolchain({ rustLld, rustLlvmVersion: "22.1.4" })); + const cfg = resolveWindowsCross( + { lto: true, baseline: false }, + mockToolchain({ rustLld, rustLlvmVersion: "22.1.4" }), + ); expect(cfg.ld).toBe(join(String(dir), "gcc-ld", "lld-link")); // Cargo-driven links (bun_shim_impl.exe) must NOT follow the swap: rustc // treats a linker inside its own gcc-ld/ as rust-lld and prepends @@ -168,18 +158,21 @@ describe.skipIf(isWindows)("Windows cross-compile LTO config (non-windows host)" // configure time instead of an opaque "Invalid record" at link time. using bare = tempDir("win-cross-rust-lld-bare", { "gcc-ld/ld.lld": "" }); const bareCfg = resolveWindowsCross( - { lto: true }, + { lto: true, baseline: false }, mockToolchain({ rustLld: join(String(bare), "gcc-ld", "ld.lld"), rustLlvmVersion: "22.1.4" }), ); expect(bareCfg.ld).toBe("/fake/llvm/bin/lld-link"); }); test("LTO selects the -lto WebKit prebuilt with a windows-keyed cache dir", () => { - const lto = webkit.source(resolveWindowsCross({ lto: true })); - if (lto.kind !== "prebuilt") throw new Error(`expected prebuilt WebKit source, got ${lto.kind}`); - expect(lto.url).toContain("bun-webkit-windows-amd64-lto.tar.gz"); - expect(lto.destDir).toContain("-windows"); - expect(lto.destDir).toEndWith("-lto"); + // Default windows x64 cross config (baseline=true, lto=true): every x64 + // WebKit is built at the nehalem floor, so the plain -lto tarball is the + // one baseline fetches too. + const def = webkit.source(resolveWindowsCross()); + if (def.kind !== "prebuilt") throw new Error(`expected prebuilt WebKit source, got ${def.kind}`); + expect(def.url).toContain("bun-webkit-windows-amd64-lto.tar.gz"); + expect(def.destDir).toContain("-windows"); + expect(def.destDir).toEndWith("-lto"); const plain = webkit.source(resolveWindowsCross({ lto: false })); if (plain.kind !== "prebuilt") throw new Error(`expected prebuilt WebKit source, got ${plain.kind}`); @@ -195,21 +188,20 @@ describe.skipIf(isWindows)("Windows cross-compile LTO config (non-windows host)" const cfg = resolveWindowsCross(); expect(rustTarget(cfg)).toBe("x86_64-pc-windows-msvc"); expect(rustTarget(resolveWindowsCross({ arch: "aarch64" }))).toBe("aarch64-pc-windows-msvc"); - // The shared CI rust-only box intentionally does NOT take windows targets - // (no winsysroot provisioned there) — the cross lanes do the full build, - // including the cargo step, on their own agent. - expect(rustCanCrossFromLinux(cfg)).toBe(false); }); - test("linux LTO config is unaffected", () => { + test("linux LTO config uses ThinLTO with WPD and no-split-lto-unit", () => { const linux = resolveConfig( - { os: "linux", arch: "x64", abi: "gnu", buildType: "Release", ci: true, buildkite: false }, + { os: "linux", arch: "x64", abi: "gnu", buildType: "Release", ci: true, buildkite: false, linuxSysroot: "/fake" }, mockToolchain({ cc: "/fake/llvm/bin/clang", cxx: "/fake/llvm/bin/clang++", ld: "/fake/llvm/bin/ld.lld" }), ); expect(linux.lto).toBe(true); const linuxFlags = computeFlags(linux); - expect(linuxFlags.cxxflags).toContain("-flto=full"); + expect(linuxFlags.cxxflags).toContain("-flto=thin"); expect(linuxFlags.cxxflags).toContain("-fwhole-program-vtables"); - expect(linuxFlags.cxxflags).not.toContain("-fno-split-lto-unit"); + // rustc bitcode says EnableSplitLTOUnit=0; clang must match so lld doesn't + // reject with "inconsistent LTO Unit splitting". WPD falls back to + // index-only mode (still devirtualizes, just without the hybrid split). + expect(linuxFlags.cxxflags).toContain("-fno-split-lto-unit"); }); }); diff --git a/test/js/bun/perf/linker-order.test.ts b/test/js/bun/perf/linker-order.test.ts index 4d7197e6f03..26e0eba5b98 100644 --- a/test/js/bun/perf/linker-order.test.ts +++ b/test/js/bun/perf/linker-order.test.ts @@ -35,6 +35,7 @@ const cfg = (overrides: Partial = {}) => canary: true, mode: "link-only", crossTarget: undefined, + canRunOnHost: true, buildDir: "/tmp/build", cwd: "/repo", ...overrides, @@ -124,8 +125,8 @@ describe("deciding whether a build generates its own order file", () => { expect(mustGenerateOrderFile(cfg(), pr, false)).toBe(false); }); - it("a cross-compiled target never does — it cannot run the binary it linked", () => { - const cross = cfg({ crossTarget: "aarch64-unknown-linux-gnu" } as Partial); + it("a target that cannot run on the host never does", () => { + const cross = cfg({ canRunOnHost: false } as Partial); expect(shouldGenerateOrderFile(cfg({ ...cross, canary: false } as Partial), ctx())).toBe(false); expect(mustGenerateOrderFile(cross, ctx(), false)).toBe(false); expect(orderFileEligible(cross, ctx())).toBe(true); // ...but it can still inherit one diff --git a/test/leaksan.supp b/test/leaksan.supp index 95ddaf7e864..69a3e1e5171 100644 --- a/test/leaksan.supp +++ b/test/leaksan.supp @@ -126,3 +126,7 @@ leak:WTF::RunLoop::dispatchAfter # LSAN's conservative stack scan no longer finds the pointers. leak:bun_runtime::cli::filter_run::run_scripts_with_filter leak:bun_runtime::cli::multi_run::run +# test/regression/issue/30205.test.ts — global_exit's final GC sweep fires +# NAPI weak finalizers which enqueue Box into the event +# loop; the loop is already exiting so they never drain. +leak:napi_internal_enqueue_finalizer diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index 72b0e9fda81..1ac51cb7848 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -158,7 +158,10 @@ describe.concurrent.skipIf(!canBuildNodeAddons())("napi", () => { // Not clear how to test for that. } }, - 10 * 1000, + // CI runs tests under bun-profile (~700 MB on linux with ThinLTO + // DWARF); --compile copies+reads+rewrites the whole thing to /tmp, + // which on debian/ubuntu is disk-backed gp3. + 30 * 1000, ); }