aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuri Victorovich <yuri@FreeBSD.org>2026-04-28 00:17:47 +0000
committerYuri Victorovich <yuri@FreeBSD.org>2026-04-28 00:17:47 +0000
commit8407c9414efb4948e63c15ec66c0cb182c1f30cd (patch)
treec293b818f0340fc3d2fa73c47913d5194a072d72
parent58d29f5aa56982cfdc02612776c3c82333c55344 (diff)
lang/bun: New port: JavaScript runtime, bundler, test runner, and package manager
-rw-r--r--lang/Makefile1
-rw-r--r--lang/bun/Makefile246
-rw-r--r--lang/bun/distinfo11
-rw-r--r--lang/bun/files/patch-build.zig42
-rw-r--r--lang/bun/files/patch-scripts_build_codegen.ts17
-rw-r--r--lang/bun/files/patch-scripts_build_config.ts38
-rw-r--r--lang/bun/files/patch-scripts_build_tools.ts36
-rw-r--r--lang/bun/files/patch-src_codegen_bake-codegen.ts48
-rw-r--r--lang/bun/files/patch-src_codegen_bundle-modules.ts32
-rw-r--r--lang/bun/files/patch-test_integration_next-pages_test_dev-server-ssr-100.test.ts39
-rw-r--r--lang/bun/files/patch-test_js_web_console_console-log-utf16.test.ts18
-rw-r--r--lang/bun/files/patch-test_regression_issue_10132.test.ts32
-rw-r--r--lang/bun/files/patch-test_regression_issue_18547.test.ts13
-rw-r--r--lang/bun/files/patch-test_regression_issue_20144_20144.test.ts13
-rw-r--r--lang/bun/files/patch-test_regression_issue_24314.test.ts37
-rw-r--r--lang/bun/pkg-descr11
16 files changed, 634 insertions, 0 deletions
diff --git a/lang/Makefile b/lang/Makefile
index e540ed9d1b06..afd36345ae4c 100644
--- a/lang/Makefile
+++ b/lang/Makefile
@@ -16,6 +16,7 @@
SUBDIR += basic256
SUBDIR += bend
SUBDIR += bsh
+ SUBDIR += bun
SUBDIR += bwbasic
SUBDIR += c
SUBDIR += cairo
diff --git a/lang/bun/Makefile b/lang/bun/Makefile
new file mode 100644
index 000000000000..5d90e89980f5
--- /dev/null
+++ b/lang/bun/Makefile
@@ -0,0 +1,246 @@
+PORTNAME= bun
+DISTVERSIONPREFIX= bun-v
+DISTVERSION= 1.3.13-93
+DISTVERSIONSUFFIX= -g146a674cf6
+CATEGORIES= lang
+MASTER_SITES= GH \
+ https://github.com/oven-sh/zig/releases/download/autobuild-04e7f6ac1e009525bc00934f20199c68f04e0a24/:ovenzig \
+ https://github.com/oven-sh/WebKit/releases/download/autobuild-${WEBKIT_VERSION}/:webkit \
+ https://nodejs.org/dist/v${NODE_VERSION}/:nodejshdr \
+ LOCAL/yuri/:bootstrap
+DISTFILES= bootstrap-x86_64-linux-musl.zip:ovenzig \
+ bun-webkit-freebsd-amd64.tar.gz:webkit \
+ node-v${NODE_VERSION}-headers.tar.gz:nodejshdr \
+ bun-1.3.13-93-bootstrap:bootstrap
+DIST_SUBDIR= ${PORTNAME}
+# Only auto-extract the GitHub source tarball; everything else is handled in post-extract.
+EXTRACT_ONLY= ${DISTNAME}${EXTRACT_SUFX}
+
+MAINTAINER= yuri@FreeBSD.org
+COMMENT= JavaScript runtime, bundler, test runner, and package manager
+WWW= https://bun.com/ \
+ https://github.com/oven-sh/bun
+
+LICENSE= LGPL20
+
+ONLY_FOR_ARCHS= amd64
+ONLY_FOR_ARCHS_REASON= bootstrap binary is only available for amd64 for now
+
+# do-fetch requires npm (node_modules build) and cargo (lolhtml vendor)
+FETCH_DEPENDS= ${LOCALBASE}/bin/npm:www/npm-node24 \
+ git:devel/git \
+ cargo:lang/rust \
+ ${LOCALBASE}/llvm21/bin/clang++:devel/llvm21 \
+ cmake:devel/cmake-core \
+ ninja:devel/ninja \
+ ${LOCALBASE}/bin/node:www/node24
+BUILD_DEPENDS= ${LOCALBASE}/llvm21/bin/clang++:devel/llvm21 \
+ cmake:devel/cmake-core \
+ git:devel/git \
+ ninja:devel/ninja \
+ cargo:lang/rust \
+ ${LOCALBASE}/bin/node:www/node24 \
+ ${LOCALBASE}/bin/npm:www/npm-node24
+TEST_DEPENDS= bash:shells/bash
+
+USES= shebangfix
+
+USE_GITHUB= yes
+GH_ACCOUNT= oven-sh
+
+SHEBANG_FILES= test/regression/issue/18239/data-generator.sh
+
+# WebKit/JavaScriptCore FreeBSD prebuilt
+WEBKIT_VERSION= bdf6aab38a9c6f99df3fd1486406ab6b74180fbb
+WEBKIT_SHORT= bdf6aab38a9c6f99
+# Node.js headers (official tarball from nodejs.org; do-fetch trims unused dirs)
+NODE_VERSION= 24.3.0
+
+# Custom tarballs are built by do-fetch and stored in DD.
+# They are NOT listed in DISTFILES so checksum phase does not verify them.
+
+# Dep tarball cache names and their upstream GitHub URLs.
+# Names are {depname}-{sha256(url)[0:16]}.tar.gz (computed by bun's fetch-cli.ts).
+# Used by do-fetch to pre-populate the bun dep tarballs cache.
+_BUN_DEP_TARBALLS= \
+ boringssl-5e15ff9594809574.tar.gz:https://github.com/oven-sh/boringssl/archive/0c5fce43b7ed5eb6001487ee48ac65766f5ddcd1.tar.gz \
+ brotli-723494d4c3a9902a.tar.gz:https://github.com/google/brotli/archive/v1.1.0.tar.gz \
+ cares-4e43539b43c0f4ae.tar.gz:https://github.com/c-ares/c-ares/archive/3ac47ee46edd8ea40370222f91613fc16c434853.tar.gz \
+ hdrhistogram-97084f213075a65e.tar.gz:https://github.com/HdrHistogram/HdrHistogram_c/archive/be60a9987ee48d0abf0d7b6a175bad8d6c1585d1.tar.gz \
+ highway-a10c8937e1b920ad.tar.gz:https://github.com/google/highway/archive/ac0d5d297b13ab1b89f48484fc7911082d76a93f.tar.gz \
+ libarchive-4296b191210d6b1b.tar.gz:https://github.com/libarchive/libarchive/archive/ded82291ab41d5e355831b96b0e1ff49e24d8939.tar.gz \
+ libdeflate-ce0e2d9805b30dcc.tar.gz:https://github.com/ebiggers/libdeflate/archive/c8c56a20f8f621e6a966b716b31f1dedab6a41e3.tar.gz \
+ lolhtml-929339b1d898e66b.tar.gz:https://github.com/cloudflare/lol-html/archive/77127cd2b8545998756e8d64e36ee2313c4bb312.tar.gz \
+ lshpack-73e0c55d12ea4fc2.tar.gz:https://github.com/litespeedtech/ls-hpack/archive/8905c024b6d052f083a3d11d0a169b3c2735c8a1.tar.gz \
+ mimalloc-9e61bdd994e99463.tar.gz:https://github.com/oven-sh/mimalloc/archive/dbbb9a0819d00d521f9f58ad4d0a336efa4ce53a.tar.gz \
+ picohttpparser-fad59b16ad4752cc.tar.gz:https://github.com/h2o/picohttpparser/archive/066d2b1e9ab820703db0837a7255d92d30f0c9f5.tar.gz \
+ zlib-655c6ecdb6fc9cd5.tar.gz:https://github.com/zlib-ng/zlib-ng/archive/12731092979c6d07f42da27da673a9f6c7b13586.tar.gz \
+ zstd-e010993a24072468.tar.gz:https://github.com/facebook/zstd/archive/f8745da6ff1ad1e7bab384bd1f9d742439278e99.tar.gz
+
+DD= ${DISTDIR}/${DIST_SUBDIR}
+BUN_CACHE_DIR= ${WRKDIR}/bun-cache
+BUN_ZIG_DIR= ${WRKDIR}/oven-zig
+BUN_BUILD_DIR= ${WRKSRC}/build/release
+
+PLIST_FILES= bin/${PORTNAME}
+
+# do-fetch: download the standard distfiles, then build the 3 custom archives
+# (node_modules, dep tarballs, cargo vendor) if they are not already cached.
+# All files land in ${DD} so make checksum and poudriere can reuse them.
+do-fetch:
+ @${MKDIR} ${DD}
+ # Fetch directly-downloadable distfiles (GH0 uses DEFAULT master site, no group suffix)
+ @${SETENV} ${_DO_FETCH_ENV} ${_MASTER_SITES_ENV} dp_SITE_FLAVOR=MASTER \
+ ${SH} ${SCRIPTSDIR}/do-fetch.sh \
+ ${DISTNAME}${EXTRACT_SUFX} \
+ ${DISTFILES:M*\:ovenzig} \
+ ${DISTFILES:M*\:webkit} \
+ ${DISTFILES:M*\:nodejshdr} \
+ ${DISTFILES:M*\:bootstrap}
+ # Build bun-node-modules tarball (npm install with workspace: → file: fixes).
+ # Uses deterministic tar so the checksum is reproducible across builds.
+ @if ! [ -f "${DD}/bun-node-modules-${DISTVERSION}.tar.gz" ]; then \
+ ${ECHO_CMD} "===> Building bun-node-modules-${DISTVERSION}.tar.gz" && \
+ _NTmp=$$(mktemp -d /tmp/bun-nmod.XXXXXX) && \
+ bsdtar xzf ${DD}/${DISTNAME}${EXTRACT_SUFX} -C $$_NTmp && \
+ _NSrc=$$_NTmp/${WRKSRC:T} && \
+ ${SED} -i '' \
+ 's|"@types/bun": "workspace:\*"|"@types/bun": "file:packages/@types/bun"|' \
+ $$_NSrc/package.json && \
+ ${SED} -i '' \
+ 's|"bun-types": "workspace:"|"bun-types": "file:../bun-types"|' \
+ $$_NSrc/packages/@types/bun/package.json && \
+ (cd $$_NSrc && ${LOCALBASE}/bin/npm install --ignore-scripts) && \
+ (cd $$_NSrc/packages/bun-error && \
+ ${LOCALBASE}/bin/npm install --ignore-scripts) && \
+ (cd $$_NSrc/src/node-fallbacks && \
+ ${LOCALBASE}/bin/npm install --ignore-scripts) && \
+ ${FIND} $$_NSrc/node_modules \
+ $$_NSrc/packages/bun-error/node_modules \
+ $$_NSrc/src/node-fallbacks/node_modules \
+ -exec ${TOUCH} -h -d 1970-01-01T00:00:00Z {} \; && \
+ (cd $$_NSrc && \
+ ${FIND} node_modules packages/bun-error/node_modules \
+ src/node-fallbacks/node_modules -print0 \
+ | LC_ALL=C ${SORT} -z \
+ | ${SETENV} -i ${TAR} czf ${DD}/bun-node-modules-${DISTVERSION}.tar.gz \
+ --format=cpio --gid 0 --uid 0 --options gzip:!timestamp \
+ --no-recursion --null -T -) && \
+ ${RM} -rf $$_NTmp; \
+ fi
+ # Build bun-dep-tarballs + bun-cargo-vendor-lolhtml tarballs.
+ # Uses deterministic tar so checksums are reproducible across builds.
+ @if ! [ -f "${DD}/bun-dep-tarballs-${DISTVERSION}.tar.gz" ] || \
+ ! [ -f "${DD}/bun-cargo-vendor-lolhtml-${DISTVERSION}.tar.gz" ]; then \
+ ${ECHO_CMD} "===> Downloading bun dep tarballs" && \
+ _DTmp=$$(mktemp -d /tmp/bun-dtp.XXXXXX) && \
+ ${MKDIR} $$_DTmp/tarballs && \
+ for _tu in ${_BUN_DEP_TARBALLS}; do \
+ _tc=$${_tu%%:*}; _url=$${_tu#*:}; \
+ if ! [ -f "$$_DTmp/tarballs/$$_tc" ]; then \
+ ${FETCH_CMD} ${FETCH_BEFORE_ARGS} $$_url ${FETCH_AFTER_ARGS} \
+ -o $$_DTmp/tarballs/$$_tc || exit 1; \
+ fi; \
+ done && \
+ if ! [ -f "${DD}/bun-dep-tarballs-${DISTVERSION}.tar.gz" ]; then \
+ ${ECHO_CMD} "===> Packaging bun-dep-tarballs-${DISTVERSION}.tar.gz" && \
+ ( cd $$_DTmp && \
+ ${FIND} tarballs -exec ${TOUCH} -h -d 1970-01-01T00:00:00Z {} \; && \
+ ${FIND} tarballs -print0 \
+ | LC_ALL=C ${SORT} -z \
+ | ${SETENV} -i ${TAR} czf ${DD}/bun-dep-tarballs-${DISTVERSION}.tar.gz \
+ --format=cpio --gid 0 --uid 0 --options gzip:!timestamp \
+ --no-recursion --null -T - ); \
+ fi && \
+ if ! [ -f "${DD}/bun-cargo-vendor-lolhtml-${DISTVERSION}.tar.gz" ]; then \
+ ${ECHO_CMD} "===> Vendoring lolhtml Rust crates" && \
+ _LTmp=$$(mktemp -d /tmp/bun-lol.XXXXXX) && \
+ bsdtar xzf $$_DTmp/tarballs/lolhtml-929339b1d898e66b.tar.gz \
+ -C $$_LTmp --strip-components 1 && \
+ (cd $$_LTmp/c-api && \
+ ${SETENV} HOME=$$_LTmp \
+ ${LOCALBASE}/bin/cargo vendor vendor) && \
+ ${FIND} $$_LTmp/c-api/vendor -exec ${TOUCH} -h -d 1970-01-01T00:00:00Z {} \; && \
+ ( cd $$_LTmp/c-api && \
+ ${FIND} vendor -print0 \
+ | LC_ALL=C ${SORT} -z \
+ | ${SETENV} -i ${TAR} czf ${DD}/bun-cargo-vendor-lolhtml-${DISTVERSION}.tar.gz \
+ --format=cpio --gid 0 --uid 0 --options gzip:!timestamp \
+ --no-recursion --null -T - ) && \
+ ${RM} -rf $$_LTmp; \
+ fi && \
+ ${RM} -rf $$_DTmp; \
+ fi
+
+post-extract:
+ # Set up bun cache directory structure
+ @${MKDIR} ${BUN_CACHE_DIR}/tarballs
+ # Extract oven-sh/zig fork (Linux binary, via linux compat)
+ @${MKDIR} ${BUN_ZIG_DIR}
+ @cd ${BUN_ZIG_DIR} && bsdtar -xf ${DD}/bootstrap-x86_64-linux-musl.zip
+ @${CHMOD} +x ${BUN_ZIG_DIR}/bootstrap-x86_64-linux-musl/zig
+ # Install pre-built native FreeBSD bun bootstrap binary
+ @${MKDIR} ${WRKDIR}/bun-bootstrap
+ @${CP} ${DD}/bun-1.3.13-93-bootstrap ${WRKDIR}/bun-bootstrap/bun
+ @${CHMOD} +x ${WRKDIR}/bun-bootstrap/bun
+ # Extract WebKit prebuilt into cache
+ @${MKDIR} ${BUN_CACHE_DIR}/webkit-${WEBKIT_SHORT}-freebsd
+ @tar xzf ${DD}/bun-webkit-freebsd-amd64.tar.gz \
+ -C ${BUN_CACHE_DIR}/webkit-${WEBKIT_SHORT}-freebsd \
+ --strip-components 1
+ @${ECHO_CMD} "${WEBKIT_VERSION}" > \
+ ${BUN_CACHE_DIR}/webkit-${WEBKIT_SHORT}-freebsd/.identity
+ # Extract dep tarballs into cache
+ @tar xzf ${DD}/bun-dep-tarballs-${DISTVERSION}.tar.gz \
+ -C ${BUN_CACHE_DIR}
+ # Extract vendored Rust crates for lolhtml
+ @${MKDIR} ${WRKDIR}/cargo-vendor-lolhtml
+ @tar xzf ${DD}/bun-cargo-vendor-lolhtml-${DISTVERSION}.tar.gz \
+ -C ${WRKDIR}/cargo-vendor-lolhtml
+ # Configure cargo to use vendored sources (via CARGO_HOME config)
+ @${MKDIR} ${WRKDIR}/.cargo
+ @${PRINTF} '[source.crates-io]\nreplace-with = "vendored-sources"\n\n[source.vendored-sources]\ndirectory = "%s/vendor"\n' \
+ ${WRKDIR}/cargo-vendor-lolhtml > ${WRKDIR}/.cargo/config.toml
+ # Extract nodejs headers into bun cache and remove dirs bun doesn't need
+ @${MKDIR} ${BUN_CACHE_DIR}/nodejs-headers-${NODE_VERSION}
+ @tar xzf ${DD}/node-v${NODE_VERSION}-headers.tar.gz \
+ -C ${BUN_CACHE_DIR}/nodejs-headers-${NODE_VERSION} \
+ --strip-components 1
+ @${RM} -rf \
+ ${BUN_CACHE_DIR}/nodejs-headers-${NODE_VERSION}/include/node/openssl \
+ ${BUN_CACHE_DIR}/nodejs-headers-${NODE_VERSION}/include/node/uv \
+ ${BUN_CACHE_DIR}/nodejs-headers-${NODE_VERSION}/include/node/uv.h
+ @${ECHO_CMD} "${NODE_VERSION}" > \
+ ${BUN_CACHE_DIR}/nodejs-headers-${NODE_VERSION}/.identity
+ # Extract node_modules into WRKSRC
+ @tar xzf ${DD}/bun-node-modules-${DISTVERSION}.tar.gz \
+ -C ${WRKSRC}
+
+do-build:
+ @cd ${WRKSRC} && \
+ ${SETENV} PATH=${LOCALBASE}/llvm21/bin:${LOCALBASE}/bin:/usr/bin:/bin \
+ BUN_ZIG_PATH=${BUN_ZIG_DIR}/bootstrap-x86_64-linux-musl \
+ BUN_HOST_OS=freebsd \
+ FREEBSD_SYSROOT=/ \
+ CARGO_HOME=${WRKDIR}/.cargo \
+ GIT_SHA=${DISTVERSIONSUFFIX:S/-g//} \
+ ${WRKDIR}/bun-bootstrap/bun scripts/build.ts \
+ --os=freebsd \
+ --cacheDir=${BUN_CACHE_DIR} \
+ --buildDir=${BUN_BUILD_DIR} \
+ --profile=release \
+ -j${MAKE_JOBS_NUMBER}
+
+do-install:
+ ${INSTALL_PROGRAM} ${BUN_BUILD_DIR}/bun ${STAGEDIR}${PREFIX}/bin/
+
+do-test:
+ @${ECHO_CMD} "==> Running Bun version check..."
+ ${STAGEDIR}${PREFIX}/bin/bun --version
+ @${ECHO_CMD} "==> Installing test dependencies..."
+ cd ${WRKSRC}/test && ${STAGEDIR}${PREFIX}/bin/bun install \
+ --frozen-lockfile --ignore-scripts
+ @${ECHO_CMD} "==> Running Bun regression tests..."
+ cd ${WRKSRC} && ${STAGEDIR}${PREFIX}/bin/bun test --timeout 120000 --parallel 1
+
+.include <bsd.port.mk>
diff --git a/lang/bun/distinfo b/lang/bun/distinfo
new file mode 100644
index 000000000000..3a7d3a4aafe4
--- /dev/null
+++ b/lang/bun/distinfo
@@ -0,0 +1,11 @@
+TIMESTAMP = 1777332350
+SHA256 (bun/bootstrap-x86_64-linux-musl.zip) = 0c09b44c0b2432d7ad8a759b685ba8ae1f883902851ac52e282ad60a89bebe42
+SIZE (bun/bootstrap-x86_64-linux-musl.zip) = 100112252
+SHA256 (bun/bun-webkit-freebsd-amd64.tar.gz) = 2bbab3f44e3b3d7961408c703271285ca3cc4d128d7ff9fc54dc5ab112e89fbf
+SIZE (bun/bun-webkit-freebsd-amd64.tar.gz) = 37660405
+SHA256 (bun/node-v24.3.0-headers.tar.gz) = 045e9bf477cd5db0ec67f8c1a63ba7f784dedfe2c581e3d0ed09b88e9115dd07
+SIZE (bun/node-v24.3.0-headers.tar.gz) = 8747815
+SHA256 (bun/bun-1.3.13-93-bootstrap) = aea7f1c221f812d726aad220ecc879914bbe49b05e5bd4e76c3155ec0a133f54
+SIZE (bun/bun-1.3.13-93-bootstrap) = 105685712
+SHA256 (bun/oven-sh-bun-bun-v1.3.13-93-g146a674cf6_GH0.tar.gz) = 12bec710dd6717f83ccaaec7fb7970828177f173639018042ca3031b851b24ff
+SIZE (bun/oven-sh-bun-bun-v1.3.13-93-g146a674cf6_GH0.tar.gz) = 54226399
diff --git a/lang/bun/files/patch-build.zig b/lang/bun/files/patch-build.zig
new file mode 100644
index 000000000000..e9da8d2ea74a
--- /dev/null
+++ b/lang/bun/files/patch-build.zig
@@ -0,0 +1,42 @@
+-- Fix Zig 0.15.x API compatibility issues when building bun for FreeBSD:
+-- 1. FreeBSD sysroot string literal: use @as([]const u8, "/") instead of bare "/"
+-- because *const [1:0]u8 cannot coerce to ?[]const u8 in oven-sh/zig fork
+-- 2. SemanticVersion format: use {d}.{d}.{d} instead of {f} for compatibility
+-- 3. validateGeneratedPath: skip panic when generated files are not yet present
+
+--- build.zig.orig 2026-04-27 07:25:40 UTC
++++ build.zig
+@@ -90,7 +90,7 @@ const BunBuildOptions = struct {
+ opts.addOption(bool, "enable_valgrind", this.enable_valgrind);
+ opts.addOption(bool, "enable_tinycc", this.enable_tinycc);
+ opts.addOption(bool, "use_mimalloc", this.use_mimalloc);
+- opts.addOption([]const u8, "reported_nodejs_version", b.fmt("{f}", .{this.reported_nodejs_version}));
++ opts.addOption([]const u8, "reported_nodejs_version", b.fmt("{d}.{d}.{d}", .{this.reported_nodejs_version.major, this.reported_nodejs_version.minor, this.reported_nodejs_version.patch}));
+ opts.addOption(bool, "zig_self_hosted_backend", this.no_llvm);
+ opts.addOption(bool, "override_no_export_cpp_apis", this.override_no_export_cpp_apis);
+
+@@ -226,7 +226,7 @@ pub fn build(b: *Build) !void {
+ // linkLibC() gets FreeBSD libc via `zig build --libc <file>`. On a
+ // native FreeBSD host the system root is the sysroot.
+ const freebsd_sysroot = b.option([]const u8, "freebsd_sysroot", "FreeBSD sysroot (extracted base.txz) for translate-c headers") orelse
+- if (os == .freebsd and builtin.os.tag == .freebsd) "/" else null;
++ if (os == .freebsd and builtin.os.tag == .freebsd) @as([]const u8, "/") else null;
+ if (os == .freebsd and freebsd_sysroot == null) {
+ std.debug.panic("-Dfreebsd_sysroot is required when cross-compiling to FreeBSD (zig does not bundle FreeBSD libc headers)", .{});
+ }
+@@ -1086,13 +1086,8 @@ fn validateGeneratedPath(path: []const u8) void {
+ }
+
+ fn validateGeneratedPath(path: []const u8) void {
+- if (!exists(path)) {
+- std.debug.panic(
+- \\Generated file '{s}' is missing!
+- \\
+- \\Make sure to use CMake and Ninja, or pass a manual codegen folder with '-Dgenerated-code=...'
+- , .{path});
+- }
++ // Skip validation - generated files may not exist before codegen runs
++ _ = path;
+ }
+
+ const WindowsShim = struct {
diff --git a/lang/bun/files/patch-scripts_build_codegen.ts b/lang/bun/files/patch-scripts_build_codegen.ts
new file mode 100644
index 000000000000..2caa71445091
--- /dev/null
+++ b/lang/bun/files/patch-scripts_build_codegen.ts
@@ -0,0 +1,17 @@
+-- Skip bun install when node_modules already exists.
+-- On FreeBSD, bun install fails due to a linux compat layer limitation
+-- with /proc/self/fd (fchdir returns ENOTDIR for symlink-based FDs).
+-- By checking for existing node_modules, the build uses pre-populated
+-- node_modules from the port's distfile instead.
+
+ 2026-04-27 07:19:58 UTC
++++ scripts/build/codegen.ts
+@@ -167,7 +167,7 @@ export function registerCodegenRules(n: Ninja, cfg: Co
+ n.rule("bun_install", {
+ command: hostWin
+ ? `cmd /c "cd /d $dir && ${bun} install --frozen-lockfile && ${touch} $stamp"`
+- : `cd $dir && ${bun} install --frozen-lockfile && ${touch} $stamp`,
++ : `cd $dir && ([ -d node_modules ] || ${bun} install --frozen-lockfile) && ${touch} $stamp`,
+ description: "install $dir",
+ restat: true,
+ // bun install can be memory-hungry and grabs a lockfile; serialize.
diff --git a/lang/bun/files/patch-scripts_build_config.ts b/lang/bun/files/patch-scripts_build_config.ts
new file mode 100644
index 000000000000..97bf1d0421df
--- /dev/null
+++ b/lang/bun/files/patch-scripts_build_config.ts
@@ -0,0 +1,38 @@
+-- Add BUN_HOST_OS environment variable support to override host OS detection.
+-- When building with a Linux bootstrap bun binary on FreeBSD, bun reports
+-- host.os = "linux" which triggers cross-compilation mode (dep_cargo_cross,
+-- rustup target add, cross-compile clang flags). Setting BUN_HOST_OS=freebsd
+-- makes the build treat FreeBSD as native, using dep_cargo (no rustup needed)
+-- and native LLVM/clang without cross-compile sysroot flags.
+--
+-- Also inline WEBKIT_VERSION to break a circular ES module dependency:
+-- deps/index.ts → deps/webkit.ts → source.ts → config.ts → deps/webkit.ts
+-- Bun 1.3.14 enforces strict TDZ for circular ES modules; inlining breaks cycle.
+
+--- scripts/build/config.ts.orig 2026-04-27 07:22:36 UTC
++++ scripts/build/config.ts
+@@ -11,7 +11,9 @@
+ import { homedir, arch as hostArch, platform as hostPlatform } from "node:os";
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
+ import { NODEJS_ABI_VERSION, NODEJS_VERSION } from "./deps/nodejs-headers.ts";
+-import { WEBKIT_VERSION } from "./deps/webkit.ts";
++// WEBKIT_VERSION inlined here (was imported from deps/webkit.ts) to break circular module dep:
++// deps/index.ts → deps/webkit.ts → source.ts → config.ts → deps/webkit.ts
++const WEBKIT_VERSION = "bdf6aab38a9c6f99df3fd1486406ab6b74180fbb";
+ import { assert, BuildError } from "./error.ts";
+ import { clangTargetArch } from "./tools.ts";
+ import { cyan, dim, green } from "./tty.ts";
+@@ -341,9 +343,12 @@
+
+ /**
+ * Host platform detection. Only used for picking defaults.
++ * The BUN_HOST_OS environment variable can override the detected OS.
++ * This is needed when using a Linux bun bootstrap binary on FreeBSD.
+ */
+ export function detectHost(): Host {
+- const plat = hostPlatform();
++ const envOs = process.env.BUN_HOST_OS;
++ const plat = envOs !== undefined ? envOs : hostPlatform();
+ const os: OS =
+ plat === "linux"
+ ? "linux"
diff --git a/lang/bun/files/patch-scripts_build_tools.ts b/lang/bun/files/patch-scripts_build_tools.ts
new file mode 100644
index 000000000000..2ef0f33dbd64
--- /dev/null
+++ b/lang/bun/files/patch-scripts_build_tools.ts
@@ -0,0 +1,36 @@
+-- Add FreeBSD support to LLVM toolchain resolution:
+-- 1. llvmSearchPaths: add /usr/local/llvmNN/bin (FreeBSD ports layout)
+-- 2. resolveLlvmToolchain: use ld.lld for FreeBSD (same as Linux)
+-- 3. llvmInstallHint: add FreeBSD install hint
+--- scripts/build/tools.ts.orig 2026-04-27 08:09:39 UTC
++++ scripts/build/tools.ts
+@@ -312,6 +312,12 @@ function llvmSearchPaths(os: OS, arch: Arch): string[]
+ paths.push(`/usr/lib/llvm${LLVM_MAJOR}/bin`);
+ }
+
++ if (os === "freebsd") {
++ // FreeBSD ports install LLVM under /usr/local/llvm${MAJOR}/bin
++ paths.push(`/usr/local/llvm${LLVM_MAJOR}/bin`);
++ paths.push(`/usr/local/llvm-${LLVM_MAJOR}/bin`);
++ }
++
+ return paths;
+ }
+
+@@ -332,6 +338,7 @@ function llvmInstallHint(os: OS): string {
+ if (os === "darwin") return `Install with: brew install llvm@${LLVM_MAJOR}`;
+ if (os === "linux")
+ return `Install with: apt install clang-${LLVM_MAJOR} lld-${LLVM_MAJOR} (or equivalent for your distro)`;
++ if (os === "freebsd") return `Install with: pkg install llvm${LLVM_MAJOR}`;
+ if (os === "windows") return `Install LLVM ${LLVM_VERSION} from https://github.com/llvm/llvm-project/releases`;
+ return "";
+ }
+@@ -419,7 +426,7 @@ export function resolveLlvmToolchain(
+ let ld: string;
+ if (os === "windows") {
+ ld = findLlvmTool("lld-link", paths, os, { checkVersion: false, required: true })?.path ?? "";
+- } else if (os === "linux") {
++ } else if (os === "linux" || os === "freebsd") {
+ ld = findLlvmTool("ld.lld", paths, os, { checkVersion: true, required: true })?.path ?? "";
+ } else {
+ ld = ""; // darwin: unused
diff --git a/lang/bun/files/patch-src_codegen_bake-codegen.ts b/lang/bun/files/patch-src_codegen_bake-codegen.ts
new file mode 100644
index 000000000000..b7c95c65c911
--- /dev/null
+++ b/lang/bun/files/patch-src_codegen_bake-codegen.ts
@@ -0,0 +1,48 @@
+-- Replace Bun.spawnSync with async Bun.spawn in bake-codegen.
+-- On FreeBSD linux compat layer, Bun.spawnSync with stdio:"pipe" fails
+-- with EINVAL because pread() is not valid on pipes. Using async Bun.spawn
+-- with Response.arrayBuffer() works correctly on FreeBSD linux compat.
+
+ 2026-04-27 07:15:02 UTC
++++ src/codegen/bake-codegen.ts
+@@ -30,20 +30,27 @@ function convertZigEnum(zig: string, names: string[])
+ return output;
+ }
+
+-function css(file: string, is_development: boolean): string {
+- const { success, stdout, stderr } = Bun.spawnSync({
++async function css(file: string, is_development: boolean): Promise<string> {
++ const proc = Bun.spawn({
+ cmd: [process.execPath, "build", file, "--minify"],
+ cwd: import.meta.dir,
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+- if (!success) throw new Error(stderr.toString("utf-8"));
+- return stdout.toString("utf-8");
++ const [stdoutBuf, stderrBuf] = await Promise.all([
++ new Response(proc.stdout).arrayBuffer(),
++ new Response(proc.stderr).arrayBuffer(),
++ ]);
++ const exitCode = await proc.exited;
++ if (exitCode !== 0) throw new Error(Buffer.from(stderrBuf).toString("utf-8"));
++ return Buffer.from(stdoutBuf).toString("utf-8");
+ }
+
+ async function run() {
+ const devServerZig = readFileSync(join(base_dir, "DevServer.zig"), "utf-8");
+ writeIfNotChanged(join(base_dir, "generated.ts"), convertZigEnum(devServerZig, ["IncomingMessageId", "MessageId"]));
+
++ const overlayCSS = await css("../bake/client/overlay.css", !!debug);
++
+ const results = await Promise.allSettled(
+ ["client", "server", "error"].map(async file => {
+ const side = file === "error" ? "client" : file;
+@@ -53,7 +60,7 @@ async function run() {
+ side: JSON.stringify(side),
+ IS_ERROR_RUNTIME: String(file === "error"),
+ IS_BUN_DEVELOPMENT: String(!!debug),
+- OVERLAY_CSS: css("../bake/client/overlay.css", !!debug),
++ OVERLAY_CSS: overlayCSS,
+ },
+ minify: {
+ syntax: !debug,
diff --git a/lang/bun/files/patch-src_codegen_bundle-modules.ts b/lang/bun/files/patch-src_codegen_bundle-modules.ts
new file mode 100644
index 000000000000..c5a3bd64989b
--- /dev/null
+++ b/lang/bun/files/patch-src_codegen_bundle-modules.ts
@@ -0,0 +1,32 @@
+-- Replace Bun.spawnSync with async Bun.spawn in bundle-modules.
+-- On FreeBSD linux compat layer, Bun.spawnSync with stdio:"pipe" fails
+-- with EINVAL because pread() is not valid on pipes. Using async Bun.spawn
+-- with Response.arrayBuffer() works correctly on FreeBSD linux compat.
+
+ 2026-04-27 07:15:02 UTC
++++ src/codegen/bundle-modules.ts
+@@ -221,12 +221,22 @@ verbose("running: ", config_cli);
+ path.join(TMP_DIR, "modules_out"),
+ ];
+ verbose("running: ", config_cli);
+-const out = Bun.spawnSync({
++const _spawnProc = Bun.spawn({
+ cmd: config_cli,
+ cwd: process.cwd(),
+ env: process.env,
+- stdio: ["pipe", "pipe", "pipe"],
++ stdio: ["ignore", "pipe", "pipe"],
+ });
++const [_spawnStdout, _spawnStderr] = await Promise.all([
++ new Response(_spawnProc.stdout).arrayBuffer(),
++ new Response(_spawnProc.stderr).arrayBuffer(),
++]);
++const _spawnExit = await _spawnProc.exited;
++const out = {
++ exitCode: _spawnExit,
++ stdout: Buffer.from(_spawnStdout),
++ stderr: Buffer.from(_spawnStderr),
++};
+ if (out.exitCode !== 0) {
+ console.error(out.stderr.toString());
+ process.exit(out.exitCode);
diff --git a/lang/bun/files/patch-test_integration_next-pages_test_dev-server-ssr-100.test.ts b/lang/bun/files/patch-test_integration_next-pages_test_dev-server-ssr-100.test.ts
new file mode 100644
index 000000000000..b60b8b94e0bd
--- /dev/null
+++ b/lang/bun/files/patch-test_integration_next-pages_test_dev-server-ssr-100.test.ts
@@ -0,0 +1,39 @@
+-- Skip gracefully when bun:internal-for-testing is unavailable (release builds).
+-- bun:internal-for-testing is only compiled into debug/development builds of bun.
+-- FreeBSD ports always build a release binary, so this module is never available.
+-- Without this patch, the test file crashes at module load time with ENOENT,
+-- causing "Unhandled error between tests" in the bun test output.
+--- test/integration/next-pages/test/dev-server-ssr-100.test.ts.orig2026-04-27 17:51:47 UTC
++++ test/integration/next-pages/test/dev-server-ssr-100.test.ts
+@@ -1,12 +1,24 @@
+ import { Subprocess } from "bun";
+-import { install_test_helpers } from "bun:internal-for-testing";
+-import { afterAll, beforeAll, expect, test } from "bun:test";
++// bun:internal-for-testing is only available in debug builds; skip gracefully in release builds.
++let install_test_helpers: any;
++try {
++ ({ install_test_helpers } = require("bun:internal-for-testing") as any);
++} catch {
++ install_test_helpers = null;
++}
++import { afterAll, beforeAll, describe, expect, test } from "bun:test";
+ import { copyFileSync } from "fs";
+ import { cp, rm } from "fs/promises";
+ import PQueue from "p-queue";
+ import { join } from "path";
+ import { StringDecoder } from "string_decoder";
+ import { bunEnv, bunExe, tmpdirSync, toMatchNodeModulesAt } from "../../../harness";
++
++if (!install_test_helpers) {
++ describe.skip("next-pages dev server (bun:internal-for-testing unavailable in release builds)", () => {
++ test("skipped", () => {});
++ });
++} else {
+ const { parseLockfile } = install_test_helpers;
+
+ expect.extend({ toMatchNodeModulesAt });
+@@ -172,3 +184,4 @@
+ },
+ timeout,
+ );
++}
diff --git a/lang/bun/files/patch-test_js_web_console_console-log-utf16.test.ts b/lang/bun/files/patch-test_js_web_console_console-log-utf16.test.ts
new file mode 100644
index 000000000000..85ca207155e1
--- /dev/null
+++ b/lang/bun/files/patch-test_js_web_console_console-log-utf16.test.ts
@@ -0,0 +1,18 @@
+-- Replace toBeEmpty() with toBe("") to avoid matcher pollution from test 16312.
+-- Test 16312 calls expect.extend(@testing-library/jest-dom/matchers) globally,
+-- which overrides bun's built-in toBeEmpty() with a DOM-only version that rejects
+-- strings. When tests run in the same process (bun test --jobs 1), subsequent
+-- tests calling toBeEmpty() on string values fail with "value must be an
+-- HTMLElement or an SVGElement". Using toBe("") is the correct, unambiguous way
+-- to assert an empty string.
+--- test/js/web/console/console-log-utf16.test.ts.orig 2026-04-27 17:38:48 UTC
++++ test/js/web/console/console-log-utf16.test.ts
+@@ -13,7 +13,7 @@ it("works with large utf-16 strings", async () => {
+ const exitCode = await proc.exited;
+ const stdout = await proc.stdout.text();
+ const stderr = await proc.stderr.text();
+- expect(stderr).toBeEmpty();
++ expect(stderr).toBe("");
+ expect(exitCode).toBe(0);
+
+ const expected = Array(10000).fill("肉醬意大利粉").join("\n");
diff --git a/lang/bun/files/patch-test_regression_issue_10132.test.ts b/lang/bun/files/patch-test_regression_issue_10132.test.ts
new file mode 100644
index 000000000000..20726729faa6
--- /dev/null
+++ b/lang/bun/files/patch-test_regression_issue_10132.test.ts
@@ -0,0 +1,32 @@
+-- Fix $.cwd() global state leak from test 10132 into subsequent test files.
+-- Test 10132 sets $.cwd() in a test body but never resets it via afterAll.
+-- When tests run in the same process (bun test --jobs 1), this leaks the
+-- deleted temp dir path as the shell cwd to following test files, causing
+-- ENOENT failures in tests 17405 and 17294 that use the bun Shell ($).
+--- test/regression/issue/10132.test.ts.orig 2026-04-27 17:36:19 UTC
++++ test/regression/issue/10132.test.ts
+@@ -1,5 +1,5 @@ import { $ } from "bun";
+ import { $ } from "bun";
+-import { beforeAll, expect, test } from "bun:test";
++import { afterAll, beforeAll, expect, test } from "bun:test";
+ import { chmodSync } from "fs";
+ import { bunExe, isPosix, tempDirWithFiles } from "harness";
+ import { join } from "path";
+@@ -50,6 +50,11 @@ echo My name is bun-hello2
+ }
+ });
+
++const originalCwd = process.cwd();
++afterAll(() => {
++ $.cwd(originalCwd);
++});
++
+ test("bun run sets cwd for script, matching npm", async () => {
+ $.cwd(dir);
+ const currentPwd = (await $`${bunExe()} run get-pwd`.text()).trim();
+@@ -80,4 +85,5 @@ test("issue #10132, bun run sets PATH", async () => {
+ join(dir, "subdir", "one", "two", "three"),
+ ].map(run),
+ );
++ $.cwd(process.cwd());
+ });
diff --git a/lang/bun/files/patch-test_regression_issue_18547.test.ts b/lang/bun/files/patch-test_regression_issue_18547.test.ts
new file mode 100644
index 000000000000..0e8ec00cdf57
--- /dev/null
+++ b/lang/bun/files/patch-test_regression_issue_18547.test.ts
@@ -0,0 +1,13 @@
+-- Use port: 0 to let the OS assign a random available port.
+-- Without this, Bun.serve defaults to port 3000, which causes EADDRINUSE
+-- failures when tests run concurrently and another test is also using port 3000.
+--- test/regression/issue/18547.test.ts.orig 2026-04-27 18:00:04 UTC
++++ test/regression/issue/18547.test.ts
+@@ -2,6 +2,7 @@ test("18547", async () => {
+
+ test("18547", async () => {
+ using serve = Bun.serve({
++ port: 0,
+ routes: {
+ "/:foo": request => {
+ request.cookies.set("sessionToken", "123456");
diff --git a/lang/bun/files/patch-test_regression_issue_20144_20144.test.ts b/lang/bun/files/patch-test_regression_issue_20144_20144.test.ts
new file mode 100644
index 000000000000..ddd9de97fbe7
--- /dev/null
+++ b/lang/bun/files/patch-test_regression_issue_20144_20144.test.ts
@@ -0,0 +1,13 @@
+-- Skip the TinyCC-based FFI test on FreeBSD; TinyCC is intentionally disabled
+-- on FreeBSD (config.ts: tinycc = !(... || freebsd)).
+--- test/regression/issue/20144/20144.test.ts.orig 2026-04-27 17:31:21 UTC
++++ test/regression/issue/20144/20144.test.ts
+@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
+ import assert from "node:assert";
+ import { spawn } from "node:child_process";
+
+-it.skipIf(process.platform === "win32")("should not time out", done => {
++it.skipIf(process.platform === "win32" || process.platform === "freebsd")("should not time out", done => {
+ const child = spawn(process.execPath, ["run", "./20144.fixture.ts"], {
+ cwd: __dirname,
+ stdio: [null, "inherit", "inherit", "ipc"],
diff --git a/lang/bun/files/patch-test_regression_issue_24314.test.ts b/lang/bun/files/patch-test_regression_issue_24314.test.ts
new file mode 100644
index 000000000000..b3591f686e36
--- /dev/null
+++ b/lang/bun/files/patch-test_regression_issue_24314.test.ts
@@ -0,0 +1,37 @@
+-- Skip gracefully when bun:internal-for-testing is unavailable (release builds).
+-- bun:internal-for-testing is only compiled into debug/development builds of bun.
+-- FreeBSD ports always build a release binary, so this module is never available.
+-- Without this patch, the test file crashes at module load time with ENOENT,
+-- causing "Unhandled error between tests" in the bun test output.
+--- test/regression/issue/24314.test.ts.orig 2026-04-27 17:51:47 UTC
++++ test/regression/issue/24314.test.ts
+@@ -1,4 +1,10 @@
+-import { readTarball } from "bun:internal-for-testing";
++// bun:internal-for-testing is only available in debug builds; skip gracefully in release builds.
++let readTarball: ((path: string) => any) | undefined;
++try {
++ ({ readTarball } = require("bun:internal-for-testing") as any);
++} catch {
++ readTarball = undefined;
++}
+ import { expect, test } from "bun:test";
+ import { bunEnv, bunExe, tempDir } from "harness";
+ import path from "node:path";
+@@ -8,7 +14,7 @@ function normalizePath(p: string): string {
+ return p.replace(/\\/g, "/");
+ }
+
+-test("bun pm pack respects changes to package.json from prepack scripts", async () => {
++test.skipIf(!readTarball)("bun pm pack respects changes to package.json from prepack scripts", async () => {
+ using dir = tempDir("pack-prepack", {
+ "package.json": JSON.stringify(
+ {
+@@ -61,7 +67,7 @@ fs.writeFileSync('package.json', JSON.stringify(pkg, n
+ expect(extractedPkg.description).toBe("MODIFIED BY PREPACK");
+ });
+
+-test("bun pm pack respects changes to package.json from prepare scripts", async () => {
++test.skipIf(!readTarball)("bun pm pack respects changes to package.json from prepare scripts", async () => {
+ using dir = tempDir("pack-prepare", {
+ "package.json": JSON.stringify(
+ {
diff --git a/lang/bun/pkg-descr b/lang/bun/pkg-descr
new file mode 100644
index 000000000000..7385e861e1a5
--- /dev/null
+++ b/lang/bun/pkg-descr
@@ -0,0 +1,11 @@
+Bun is a fast JavaScript runtime, bundler, transpiler, and
+package manager -- all in one.
+
+Key features:
+- Up to 4x faster startup time than Node.js
+- Native TypeScript and JSX support (no separate transpilation step)
+- Built-in package manager (npm-compatible) with global caching
+- Built-in test runner, bundler, and transpiler
+- Web-standard APIs: fetch, WebSocket, ReadableStream, etc.
+- Node.js compatibility: most Node.js built-in modules work out of the box
+- Zig, C++, and Rust -- for maximum performance