Compare commits

..

64 Commits

Author SHA1 Message Date
Sosuke Suzuki
28722f6f1b fix: Proxy-wrapped arrays causing null deref crashes in isArray+jsCast paths
JSC::isArray() follows the ECMA-262 IsArray abstract operation, which
returns true for Proxy objects wrapping arrays. However, jsCast<JSArray*>
and jsDynamicCast<JSArray*> both fail on Proxy (assertion/nullptr), so
any code doing `if (isArray(x)) { jsCast<JSArray*>(x)->... }` would crash.

Fixes:
- Buffer.concat(new Proxy([], {})) -> SEGV at 0x4 (now iterates via get())
- process.setgroups(new Proxy([], {})) -> SEGV at 0x4 (now throws TypeError)
- vm.compileFunction("", new Proxy([], {})) -> debug assertion (now throws)
- vm.compileFunction contextExtensions with Proxy -> debug assertion (now throws)
- new Bun.CookieMap(new Proxy([], {})) -> debug assertion (now falls through to record path)
- expect(proxy).toEqual(expect.arrayContaining([...])) -> UBSan null deref (now FAIL)
- jsHTTPSetHeader with Proxy value -> debug assertion (now falls through to single-value path)

Buffer.concat is the only one that supports Proxy iteration (Node.js compat).
Others reject Proxy with TypeError since Node.js also rejects/crashes on them.

Also replaces getIndexQuickly with getIndex + exception check in NodeVM.cpp
to handle sparse arrays safely.
2026-02-28 02:21:46 +09:00
robobun
6e317c861f fix(http): harden request header buffer bounds in buildRequest (#27501)
## Summary

- Cap the number of user-supplied headers written into the fixed-size
`shared_request_headers_buf` (256 entries) in `buildRequest` to prevent
out-of-bounds writes when a `fetch()` call includes more headers than
the buffer can hold.
- Six slots are reserved for default headers (Connection, User-Agent,
Accept, Host, Accept-Encoding, Content-Length/Transfer-Encoding),
leaving 250 for user-supplied headers. Excess headers are silently
dropped while their semantic flags (e.g. `override_host_header`) are
still processed correctly.
- Added tests verifying that fetch with 300 headers completes without
crashing and that 250 custom headers are all delivered correctly.

## Test plan

- [x] `bun bd test test/js/bun/http/fetch-header-count-limit.test.ts`
passes (2 tests)
- [ ] CI passes on all platforms

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-26 23:38:40 -08:00
robobun
5c9172cf34 feat: implement Bun.sliceAnsi for ANSI & grapheme-aware string slicing (#26963)
## `Bun.sliceAnsi(str, start?, end?, options?)`

Replaces both `slice-ansi` and `cli-truncate` npm packages. Slices
strings by terminal column width while preserving ANSI escape codes (SGR
colors, OSC 8 hyperlinks) and respecting grapheme cluster boundaries
(emoji, combining marks, flags).

```ts
// Plain slice (slice-ansi replacement)
Bun.sliceAnsi(line, from, to)
Bun.sliceAnsi("\x1b[31mhello\x1b[39m", 1, 4)  // "\x1b[31mell\x1b[39m"

// Truncation with ellipsis (cli-truncate replacement)
Bun.sliceAnsi("unicorn", 0, 4, "…")              // "uni…"
Bun.sliceAnsi("unicorn", -4, undefined, "…")     // "…orn"
```

The ellipsis is emitted **inside** active SGR styles (inherits
color/bold) but **outside** hyperlinks. Also supports `{
ambiguousIsNarrow }` matching `stringWidth`/`wrapAnsi`.

---

## Design

### Three-tier dispatch

| Tier | Input | Passes | Allocation |
|------|-------|--------|------------|
| **SIMD ASCII** | All code units ∈ `[0x20, 0x7E]`, or slice range
strictly inside the ASCII prefix | 1 SIMD scan | Zero-copy when nothing
cut |
| **Single-pass streaming** | Non-negative indices (99% of calls) |
**1** input walk | Stack only |
| **Negative indices** | `start < 0` or `end < 0` | 2 (width + emit) |
Stack only |

### Single-pass streaming emit (the hot path)

- `position` advances **only at cluster boundaries** (when
`graphemeBreak` says a new cluster starts) — always correct at decision
points, no correction needed.
- Inline grapheme tracking — no Vector, no pre-pass.
- **SIMD skip-ahead**: `findEscapeCharacter` finds the next escape byte,
then **bulk-emit** the ASCII-printable sub-run in one `append` (process
`asciiLen - 1` chars, leave the last for per-char to seed grapheme
state).
- **One tiny lookahead**: 4-entry inline buffer for ANSI between
consecutive visible chars. Flushed when the next char's break status is
known (continuation → flush all; break past-end → filter close-only).
- **Lazy `cutEnd` for ellipsis**: speculative zone `[end - ew, end)` →
side buffer. Cut detected → discard zone, emit ellipsis. EOF first →
flush zone, cancel ellipsis.

### Debug build bench (200k iters)

| Path | ns/op |
|------|------:|
| ASCII SIMD fast path | ~2,300 |
| ASCII no-op (zero-copy) | ~2,100 |
| ANSI + bulk-ASCII emit | ~17,500 |
| CJK (per-char width-2) | ~9,500 |
| ZWJ emoji (clustering) | ~19,100 |
| Negative index (2-pass) | ~128,000 |

---

## Correctness & hardening

### Fixes found by fuzzing

- **DoS**: unterminated DCS/SOS/PM/APC (`\x90`, `\x98`, `\x9E`, `\x9F`
or ESC variants) previously consumed to EOF. A single `\x90` byte would
swallow the entire string. Now treated as a standalone width-0 control
char — matches `Bun.stringWidth`.
- **`Bun.stringWidth` grapheme bug**: `prev` was updated for ALL bytes
including ANSI. After `\x1b[1m`, `prev='m'`; a following
VS16/ZWJ/combining mark would `graphemeBreak('m', FE0F) = false` →
`add()` on uninitialized state → width 1 instead of 0. Fixed with
separate `prev_visible`.

### Bounds & overflow

- Index resolution matches JSC's `stringSlice<double>`: clamp in double
space, cast to `size_t` only after `[0, totalW]` verified. No int64, no
UB.
- `SgrParams` is a fixed 32-entry stack struct (ECMA-48 caps at 16,
xterm ~30). Overflow → opaque passthrough. Param accumulator clamped at
100,000.
- Ellipsis passed as zero-copy `StringView` — never materialized.

---

## Shared helpers (`ANSIHelpers.h`)

- `firstNonAsciiPrintable<Lane>(span) → index` — SIMD range check via
wrapping sub + unsigned compare, templated for Latin-1/UTF-16
- `sgrCloseCode` / `isSgrEndCode` — dense jump-table switch
- `decodeUTF16` — thin wrapper over ICU's `U16_NEXT` (wrapAnsi now also
uses this)
- `findEscapeCharacter` now matches `0x9C` (C1 ST) for SIMD/tail
consistency

---

## Tests

**347 tests, 5,647 assertions** across 4 files:

- `sliceAnsi.test.ts` — 152 unit tests (upstream slice-ansi parity, OSC
8 hyperlinks, grapheme edge cases, ellipsis style inheritance,
`ambiguousIsNarrow`)
- `sliceAnsi-fuzz.test.ts` — 48 property/adversarial tests (seeded PRNG,
SIMD stride boundaries, unterminated sequences, spec-zone ordering,
encoding equivalence, exception safety, negative-index consistency, 300+
property checks per invariant)
- `stringWidth.test.ts` — +5 for the ANSI-grapheme invariant
`stringWidth(s) == stringWidth(stripANSI(s))`
- `wrapAnsi.test.ts` — 32 regression (unchanged)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-26 23:36:42 -08:00
robobun
c099ad3fff harden Buffer.compare offset bounds validation (#27496)
## Summary
- Tighten bounds checks in `Buffer.prototype.compare` to properly
validate `targetEnd` and `sourceEnd` against their respective buffer
lengths unconditionally, matching Node.js semantics
- Previously certain combinations of start/end offset values could
bypass the range validation due to conjunctive check conditions; now end
values are checked first, then zero-length ranges return early, then
start values are validated
- Add comprehensive test coverage for Buffer.compare bounds edge cases

## Test plan
- [x] `bun bd test test/js/node/buffer-compare-bounds.test.ts` — 13/13
pass
- [x] `bun bd test/js/node/test/parallel/test-buffer-compare-offset.js`
— passes cleanly
- [x] `bun bd test/js/node/test/parallel/test-buffer-compare.js` —
passes cleanly
- [x] `bun bd test test/js/node/buffer.test.js` — 457/457 pass
- [x] `USE_SYSTEM_BUN=1 bun test
test/js/node/buffer-compare-bounds.test.ts` — 2 failures confirm tests
catch the issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-26 23:24:15 -08:00
Jarred Sumner
6450e91aa4 Bump 2026-02-26 23:02:43 -08:00
Jarred Sumner
57a36224f3 Deflake test/js/web/fetch/fetch.tls.test.ts 2026-02-26 23:02:14 -08:00
Jarred Sumner
49937251ba fix(resolver): prevent buffer overflow on very long import paths (#27492)
## Summary
- `_joinAbsStringBuf` used a fixed `MAX_PATH_BYTES * 2` stack buffer for
its scratch space; joining a long user-provided import specifier with
tsconfig `baseUrl` / `node_modules` / source dir would overflow it and
panic in `normalizeStringGenericTZ`.
- Replace the fixed buffer with a `JoinScratch` stack-fallback allocator
sized to the input (zero-alloc for normal paths, heap-alloc only for
pathological inputs).
- Add `absBufChecked` / `joinAbsStringBufChecked` that return `null`
when the normalized result would still exceed the destination buffer,
and switch resolver call sites that handle user-controlled specifiers to
use it (treating overflow as not-found).
- Guard `ESModule.Package.parseSubpath`, tsconfig `paths` wildcard
concat, and the VM cache-bust join against overflow.

## Test plan
- [ ] `bun bd test test/js/bun/resolve/resolve-error.test.ts` — new test
exercises bare packages via tsconfig baseUrl, tsconfig `paths`
wildcards, relative paths, and long paths with `..` normalization, none
of which should crash
- [ ] CI green on all platforms (Windows path joining also changed)

🤖 Generated with [Claude Code](https://claude.com/claude-code) (0%
31-shotted by claude)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:00:32 -08:00
robobun
06858cbef0 fix(http): harden chunked encoding hex digit parsing (#27497)
## Summary

- Tightens hex digit validation in the chunked Transfer-Encoding parser
(`ChunkedEncoding.h`) to strictly accept only RFC 9110 HEXDIG characters
(`0-9`, `a-f`, `A-F`)
- The previous implementation used arithmetic range checks that were
slightly too permissive, accepting certain non-HEXDIG ASCII characters
(e.g. those between `'9'` and `'A'` or after `'F'`/`'f'` in the ASCII
table)
- This aligns Bun's chunked parser with other strict HTTP
implementations (nginx, Apache, HAProxy, Node.js) and ensures consistent
chunk size interpretation across all servers in a proxy chain

## Test plan

- [x] Added 30 new tests in `test/js/bun/http/request-smuggling.test.ts`
covering:
  - Valid hex digits (`0-9`, `a-f`, `A-F`) are accepted
  - Multi-digit hex chunk sizes work correctly
  - Invalid characters `G`, `g`, `Z`, `z`, `x`, `X` are rejected
- ASCII 58-64 characters (`:`, `<`, `=`, `>`, `?`, `@`) between '9' and
'A' are rejected
- Other special characters (`!`, `#`, `$`, `%`, `^`, `&`, `*`, etc.) are
rejected
- [x] Verified new tests fail on system bun (before fix) and pass on
debug build (after fix)
- [x] All 44 tests in `request-smuggling.test.ts` pass (14 existing + 30
new)
- [x] No regressions in existing chunked encoding tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-26 22:55:43 -08:00
robobun
492e0f533c fix(http): harden header parsing for pipelined requests with empty headers (#27494)
## Summary

- Write a null terminator to the headers array when the empty-headers
early return path is taken in `getHeaders()` (`HttpParser.h:723`). This
matches the existing behavior on the normal parsing path (line 796) and
ensures headers are properly isolated between pipelined requests.
- Without this, when a pipelined request has no headers (request line
immediately followed by `\r\n\r\n`), stale header `string_view`s from
the previous request on the same connection could remain in the reused
`HttpRequest` object's headers array.

## Test plan

- [x] Added 3 new pipelined request header isolation tests to
`test/js/bun/http/request-smuggling.test.ts`
- [x] Verified new test (`pipelined headerless request is rejected and
does not inherit stale content-length`) **fails without the fix** and
**passes with the fix**
- [x] All 17 tests in the request-smuggling test file pass
- [x] `bun bd test test/js/bun/http/request-smuggling.test.ts` — all
pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-26 22:31:00 -08:00
Jarred Sumner
1524632cbb fIx some CI things 2026-02-26 21:53:53 -08:00
Dylan Conway
5b8b72522c ci: move Windows code signing to dedicated x64 step (#27451)
## What does this PR do?

Moves Windows code signing from an inline CMake `POST_BUILD` step to a
dedicated Buildkite step (`windows-sign`) that runs on an x64 agent
after all Windows builds complete.

### Why

DigiCert `smctl` is x64-only and silently fails under ARM64 emulation.
With the old inline approach, ARM64 builds were never signed (`ci.mjs`
skipped it with `target.arch !== "aarch64"`). Now that we're shipping
Windows ARM64, we need all Windows binaries signed.

### How it works

```
windows-x64-build-bun          ─┐
windows-x64-baseline-build-bun  ├─→ windows-sign (x64 agent) ─→ release
windows-aarch64-build-bun      ─┘
```

The `windows-sign` step:
1. Downloads all 6 Windows zips (x64, x64-baseline, aarch64 × {release,
profile})
2. Extracts each, signs the exe with smctl, re-packs
3. Re-uploads with the **same filenames**
4. `upload-release.sh` pins Windows artifact downloads to `--step
windows-sign` to guarantee signed zips are released

### When signing runs

- On `main` with non-canary builds (normal release path)
- When `[sign windows]` is in the commit message (for testing on a
branch — **this PR uses it**)

Canary builds are never signed (DigiCert charges per signature).

### Cleanup

- Removed `ENABLE_WINDOWS_CODESIGNING` CMake option
- Removed inline `POST_BUILD` signing from `BuildBun.cmake`
- Removed SM_* secret fetching from `scripts/build.mjs`
- Replaced `sign-windows.ps1` (2-exe signer) with
`sign-windows-artifacts.ps1` (batch zip signer)

### Testing

The commit message contains `[sign windows]` so this PR's CI should run
the sign step. Will verify:
- All 6 zips are downloaded, signed, re-uploaded
- `Get-AuthenticodeSignature` verification passes for each exe
- smctl healthcheck works on the x64 test agent

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-26 17:15:44 -08:00
igorkofman
9e3330a9ad fix(websocket): route sendBuffer through proxy tunnel TLS layer (#27433)
## Summary

`sendBuffer()` was writing directly to `this.tcp` (the raw socket),
which is **detached** in proxy tunnel mode (`wss://` through HTTP
CONNECT proxy). This caused unencrypted WebSocket frame data to be
written to a detached socket, corrupting the connection and causing
immediate disconnection (close code 1006).

## Root Cause

The fast path in `writeString` → `enqueueEncodedBytes` correctly checks
for `proxy_tunnel` and routes through `tunnel.write()`. But the slow
path (`sendData` → `sendDataUncompressed` → `sendBuffer`), taken when
there is backpressure or the data needs to be buffered in `send_buffer`,
bypassed the tunnel entirely.

Under **bidirectional traffic** (simultaneous reads and writes),
backpressure builds up and pushes writes through the `sendBuffer` path,
killing the connection within seconds.

## Reproduction

The bug manifests when using `wss://` through an HTTP CONNECT proxy with
bidirectional WebSocket traffic. Specifically this was causing constant
disconnections in Claude Code when using Bun's native WebSocket client —
the ping/pong keepalive mechanism never received pong responses because
the connection died before they could arrive.

Key conditions:
- `wss://` (TLS) through an HTTP CONNECT proxy (uses
`WebSocketProxyTunnel`)
- Bidirectional traffic (client writes AND receives data simultaneously)
- Works fine without proxy, or without TLS, or with read-only traffic

## Fix

- `sendBuffer`: Check for `proxy_tunnel` and route writes through
`tunnel.write()` instead of `this.tcp.write()`
- `sendDataUncompressed` (2 locations): Guard debug assertions
(`isShutdown`/`isClosed`/`isEstablished`) with `proxy_tunnel == null`
since they crash on the detached socket

## Test

Added `test-ws-bidir-proxy.test.ts` which sets up a TLS WebSocket
server, HTTP CONNECT proxy, and a client that does simultaneous
bidirectional traffic with ping/pong. Before this fix, the test fails
with close code 1006 after ~13 messages and 0 pongs. After the fix, it
completes with 9+ pongs and 140+ messages.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-26 11:08:41 -08:00
Jarred Sumner
30e609e080 Windows ARM64 2026-02-25 20:55:23 -08:00
Luke Parker
84e4a5ce9c fix(windows): avoid standalone worker dotenv crash (#27434)
### What does this PR do?

Fixes #27431.

- fixes a Windows standalone executable crash when
`compile.autoloadDotenv = false`, a `.env` file exists in the runtime
cwd, and the executable spawns a `Worker`
- gives worker startup its own cloned `DotEnv.Loader` before
`configureDefines()`, so dotenv loading does not mutate env state owned
by another thread
- aligns worker startup with other Bun runtime paths by wiring
`resolver.env_loader = transpiler.env`
- extracts standalone runtime flag propagation into
`applyStandaloneRuntimeFlags(...)` so main and worker startup share the
same env/tsconfig/package.json behavior
- adds regression coverage in `test/regression/issue/27431.test.ts` and
bundler coverage in `test/bundler/bundler_compile_autoload.test.ts`

### How did you verify your code works?

- reproduced the original crash with `bun test
regression/issue/27431.test.ts` on stock `1.3.10-canary.104`; the test
fails on unpatched Bun
- rebuilt `build/debug/bun-debug.exe` with this patch and ran
`build/debug/bun-debug.exe test regression/issue/27431.test.ts`; the
test passes on the patched build
- manually validated the minimal repro from
`https://github.com/Hona/bun1310-minimal-repro` against the patched
`bun-debug.exe`; the standalone executable no longer crashes and still
keeps dotenv disabled (`process.env` does not pick up `.env`)
2026-02-25 19:49:56 -08:00
Jarred Sumner
89c70a76e8 feat(repl): add -e/-p flags, docs page, and shell completions (#27436)
## Summary

- Adds `bun repl -e <script>` / `-p <script>` for non-interactive
evaluation using REPL semantics (object literal wrapping, declaration
hoisting), draining the event loop before exit. Returns exit code 1 on
error.
- Adds `docs/runtime/repl.mdx` documenting the interactive REPL
(commands, keybindings, special variables, top-level await, imports) and
the new non-interactive mode.
- Updates bash/fish/zsh completions for the `repl` subcommand and its
flags.

## Test plan

- [x] `bun bd test test/js/bun/repl/repl.test.ts` — all 103 tests pass
(20 new)
- [x] `USE_SYSTEM_BUN=1 bun test` — new tests fail (validates they test
new behavior)
- [x] `bun run zig:check-all` — compiles on all platforms
- [x] `bash -n` / `fish -n` / `zsh -n` syntax checks on completion files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-25 19:49:44 -08:00
robobun
b2d8504a09 fix(spawn): remove shutdown() on subprocess stdio socketpairs (#27435)
## Summary

- Remove `shutdown()` calls on subprocess stdio socketpair file
descriptors that were causing Python asyncio-based MCP servers to break

## Root Cause

Bun uses `SOCK_STREAM` socketpairs for subprocess stdio pipes. After
creating each socketpair, it called `shutdown(SHUT_WR)` on the parent's
read end (for stdout/stderr) and `shutdown(SHUT_RD)` on the parent's
write end (for stdin) to make them unidirectional.

On `SOCK_STREAM` sockets, `shutdown(fd, SHUT_WR)` sends a **FIN** to the
peer. Python's `asyncio.connect_write_pipe()` registers an `EPOLLIN`
watcher on the write pipe fd to detect peer closure. The FIN from
`shutdown()` triggers an immediate `EPOLLIN` event, causing asyncio to
interpret it as "connection closed" and tear down the write transport —
even though the pipe should remain open.

This broke **all Python MCP servers** using the `model_context_protocol`
SDK (which uses `connect_write_pipe()` in its stdio transport) whenever
they took more than a few seconds to initialize. Node.js does not have
this issue because it does not call `shutdown()` on its socketpairs.

## Fix

Remove the `shutdown()` calls entirely. The socketpairs are already used
unidirectionally by convention, and the `shutdown()` calls provided no
functional benefit while causing compatibility issues with any program
that polls its stdio fds for readability/writability events.

## Test plan

- [x] Added regression test
`test/js/bun/spawn/spawn-socketpair-shutdown.test.ts` with 3 test cases:
  - Subprocess stdout pipe stays writable after idle delay
  - Python asyncio `connect_write_pipe` works correctly with idle period
  - Subprocess stdin pipe stays readable for child after idle delay
- [x] Verified test fails on system bun (without fix) and passes on
debug build (with fix)
- [x] Verified existing spawn tests still pass
(`spawn-streaming-stdout`, `spawn-stdin-readable-stream`)
- [x] Verified original bug report repro script works with the fix


🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-25 18:25:13 -08:00
Ciro Spaciari
e735bffaa9 fix(http): enable TLS keepalive for custom SSL configs (#27385)
## Summary

- **Enable keepalive for custom TLS configs (mTLS):** Previously, all
connections using custom TLS configurations (client certificates, custom
CA, etc.) had `disable_keepalive=true` forced, causing a new TCP+TLS
handshake on every request. This removes that restriction and properly
tracks SSL contexts per connection.

- **Intern SSLConfig with reference counting:** Identical TLS
configurations are now deduplicated via a global registry
(`SSLConfig.GlobalRegistry`), enabling O(1) pointer-equality lookups
instead of O(n) content comparisons. Uses `ThreadSafeRefCount` for safe
lifetime management across threads.

- **Bounded SSL context cache with LRU eviction:** The custom SSL
context map in `HTTPThread` is now bounded (max 60 entries, 30-minute
TTL) with proper cleanup of both SSL contexts and their associated
SSLConfig references when evicted.

- **Correct keepalive pool isolation:** Pooled sockets now track their
`ssl_config` (with refcount) and `owner` context, ensuring connections
are only reused when the TLS configuration matches exactly, and sockets
return to the correct pool on release.

Fixes #27358

## Changed files

- `src/bun.js/api/server/SSLConfig.zig` — ref counting, content hashing,
GlobalRegistry interning
- `src/bun.js/webcore/fetch.zig` — intern SSLConfig on creation, deref
on cleanup
- `src/http.zig` — `custom_ssl_ctx` field, `getSslCtx()` helper, updated
all callback sites
- `src/http/HTTPContext.zig` — `ssl_config`/`owner` on PooledSocket,
pointer-equality matching
- `src/http/HTTPThread.zig` — `SslContextCacheEntry` with timestamps,
TTL + LRU eviction

## Test plan

- [x] `test/regression/issue/27358.test.ts` — verifies keepalive
connection reuse with custom TLS and isolation between different configs
- [x] `test/js/bun/http/tls-keepalive.test.ts` — comprehensive tests:
keepalive reuse, config isolation, stress test (50 sequential requests),
keepalive-disabled control
- [x] `test/js/bun/http/tls-keepalive-leak-fixture.js` — memory leak
detection fixture (50k requests with same config, 200 requests with
distinct configs)

## Changelog
<!-- CHANGELOG:START -->
Fixed a bug where HTTP connections using custom TLS configurations
(mTLS, custom CA certificates) could not reuse keepalive connections,
causing a new TCP+TLS handshake for every request and leaking SSL
contexts. Custom TLS connections now properly participate in keepalive
pooling with correct isolation between different configurations.
<!-- CHANGELOG:END -->

🤖 Generated with [Claude Code](https://claude.com/claude-code) (0%
16-shotted by claude-opus-4-6, 3 memories recalled)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-25 15:39:13 -08:00
Dylan Conway
347c288d75 fix(shell): seq crashes when called with only flags and no numeric args (#27415)
## What

Fixes a null pointer dereference crash in the `seq` shell builtin when
called with only flags and no numeric arguments.

## Reproduction

```js
await Bun.$`seq -w`       // crash
await Bun.$`seq -s ,`     // crash
await Bun.$`seq -t .`     // crash
```

```
panic(main thread): attempt to use null value
src/shell/builtin/seq.zig:47:31
```

Also crashes release builds (segfault).

## Root cause

The flag-parsing loop at line 17 consumes all arguments. When the user
passes only flags, the iterator is exhausted after the loop exits. Line
47 then calls `iter.next().?` which panics on `null`.

The existing `args.len == 0` check on line 14 only catches the case
where no args are passed at all — it does not cover the case where all
args are consumed as flags.

## Fix

Changed `.?` to `orelse return this.fail(usageString)`, matching the
behavior when `seq` is called with zero arguments.
2026-02-25 15:28:43 -08:00
robobun
6cc1a70198 fix(streams): preserve AsyncLocalStorage context in stream.finished callback (#27429)
## Summary
- Bind the `stream.finished` callback with `AsyncLocalStorage.bind()`
before wrapping with `once()`, matching [Node.js
behavior](https://github.com/nodejs/node/blob/main/lib/internal/streams/end-of-stream.js#L70).
Without this, the async context active when `finished()` is called is
lost by the time the callback fires.

Closes #27428

## Test plan
- [x] Added regression test `test/regression/issue/27428.test.ts` that
spawns an HTTP server using `stream.finished` inside
`AsyncLocalStorage.run()` and verifies the store is preserved in the
callback
- [x] Verified test fails with system bun (`USE_SYSTEM_BUN=1`) and
passes with the debug build
- [x] Existing stream finished tests (`test-stream-finished.js`,
`test-stream-end-of-streams.js`, `test-http-outgoing-finished.js`,
`test-http-client-finished.js`) continue to pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-25 15:28:17 -08:00
robobun
fa3a30f075 feat(repl): implement native Zig REPL with full TUI support (#26304)
## Summary

This PR implements a native Zig REPL for Bun with full TUI (Text User
Interface) support, providing a modern and feature-rich interactive
experience.

### Features

- **Syntax highlighting** using `QuickAndDirtySyntaxHighlighter` for
colorized JavaScript code
- **Full line editing** with Emacs-style keybindings:
  - `Ctrl+A/E` - Move to start/end of line
  - `Ctrl+B/F` - Move backward/forward one character
  - `Ctrl+K/U` - Kill to end/start of line
  - `Ctrl+W` - Delete word backward
  - `Ctrl+L` - Clear screen
  - Arrow keys for cursor movement
- **Persistent history** with file storage (`~/.bun_repl_history`)
  - Up/Down arrow for history navigation
  - `Ctrl+P/N` also works for history
- **Tab completion** for properties and commands
- **Multi-line input support** with automatic continuation detection
- **REPL commands**: `.help`, `.exit`, `.clear`, `.load`, `.save`,
`.editor`
- **Special variables**:
  - `_` - Contains the result of the last expression
  - `_error` - Contains the last error that occurred
- **Result formatting** with `util.inspect` integration
- **replMode transforms** for proper REPL semantics:
  - Expression result capture via `{ value: expr }` wrapper
- Variable hoisting for persistence across REPL lines (`const`/`let` →
`var`)
  - Function and class declaration hoisting
  - Top-level await support with async IIFE wrapper
  - Object literal detection (no parentheses needed for `{ a: 1 }`)

### Implementation

The REPL is implemented in pure Zig (`src/repl.zig`) with C++ bindings
for JSC integration:
- Uses raw terminal mode for character-by-character input
- Integrates with Bun's existing `VirtualMachine` for JavaScript
evaluation
- Uses the parser with `repl_mode=true` to apply REPL-specific AST
transforms
- Provides access to all Bun globals (`Bun`, `Buffer`, `console`,
`process`, etc.)

### Files Changed

- `src/repl.zig` - Main REPL implementation (~1500 lines)
- `src/cli/repl_command.zig` - CLI entry point
- `src/bun.js/bindings/bindings.cpp` - C++ REPL functions
- `src/bun.js/bindings/headers.h` - C++ declarations
- `src/ast/repl_transforms.zig` - REPL-specific AST transforms
(cherry-picked from jarred/repl-mode)
- `test/js/bun/repl/repl.test.ts` - Comprehensive tests

## Test Plan

- [x] Run `bun bd test test/js/bun/repl/repl.test.ts` - 27 tests pass
- [x] Manual testing of interactive features:
  - Basic expression evaluation
  - Special variables `_` and `_error`
  - History navigation
  - Tab completion
  - Multi-line input
  - REPL commands
  - Top-level await
  - Variable persistence
- [x] Verified REPL starts without downloading packages (fixes #26058)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-25 15:15:37 -08:00
HK-SHAO
32a89c4334 fix(docs): code block syntax for server.tsx in SSR guide (#27417)
### What does this PR do?

TSX files may contain XML-like syntax, but TS files cannot.
2026-02-25 16:53:49 +00:00
robobun
c643e0fad8 fix(fuzzilli): prevent crash from fprintf on null FILE* in FUZZILLI_PRINT handler (#27310)
## Summary
- The `fuzzilli('FUZZILLI_PRINT', ...)` native function handler in
FuzzilliREPRL.cpp called `fdopen(103, "w")` on every invocation and
passed the result directly to `fprintf()` without a NULL check
- When running the debug-fuzz binary outside the REPRL harness (where fd
103 is not open), `fdopen()` returns NULL, and `fprintf(NULL, ...)`
causes a SIGSEGV crash
- Fix: make the `FILE*` static (so `fdopen` is called once, avoiding fd
leaks) and guard `fprintf`/`fflush` behind a NULL check

## Crash reproduction
```js
// The crash is triggered when the Fuzzilli explore framework calls
// fuzzilli('FUZZILLI_PRINT', ...) on the native fuzzilli() function
// while running outside the REPRL harness (fd 103 not open).
// Minimal reproduction:
fuzzilli('FUZZILLI_PRINT', 'hello');
```

Running the above with the debug-fuzz binary (which registers the native
`fuzzilli` function) causes SIGSEGV in `__vfprintf_internal` due to NULL
FILE*.

## Test plan
- [x] Verified crash reproduces 10/10 times with the original binary
- [x] Verified 0/10 crashes with the fixed binary
- [x] Fix is trivially correct: static FILE* + NULL guard

Co-authored-by: Alistair Smith <alistair@anthropic.com>
2026-02-25 12:52:12 +00:00
robobun
2222aa9f47 fix(bundler): write external sourcemap .map files for bun build --compile (#27396)
## Summary
- When using `bun build --compile --sourcemap=external`, the `.map`
files were embedded in the executable but never written to disk. This
fix writes them next to the compiled executable.
- With `--splitting` enabled, multiple chunks each produce their own
sourcemap. Previously all would overwrite a single `{outfile}.map`; now
each `.map` file preserves its chunk-specific name (e.g.,
`chunk-XXXXX.js.map`).
- Fixes both the JavaScript API (`Bun.build`) and CLI (`bun build`) code
paths.

## Test plan
- [x] `bun bd test test/bundler/bun-build-compile-sourcemap.test.ts` —
all 8 tests pass
- [x] New test: `compile with sourcemap: external writes .map file to
disk` — verifies a single `.map` file is written and contains valid
sourcemap JSON
- [x] New test: `compile without sourcemap does not write .map file` —
verifies no `.map` file appears without the flag
- [x] New test: `compile with splitting and external sourcemap writes
multiple .map files` — verifies each chunk gets its own uniquely-named
`.map` file on disk
- [x] Verified new tests fail with `USE_SYSTEM_BUN=1` (confirming they
test the fix, not pre-existing behavior)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-24 22:02:51 -08:00
Martin Amps
38e4340d28 fix(http): prevent duplicate Transfer-Encoding header in node:http (#27398)
### What does this PR do?

Fixes https://github.com/oven-sh/bun/issues/21201. Ran into this today
during a migration from node->bun. TLDR if you set `Transfer-Encoding:
chunked` via `res.writeHead()`, Bun sends the header twice because two
layers independently add it:

1. `writeFetchHeadersToUWSResponse` (NodeHTTP.cpp) writes the user's
headers to the response buffer
2. uWS's `HttpResponse::write()` auto-inserts `Transfer-Encoding:
chunked` since no flag indicates it was already set

Added a `HTTP_WROTE_TRANSFER_ENCODING_HEADER` flag copying the pattern
for `Content-Length` and `Date` deduping, checked before auto-insertion
in `write()`, `flushHeaders()`, and `sendTerminatingChunk()`.

### How did you verify your code works?

Minimal repro:

```
import http from "node:http";
import net from "node:net";

http.createServer((_, res) => {
  res.writeHead(200, { "Transfer-Encoding": "chunked" });
  res.write("ok");
}).listen(0, "127.0.0.1", function () {
  const s = net.createConnection(this.address().port, "127.0.0.1", () =>
    s.write("GET / HTTP/1.1\r\nHost: x\r\n\r\n"));
  s.on("data", (d) => {
    console.log(d.toString().split("\r\n\r\n")[0]);
    s.destroy(); this.close();
  });
});
```

Node working (showing header once)

```
$ node --version
v22.21.1

$ node /Users/mamps/code/labs/conway/tmp/bun-duplicate-te.mjs
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Tue, 24 Feb 2026 06:14:50 GMT
Connection: keep-alive
Keep-Alive: timeout=5
```

Bun bug (duplicate header):
```
$ bun --version
1.3.9

$ bun bun-duplicate-te.mjs
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Tue, 24 Feb 2026 06:13:55 GMT
Transfer-Encoding: chunked
```

Bun fixed:
```
$ ./build/debug/bun-debug --version
1.3.10-debug

$ BUN_DEBUG_QUIET_LOGS=1 ./build/debug/bun-debug /tmp/bun-duplicate-te.mjs
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Tue, 24 Feb 2026 06:15:53 GMT
```
2026-02-23 23:04:19 -08:00
robobun
6b1d6c769b fix(sys): remove MSG_NOSIGNAL from recvfrom flags (#27390)
## Summary

- `MSG_NOSIGNAL` is only valid for send operations (`send`, `sendto`,
`sendmsg`), not receive operations (`recv`, `recvfrom`, `recvmsg`).
Passing it to `recvfrom` causes `EINVAL` in strict environments like
gVisor (Google Cloud Run).
- Split the shared `socket_flags_nonblock` constant into
`recv_flags_nonblock` (`MSG_DONTWAIT` only) and `send_flags_nonblock`
(`MSG_DONTWAIT | MSG_NOSIGNAL`).

Closes #27389

## Test plan

- [x] Added regression test `test/regression/issue/27389.test.ts` that
exercises the socket recv path
- [x] Debug build passes (`bun bd test
test/regression/issue/27389.test.ts`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-23 18:24:37 -08:00
robobun
ac6269a2cb fix(docs): update TanStack Start guide to use non-deprecated CLI (#27377)
## Summary
- Replace deprecated `bun create @tanstack/start@latest` with `bunx
@tanstack/cli create` in the TanStack Start guide
- The `@tanstack/create-start` package now prints a runtime deprecation
warning: *"@tanstack/create-start is deprecated. Use `tanstack create`
or `npx @tanstack/cli create` instead."*

Fixes #27374

## Test plan
- [x] Verified `bunx @tanstack/cli create --help` works correctly
- [x] Verified `bunx @tanstack/create-start@latest` shows the
deprecation warning

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alistair Smith <alistair@anthropic.com>
2026-02-23 16:49:09 +00:00
Jarred Sumner
6314363663 feat(bundler): barrel import optimization — skip parsing unused submodules (#26892) 2026-02-23 05:28:01 -08:00
robobun
77b6406415 Add arraybuffer output option to Bun.generateHeapSnapshot("v8") (#26861)
## Summary
- Add `Bun.generateHeapSnapshot("v8", "arraybuffer")` which returns the
heap snapshot as an `ArrayBuffer` instead of a `string`
- Avoids potential integer overflow crashes in `WTF::String` when heap
snapshots approach max uint32 length
- Eliminates the overhead of creating a JavaScript string for large
snapshots
- The `ArrayBuffer` contains UTF-8 encoded JSON that can be written
directly to a file or decoded with `TextDecoder`
- Updates TypeScript types in `bun.d.ts` with the new overload
- Adds tests for the new `"arraybuffer"` encoding option

Depends on oven-sh/WebKit#158 for the
`BunV8HeapSnapshotBuilder::jsonBytes()` method.

## Test plan
- [x] `Bun.generateHeapSnapshot("v8", "arraybuffer")` returns a valid
`ArrayBuffer`
- [x] Decoded ArrayBuffer produces valid JSON parseable as a V8 heap
snapshot
- [x] Existing `Bun.generateHeapSnapshot("v8")` string output still
works
- [x] Tests fail with `USE_SYSTEM_BUN=1` (system bun doesn't support the
new argument)
- [x] Tests pass with the debug-local build

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-23 01:48:56 -08:00
Jarred Sumner
4d9752a1f0 Upgrade WebKit to f03492d0636f (#26922)
# JavaScriptCore Upstream Changes: 1b6f54d1c872..upstream/main

**Date range**: Feb 5 -- Feb 11, 2026
**Total JSC commits**: 38

---

## 1. New Features

### WebAssembly JSPI (JavaScript Promise Integration)
**Commit**: `53e97afd3421` -- [JSC] JSPI Implementation
**Files changed**: 59 files, +3879/-148 lines

Full implementation of the [WebAssembly JSPI
proposal](https://github.com/WebAssembly/js-promise-integration). Adds
two new APIs:

- `WebAssembly.promising(wasmFun)` -- wraps a wasm function so it
returns a Promise
- `new WebAssembly.Suspending(jsFun)` -- wraps a JS function so wasm can
suspend on its result

Controlled by `useJSPI` feature flag (disabled by default). This is a
large change that introduces:

- New classes: `EvacuatedStack`, `PinballCompletion`, `JSPIContext`,
`WebAssemblySuspending`, `WebAssemblyPromising`,
`JSWebAssemblySuspendError`
- New `JSPIContext` struct added to `VM` (field `topJSPIContext`)
- New `pinballCompletionStructure` in VM
- New `WebAssemblySuspendError` and `WebAssemblySuspending` lazy class
structures in JSGlobalObject
- New methods on VM: `addEvacuatedStackSlice`,
`removeEvacuatedStackSlice`, `gatherEvacuatedStackRoots`
- New iso subspace: `pinballCompletionSpace`

**Follow-up**: `f67441012b90` -- JSPI cages should only generate JIT
code when JITCage is enabled

### WebAssembly Memory64 in BBQ Tier
**Commit**: `7b98e4b17594` -- Add support for Memory64 in BBQ Tier

Extends Memory64 support from the interpreter to the BBQ JIT tier,
allowing wasm modules to address >4GB of memory.

### WebAssembly Multi-Memory (instantiation only)
**Commit**: `bdf26416947e` -- Support instantiating multiple wasm
memories

Adds support for instantiating multiple memories in wasm modules (but
not executing instructions that use memories other than index 0). Gated
behind `useWasmMultiMemory` flag.

### LOL (Lightweight Optimizing Language) JIT -- New Tier (In Progress)
A new JIT compilation tier "LOL" is being built incrementally across
multiple commits:

- `dd56e0f5b991` -- Add get_argument/argument_count bytecodes
- `fb43184a0a77` -- Add to_primitive-like bytecodes
- `f52fa4a30c76` -- Add switch/throw bytecodes
- `7fd04c82d291` -- Add support for this/prototype bytecodes
- `11127b8a61e0` -- Add support for op_ret

These commits add new files under `Source/JavaScriptCore/lol/`
(LOLJIT.cpp, LOLJIT.h, LOLRegisterAllocator.h). This appears to be a new
lightweight JIT tier between Baseline and DFG.

---

## 2. Performance Improvements

### Deep Rope String Slicing -- 168x faster
**Commit**: `93f2fd68619b` -- [JSC] Limit rope traversal depth in
`tryJSSubstringImpl`

Limits rope traversal depth to 16 in `tryJSSubstringImpl`. Deep rope
strings from repeated concatenation (`s += 'A'`) previously caused
O(n^2) behavior. When the depth limit is exceeded, it falls back to
`resolveRope` to flatten the string.

This directly addresses a performance issue reported against Bun where
it was significantly slower than Node.js.

### String#endsWith DFG/FTL Optimization -- up to 10.5x faster
**Commit**: `901865149859` -- [JSC] Optimize `String#endsWith` in
DFG/FTL

Adds a new DFG/FTL intrinsic `StringPrototypeEndsWithIntrinsic` for
`String.prototype.endsWith`. Constant folding case is 10.5x faster;
general case is 1.45x faster.

### RegExp Flag Getters in DFG/FTL and IC -- 1.6x faster
**Commit**: `1fe86a244d00` -- [JSC] Handle RegExp flag getters in
DFG/FTL and IC

Adds DFG/FTL and inline cache support for RegExp flag property getters
(`.global`, `.ignoreCase`, `.multiline`, `.dotAll`, `.sticky`,
`.unicode`, `.unicodeSets`, `.hasIndices`).

### String#@@iterator as NewInternalFieldObject in DFG/FTL
**Commit**: `44cba41d8eef` -- [JSC] Handle `String#@@iterator` as
`NewInternalFieldObject` in DFG/FTL

Optimizes string iterator creation in DFG/FTL by treating it as a
`NewInternalFieldObject`, enabling allocation sinking.

### ArithMod(Int52) in DFG/FTL
**Commit**: `49a21e7ff327` -- [JSC] Add ArithMod(Int52Use, Int52Use) in
DFG / FTL
**Follow-ups**: `f96be6066671` (constant folding fix), `c8f283248f3f`
(NaNOrInfinity fix)

Adds Int52 modulo support in DFG/FTL, avoiding expensive `fmod` double
operations when inputs are integer-like doubles.

### ArithDiv(Int52) in DFG/FTL -- REVERTED
**Commit**: `582cb0306b7c` -- [JSC] Add ArithDiv(Int52Use, Int52Use) in
DFG / FTL
**Revert**: `2e967edd1dc0` -- Reverted due to JetStream3 regressions

### Intl formatToParts Pre-built Structure -- up to 1.15x faster
**Commit**: `a5dd9753d23c` -- [JSC] Optimize Intl formatToParts methods
with pre-built Structure

Optimizes Intl's `formatToParts` methods by using pre-built Structures
for the returned part objects.

### JSBigInt Inline Storage
**Commit**: `dbc50284d4cf` -- [JSC] Inline storage into JSBigInt
**Related**: `304cf0713b9d` -- Remove JSBigInt::rightTrim and
JSBigInt::tryRightTrim

Major structural change to JSBigInt: digits are now stored inline
(trailing storage) instead of via a separate `CagedBarrierPtr`. This
eliminates the separate allocation and pointer indirection.
`tryRightTrim` and `rightTrim` removed; `createFrom` renamed to
`tryCreateFrom` with span-based API.

### WYHash Always Enabled for Strings
**Commit**: `14ba1421ca08` -- [JSC] Always use WYHash for strings

Previously, WYHash had a threshold (disabled for short strings on weak
devices). Now it's enabled regardless of string size.

### JIT Worklist Thread Count: 3 -> 4
**Commit**: `453c578cadf6` -- [JSC] Bump JIT worklist thread number from
3 to 4

### Greedy Register Allocator Improvements
**Commit**: `b642ae17aade` -- [JSC] Proactively coalesce spill slots
during register allocation
**Commit**: `a78705db7c08` -- [JSC] GreedyRegAlloc: amortize cost of
clearing the visited set in the evict loop

---

## 3. Bug Fixes

### RegExp (YARR) Fixes
- `dea6808af40e` -- **Fix stale captures in FixedCount groups in
MatchOnly mode**: `.test()` was not clearing captures between FixedCount
iterations, causing stale values visible to backreferences.

- `437e137889ea` -- **Fix infinite loop in JIT for non-greedy
backreference to zero-width capture**: A non-greedy backreference like
`\1*?` could loop forever when the referenced capture was undefined or
empty.

- `d4f884d21c0e` -- **Fix backtracking in NestedAlternativeEnd**: Wrong
jump target was used when backtracking from NestedAlternativeEnd.

### ARM64 Cached Immediate Fix
**Commit**: `8396ad321ad0` -- [JSC] Fix edge case issue of cached imm in
ARM64

TrustedImm32(-1) was not zero-extended when cached, causing wrong reuse
when followed by TrustedImm64(-1).

### Wasm Fixes
- `cea8513d43ef` -- **Fix RefCast/RefTest folding in B3**: The condition
was inverted, causing wrong cast results.
- `629632da96e1` -- **Fix debugger races with idle and active VMs in
stop-the-world**
- `e5391ad90f47` -- **Stack check on IPInt->BBQ loop OSR entry**
- `fd15e0d70ab1` -- **Use FrameTracer in wasm table_init operations**

### Structure Heap Alignment Assert Fix
**Commit**: `47b52526fd42` -- [JSC] Fix startOfStructureHeap alignment
RELEASE_ASSERT

---

## 4. Bun-side Changes

- Updated `WEBKIT_VERSION` to `f03492d0636f`
- Updated `SerializedScriptValue.cpp`: replaced removed
`JSBigInt::tryRightTrim` with `JSBigInt::tryCreateFrom` span-based API
- Fixed `CloneSerializer`/`CloneDeserializer` inheritance (`private` ->
`public CloneBase`)
- Explicitly disabled `ENABLE_MEDIA_SOURCE`, `ENABLE_MEDIA_STREAM`,
`ENABLE_WEB_RTC` in `build.ts` and `SetupWebKit.cmake` for JSCOnly port

---

## 5. Summary by Component

| Component | Commits | Key Changes |
|-----------|---------|-------------|
| **WASM** | 9 | JSPI implementation, Memory64 BBQ, multi-memory,
RefCast fix, stack checks, debugger races |
| **DFG/FTL** | 6 | String#endsWith opt, RegExp flags opt,
String@@iterator opt, ArithMod(Int52) |
| **YARR (RegExp)** | 3 | Stale captures fix, infinite loop fix,
NestedAlternativeEnd backtracking fix |
| **B3/Air** | 3 | Spill slot coalescing, generational set for eviction,
RefCast/RefTest fix |
| **LOL JIT** | 5 | New JIT tier being built incrementally |
| **Runtime** | 6 | BigInt inline storage, WYHash always-on, Intl
formatToParts opt, ARM64 imm fix, rope depth limit |

---------

Co-authored-by: Sosuke Suzuki <sosuke@bun.com>
2026-02-23 01:28:37 -08:00
robobun
51a74a81fd fix(windows): validate GetFinalPathNameByHandleW return length to prevent buffer overflows (#27343)
## Summary

- `GetFinalPathNameByHandleW` returns a value `>= buffer size` when the
path doesn't fit, but 4 out of 5 call sites only checked for `== 0` (the
error case). This could cause out-of-bounds reads/writes when Windows
paths exceed the buffer capacity — e.g. deeply nested `node_modules` or
`\\?\` extended paths.
- Also fixed `PackageInstall.zig:1264` passing `dest_buf.len` (a
`PathBuffer` = 4096 bytes) instead of `wbuf.len` (a `WPathBuffer` =
32767 u16 elements) as the buffer size parameter to the API.

| File | Line | Issue |
|------|------|-------|
| `src/windows.zig` | 3419 | `GetFinalPathNameByHandle` wrapper missing
`>= len` check |
| `src/install/PackageInstall.zig` | 538 | OOB write at
`state.buf[dest_path_length]` |
| `src/install/PackageInstall.zig` | 562 | OOB slice
`state.buf2[0..cache_path_length]` |
| `src/install/PackageInstall.zig` | 1261 | Wrong buffer size param +
missing bounds check |
| `src/bun.js/node/node_fs.zig` | 6578 | OOB slice in symlink path
resolution during `copyFile` |
| `src/install/isolated_install/Installer.zig` | 556 | `setLength` with
unchecked length |

The sole correct call site (`src/sys.zig:4062`) already had the proper
`>= len` check and was used as the reference pattern.

## Test plan

- [x] `bun run zig:check-windows` passes (x86_64 + aarch64, Debug +
ReleaseFast)
- [ ] Windows CI passes
- [ ] Test with deeply nested `node_modules` paths on Windows (>260
chars)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:45:34 -08:00
Dylan Conway
cb3c39be23 ci: add Intel SDE baseline verification for Windows, unify baseline checks (#27121)
Adds a unified baseline verification script
(`scripts/verify-baseline.ts`) that combines basic CPU instruction
checks and JIT stress test fixtures into a single step.

**Changes:**
- New TypeScript script replaces separate `verify-baseline-cpu.sh` and
`verify-jit-stress-qemu.sh` CI steps
- Adds Windows x64 baseline verification using Intel SDE v9.58 with
Nehalem emulation
- Linux continues to use QEMU (Nehalem for x64, Cortex-A53 for aarch64)
- SDE violations are detected by checking output for `SDE-ERROR`
messages rather than exit codes, avoiding false positives from
application errors
- JIT stress fixtures now run on every build instead of only when WebKit
changes

**Platform support:**
| Platform | Emulator | CPU Model |
|----------|----------|-----------|
| Linux x64 baseline | QEMU | Nehalem (SSE4.2, no AVX) |
| Linux aarch64 | QEMU | Cortex-A53 (no LSE/SVE) |
| Windows x64 baseline | Intel SDE | Nehalem (no AVX) |

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 14:24:21 -08:00
robobun
bc98025d93 fix(spawn): close libuv pipes before freeing to prevent handle queue corruption (#27064)
## Summary

Fixes #27063

On Windows, when `Bun.spawn` fails (e.g., ENOENT for a nonexistent
executable), pipes that were already initialized with `uv_pipe_init`
were being freed directly with `allocator.destroy()` without first
calling `uv_close()`. This left dangling pointers in libuv's
`handle_queue` linked list, corrupting it. Subsequent spawn calls would
crash with a segfault when inserting new handles into the corrupted
list.

Three sites were freeing pipe handles without `uv_close`:

- **`process.zig` `Stdio.deinit()`**: When spawn failed,
already-initialized pipes were freed without `uv_close()`. Now uses
`closePipeAndDestroy()` which checks `pipe.loop` to determine if the
pipe was registered with the event loop.
- **`process.zig` `spawnProcessWindows` IPC handling**: Unsupported IPC
pipes in stdin/stdout/stderr were freed directly. Now uses the same safe
close-then-destroy pattern.
- **`source.zig` `openPipe()`**: If `pipe.open(fd)` failed after
`pipe.init()` succeeded, the pipe was destroyed directly. Now calls
`uv_close()` with a callback that frees the memory.

Additionally, pipe allocations in `stdio.zig` are now zero-initialized
so that the `loop` field is reliably `null` before `uv_pipe_init`,
enabling the init detection in `deinit`.

## Test plan

- [x] Added regression test `test/regression/issue/27063.test.ts` that
spawns nonexistent executables repeatedly and verifies a valid spawn
still works afterward
- [x] Verified existing spawn tests pass (`exit-code.test.ts`,
`spawnSync.test.ts` — timing-related pre-existing flakes only)
- [x] Debug build compiles successfully
- [ ] Windows CI should verify the fix prevents the segfault


🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-21 14:04:25 -08:00
Jarred Sumner
b371bf9420 fix(install): resolve DT_UNKNOWN entries on NFS/FUSE filesystems (#27008)
## Summary

- Fixes `bun install` producing incomplete `node_modules` on NFS, FUSE,
and some bind mount filesystems
- On these filesystems, `getdents64` returns `DT_UNKNOWN` for `d_type`
instead of `DT_DIR`/`DT_REG`
- The directory walker was silently skipping these entries, causing
missing files (e.g., 484 instead of 1070 for `@sinclair/typebox`)
- When an entry has unknown kind, we now fall back to `fstatat()` to
resolve the actual file type

## Test plan

- [x] Reproduced with Docker NFS environment: npm installs 1071 files,
bun installs only 484
- [x] Verified fix: bun-debug now installs 1070 files (matching npm
minus `.package-lock.json`)
- [x] Second install from cache also works correctly (1070 files)
- [x] `bun run zig:check-all` passes on all 16 platform targets
- [ ] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 02:18:47 -08:00
Jarred Sumner
e6ec92244c fix(bindgen): hoist WTF::String temps to dispatch scope to prevent use-after-free (#27324)
## Summary

Fixes a use-after-free in bindgen v1 generated C++ bindings that causes
`"switch on corrupt value"` panics in `String.deref` on Windows. This is
a top crash (500+ reports across v1.3.3–v1.3.9), predominantly affecting
standalone executables.

## Root Cause

`Bun::toString(WTF::String&)` copies the raw `StringImpl*` pointer
**without adding a reference**. For optional string arguments with
defaults and dictionary string fields, the generated code declares
`WTF::String` inside an `if` block, but the resulting `BunString`
outlives it:

```cpp
BunString argStr;
if (!arg.isUndefinedOrNull()) {
    WTF::String wtfString_0 = WebCore::convert<IDLDOMString>(...);
    argStr = Bun::toString(wtfString_0);  // copies pointer, no ref
}  // ← wtfString_0 destroyed here, drops ref → StringImpl may be freed
// argStr now holds a dangling pointer to freed memory
```

When the freed memory is reused, `String.deref()` reads garbage for the
tag field → `"switch on corrupt value"` panic.

### Why it was Windows-only / elevated recently

- The mimalloc v3 update (shipped in v1.3.7/v1.3.8) changed heap reuse
patterns on Windows, causing freed memory to be overwritten more
aggressively — turning a latent UAF into a frequent crash
- The mimalloc v3 revert in v1.3.9 reduced crash frequency back to
baseline but did not fix the underlying bug
- A [previous fix](https://github.com/oven-sh/bun/pull/26717) was
reverted due to unrelated CI failures

## Fix

Hoist all `WTF::String` temporaries to the same scope as the Zig
dispatch call, so they stay alive until the `BunString` values are
consumed:

1. **Function string arguments**: `WTF::String` is declared at the top
of the generated function, before any `if` blocks for optional arguments
2. **Dictionary string fields**: The `convert*` function accepts
`WTF::String&` references owned by the caller, so the string data
outlives the `convert*` function and remains valid through the dispatch
call

This approach is exception-safe — `WTF::String` destructors handle
cleanup automatically on all exit paths (normal return,
`RETURN_IF_EXCEPTION`, etc.) with no leaked refs.

### Difference from the previous fix

The [previous fix](https://github.com/oven-sh/bun/pull/26717) hoisted
`WTF::String` for function arguments but kept dictionary field temps
**inside** the `convert*` function. This left dictionary string fields
as use-after-return — `result->encoding` would be a dangling pointer
after `convert*` returned. This fix correctly passes `WTF::String&` refs
from the dispatch scope through to the `convert*` function.

### Affected call sites

Only 2 call sites have the vulnerable pattern (`DOMString` +
`.default(...)`):
- `Bun.stringWidth()` — `str: t.DOMString.default("")`  
- `os.userInfo()` — `encoding: t.DOMString.default("")` in
`UserInfoOptions` dictionary

Note: bindgen v2 is not affected — it uses `releaseImpl().leakRef()`
which transfers ownership correctly.
2026-02-21 02:15:52 -08:00
Dylan Conway
b509acb533 Revert "fix: clean up ESM registry when require() of ESM module fails… (#27325)
… (#27288)"

This reverts commit 21c3439bb4.
2026-02-21 01:14:27 -08:00
robobun
ede635b8a9 fix(install): store tarball integrity hash in lockfile for HTTPS dependencies (#27018)
## Summary
- HTTPS/URL tarball dependencies were not having their integrity hash
stored in the lockfile, allowing a malicious server to change the
tarball contents without detection on subsequent installs
- Now computes a sha512 hash from the downloaded tarball bytes during
extraction and stores it in both the binary lockfile and text bun.lock
- The hash is verified on re-download, matching the behavior of npm
registry packages
- Old lockfiles without integrity hashes continue to work (backward
compatible)

## Changes
- `src/install/integrity.zig`: Added `Integrity.forBytes()` to compute
sha512 from raw bytes
- `src/install/install.zig`: Added `integrity` field to `ExtractData`
struct
- `src/install/PackageManagerTask.zig`: Compute hash from tarball bytes
for both remote and local tarball tasks
- `src/install/PackageManager/processDependencyList.zig`: Set
`package.meta.integrity` from computed hash
- `src/install/lockfile/bun.lock.zig`: Serialize/deserialize integrity
for `remote_tarball` and `local_tarball` types

## Test plan
- [x] Integrity hash is stored in text lockfile for tarball URL deps
- [x] Integrity hash is consistent/deterministic across reinstalls
- [x] Integrity mismatch (changed tarball content) causes install
failure
- [x] Old lockfiles without integrity still install successfully
(backward compat)
- [x] Fresh installs produce integrity hashes
- [x] All 12 existing tarball tests pass (no regressions)
- [x] Tests fail with `USE_SYSTEM_BUN=1` (confirms fix is effective)

Fixes GHSA-jfhr-4v9p-9rm4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-20 23:52:45 -08:00
Jarred Sumner
ebb3730166 Update ci.mjs 2026-02-20 23:17:53 -08:00
robobun
76ceb26e0a fix(socket): prevent null deref in Listener.getsockname (#27303)
## Summary
- Fix null pointer dereference in `Listener.getsockname()` when called
without an object argument (or with a non-object argument)
- `getsockname()` wrote properties directly into its first argument via
`.put()`, which calls `getObject()` in C++ — this returns null for
non-object values like `undefined`, causing a crash at
`BunString.cpp:942`
- Now validates the argument is an object first; if not, creates a new
empty object, writes properties into it, and returns it

## Crash reproduction
```js
const listener = Bun.listen({
    hostname: "localhost",
    port: 0,
    socket: { data() {} },
});
listener.getsockname(); // Segfault - null pointer dereference
```

## Test plan
- [x] Added `test/js/bun/http/listener-getsockname.test.ts` with tests
for calling `getsockname()` with no argument, with an object argument,
and with a non-object argument
- [x] Verified test crashes with system bun and passes with patched
build
- [x] Verified original fuzzer reproduction no longer crashes

---------

Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 22:50:44 -08:00
robobun
06f26e5f01 fix: use BoringSSL for std.crypto random seed to support older Linux kernels (#27282)
## Summary

- Override Zig's default `cryptoRandomSeed` in `std_options` to use
BoringSSL's `RAND_bytes` (via `bun.csprng`) instead of the `getrandom()`
syscall
- On Linux kernels < 3.17 (e.g. Synology NAS with kernel 3.10), the
`getrandom` syscall doesn't exist and returns `ENOSYS`, causing Zig's
stdlib to panic with `"getrandom() failed to provide entropy"`
- BoringSSL already handles this gracefully by falling back to
`/dev/urandom`

## Details

Bun already uses BoringSSL's `RAND_bytes` for all its own cryptographic
random needs (`bun.csprng`). However, Zig's standard library
`std.crypto.random` uses a separate code path that calls the `getrandom`
syscall directly, with no fallback for `ENOSYS`.

Zig's `std.Options` struct provides a `cryptoRandomSeed` override for
exactly this purpose. This PR sets it to `bun.csprng` in both
`src/main.zig` and `src/main_test.zig`.

## Test plan

- [x] `bun bd` compiles successfully
- [x] `crypto.getRandomValues()`, `crypto.randomUUID()`, and
`require("crypto").randomFillSync()` all work correctly
- Cannot write a meaningful automated regression test since reproducing
requires a Linux kernel < 3.17

Closes #27279

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-20 21:13:51 -08:00
robobun
9f5970938f fix(spawn): prevent integer overflow in getArgv with large array length (#27316)
## Crash
Integer overflow panic in `getArgv` when `Bun.spawn`/`Bun.spawnSync`
receives an array with `.length` near u32 max (e.g. 4294967295).

## Reproduction
```js
const arr = ["echo", "hello"];
Object.defineProperty(arr, "length", { value: 4294967295 });
Bun.spawnSync(arr);
```

## Root Cause
`JSArrayIterator.len` is a `u32` derived from the JS array's `.length`
property. In `getArgv`, the expression `cmds_array.len + 2` (for argv0 +
null terminator) overflows `u32` arithmetic when `len` is close to `u32`
max. This causes a panic in debug builds and a segfault in release
builds. Additionally, the validation checks (`isEmptyOrUndefinedOrNull`
and `len == 0`) were placed after the overflowing `initCapacity` call,
so they couldn't prevent the crash.

## Fix
- Move validation checks before the `initCapacity` call
- Add a length check rejecting arrays with length > `u32 max - 2`
- Widen `cmds_array.len` to `usize` before adding 2 to prevent overflow
- Use `try argv.append()` instead of `appendAssumeCapacity` for safety

## Verification
- Reproduction no longer crashes (throws clean "cmd array is too large"
error)
- Normal `Bun.spawn`/`Bun.spawnSync` usage unaffected
- Added regression test at
`test/js/bun/spawn/spawn-large-array-length.test.ts`

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 20:24:22 -08:00
Jarred Sumner
c01a5e08be Bring back the slop label 2026-02-20 17:54:21 -08:00
SUZUKI Sosuke
e9db16c257 fix: release ReadableStream Strong ref on S3 download stream cancel (#27277)
## Summary

Same issue as #27191 (FetchTasklet), but in `S3DownloadStreamWrapper`
(`src/s3/client.zig`).

When a streaming S3 download body is cancelled via `reader.cancel()`,
`S3DownloadStreamWrapper.readable_stream_ref` (a `ReadableStream.Strong`
GC root) was never released. The S3 download continued in the
background, and the Strong ref prevented GC of the ReadableStream —
leaking memory until the download eventually completed.

## Root Cause

`ByteStream.onCancel()` cleaned up its own state but **did not notify
the `S3DownloadStreamWrapper`**. The wrapper only called `deinit()`
(which releases the Strong ref) when `has_more == false` — i.e., when
the S3 download fully completed. If the user cancelled the stream
mid-download, the Strong ref was held until the entire file finished
downloading in the background.

This is the exact same pattern that was fixed for `FetchTasklet` in
#27191.

## Fix

- Register a `cancel_handler` on the `ByteStream.Source` that releases
`readable_stream_ref` when the stream is cancelled. The download
callback will see `readable_stream_ref.get()` return `null` and skip
data delivery until the download finishes and `deinit()` cleans up the
remaining resources.
- Add `clearStreamCancelHandler()` in `deinit()` to null the
`cancel_handler`/`cancel_ctx` on the `ByteStream.Source`, preventing
use-after-free when the wrapper is freed before `cancel()` is called
(e.g., download completes normally).

## Test

Added `test/js/bun/s3/s3-stream-cancel-leak.test.ts` — uses a raw TCP
server (`Bun.listen`) that mocks an S3 GET response: sends one HTTP
chunk then keeps the connection open. Client streams 30 times via
`s3.file().stream()`, reads one chunk, cancels, then asserts
`heapStats().objectTypeCounts.ReadableStream` does not accumulate.
Before the fix, all 30 ReadableStreams leaked; after the fix, 0 leak.
2026-02-20 17:49:01 -08:00
robobun
21c3439bb4 fix: clean up ESM registry when require() of ESM module fails (#27288)
## Summary

- When `require()` loads an ESM module (`.mjs`) that throws during
evaluation, the module was removed from `requireMap` but left in the ESM
registry (`Loader.registry`) in a partially-initialized state
- A subsequent `import()` of the same module would find this corrupt
entry and throw `ReferenceError: Cannot access 'foo' before
initialization` instead of re-throwing the original evaluation error
- Fix by also deleting the module from `Loader.registry` in both
`overridableRequire` and `requireESMFromHijackedExtension` when ESM
evaluation fails, allowing `import()` to re-evaluate from scratch

Closes #27287

## Test plan

- [x] Added regression test in `test/regression/issue/27287.test.ts`
- [x] Verified test fails with system bun (`USE_SYSTEM_BUN=1`)
- [x] Verified test passes with `bun bd test`
- [x] Manual verification: `require()` correctly throws original error,
`import()` now re-throws the same error instead of `ReferenceError`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alistair Smith <alistair@anthropic.com>
2026-02-20 17:08:50 -08:00
Dylan Conway
7baf50f379 fix(http): align DeadSocket to prevent crash on Windows ARM64 stable builds (#27290)
## Summary

- `DeadSocket` only contains a `u8` field (alignment 1), so the linker
could place `DeadSocket.dead_socket` at a non-8-byte-aligned address
- When `markTaggedSocketAsDead` creates a tagged pointer embedding this
address and passes it through `bun.cast(**anyopaque, ...)`, the
`@alignCast` panics with "incorrect alignment" because the bottom bits
of the tagged value come from the unaligned address
- Fix: add `align(@alignOf(usize))` to the `dead_socket` variable
declaration

This only manifested on stable (non-canary) Windows ARM64 builds because
the binary layout differs when `ci_assert` is false, shifting the static
variable to a non-aligned address. Canary builds happened to place it at
an aligned address by coincidence.

## Test plan

- [x] Verified `fetch('https://example.com')` no longer crashes on
Windows ARM64 stable build (ENABLE_CANARY=OFF)
- [x] Verified 5 sequential HTTPS fetches complete successfully
- [x] Verified the fix is a single-line change with no behavioral side
effects

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 13:51:24 -08:00
robobun
76754a8ead fix(shell): support -e and -E flags in builtin echo (#27144)
## Summary

- Bun's builtin `echo` only supported the `-n` flag. The `-e` flag (and
`-E`) were treated as literal text, causing `echo -e password` to output
`-e password` instead of `password`. This broke common patterns like
`echo -e $password | sudo -S ...`.
- Added full `-e` (enable backslash escapes) and `-E` (disable backslash
escapes) flag support, matching bash behavior including combined flags
like `-ne`, `-en`, `-eE`, `-Ee`.
- Supported escape sequences: `\\`, `\a`, `\b`, `\c`, `\e`/`\E`, `\f`,
`\n`, `\r`, `\t`, `\v`, `\0nnn` (octal), `\xHH` (hex).

Closes #17405

## Test plan

- [x] Added 22 tests in `test/regression/issue/17405.test.ts` covering
all escape sequences, flag combinations, and the original issue scenario
- [x] Verified tests fail with system bun (19/22 fail) and pass with
debug build (22/22 pass)
- [x] Verified existing shell tests (`bunshell.test.ts`) still pass —
all 27 echo-related tests pass, no regressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-19 20:21:49 -08:00
robobun
ecd4e680eb fix(router): don't cache file descriptors in Route.parse to prevent stale fd reuse (#27164)
## Summary
- `FileSystemRouter.Route.parse()` was caching file descriptors in the
global entry cache (`entry.cache.fd`). When `Bun.build()` later closed
these fds during `ParseTask`, the cache still referenced them.
Subsequent `Bun.build()` calls would find these stale fds, pass them to
`readFileWithAllocator`, and `seekTo(0)` would fail with EBADF (errno
9).
- The fix ensures `Route.parse` always closes the file it opens for
`getFdPath` instead of caching it in the shared entry. The fd was only
used to resolve the absolute path via `getFdPath`, so caching was
unnecessary and harmful.

Closes #18242

## Test plan
- [x] Added regression test `test/regression/issue/18242.test.ts` that
creates a `FileSystemRouter` and runs `Bun.build()` three times
sequentially
- [x] Test passes with `bun bd test test/regression/issue/18242.test.ts`
- [x] Test fails with `USE_SYSTEM_BUN=1 bun test
test/regression/issue/18242.test.ts` (system bun v1.3.9)
- [x] Verified 5 sequential builds work correctly after the fix

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-19 20:15:11 -08:00
robobun
044bb00382 fix(sqlite): finalize transaction statements on close() to prevent "database is locked" (#27202)
## Summary

- Fixes `db.close(true)` throwing "database is locked" after using
`db.transaction()`
- The `getController` function creates prepared statements via
`db.prepare()` which bypasses the query cache, so they were never
finalized during `close()`
- `close()` now explicitly finalizes any cached transaction controller
statements before calling `sqlite3_close()`

Fixes #14709

## Test plan

- [x] New regression tests in `test/regression/issue/14709.test.ts`
covering:
  - Basic `close(true)` after `transaction()`
  - `close(true)` after transaction with actual work
  - `using` declaration (calls `close(true)` via `Symbol.dispose`)
  - Multiple transaction types (deferred, immediate, exclusive)
  - Nested transactions
- [x] All new tests fail with system bun (`USE_SYSTEM_BUN=1`) and pass
with debug build
- [x] Existing SQLite test suite (`test/js/bun/sqlite/sqlite.test.js`)
passes with no regressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 20:11:02 -08:00
robobun
655aab845d fix(css): prevent style rule deduplication across @property boundaries (#27119)
## Summary

- Fixes the CSS bundler incorrectly removing a `:root` selector when it
appears before an `@property` at-rule and another `:root` exists after
it
- The deduplication logic in `CssRuleList.minify()` was merging style
rules across non-style rule boundaries (like `@property`), which changes
CSS semantics
- Clears the `style_rules` deduplication map when a non-style rule is
appended, preventing merges across these boundaries

## Test plan

- [x] Added regression test in `test/regression/issue/27117.test.ts`
- [x] Verified test fails with system bun (`USE_SYSTEM_BUN=1`) —
reproduces the bug
- [x] Verified test passes with debug build (`bun bd test`)
- [x] Verified adjacent `:root` rules (without intervening at-rules) are
still correctly merged
- [x] All existing CSS bundler tests pass
(`test/bundler/esbuild/css.test.ts` — 53 tests)
- [x] All CSS modules tests pass (`test/bundler/css/css-modules.test.ts`
— 3 tests)

Closes #27117

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 20:03:18 -08:00
robobun
4141ef1edf fix(shell): fix unicode cursor tracking causing __bunstr_N leak in output (#27226)
## Summary
- Fixed `srcBytesAtCursor()` and `cursorPos()` in the shell lexer's
unicode path (`ShellCharIter(.wtf8)`) to use `self.src.cursor.i` instead
of `self.src.iter.i`, which was permanently stuck at 0
- Fixed `bumpCursorAscii()` to properly decode the codepoint at the new
cursor position instead of storing the last digit character, which
caused the wrong character to be returned on the next read

## Root Cause
When the shell template literal source contained multi-byte UTF-8
characters (e.g., `Í`, `€`), the `LexerUnicode` path was used. In this
path, `srcBytesAtCursor()` and `cursorPos()` referenced
`self.src.iter.i` — the `CodepointIterator`'s internal field that is
never modified (the `next()` method takes `*const Iterator`). This
meant:

1. `srcBytesAtCursor()` always returned bytes from position 0 (the start
of the source)
2. `looksLikeJSStringRef()` checked for `__bunstr_` at position 0
instead of the current cursor position, failing to match
3. The `\x08__bunstr_N` reference was passed through as literal text
into the shell output

This only occurred when **both** conditions were met:
- An interpolated value contained a space (triggering
`needsEscapeBunstr` → stored as `__bunstr_N` ref)
- A subsequent value contained multi-byte UTF-8 (triggering
`LexerUnicode` instead of `LexerAscii`)

Closes #17244

## Test plan
- [x] Added regression tests in `test/regression/issue/17244.test.ts`
- [x] Verified tests fail with `USE_SYSTEM_BUN=1` (system bun 1.3.9)
- [x] Verified tests pass with `bun bd test`
- [x] Ran existing shell tests (`bunshell.test.ts`,
`bunshell-default.test.ts`, `bunshell-instance.test.ts`) — no
regressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 19:38:33 -08:00
Jarred Sumner
e57593759f Update no-validate-leaksan.txt 2026-02-19 16:35:56 -08:00
robobun
e7cf4b77ba fix(css): strip leading @layer declarations from bundled CSS output (#27131)
## Summary
- When bundling CSS with `@layer` declarations (e.g. `@layer one;`)
followed by `@import` rules with `layer()`, the bundler left the bare
`@layer` statements and `@import` lines in the output even though their
content was already inlined into `@layer` blocks
- The fix adds `.layer_statement` to the leading-rule filter in
`prepareCssAstsForChunk`, which already stripped `@import` and
`.ignored` rules but missed `@layer` statement rules

Closes #20546

## Test plan
- [x] New regression test in `test/regression/issue/20546.test.ts`
covers both separate `@layer` statements and comma syntax
- [x] Test fails with system bun (`USE_SYSTEM_BUN=1`) confirming the bug
- [x] Test passes with debug build (`bun bd test`)
- [x] All 53 existing CSS bundler tests pass
(`test/bundler/esbuild/css.test.ts`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 12:31:47 -08:00
robobun
2e5e21015f fix(bundler): emit valid JS for unused dynamic imports (#27176)
## Summary

- Fixes `bun build` producing syntactically invalid JavaScript
(`Promise.resolve().then(() => )`) for unused dynamic imports like `void
import("./dep.ts")` or bare `import("./dep.ts")` expression statements
- When `exports_ref` is cleared for unused results but the `.then(() =>
...)` wrapper was still emitted, the arrow function body was empty. Now
skips the `.then()` wrapper entirely when there's nothing to execute
inside the callback, producing just `Promise.resolve()`
- The bug only affected cases where the import result was unused —
`const x = import(...)`, `await import(...)`, and `.then()` chains were
already correct

Closes #24709

## Test plan

- [x] Added regression test in `test/regression/issue/24709.test.ts`
that validates both `void import()` and bare `import()` statement cases
- [x] Verified test fails with system bun (reproduces the bug) and
passes with debug build (fix works)
- [x] Verified used dynamic imports (`const m = await import(...)`)
still produce correct `.then(() => exports)` output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 12:30:15 -08:00
SUZUKI Sosuke
b04303cb23 fix(gc): fix three GC safety issues (speculative fix for BUN-Q81) (#27190)
## Speculative fix for
[BUN-Q81](https://bun-p9.sentry.io/issues/BUN-Q81)

BUN-Q81 is a long-standing `SlotVisitor::drain` segfault during GC
marking (150 occurrences since July 2025, across v1.1.10 through
v1.3.10). A full audit of the codebase for GC safety issues found three
bugs:

### 1. `JSCommonJSModule::m_overriddenCompile` not visited in
`visitChildren`

`m_overriddenCompile` is a `WriteBarrier<Unknown>` that stores the
overridden `module._compile` function (used by `ts-node`, `pirates`,
`@swc-node/register`, etc.). It was the only WriteBarrier field in the
class not visited by `visitChildrenImpl`, making it invisible to the GC.
The pointed-to function could be prematurely collected, and subsequent
GC marking would follow the dangling WriteBarrier pointer into freed
memory.

**This is the strongest candidate for BUN-Q81.**

### 2. `JSSQLStatement::userPrototype` — wrong owner in
`WriteBarrier::set()`

```cpp
// Before (wrong):
castedThis->userPrototype.set(vm, classObject, prototype.getObject());
// After (correct):
castedThis->userPrototype.set(vm, castedThis, prototype.getObject());
```

The owner parameter must be the object containing the WriteBarrier so
the GC's remembered set is updated correctly. All other `.set()` calls
in the same file correctly use `castedThis`.

### 3. `NodeVMSpecialSandbox` — missing `visitChildren` entirely

`NodeVMSpecialSandbox` has a `WriteBarrier<NodeVMGlobalObject>
m_parentGlobal` member but had no `visitChildren` implementation. Added
the standard boilerplate.
2026-02-19 12:28:49 -08:00
SUZUKI Sosuke
b6eaa96e56 fix: release ReadableStream Strong ref on fetch body cancel (#27191)
## Summary

When a streaming HTTP response body is cancelled via `reader.cancel()`
or `body.cancel()`, `FetchTasklet.readable_stream_ref` (a
`ReadableStream.Strong` GC root) was never released. This caused
ReadableStream objects, associated Promises, and Uint8Array buffers to
be retained indefinitely — leaking ~260KB per cancelled streaming
request.

## Root Cause

`ByteStream.onCancel()` cleaned up its own state (`done = true`, buffer
freed, pending promise resolved) but **did not notify the
FetchTasklet**. The Strong ref was only released when:
- `has_more` became `false` (HTTP response fully received) — but the
server may keep the connection open
- `Bun__FetchResponse_finalize` — but this checks
`readable_stream_ref.held.has()` and **skips cleanup when the Strong ref
is set** (line 958)

This created a circular dependency: the Strong ref prevented GC, and the
finalizer skipped cleanup because the Strong ref existed.

## Fix

Add a `cancel_handler` callback to `NewSource` (`ReadableStream.zig`)
that propagates cancel events to the data producer. `FetchTasklet`
registers this callback via `Body.PendingValue.onStreamCancelled`. When
the stream is cancelled, the handler calls
`ignoreRemainingResponseBody()` to release the Strong ref, stop
processing further HTTP data, and unref the event loop.

To prevent use-after-free when `FetchTasklet` is freed before `cancel()`
is called (e.g., HTTP response completes normally, then user cancels the
orphaned stream), `clearStreamCancelHandler()` nulls the
`cancel_handler` on the `ByteStream.Source` at all 3 sites where
`readable_stream_ref` is released.

## Test

Added `test/js/web/fetch/fetch-stream-cancel-leak.test.ts` — uses a raw
TCP server (`Bun.listen`) that sends one HTTP chunk then keeps the
connection open. Client fetches 30 times, reads one chunk, cancels, then
asserts `heapStats().objectTypeCounts.ReadableStream` does not
accumulate. Before the fix, all 30 ReadableStreams leaked; after the
fix, 0 leak.
2026-02-19 12:22:43 -08:00
robobun
6a8f33e7b1 fix(windows): close libuv pipes before freeing to prevent handle_queue corruption (#27124) 2026-02-19 00:29:43 -08:00
Jarred Sumner
c3ae343fc9 fix(windows): use-after-free in WindowsStreamingWriter (#27122) 2026-02-19 00:29:15 -08:00
robobun
1eef4368ea fix: increase robobun PR query limit from 200 to 1000 (#27126) 2026-02-19 00:20:50 -08:00
robobun
6e240de4e2 Add workflow to close stale robobun PRs older than 90 days (#27125) 2026-02-19 00:16:38 -08:00
SUZUKI Sosuke
e216be966e fix: avoid GC allocation inside ObjectInitializationScope (#27111)
## Summary
- Pre-convert strings to JSValues using `MarkedArgumentBuffer` before
entering `ObjectInitializationScope` in `JSC__JSObject__putRecord` and
`JSC__JSValue__putRecord`, since `jsString()` allocates GC cells which
is not allowed inside the scope
- Remove unused `ObjectInitializationScope` declaration in
`JSSQLStatement.cpp`'s `initializeColumnNames`

## Test plan
- [ ] Verify `bun bd test` passes for existing tests that exercise
`putRecord` paths (e.g., HTTP header handling, SQLite column names)
- [ ] Run with `BUN_JSC_validateExceptionChecks=1` to confirm no
exception scope violations

## Changelog
<!-- CHANGELOG:START -->
<!-- CHANGELOG:END -->

🤖 Generated with [Claude Code](https://claude.com/claude-code) (0%
9-shotted by claude-opus-4-6)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-18 22:54:59 -08:00
Jarred Sumner
e84bee5d58 Fixes #26979 (#27118)
### What does this PR do?

Fixes https://github.com/oven-sh/bun/issues/26979


### How did you verify your code works?

The test in https://github.com/oven-sh/bun/issues/26979 successfully
reproduced the issue. Thank you!
2026-02-18 21:44:57 -08:00
SUZUKI Sosuke
fb2f304100 fix(node:fs): remove unnecessary path buffer pool alloc on Windows (#27115)
## Summary

- Removes an unnecessary 64KB `path_buffer_pool` allocation in
`PathLike.sliceZWithForceCopy` on Windows for paths that already have a
drive letter
- For drive-letter paths (e.g. `C:\foo\bar`),
`resolveCWDWithExternalBufZ` just does a memcpy, so the intermediate
buffer is unnecessary — we can pass the input slice directly to
`normalizeBuf`
- Eliminates an OOM crash path where `ObjectPool.get()` would panic via
`catch unreachable` when the allocator fails

## Test plan

- [ ] Verify Windows CI passes (this code path is Windows-only)
- [ ] Verify node:fs operations with absolute Windows paths still work
correctly
- [ ] Monitor BUN-Z4V crash reports after deployment to confirm fix

## Context

Speculative fix for BUN-Z4V (124 occurrences on Windows) showing `Panic:
attempt to unwrap error: OutOfMemory` in `sliceZWithForceCopy` →
`path_buffer_pool.get()` → `allocBytesWithAlignment`. We have not been
able to reproduce the crash locally, but the code analysis shows the
allocation is unnecessary for the drive-letter path case.

## Changelog
<!-- CHANGELOG:START -->
Fixed a crash on Windows (`OutOfMemory` panic) in `node:fs` path
handling when the system is under memory pressure.
<!-- CHANGELOG:END -->

🤖 Generated with [Claude Code](https://claude.com/claude-code) (0%
8-shotted by claude-opus-4-6)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:16:16 -08:00
Jarred Sumner
a350d496cb Revert "fix(bundler): place standalone HTML scripts in <head> to preserve execution order (#27114)"
This reverts commit 9e32360195.
2026-02-18 19:08:03 -08:00
robobun
9e32360195 fix(bundler): place standalone HTML scripts in <head> to preserve execution order (#27114)
## Summary

- Fix standalone HTML mode (`--compile --target=browser`) placing
bundled JS as `<script type="module">` before `</body>`, which broke
execution order with existing inline body scripts
- Move bundled JS into `<head>` as a classic `<script>` (not
`type="module"`) so it executes synchronously before inline body
scripts, preserving the original script execution order
- Remove the now-unnecessary `addBodyTags()` function and associated
body script injection paths

Fixes #27113

## Test plan

- [x] Added regression test `test/regression/issue/27113.test.ts` that
verifies head scripts appear before `</head>` and don't use
`type="module"`
- [x] Updated existing standalone HTML tests in
`test/bundler/standalone.test.ts` to reflect the change from `<script
type="module">` to classic `<script>`
- [x] All 18 standalone tests pass (`bun bd test
test/bundler/standalone.test.ts`)
- [x] Regression test passes (`bun bd test
test/regression/issue/27113.test.ts`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-18 19:07:10 -08:00
Dylan Conway
9785af304c Windows arm64 CI (#26746)
### What does this PR do?
Sets up ci for windows arm64
### How did you verify your code works?

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-18 18:08:10 -08:00
249 changed files with 18711 additions and 2473 deletions

View File

@@ -99,6 +99,23 @@ function getTargetLabel(target) {
* @property {string[]} [features]
*/
// Azure VM sizes for Windows CI runners.
// DDSv6 = x64, DPSv6 = ARM64 (Cobalt 100). Quota: 100 cores per family in eastus2.
const azureVmSizes = {
"windows-x64": {
build: "Standard_D16ds_v6", // 16 vCPU, 64 GiB — C++ build, link
test: "Standard_D4ds_v6", // 4 vCPU, 16 GiB — test shards
},
"windows-aarch64": {
build: "Standard_D16ps_v6", // 16 vCPU, 64 GiB — C++ build, link
test: "Standard_D4ps_v6", // 4 vCPU, 16 GiB — test shards
},
};
function getAzureVmSize(os, arch, tier = "build") {
return azureVmSizes[`${os}-${arch}`]?.[tier];
}
/**
* @type {Platform[]}
*/
@@ -114,8 +131,7 @@ const buildPlatforms = [
{ os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.23" },
{ os: "windows", arch: "x64", release: "2019" },
{ os: "windows", arch: "x64", baseline: true, release: "2019" },
// TODO: Re-enable when Windows ARM64 VS component installation is resolved on Buildkite runners
// { os: "windows", arch: "aarch64", release: "2019" },
{ os: "windows", arch: "aarch64", release: "11" },
];
/**
@@ -138,8 +154,7 @@ const testPlatforms = [
{ 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" },
// TODO: Enable when Windows ARM64 CI runners are ready
// { os: "windows", arch: "aarch64", release: "2019", tier: "oldest" },
{ os: "windows", arch: "aarch64", release: "11", tier: "latest" },
];
/**
@@ -304,15 +319,8 @@ function getCppAgent(platform, options) {
};
}
// Cross-compile Windows ARM64 from x64 runners
if (os === "windows" && arch === "aarch64") {
return getEc2Agent({ ...platform, arch: "x64" }, options, {
instanceType: "c7i.4xlarge",
});
}
return getEc2Agent(platform, options, {
instanceType: arch === "aarch64" ? "c8g.4xlarge" : "c7i.4xlarge",
instanceType: os === "windows" ? getAzureVmSize(os, arch) : arch === "aarch64" ? "c8g.4xlarge" : "c7i.4xlarge",
});
}
@@ -333,10 +341,8 @@ function getLinkBunAgent(platform, options) {
}
if (os === "windows") {
// Cross-compile Windows ARM64 from x64 runners
const agentPlatform = arch === "aarch64" ? { ...platform, arch: "x64" } : platform;
return getEc2Agent(agentPlatform, options, {
instanceType: "r7i.large",
return getEc2Agent(platform, options, {
instanceType: getAzureVmSize(os, arch),
});
}
@@ -363,7 +369,17 @@ function getZigPlatform() {
* @param {PipelineOptions} options
* @returns {Agent}
*/
function getZigAgent(_platform, options) {
function getZigAgent(platform, options) {
const { os, arch } = platform;
// Windows builds Zig natively on Azure
if (os === "windows") {
return getEc2Agent(platform, options, {
instanceType: getAzureVmSize(os, arch),
});
}
// Everything else cross-compiles from Linux aarch64
return getEc2Agent(getZigPlatform(), options, {
instanceType: "r8g.large",
});
@@ -388,7 +404,7 @@ function getTestAgent(platform, options) {
// TODO: delete this block when we upgrade to mimalloc v3
if (os === "windows") {
return getEc2Agent(platform, options, {
instanceType: "c7i.2xlarge",
instanceType: getAzureVmSize(os, arch, "test"),
cpuCount: 2,
threadsPerCore: 1,
});
@@ -456,26 +472,13 @@ function getBuildCommand(target, options, label) {
const { profile } = target;
const buildProfile = profile || "release";
if (target.os === "windows" && label === "build-bun") {
// Only sign release builds, not canary builds (DigiCert charges per signature)
const enableSigning = !options.canary ? " -DENABLE_WINDOWS_CODESIGNING=ON" : "";
return `bun run build:${buildProfile}${enableSigning}`;
}
// Windows code signing is handled by a dedicated 'windows-sign' step after
// all Windows builds complete — see getWindowsSignStep(). smctl is x64-only,
// so signing on the build agent wouldn't work for ARM64 anyway.
return `bun run build:${buildProfile}`;
}
/**
* Get extra flags needed when cross-compiling Windows ARM64 from x64.
* Applied to C++ and link steps (not Zig, which has its own toolchain handling).
*/
function getWindowsArm64CrossFlags(target) {
if (target.os === "windows" && target.arch === "aarch64") {
return " --toolchain windows-aarch64";
}
return "";
}
/**
* @param {Platform} platform
* @param {PipelineOptions} options
@@ -483,7 +486,6 @@ function getWindowsArm64CrossFlags(target) {
*/
function getBuildCppStep(platform, options) {
const command = getBuildCommand(platform, options);
const crossFlags = getWindowsArm64CrossFlags(platform);
return {
key: `${getTargetKey(platform)}-build-cpp`,
@@ -498,7 +500,7 @@ function getBuildCppStep(platform, options) {
// We used to build the C++ dependencies and bun in separate steps.
// However, as long as the zig build takes longer than both sequentially,
// it's cheaper to run them in the same step. Can be revisited in the future.
command: [`${command}${crossFlags} --target bun`, `${command}${crossFlags} --target dependencies`],
command: [`${command} --target bun`, `${command} --target dependencies`],
};
}
@@ -524,7 +526,10 @@ function getBuildToolchain(target) {
* @returns {Step}
*/
function getBuildZigStep(platform, options) {
const { os, arch } = platform;
const toolchain = getBuildToolchain(platform);
// Native Windows builds don't need a cross-compilation toolchain
const toolchainArg = os === "windows" ? "" : ` --toolchain ${toolchain}`;
return {
key: `${getTargetKey(platform)}-build-zig`,
retry: getRetry(),
@@ -532,7 +537,7 @@ function getBuildZigStep(platform, options) {
agents: getZigAgent(platform, options),
cancel_on_build_failing: isMergeQueue(),
env: getBuildEnv(platform, options),
command: `${getBuildCommand(platform, options)} --target bun-zig --toolchain ${toolchain}`,
command: `${getBuildCommand(platform, options)} --target bun-zig${toolchainArg}`,
timeout_in_minutes: 35,
};
}
@@ -555,7 +560,7 @@ function getLinkBunStep(platform, options) {
ASAN_OPTIONS: "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=0",
...getBuildEnv(platform, options),
},
command: `${getBuildCommand(platform, options, "build-bun")}${getWindowsArm64CrossFlags(platform)} --target bun`,
command: `${getBuildCommand(platform, options, "build-bun")} --target bun`,
};
}
@@ -586,8 +591,35 @@ function getTargetTriplet(platform) {
*/
function needsBaselineVerification(platform) {
const { os, arch, baseline } = platform;
if (os !== "linux") return false;
return (arch === "x64" && baseline) || arch === "aarch64";
if (os === "linux") return (arch === "x64" && baseline) || arch === "aarch64";
if (os === "windows") return arch === "x64" && baseline;
return false;
}
/**
* Returns the emulator binary name for the given platform.
* Linux uses QEMU user-mode; Windows uses Intel SDE.
* @param {Platform} platform
* @returns {string}
*/
function getEmulatorBinary(platform) {
const { os, arch } = platform;
if (os === "windows") return "sde-external/sde.exe";
if (arch === "aarch64") return "qemu-aarch64-static";
return "qemu-x86_64-static";
}
const SDE_VERSION = "9.58.0-2025-06-16";
const SDE_URL = `https://downloadmirror.intel.com/859732/sde-external-${SDE_VERSION}-win.tar.xz`;
/**
* @param {Platform} platform
* @param {PipelineOptions} options
* @returns {Step}
*/
function hasWebKitChanges(options) {
const { changedFiles = [] } = options;
return changedFiles.some(file => file.includes("SetupWebKit.cmake"));
}
/**
@@ -596,9 +628,31 @@ function needsBaselineVerification(platform) {
* @returns {Step}
*/
function getVerifyBaselineStep(platform, options) {
const { arch } = platform;
const { os } = platform;
const targetKey = getTargetKey(platform);
const archArg = arch === "x64" ? "x64" : "aarch64";
const triplet = getTargetTriplet(platform);
const emulator = getEmulatorBinary(platform);
const jitStressFlag = hasWebKitChanges(options) ? " --jit-stress" : "";
const setupCommands =
os === "windows"
? [
`echo Downloading build artifacts...`,
`buildkite-agent artifact download ${triplet}.zip . --step ${targetKey}-build-bun`,
`echo Extracting ${triplet}.zip...`,
`tar -xf ${triplet}.zip`,
`echo Downloading Intel SDE...`,
`curl.exe -fsSL -o sde.tar.xz "${SDE_URL}"`,
`echo Extracting Intel SDE...`,
`7z x -y sde.tar.xz`,
`7z x -y sde.tar`,
`ren sde-external-${SDE_VERSION}-win sde-external`,
]
: [
`buildkite-agent artifact download '*.zip' . --step ${targetKey}-build-bun`,
`unzip -o '${triplet}.zip'`,
`chmod +x ${triplet}/bun`,
];
return {
key: `${targetKey}-verify-baseline`,
@@ -607,57 +661,10 @@ function getVerifyBaselineStep(platform, options) {
agents: getLinkBunAgent(platform, options),
retry: getRetry(),
cancel_on_build_failing: isMergeQueue(),
timeout_in_minutes: 5,
timeout_in_minutes: hasWebKitChanges(options) ? 30 : 10,
command: [
`buildkite-agent artifact download '*.zip' . --step ${targetKey}-build-bun`,
`unzip -o '${getTargetTriplet(platform)}.zip'`,
`unzip -o '${getTargetTriplet(platform)}-profile.zip'`,
`chmod +x ${getTargetTriplet(platform)}/bun ${getTargetTriplet(platform)}-profile/bun-profile`,
`./scripts/verify-baseline-cpu.sh --arch ${archArg} --binary ${getTargetTriplet(platform)}/bun`,
`./scripts/verify-baseline-cpu.sh --arch ${archArg} --binary ${getTargetTriplet(platform)}-profile/bun-profile`,
],
};
}
/**
* Returns true if the PR modifies SetupWebKit.cmake (WebKit version changes).
* JIT stress tests under QEMU should run when WebKit is updated to catch
* JIT-generated code that uses unsupported CPU instructions.
* @param {PipelineOptions} options
* @returns {boolean}
*/
function hasWebKitChanges(options) {
const { changedFiles = [] } = options;
return changedFiles.some(file => file.includes("SetupWebKit.cmake"));
}
/**
* Returns a step that runs JSC JIT stress tests under QEMU.
* This verifies that JIT-compiled code doesn't use CPU instructions
* beyond the baseline target (no AVX on x64, no LSE on aarch64).
* @param {Platform} platform
* @param {PipelineOptions} options
* @returns {Step}
*/
function getJitStressTestStep(platform, options) {
const { arch } = platform;
const targetKey = getTargetKey(platform);
const archArg = arch === "x64" ? "x64" : "aarch64";
return {
key: `${targetKey}-jit-stress-qemu`,
label: `${getTargetLabel(platform)} - jit-stress-qemu`,
depends_on: [`${targetKey}-build-bun`],
agents: getLinkBunAgent(platform, options),
retry: getRetry(),
cancel_on_build_failing: isMergeQueue(),
// JIT stress tests are slow under QEMU emulation
timeout_in_minutes: 30,
command: [
`buildkite-agent artifact download '*.zip' . --step ${targetKey}-build-bun`,
`unzip -o '${getTargetTriplet(platform)}.zip'`,
`chmod +x ${getTargetTriplet(platform)}/bun`,
`./scripts/verify-jit-stress-qemu.sh --arch ${archArg} --binary ${getTargetTriplet(platform)}/bun`,
...setupCommands,
`bun scripts/verify-baseline.ts --binary ${triplet}/${os === "windows" ? "bun.exe" : "bun"} --emulator ${emulator}${jitStressFlag}`,
],
};
}
@@ -717,14 +724,14 @@ function getTestBunStep(platform, options, testOptions = {}) {
agents: getTestAgent(platform, options),
retry: getRetry(),
cancel_on_build_failing: isMergeQueue(),
parallelism: os === "darwin" ? 2 : 20,
parallelism: os === "darwin" ? 2 : os === "windows" ? 8 : 20,
timeout_in_minutes: profile === "asan" || os === "windows" ? 45 : 30,
env: {
ASAN_OPTIONS: "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=0",
},
command:
os === "windows"
? `node .\\scripts\\runner.node.mjs ${args.join(" ")}`
? `pwsh -NoProfile -File .\\scripts\\vs-shell.ps1 node .\\scripts\\runner.node.mjs ${args.join(" ")}`
: `./scripts/runner.node.mjs ${args.join(" ")}`,
};
}
@@ -739,6 +746,7 @@ function getBuildImageStep(platform, options) {
const { publishImages } = options;
const action = publishImages ? "publish-image" : "create-image";
const cloud = os === "windows" ? "azure" : "aws";
const command = [
"node",
"./scripts/machine.mjs",
@@ -747,7 +755,7 @@ function getBuildImageStep(platform, options) {
`--arch=${arch}`,
distro && `--distro=${distro}`,
`--release=${release}`,
"--cloud=aws",
`--cloud=${cloud}`,
"--ci",
"--authorized-org=oven-sh",
];
@@ -772,23 +780,73 @@ function getBuildImageStep(platform, options) {
}
/**
* @param {Platform[]} buildPlatforms
* Batch-signs all Windows artifacts on an x64 agent. DigiCert smctl is x64-only
* and silently fails under ARM64 emulation, so signing must happen here instead
* of inline during each build. Re-uploads signed zips with the same names so
* the release step picks them up transparently.
* @param {Platform[]} windowsPlatforms
* @param {PipelineOptions} options
* @returns {Step}
*/
function getReleaseStep(buildPlatforms, options) {
function getWindowsSignStep(windowsPlatforms, options) {
// Each build-bun step produces two zips: <triplet>-profile.zip and <triplet>.zip
const artifacts = [];
const buildSteps = [];
for (const platform of windowsPlatforms) {
const triplet = getTargetTriplet(platform);
const stepKey = `${getTargetKey(platform)}-build-bun`;
artifacts.push(`${triplet}-profile.zip`, `${triplet}.zip`);
buildSteps.push(stepKey, stepKey);
}
// Run on an x64 build agent — smctl doesn't work on ARM64
const signPlatform = windowsPlatforms.find(p => p.arch === "x64" && !p.baseline) ?? windowsPlatforms[0];
return {
key: "windows-sign",
label: `${getBuildkiteEmoji("windows")} sign`,
depends_on: windowsPlatforms.map(p => `${getTargetKey(p)}-build-bun`),
agents: getEc2Agent(signPlatform, options, {
instanceType: getAzureVmSize("windows", "x64", "test"),
}),
retry: getRetry(),
cancel_on_build_failing: isMergeQueue(),
command: [
`powershell -NoProfile -ExecutionPolicy Bypass -File .buildkite/scripts/sign-windows-artifacts.ps1 ` +
`-Artifacts ${artifacts.join(",")} ` +
`-BuildSteps ${buildSteps.join(",")}`,
],
};
}
/**
* @param {Platform[]} buildPlatforms
* @param {PipelineOptions} options
* @param {{ signed: boolean }} [extra]
* @returns {Step}
*/
function getReleaseStep(buildPlatforms, options, { signed = false } = {}) {
const { canary } = options;
const revision = typeof canary === "number" ? canary : 1;
// When signing ran, depend on windows-sign instead of the raw Windows builds
// so we wait for signed artifacts before releasing.
const depends_on = signed
? [...buildPlatforms.filter(p => p.os !== "windows").map(p => `${getTargetKey(p)}-build-bun`), "windows-sign"]
: buildPlatforms.map(platform => `${getTargetKey(platform)}-build-bun`);
return {
key: "release",
label: getBuildkiteEmoji("rocket"),
agents: {
queue: "test-darwin",
},
depends_on: buildPlatforms.map(platform => `${getTargetKey(platform)}-build-bun`),
depends_on,
env: {
CANARY: revision,
// Tells upload-release.sh to fetch Windows zips from the sign step
// (same filenames, but the signed re-uploads are the ones we want).
WINDOWS_ARTIFACT_STEP: signed ? "windows-sign" : "",
},
command: ".buildkite/scripts/upload-release.sh",
};
@@ -894,6 +952,7 @@ function getBenchmarkStep() {
* @property {string | boolean} [forceBuilds]
* @property {string | boolean} [forceTests]
* @property {string | boolean} [buildImages]
* @property {string | boolean} [signWindows]
* @property {string | boolean} [publishImages]
* @property {number} [canary]
* @property {Platform[]} [buildPlatforms]
@@ -1169,9 +1228,11 @@ async function getPipelineOptions() {
skipBuilds: parseOption(/\[(skip builds?|no builds?|only tests?)\]/i),
forceBuilds: parseOption(/\[(force builds?)\]/i),
skipTests: parseOption(/\[(skip tests?|no tests?|only builds?)\]/i),
buildImages: parseOption(/\[(build images?)\]/i),
signWindows: parseOption(/\[(sign windows)\]/i),
buildImages: parseOption(/\[(build (?:(?:windows|linux) )?images?)\]/i),
dryRun: parseOption(/\[(dry run)\]/i),
publishImages: parseOption(/\[(publish images?)\]/i),
publishImages: parseOption(/\[(publish (?:(?:windows|linux) )?images?)\]/i),
imageFilter: (commitMessage.match(/\[(?:build|publish) (windows|linux) images?\]/i) || [])[1]?.toLowerCase(),
buildPlatforms: Array.from(buildPlatformsMap.values()),
testPlatforms: Array.from(testPlatformsMap.values()),
};
@@ -1196,13 +1257,12 @@ async function getPipeline(options = {}) {
return;
}
const { buildPlatforms = [], testPlatforms = [], buildImages, publishImages } = options;
const { buildPlatforms = [], testPlatforms = [], buildImages, publishImages, imageFilter } = options;
const imagePlatforms = new Map(
buildImages || publishImages
? [...buildPlatforms, ...testPlatforms]
.filter(({ os }) => os !== "darwin")
// Windows ARM64 cross-compiles from x64 runners, no separate image needed
.filter(({ os, arch }) => !(os === "windows" && arch === "aarch64"))
.filter(({ os, distro }) => !imageFilter || os === imageFilter || distro === imageFilter)
.map(platform => [getImageKey(platform), platform])
: [],
);
@@ -1256,10 +1316,6 @@ async function getPipeline(options = {}) {
if (needsBaselineVerification(target)) {
steps.push(getVerifyBaselineStep(target, options));
// Run JIT stress tests under QEMU when WebKit is updated
if (hasWebKitChanges(options)) {
steps.push(getJitStressTestStep(target, options));
}
}
return getStepWithDependsOn(
@@ -1287,8 +1343,19 @@ async function getPipeline(options = {}) {
}
}
// Sign Windows builds on release (non-canary main) or when [sign windows]
// is in the commit message (for testing the sign step on a branch).
// DigiCert charges per signature, so canary builds are never signed.
const shouldSignWindows = (isMainBranch() && !options.canary) || options.signWindows;
if (shouldSignWindows) {
const windowsPlatforms = buildPlatforms.filter(p => p.os === "windows");
if (windowsPlatforms.length > 0) {
steps.push(getWindowsSignStep(windowsPlatforms, options));
}
}
if (isMainBranch()) {
steps.push(getReleaseStep(buildPlatforms, options));
steps.push(getReleaseStep(buildPlatforms, options, { signed: shouldSignWindows }));
}
steps.push(getBenchmarkStep());
@@ -1341,6 +1408,10 @@ async function main() {
{ headers: { Authorization: `Bearer ${getSecret("GITHUB_TOKEN")}` } },
);
const doc = await res.json();
if (!Array.isArray(doc)) {
console.error(`-> page ${i}, unexpected response:`, JSON.stringify(doc));
break;
}
console.log(`-> page ${i}, found ${doc.length} items`);
if (doc.length === 0) break;
for (const { filename, status } of doc) {
@@ -1355,7 +1426,7 @@ async function main() {
} catch (e) {
console.error(e);
}
if (allFiles.every(filename => filename.startsWith("docs/"))) {
if (allFiles.length > 0 && allFiles.every(filename => filename.startsWith("docs/"))) {
console.log(`- PR is only docs, skipping tests!`);
return;
}

View File

@@ -0,0 +1,281 @@
# Batch Windows code signing for all bun-windows-*.zip Buildkite artifacts.
#
# This runs as a dedicated pipeline step on a Windows x64 agent after all
# Windows build-bun steps complete. Signing is done here instead of inline
# during each build because DigiCert smctl is x64-only and silently fails
# under ARM64 emulation.
#
# Each zip is downloaded, its exe signed in place, and the zip is re-packed
# with the same name so downstream steps (release, tests) see signed binaries.
param(
# Comma-separated list. powershell.exe -File passes everything as
# literal strings, so [string[]] with "a,b,c" becomes a 1-element array.
[Parameter(Mandatory=$true)]
[string]$Artifacts,
# Comma-separated, same length as Artifacts, mapping each zip to its source step.
[Parameter(Mandatory=$true)]
[string]$BuildSteps
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$ArtifactList = $Artifacts -split ","
$BuildStepList = $BuildSteps -split ","
# smctl shells out to signtool.exe which is only in PATH when the VS dev
# environment is loaded. Dot-source the existing helper to set it up.
. $PSScriptRoot\..\..\scripts\vs-shell.ps1
function Log-Info {
param([string]$Message)
Write-Host "[INFO] $Message" -ForegroundColor Cyan
}
function Log-Success {
param([string]$Message)
Write-Host "[SUCCESS] $Message" -ForegroundColor Green
}
function Log-Error {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
}
function Log-Debug {
param([string]$Message)
if ($env:DEBUG -eq "true" -or $env:DEBUG -eq "1") {
Write-Host "[DEBUG] $Message" -ForegroundColor Gray
}
}
function Get-BuildkiteSecret {
param([string]$Name)
$value = & buildkite-agent secret get $Name 2>&1
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrEmpty($value)) {
throw "Failed to fetch Buildkite secret: $Name"
}
return $value
}
function Ensure-Secrets {
Log-Info "Fetching signing secrets from Buildkite..."
$env:SM_API_KEY = Get-BuildkiteSecret "SM_API_KEY"
$env:SM_CLIENT_CERT_PASSWORD = Get-BuildkiteSecret "SM_CLIENT_CERT_PASSWORD"
$env:SM_CLIENT_CERT_FILE = Get-BuildkiteSecret "SM_CLIENT_CERT_FILE"
$env:SM_KEYPAIR_ALIAS = Get-BuildkiteSecret "SM_KEYPAIR_ALIAS"
$env:SM_HOST = Get-BuildkiteSecret "SM_HOST"
Log-Success "All signing secrets fetched"
}
function Setup-Certificate {
Log-Info "Decoding client certificate..."
try {
$tempCertPath = Join-Path $env:TEMP "digicert_cert_$(Get-Random).p12"
$certBytes = [System.Convert]::FromBase64String($env:SM_CLIENT_CERT_FILE)
[System.IO.File]::WriteAllBytes($tempCertPath, $certBytes)
$fileSize = (Get-Item $tempCertPath).Length
if ($fileSize -lt 100) {
throw "Decoded certificate too small: $fileSize bytes"
}
$env:SM_CLIENT_CERT_FILE = $tempCertPath
$script:TempCertPath = $tempCertPath
Log-Success "Certificate decoded ($fileSize bytes)"
} catch {
if (Test-Path $env:SM_CLIENT_CERT_FILE) {
Log-Info "Using certificate file path directly: $env:SM_CLIENT_CERT_FILE"
} else {
throw "SM_CLIENT_CERT_FILE is neither valid base64 nor an existing file"
}
}
}
function Install-KeyLocker {
Log-Info "Setting up DigiCert KeyLocker tools..."
$installDir = "C:\BuildTools\DigiCert"
$smctlPath = Join-Path $installDir "smctl.exe"
if (Test-Path $smctlPath) {
Log-Success "smctl already installed at $smctlPath"
$env:PATH = "$installDir;$env:PATH"
return $smctlPath
}
if (!(Test-Path $installDir)) {
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
}
# smctl is x64-only; this script must run on an x64 agent
$msiUrl = "https://bun-ci-assets.bun.sh/Keylockertools-windows-x64.msi"
$msiPath = Join-Path $env:TEMP "Keylockertools-windows-x64.msi"
Log-Info "Downloading KeyLocker MSI from $msiUrl"
if (Test-Path $msiPath) { Remove-Item $msiPath -Force }
(New-Object System.Net.WebClient).DownloadFile($msiUrl, $msiPath)
if (!(Test-Path $msiPath)) { throw "MSI download failed" }
Log-Info "Installing KeyLocker MSI..."
$proc = Start-Process -FilePath "msiexec.exe" -Wait -PassThru -NoNewWindow -ArgumentList @(
"/i", "`"$msiPath`"",
"/quiet", "/norestart",
"TARGETDIR=`"$installDir`"",
"INSTALLDIR=`"$installDir`"",
"ACCEPT_EULA=1",
"ADDLOCAL=ALL"
)
if ($proc.ExitCode -ne 0) {
throw "MSI install failed with exit code $($proc.ExitCode)"
}
if (!(Test-Path $smctlPath)) {
$found = Get-ChildItem -Path $installDir -Filter "smctl.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($found) {
$smctlPath = $found.FullName
$installDir = $found.DirectoryName
} else {
throw "smctl.exe not found after install"
}
}
$env:PATH = "$installDir;$env:PATH"
Log-Success "smctl installed at $smctlPath"
return $smctlPath
}
function Configure-KeyLocker {
param([string]$Smctl)
Log-Info "Configuring KeyLocker..."
$version = & $Smctl --version 2>&1
Log-Debug "smctl version: $version"
$saveOut = & $Smctl credentials save $env:SM_API_KEY $env:SM_CLIENT_CERT_PASSWORD 2>&1 | Out-String
Log-Debug "credentials save: $saveOut"
$healthOut = & $Smctl healthcheck 2>&1 | Out-String
Log-Debug "healthcheck: $healthOut"
if ($healthOut -notlike "*Healthy*" -and $healthOut -notlike "*SUCCESS*" -and $LASTEXITCODE -ne 0) {
Log-Error "healthcheck output: $healthOut"
# Don't throw — healthcheck is sometimes flaky but signing still works
}
$syncOut = & $Smctl windows certsync 2>&1 | Out-String
Log-Debug "certsync: $syncOut"
Log-Success "KeyLocker configured"
}
function Download-Artifact {
param([string]$Name, [string]$StepKey)
Log-Info "Downloading $Name from step $StepKey"
& buildkite-agent artifact download $Name . --step $StepKey
if ($LASTEXITCODE -ne 0 -or !(Test-Path $Name)) {
throw "Failed to download artifact: $Name"
}
Log-Success "Downloaded $Name ($((Get-Item $Name).Length) bytes)"
}
function Sign-Exe {
param([string]$ExePath, [string]$Smctl)
$fileName = Split-Path $ExePath -Leaf
Log-Info "Signing $fileName ($((Get-Item $ExePath).Length) bytes)..."
$existing = Get-AuthenticodeSignature $ExePath
if ($existing.Status -eq "Valid") {
Log-Info "$fileName already signed by $($existing.SignerCertificate.Subject), skipping"
return
}
$out = & $Smctl sign --keypair-alias $env:SM_KEYPAIR_ALIAS --input $ExePath --verbose 2>&1 | Out-String
Log-Info "smctl output: $out"
# smctl exits 0 even on failure — must also check output text
if ($LASTEXITCODE -ne 0 -or $out -like "*FAILED*" -or $out -like "*error*") {
throw "Signing failed for $fileName (exit $LASTEXITCODE): $out"
}
$sig = Get-AuthenticodeSignature $ExePath
if ($sig.Status -ne "Valid") {
throw "$fileName signature verification failed: $($sig.Status) - $($sig.StatusMessage)"
}
Log-Success "$fileName signed by $($sig.SignerCertificate.Subject)"
}
function Sign-Artifact {
param([string]$ZipName, [string]$Smctl)
Write-Host "================================================" -ForegroundColor Cyan
Write-Host " Signing $ZipName" -ForegroundColor Cyan
Write-Host "================================================" -ForegroundColor Cyan
$extractDir = [System.IO.Path]::GetFileNameWithoutExtension($ZipName)
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
Log-Info "Extracting $ZipName"
Expand-Archive -Path $ZipName -DestinationPath . -Force
if (!(Test-Path $extractDir)) {
throw "Expected directory $extractDir not found after extraction"
}
$exes = Get-ChildItem -Path $extractDir -Filter "*.exe"
if ($exes.Count -eq 0) {
throw "No .exe files found in $extractDir"
}
foreach ($exe in $exes) {
Sign-Exe -ExePath $exe.FullName -Smctl $Smctl
}
Log-Info "Re-packing $ZipName"
Remove-Item $ZipName -Force
Compress-Archive -Path $extractDir -DestinationPath $ZipName -CompressionLevel Optimal
Remove-Item $extractDir -Recurse -Force
Log-Info "Uploading signed $ZipName"
& buildkite-agent artifact upload $ZipName
if ($LASTEXITCODE -ne 0) {
throw "Failed to upload $ZipName"
}
Log-Success "$ZipName signed and uploaded"
}
# Main
try {
Write-Host "================================================" -ForegroundColor Cyan
Write-Host " Windows Artifact Code Signing" -ForegroundColor Cyan
Write-Host "================================================" -ForegroundColor Cyan
if ($ArtifactList.Count -ne $BuildStepList.Count) {
throw "Artifact count ($($ArtifactList.Count)) must match BuildStep count ($($BuildStepList.Count))"
}
Log-Info "Will sign $($ArtifactList.Count) artifacts: $($ArtifactList -join ', ')"
Ensure-Secrets
Setup-Certificate
$smctl = Install-KeyLocker
Configure-KeyLocker -Smctl $smctl
for ($i = 0; $i -lt $ArtifactList.Count; $i++) {
Download-Artifact -Name $ArtifactList[$i] -StepKey $BuildStepList[$i]
Sign-Artifact -ZipName $ArtifactList[$i] -Smctl $smctl
}
Write-Host "================================================" -ForegroundColor Green
Write-Host " All artifacts signed successfully" -ForegroundColor Green
Write-Host "================================================" -ForegroundColor Green
exit 0
} catch {
Log-Error "Signing failed: $_"
exit 1
} finally {
if ($script:TempCertPath -and (Test-Path $script:TempCertPath)) {
Remove-Item $script:TempCertPath -Force -ErrorAction SilentlyContinue
}
}

View File

@@ -1,470 +0,0 @@
# Windows Code Signing Script for Bun
# Uses DigiCert KeyLocker for Authenticode signing
# Native PowerShell implementation - no path translation issues
param(
[Parameter(Mandatory=$true)]
[string]$BunProfileExe,
[Parameter(Mandatory=$true)]
[string]$BunExe
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
# Logging functions
function Log-Info {
param([string]$Message)
Write-Host "[INFO] $Message" -ForegroundColor Cyan
}
function Log-Success {
param([string]$Message)
Write-Host "[SUCCESS] $Message" -ForegroundColor Green
}
function Log-Error {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
}
function Log-Debug {
param([string]$Message)
if ($env:DEBUG -eq "true" -or $env:DEBUG -eq "1") {
Write-Host "[DEBUG] $Message" -ForegroundColor Gray
}
}
# Detect system architecture
$script:IsARM64 = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64
$script:VsArch = if ($script:IsARM64) { "arm64" } else { "amd64" }
# Load Visual Studio environment if not already loaded
function Ensure-VSEnvironment {
if ($null -eq $env:VSINSTALLDIR) {
Log-Info "Loading Visual Studio environment for $script:VsArch..."
$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
if (!(Test-Path $vswhere)) {
throw "Command not found: vswhere (did you install Visual Studio?)"
}
$vsDir = & $vswhere -prerelease -latest -property installationPath
if ($null -eq $vsDir) {
$vsDir = Get-ChildItem -Path "C:\Program Files\Microsoft Visual Studio\2022" -Directory -ErrorAction SilentlyContinue
if ($null -eq $vsDir) {
throw "Visual Studio directory not found."
}
$vsDir = $vsDir.FullName
}
Push-Location $vsDir
try {
$vsShell = Join-Path -Path $vsDir -ChildPath "Common7\Tools\Launch-VsDevShell.ps1"
. $vsShell -Arch $script:VsArch -HostArch $script:VsArch
} finally {
Pop-Location
}
Log-Success "Visual Studio environment loaded"
}
if ($env:VSCMD_ARG_TGT_ARCH -eq "x86") {
throw "Visual Studio environment is targeting 32 bit x86, but only 64-bit architectures (x64/arm64) are supported."
}
}
# Check for required environment variables
function Check-Environment {
Log-Info "Checking environment variables..."
$required = @{
"SM_API_KEY" = $env:SM_API_KEY
"SM_CLIENT_CERT_PASSWORD" = $env:SM_CLIENT_CERT_PASSWORD
"SM_KEYPAIR_ALIAS" = $env:SM_KEYPAIR_ALIAS
"SM_HOST" = $env:SM_HOST
"SM_CLIENT_CERT_FILE" = $env:SM_CLIENT_CERT_FILE
}
$missing = @()
foreach ($key in $required.Keys) {
if ([string]::IsNullOrEmpty($required[$key])) {
$missing += $key
} else {
Log-Debug "$key is set (length: $($required[$key].Length))"
}
}
if ($missing.Count -gt 0) {
throw "Missing required environment variables: $($missing -join ', ')"
}
Log-Success "All required environment variables are present"
}
# Setup certificate file
function Setup-Certificate {
Log-Info "Setting up certificate..."
# Always try to decode as base64 first
# If it fails, then treat as file path
try {
Log-Info "Attempting to decode certificate as base64..."
Log-Debug "Input string length: $($env:SM_CLIENT_CERT_FILE.Length) characters"
$tempCertPath = Join-Path $env:TEMP "digicert_cert_$(Get-Random).p12"
# Try to decode as base64
$certBytes = [System.Convert]::FromBase64String($env:SM_CLIENT_CERT_FILE)
[System.IO.File]::WriteAllBytes($tempCertPath, $certBytes)
# Validate the decoded certificate size
$fileSize = (Get-Item $tempCertPath).Length
if ($fileSize -lt 100) {
throw "Decoded certificate too small: $fileSize bytes (expected >100 bytes)"
}
# Update environment to point to file
$env:SM_CLIENT_CERT_FILE = $tempCertPath
Log-Success "Certificate decoded and written to: $tempCertPath"
Log-Debug "Decoded certificate file size: $fileSize bytes"
# Register cleanup
$global:TEMP_CERT_PATH = $tempCertPath
} catch {
# If base64 decode fails, check if it's a file path
Log-Info "Base64 decode failed, checking if it's a file path..."
Log-Debug "Decode error: $_"
if (Test-Path $env:SM_CLIENT_CERT_FILE) {
$fileSize = (Get-Item $env:SM_CLIENT_CERT_FILE).Length
# Validate file size
if ($fileSize -lt 100) {
throw "Certificate file too small: $fileSize bytes at $env:SM_CLIENT_CERT_FILE (possibly corrupted)"
}
Log-Info "Using certificate file: $env:SM_CLIENT_CERT_FILE"
Log-Debug "Certificate file size: $fileSize bytes"
} else {
throw "SM_CLIENT_CERT_FILE is neither valid base64 nor an existing file: $env:SM_CLIENT_CERT_FILE"
}
}
}
# Install DigiCert KeyLocker tools
function Install-KeyLocker {
Log-Info "Setting up DigiCert KeyLocker tools..."
# Define our controlled installation directory
$installDir = "C:\BuildTools\DigiCert"
$smctlPath = Join-Path $installDir "smctl.exe"
# Check if already installed in our controlled location
if (Test-Path $smctlPath) {
Log-Success "KeyLocker tools already installed at: $smctlPath"
# Add to PATH if not already there
if ($env:PATH -notlike "*$installDir*") {
$env:PATH = "$installDir;$env:PATH"
Log-Info "Added to PATH: $installDir"
}
return $smctlPath
}
Log-Info "Installing KeyLocker tools to: $installDir"
# Create the installation directory if it doesn't exist
if (!(Test-Path $installDir)) {
Log-Info "Creating installation directory: $installDir"
try {
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
Log-Success "Created directory: $installDir"
} catch {
throw "Failed to create directory $installDir : $_"
}
}
# Download MSI installer
# Note: KeyLocker tools currently only available for x64, but works on ARM64 via emulation
$msiArch = "x64"
$msiUrl = "https://bun-ci-assets.bun.sh/Keylockertools-windows-${msiArch}.msi"
$msiPath = Join-Path $env:TEMP "Keylockertools-windows-${msiArch}.msi"
Log-Info "Downloading MSI from: $msiUrl"
Log-Info "Downloading to: $msiPath"
try {
# Remove existing MSI if present
if (Test-Path $msiPath) {
Remove-Item $msiPath -Force
Log-Debug "Removed existing MSI file"
}
# Download with progress tracking
$webClient = New-Object System.Net.WebClient
$webClient.DownloadFile($msiUrl, $msiPath)
if (!(Test-Path $msiPath)) {
throw "MSI download failed - file not found"
}
$fileSize = (Get-Item $msiPath).Length
Log-Success "MSI downloaded successfully (size: $fileSize bytes)"
} catch {
throw "Failed to download MSI: $_"
}
# Install MSI
Log-Info "Installing MSI..."
Log-Debug "MSI path: $msiPath"
Log-Debug "File exists: $(Test-Path $msiPath)"
Log-Debug "File size: $((Get-Item $msiPath).Length) bytes"
# Check if running as administrator
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Log-Info "Running as administrator: $isAdmin"
# Install MSI silently to our controlled directory
$arguments = @(
"/i", "`"$msiPath`"",
"/quiet",
"/norestart",
"TARGETDIR=`"$installDir`"",
"INSTALLDIR=`"$installDir`"",
"ACCEPT_EULA=1",
"ADDLOCAL=ALL"
)
Log-Debug "Running: msiexec.exe $($arguments -join ' ')"
Log-Info "Installing to: $installDir"
$process = Start-Process -FilePath "msiexec.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow
if ($process.ExitCode -ne 0) {
Log-Error "MSI installation failed with exit code: $($process.ExitCode)"
# Try to get error details from event log
try {
$events = Get-WinEvent -LogName "Application" -MaxEvents 10 |
Where-Object { $_.ProviderName -eq "MsiInstaller" -and $_.TimeCreated -gt (Get-Date).AddMinutes(-1) }
foreach ($event in $events) {
Log-Debug "MSI Event: $($event.Message)"
}
} catch {
Log-Debug "Could not retrieve MSI installation events"
}
throw "MSI installation failed with exit code: $($process.ExitCode)"
}
Log-Success "MSI installation completed"
# Wait for installation to complete
Start-Sleep -Seconds 2
# Verify smctl.exe exists in our controlled location
if (Test-Path $smctlPath) {
Log-Success "KeyLocker tools installed successfully at: $smctlPath"
# Add to PATH
$env:PATH = "$installDir;$env:PATH"
Log-Info "Added to PATH: $installDir"
return $smctlPath
}
# If not in our expected location, check if it installed somewhere in the directory
$found = Get-ChildItem -Path $installDir -Filter "smctl.exe" -Recurse -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($found) {
Log-Success "Found smctl.exe at: $($found.FullName)"
$smctlDir = $found.DirectoryName
$env:PATH = "$smctlDir;$env:PATH"
return $found.FullName
}
throw "KeyLocker tools installation succeeded but smctl.exe not found in $installDir"
}
# Configure KeyLocker
function Configure-KeyLocker {
param([string]$SmctlPath)
Log-Info "Configuring KeyLocker..."
# Verify smctl is accessible
try {
$version = & $SmctlPath --version 2>&1
Log-Debug "smctl version: $version"
} catch {
throw "Failed to run smctl: $_"
}
# Configure KeyLocker credentials and environment
Log-Info "Configuring KeyLocker credentials..."
try {
# Save credentials (API key and password)
Log-Info "Saving credentials to OS store..."
$saveOutput = & $SmctlPath credentials save $env:SM_API_KEY $env:SM_CLIENT_CERT_PASSWORD 2>&1 | Out-String
Log-Debug "Credentials save output: $saveOutput"
if ($saveOutput -like "*Credentials saved*") {
Log-Success "Credentials saved successfully"
}
# Set environment variables for smctl
Log-Info "Setting KeyLocker environment variables..."
$env:SM_HOST = $env:SM_HOST # Already set, but ensure it's available
$env:SM_API_KEY = $env:SM_API_KEY # Already set
$env:SM_CLIENT_CERT_FILE = $env:SM_CLIENT_CERT_FILE # Path to decoded cert file
Log-Debug "SM_HOST: $env:SM_HOST"
Log-Debug "SM_CLIENT_CERT_FILE: $env:SM_CLIENT_CERT_FILE"
# Run health check
Log-Info "Running KeyLocker health check..."
$healthOutput = & $SmctlPath healthcheck 2>&1 | Out-String
Log-Debug "Health check output: $healthOutput"
if ($healthOutput -like "*Healthy*" -or $healthOutput -like "*SUCCESS*" -or $LASTEXITCODE -eq 0) {
Log-Success "KeyLocker health check passed"
} else {
Log-Error "Health check failed: $healthOutput"
# Don't throw here, sometimes healthcheck is flaky but signing still works
}
# Sync certificates to Windows certificate store
Log-Info "Syncing certificates to Windows store..."
$syncOutput = & $SmctlPath windows certsync 2>&1 | Out-String
Log-Debug "Certificate sync output: $syncOutput"
if ($syncOutput -like "*success*" -or $syncOutput -like "*synced*" -or $LASTEXITCODE -eq 0) {
Log-Success "Certificates synced to Windows store"
} else {
Log-Info "Certificate sync output: $syncOutput"
}
} catch {
throw "Failed to configure KeyLocker: $_"
}
}
# Sign an executable
function Sign-Executable {
param(
[string]$ExePath,
[string]$SmctlPath
)
if (!(Test-Path $ExePath)) {
throw "Executable not found: $ExePath"
}
$fileName = Split-Path $ExePath -Leaf
Log-Info "Signing $fileName..."
Log-Debug "Full path: $ExePath"
Log-Debug "File size: $((Get-Item $ExePath).Length) bytes"
# Check if already signed
$existingSig = Get-AuthenticodeSignature $ExePath
if ($existingSig.Status -eq "Valid") {
Log-Info "$fileName is already signed by: $($existingSig.SignerCertificate.Subject)"
Log-Info "Skipping re-signing"
return
}
# Sign the executable using smctl
try {
# smctl sign command with keypair-alias
$signArgs = @(
"sign",
"--keypair-alias", $env:SM_KEYPAIR_ALIAS,
"--input", $ExePath,
"--verbose"
)
Log-Debug "Running: $SmctlPath $($signArgs -join ' ')"
$signOutput = & $SmctlPath $signArgs 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
Log-Error "Signing output: $signOutput"
throw "Signing failed with exit code: $LASTEXITCODE"
}
Log-Debug "Signing output: $signOutput"
Log-Success "Signing command completed"
} catch {
throw "Failed to sign $fileName : $_"
}
# Verify signature
$newSig = Get-AuthenticodeSignature $ExePath
if ($newSig.Status -eq "Valid") {
Log-Success "$fileName signed successfully"
Log-Info "Signed by: $($newSig.SignerCertificate.Subject)"
Log-Info "Thumbprint: $($newSig.SignerCertificate.Thumbprint)"
Log-Info "Valid from: $($newSig.SignerCertificate.NotBefore) to $($newSig.SignerCertificate.NotAfter)"
} else {
throw "$fileName signature verification failed: $($newSig.Status) - $($newSig.StatusMessage)"
}
}
# Cleanup function
function Cleanup {
if ($global:TEMP_CERT_PATH -and (Test-Path $global:TEMP_CERT_PATH)) {
try {
Remove-Item $global:TEMP_CERT_PATH -Force
Log-Info "Cleaned up temporary certificate"
} catch {
Log-Error "Failed to cleanup temporary certificate: $_"
}
}
}
# Main execution
try {
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Windows Code Signing for Bun" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
# Ensure we're in a VS environment
Ensure-VSEnvironment
# Check environment variables
Check-Environment
# Setup certificate
Setup-Certificate
# Install and configure KeyLocker
$smctlPath = Install-KeyLocker
Configure-KeyLocker -SmctlPath $smctlPath
# Sign both executables
Sign-Executable -ExePath $BunProfileExe -SmctlPath $smctlPath
Sign-Executable -ExePath $BunExe -SmctlPath $smctlPath
Write-Host "========================================" -ForegroundColor Green
Write-Host " Code signing completed successfully!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
exit 0
} catch {
Log-Error "Code signing failed: $_"
exit 1
} finally {
Cleanup
}

View File

@@ -121,7 +121,14 @@ function download_buildkite_artifact() {
if [ -z "$dir" ]; then
dir="."
fi
run_command buildkite-agent artifact download "$name" "$dir"
# When signing ran, Windows zips exist in two steps with the same name
# (build-bun unsigned, windows-sign signed). Pin to the sign step to
# guarantee we get the signed one.
local step_args=()
if [[ -n "$WINDOWS_ARTIFACT_STEP" && "$name" == bun-windows-* ]]; then
step_args=(--step "$WINDOWS_ARTIFACT_STEP")
fi
run_command buildkite-agent artifact download "$name" "$dir" "${step_args[@]}"
if [ ! -f "$dir/$name" ]; then
echo "error: Cannot find Buildkite artifact: $name"
exit 1

View File

@@ -0,0 +1,30 @@
name: Close stale robobun PRs
on:
schedule:
- cron: "30 0 * * *"
workflow_dispatch:
jobs:
close-stale-robobun-prs:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
pull-requests: write
steps:
- name: Close stale robobun PRs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
ninety_days_ago=$(date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%SZ)
gh pr list \
--author robobun \
--state open \
--json number,updatedAt \
--limit 1000 \
--jq ".[] | select(.updatedAt < \"$ninety_days_ago\") | .number" |
while read -r pr_number; do
echo "Closing PR #$pr_number (last updated before $ninety_days_ago)"
gh pr close "$pr_number" --comment "Closing this PR because it has been inactive for more than 90 days."
done

33
.github/workflows/on-slop.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Close AI Slop PRs
on:
pull_request_target:
types: [labeled]
jobs:
on-slop:
runs-on: ubuntu-latest
if: github.event.label.name == 'slop' && github.repository == 'oven-sh/bun'
permissions:
issues: write
pull-requests: write
steps:
- name: Comment and close PR
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: 'This PR has been closed because it was flagged as AI slop.\n\nMany AI-generated PRs are fine, but this one was identified as having one or more of the following issues:\n- Fails to verify the problem actually exists\n- Fails to test that the fix works\n- Makes incorrect assumptions about the codebase\n- Submits changes that are incomplete or misleading\n\nIf you believe this was done in error, please leave a comment explaining why.'
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
title: 'ai slop',
body: 'This PR has been marked as AI slop and the description has been updated to avoid confusion or misleading reviewers.\n\nMany AI PRs are fine, but sometimes they submit a PR too early, fail to test if the problem is real, fail to reproduce the problem, or fail to test that the problem is fixed. If you think this PR is not AI slop, please leave a comment.',
state: 'closed'
});

View File

@@ -161,6 +161,31 @@ test("(multi-file test) my feature", async () => {
- `src/sql/` - SQL database integrations
- `src/bake/` - Server-side rendering framework
#### Vendored Dependencies (`vendor/`)
Third-party C/C++ libraries are vendored locally and can be read from disk (these are not git submodules):
- `vendor/boringssl/` - BoringSSL (TLS/crypto)
- `vendor/brotli/` - Brotli compression
- `vendor/cares/` - c-ares (async DNS)
- `vendor/hdrhistogram/` - HdrHistogram (latency tracking)
- `vendor/highway/` - Google Highway (SIMD)
- `vendor/libarchive/` - libarchive (tar/zip)
- `vendor/libdeflate/` - libdeflate (fast deflate)
- `vendor/libuv/` - libuv (Windows event loop)
- `vendor/lolhtml/` - lol-html (HTML rewriter)
- `vendor/lshpack/` - ls-hpack (HTTP/2 HPACK)
- `vendor/mimalloc/` - mimalloc (memory allocator)
- `vendor/nodejs/` - Node.js headers (compatibility)
- `vendor/picohttpparser/` - PicoHTTPParser (HTTP parsing)
- `vendor/tinycc/` - TinyCC (FFI JIT compiler, fork: oven-sh/tinycc)
- `vendor/WebKit/` - WebKit/JavaScriptCore (JS engine)
- `vendor/zig/` - Zig compiler/stdlib
- `vendor/zlib/` - zlib (compression, cloudflare fork)
- `vendor/zstd/` - Zstandard (compression)
Build configuration for these is in `cmake/targets/Build*.cmake`.
### JavaScript Class Implementation (C++)
When implementing JavaScript classes in C++:

2
LATEST
View File

@@ -1 +1 @@
1.3.9
1.3.10

View File

@@ -43,7 +43,7 @@ bunx cowsay 'Hello, world!' # execute a package
## Install
Bun supports Linux (x64 & arm64), macOS (x64 & Apple Silicon) and Windows (x64).
Bun supports Linux (x64 & arm64), macOS (x64 & Apple Silicon) and Windows (x64 & arm64).
> **Linux users** — Kernel version 5.6 or higher is strongly recommended, but the minimum is 5.1.

View File

@@ -11,6 +11,7 @@
"@swc/core": "^1.2.133",
"benchmark": "^2.1.4",
"braces": "^3.0.2",
"cli-truncate": "^5.1.1",
"color": "^4.2.3",
"esbuild": "^0.14.12",
"eventemitter3": "^5.0.0",
@@ -25,6 +26,7 @@
"react-markdown": "^9.0.3",
"remark": "^15.0.1",
"remark-html": "^16.0.1",
"slice-ansi": "^8.0.0",
"string-width": "7.1.0",
"strip-ansi": "^7.1.0",
"tar": "^7.4.3",
@@ -222,6 +224,8 @@
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -394,6 +398,8 @@
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
@@ -598,6 +604,8 @@
"slash": ["slash@4.0.0", "", {}, "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew=="],
"slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
@@ -684,10 +692,16 @@
"avvio/fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"cli-truncate/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"cli-truncate/string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"fastify/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"is-fullwidth-code-point/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
@@ -698,8 +712,14 @@
"@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
"cli-truncate/string-width/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
"cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
}
}

View File

@@ -7,6 +7,7 @@
"@swc/core": "^1.2.133",
"benchmark": "^2.1.4",
"braces": "^3.0.2",
"cli-truncate": "^5.1.1",
"color": "^4.2.3",
"esbuild": "^0.14.12",
"eventemitter3": "^5.0.0",
@@ -21,6 +22,7 @@
"react-markdown": "^9.0.3",
"remark": "^15.0.1",
"remark-html": "^16.0.1",
"slice-ansi": "^8.0.0",
"string-width": "7.1.0",
"strip-ansi": "^7.1.0",
"tar": "^7.4.3",

View File

@@ -0,0 +1,108 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { bench, group, run } from "../runner.mjs";
// Benchmark 1: queueMicrotask throughput
// Tests the BunPerformMicrotaskJob handler path directly.
// The optimization removes the JS trampoline and uses callMicrotask.
group("queueMicrotask throughput", () => {
bench("queueMicrotask 1k", () => {
return new Promise(resolve => {
let remaining = 1000;
const tick = () => {
if (--remaining === 0) resolve();
else queueMicrotask(tick);
};
queueMicrotask(tick);
});
});
bench("queueMicrotask 10k", () => {
return new Promise(resolve => {
let remaining = 10000;
const tick = () => {
if (--remaining === 0) resolve();
else queueMicrotask(tick);
};
queueMicrotask(tick);
});
});
});
// Benchmark 2: Promise.resolve chain
// Each .then() queues a microtask via the promise machinery.
// Benefits from smaller QueuedTask (better cache locality in the Deque).
group("Promise.resolve chain", () => {
bench("Promise chain 1k", () => {
let p = Promise.resolve();
for (let i = 0; i < 1000; i++) {
p = p.then(() => {});
}
return p;
});
bench("Promise chain 10k", () => {
let p = Promise.resolve();
for (let i = 0; i < 10000; i++) {
p = p.then(() => {});
}
return p;
});
});
// Benchmark 3: Promise.all (many simultaneous resolves)
// All promises resolve at once, flooding the microtask queue.
// Smaller QueuedTask = less memory, better cache utilization.
group("Promise.all simultaneous", () => {
bench("Promise.all 1k", () => {
const promises = [];
for (let i = 0; i < 1000; i++) {
promises.push(Promise.resolve(i));
}
return Promise.all(promises);
});
bench("Promise.all 10k", () => {
const promises = [];
for (let i = 0; i < 10000; i++) {
promises.push(Promise.resolve(i));
}
return Promise.all(promises);
});
});
// Benchmark 4: queueMicrotask with AsyncLocalStorage
// Tests the inlined async context save/restore path.
// Previously went through performMicrotaskFunction JS trampoline.
group("queueMicrotask + AsyncLocalStorage", () => {
const als = new AsyncLocalStorage();
bench("ALS.run + queueMicrotask 1k", () => {
return als.run({ id: 1 }, () => {
return new Promise(resolve => {
let remaining = 1000;
const tick = () => {
als.getStore(); // force context read
if (--remaining === 0) resolve();
else queueMicrotask(tick);
};
queueMicrotask(tick);
});
});
});
});
// Benchmark 5: async/await (each await queues microtasks)
group("async/await chain", () => {
async function asyncChain(n) {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += await Promise.resolve(i);
}
return sum;
}
bench("async/await 1k", () => asyncChain(1000));
bench("async/await 10k", () => asyncChain(10000));
});
await run();

View File

@@ -0,0 +1,227 @@
// Compares Bun.sliceAnsi against npm slice-ansi and cli-truncate.
// Bun.sliceAnsi replaces both packages with one function:
// slice-ansi → Bun.sliceAnsi(s, start, end)
// cli-truncate → Bun.sliceAnsi(s, 0, max, ellipsis) / Bun.sliceAnsi(s, -max, undefined, ellipsis)
import npmCliTruncate from "cli-truncate";
import npmSliceAnsi from "slice-ansi";
import { bench, run, summary } from "../runner.mjs";
// Under Node (or any runtime without Bun.sliceAnsi), we only run the npm side
// of each pair — no point benching npm against itself. Under Bun with
// FORCE_NPM=1, we still run both to measure the npm impl cost under JSC.
const hasBunSliceAnsi = typeof Bun !== "undefined" && typeof Bun.sliceAnsi === "function";
const useBun = hasBunSliceAnsi && !process.env.FORCE_NPM;
// `maybeBench` registers the Bun-side bench only when useBun is true, so under
// Node each summary() collapses to a single npm entry with no false "1.0x" noise.
const maybeBench = useBun ? bench : () => {};
if (hasBunSliceAnsi) {
console.log(`[slice-ansi bench] ${useBun ? "Bun.sliceAnsi vs npm" : "npm-only (FORCE_NPM=1)"}\n`);
} else {
console.log(`[slice-ansi bench] Bun.sliceAnsi unavailable — running npm-only\n`);
}
// Wrappers so the call site stays monomorphic:
const bunSlice = useBun ? Bun.sliceAnsi : () => {};
const bunTruncEnd = useBun ? (s, n, e) => Bun.sliceAnsi(s, 0, n, e) : () => {};
const bunTruncStart = useBun ? (s, n, e) => Bun.sliceAnsi(s, -n, undefined, e) : () => {};
// ============================================================================
// Fixtures — cover the tiers of Bun.sliceAnsi's dispatch:
// 1. Pure ASCII → SIMD fast path (direct substring)
// 2. ASCII + ANSI → single-pass streaming emit with bulk-ASCII runs
// 3. CJK / emoji → per-char width, inline grapheme tracking
// 4. ZWJ emoji / combining marks → clustering path
// ============================================================================
const red = s => `\x1b[31m${s}\x1b[39m`;
const green = s => `\x1b[32m${s}\x1b[39m`;
const bold = s => `\x1b[1m${s}\x1b[22m`;
const truecolor = (r, g, b, s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
const link = (url, s) => `\x1b]8;;${url}\x07${s}\x1b]8;;\x07`;
// Tier 1: pure ASCII (SIMD fast path)
const asciiShort = "The quick brown fox jumps over the lazy dog.";
const asciiLong = "The quick brown fox jumps over the lazy dog. ".repeat(100);
// Tier 2: ASCII + ANSI codes (streaming + bulk-ASCII emit)
const ansiShort = `The ${red("quick")} ${green("brown")} fox ${bold("jumps")} over the lazy dog.`;
const ansiMedium =
`The ${red("quick brown fox")} jumps ${green("over the lazy dog")} and ${bold("runs away")}. `.repeat(10);
const ansiLong = `The ${red("quick brown fox")} jumps ${green("over the lazy dog")} and ${bold("runs away")}. `.repeat(
100,
);
// Dense ANSI: SGR between every few chars (stresses pending buffer)
const ansiDense = `${red("ab")}${green("cd")}${bold("ef")}${truecolor(255, 128, 64, "gh")}`.repeat(50);
// Tier 3: CJK (width 2, no clustering)
const cjk = "日本語のテキストをスライスするテストです。全角文字は幅2としてカウントされます。".repeat(10);
const cjkAnsi = red("日本語のテキストを") + green("スライスするテスト") + "です。".repeat(10);
// Tier 4: grapheme clustering
const emoji = "Hello 👋 World 🌍! Test 🧪 emoji 😀 slicing 📦!".repeat(10);
// ZWJ family emoji — worst case for clustering (4 codepoints + 3 ZWJ per cluster)
const zwj = "Family: 👨‍👩‍👧‍👦 and 👩‍💻 technologist! ".repeat(20);
// Skin tone modifiers
const skinTone = "Wave 👋🏽 handshake 🤝🏻 thumbs 👍🏿 ok 👌🏼!".repeat(20);
// Combining marks (café → c-a-f-e + ́)
const combining = "cafe\u0301 re\u0301sume\u0301 na\u0131\u0308ve pi\u00f1ata ".repeat(30);
// Hyperlinks (OSC 8)
const hyperlinks = link("https://bun.sh", "Check out Bun, it's fast! ").repeat(20);
// ============================================================================
// Slice benchmarks (vs slice-ansi)
// ============================================================================
// Tier 1: pure ASCII — Bun's SIMD fast path should be near-memcpy.
summary(() => {
bench("ascii-short [0,20) — npm slice-ansi", () => npmSliceAnsi(asciiShort, 0, 20));
maybeBench("ascii-short [0,20) — Bun.sliceAnsi ", () => bunSlice(asciiShort, 0, 20));
});
summary(() => {
bench("ascii-long [0,1000) — npm slice-ansi", () => npmSliceAnsi(asciiLong, 0, 1000));
maybeBench("ascii-long [0,1000) — Bun.sliceAnsi ", () => bunSlice(asciiLong, 0, 1000));
});
// Zero-copy case: slice covers whole string. Bun returns the input JSString.
summary(() => {
bench("ascii-long no-op (whole string) — npm slice-ansi", () => npmSliceAnsi(asciiLong, 0));
maybeBench("ascii-long no-op (whole string) — Bun.sliceAnsi ", () => bunSlice(asciiLong, 0));
});
// Tier 2: ANSI — Bun's bulk-ASCII-run emit vs npm's per-token walk.
summary(() => {
bench("ansi-short [0,30) — npm slice-ansi", () => npmSliceAnsi(ansiShort, 0, 30));
maybeBench("ansi-short [0,30) — Bun.sliceAnsi ", () => bunSlice(ansiShort, 0, 30));
});
summary(() => {
bench("ansi-medium [10,200) — npm slice-ansi", () => npmSliceAnsi(ansiMedium, 10, 200));
maybeBench("ansi-medium [10,200) — Bun.sliceAnsi ", () => bunSlice(ansiMedium, 10, 200));
});
summary(() => {
bench("ansi-long [0,2000) — npm slice-ansi", () => npmSliceAnsi(ansiLong, 0, 2000));
maybeBench("ansi-long [0,2000) — Bun.sliceAnsi ", () => bunSlice(ansiLong, 0, 2000));
});
summary(() => {
bench("ansi-dense (SGR every 2 chars) — npm slice-ansi", () => npmSliceAnsi(ansiDense, 0, 100));
maybeBench("ansi-dense (SGR every 2 chars) — Bun.sliceAnsi ", () => bunSlice(ansiDense, 0, 100));
});
// Tier 3: CJK (width 2, no clustering)
summary(() => {
bench("cjk [0,100) — npm slice-ansi", () => npmSliceAnsi(cjk, 0, 100));
maybeBench("cjk [0,100) — Bun.sliceAnsi ", () => bunSlice(cjk, 0, 100));
});
summary(() => {
bench("cjk+ansi [0,100) — npm slice-ansi", () => npmSliceAnsi(cjkAnsi, 0, 100));
maybeBench("cjk+ansi [0,100) — Bun.sliceAnsi ", () => bunSlice(cjkAnsi, 0, 100));
});
// Tier 4: grapheme clustering
summary(() => {
bench("emoji [0,100) — npm slice-ansi", () => npmSliceAnsi(emoji, 0, 100));
maybeBench("emoji [0,100) — Bun.sliceAnsi ", () => bunSlice(emoji, 0, 100));
});
summary(() => {
bench("zwj-family [0,100) — npm slice-ansi", () => npmSliceAnsi(zwj, 0, 100));
maybeBench("zwj-family [0,100) — Bun.sliceAnsi ", () => bunSlice(zwj, 0, 100));
});
summary(() => {
bench("skin-tone [0,100) — npm slice-ansi", () => npmSliceAnsi(skinTone, 0, 100));
maybeBench("skin-tone [0,100) — Bun.sliceAnsi ", () => bunSlice(skinTone, 0, 100));
});
summary(() => {
bench("combining-marks [0,100) — npm slice-ansi", () => npmSliceAnsi(combining, 0, 100));
maybeBench("combining-marks [0,100) — Bun.sliceAnsi ", () => bunSlice(combining, 0, 100));
});
// OSC 8 hyperlinks
summary(() => {
bench("hyperlinks [0,100) — npm slice-ansi", () => npmSliceAnsi(hyperlinks, 0, 100));
maybeBench("hyperlinks [0,100) — Bun.sliceAnsi ", () => bunSlice(hyperlinks, 0, 100));
});
// ============================================================================
// Truncate benchmarks (vs cli-truncate)
// ============================================================================
// cli-truncate internally calls slice-ansi, so Bun should win by a similar
// margin. The interesting comparison is the lazy-cutEnd speculative zone vs
// cli-truncate's eager stringWidth pre-pass.
summary(() => {
bench("truncate-end ascii-short — npm cli-truncate", () => npmCliTruncate(asciiShort, 20));
maybeBench("truncate-end ascii-short — Bun.sliceAnsi ", () => bunTruncEnd(asciiShort, 20, "…"));
});
summary(() => {
bench("truncate-end ansi-long — npm cli-truncate", () => npmCliTruncate(ansiLong, 200));
maybeBench("truncate-end ansi-long — Bun.sliceAnsi ", () => bunTruncEnd(ansiLong, 200, "…"));
});
summary(() => {
bench("truncate-start ansi-long — npm cli-truncate", () => npmCliTruncate(ansiLong, 200, { position: "start" }));
// Negative index → Bun's 2-pass path (computeTotalWidth pre-pass).
maybeBench("truncate-start ansi-long — Bun.sliceAnsi ", () => bunTruncStart(ansiLong, 200, "…"));
});
summary(() => {
bench("truncate-end emoji — npm cli-truncate", () => npmCliTruncate(emoji, 50));
maybeBench("truncate-end emoji — Bun.sliceAnsi ", () => bunTruncEnd(emoji, 50, "…"));
});
// No-cut case: string already fits. cli-truncate calls stringWidth + early returns.
// Bun's lazy cutEnd detection means it walks once but detects no cut at EOF.
summary(() => {
bench("truncate no-cut (fits) — npm cli-truncate", () => npmCliTruncate(asciiShort, 100));
maybeBench("truncate no-cut (fits) — Bun.sliceAnsi ", () => bunTruncEnd(asciiShort, 100, "…"));
});
// ============================================================================
// Real-world: ink-style viewport clipping (hot path for terminal UI rendering)
// ============================================================================
// Simulates ink's output.ts sliceAnsi(line, from, to) call in the render loop.
// Each line is colored and gets clipped to the viewport width.
const logLine = `${bold("[2024-01-15 12:34:56]")} ${red("ERROR")} Connection to ${link("https://api.example.com", "api.example.com")} timed out after 30s (attempt 3/5)`;
summary(() => {
bench("ink-clip (80-col viewport) — npm slice-ansi", () => npmSliceAnsi(logLine, 0, 80));
maybeBench("ink-clip (80-col viewport) — Bun.sliceAnsi ", () => bunSlice(logLine, 0, 80));
});
// ============================================================================
// Correctness spot-check (fail fast if results diverge on simple cases)
// ============================================================================
if (useBun) {
const checks = [
[asciiShort, 0, 20],
[ansiShort, 5, 30],
[cjk, 0, 50],
];
for (const [s, a, b] of checks) {
// slice-ansi and Bun.sliceAnsi may differ in exact ANSI byte ordering for
// close codes, but stripped visible content should match.
const npm = npmSliceAnsi(s, a, b).replace(/\x1b\[[\d;]*m/g, "");
const bun = bunSlice(s, a, b).replace(/\x1b\[[\d;]*m/g, "");
if (npm !== bun) {
throw new Error(
`Correctness check failed for [${a},${b}): npm=${JSON.stringify(npm)} bun=${JSON.stringify(bun)}`,
);
}
}
}
await run();

View File

@@ -434,7 +434,7 @@ function(register_command)
endif()
# SKIP_CODEGEN: Skip commands that use BUN_EXECUTABLE if all outputs exist
# This is used for Windows ARM64 builds where x64 bun crashes under emulation
# Useful for bootstrapping new platforms where bun may not be available
if(SKIP_CODEGEN AND CMD_EXECUTABLE STREQUAL "${BUN_EXECUTABLE}")
set(ALL_OUTPUTS_EXIST TRUE)
foreach(output ${CMD_OUTPUTS})
@@ -456,7 +456,7 @@ function(register_command)
endif()
return()
else()
message(FATAL_ERROR "SKIP_CODEGEN: Cannot skip ${CMD_TARGET} - missing outputs. Run codegen on x64 first.")
message(FATAL_ERROR "SKIP_CODEGEN: Cannot skip ${CMD_TARGET} - missing outputs.")
endif()
endif()
@@ -831,13 +831,6 @@ function(register_cmake_command)
list(APPEND MAKE_EFFECTIVE_ARGS "-DCMAKE_${flag}=${MAKE_${flag}}")
endforeach()
# Workaround for CMake 4.1.0 bug: Force correct machine type for Windows ARM64
# Use toolchain file and set CMP0197 policy to prevent duplicate /machine: flags
if(WIN32 AND CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64|AARCH64")
list(APPEND MAKE_EFFECTIVE_ARGS "-DCMAKE_TOOLCHAIN_FILE=${CWD}/cmake/toolchains/windows-aarch64.cmake")
list(APPEND MAKE_EFFECTIVE_ARGS "-DCMAKE_POLICY_DEFAULT_CMP0197=NEW")
list(APPEND MAKE_EFFECTIVE_ARGS "-DCMAKE_PROJECT_INCLUDE=${CWD}/cmake/arm64-static-lib-fix.cmake")
endif()
if(DEFINED FRESH)
list(APPEND MAKE_EFFECTIVE_ARGS --fresh)

View File

@@ -4,7 +4,7 @@ endif()
optionx(BUN_LINK_ONLY BOOL "If only the linking step should be built" DEFAULT OFF)
optionx(BUN_CPP_ONLY BOOL "If only the C++ part of Bun should be built" DEFAULT OFF)
optionx(SKIP_CODEGEN BOOL "Skip JavaScript codegen (for Windows ARM64 debug)" DEFAULT OFF)
optionx(SKIP_CODEGEN BOOL "Skip JavaScript codegen (useful for bootstrapping new platforms)" DEFAULT OFF)
optionx(BUILDKITE BOOL "If Buildkite is enabled" DEFAULT OFF)
optionx(GITHUB_ACTIONS BOOL "If GitHub Actions is enabled" DEFAULT OFF)
@@ -58,35 +58,6 @@ else()
message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}")
endif()
# CMake 4.0+ policy CMP0197 controls how MSVC machine type flags are handled
# Setting to NEW prevents duplicate /machine: flags being added to linker commands
if(WIN32 AND ARCH STREQUAL "aarch64")
set(CMAKE_POLICY_DEFAULT_CMP0197 NEW)
set(CMAKE_MSVC_CMP0197 NEW)
# Set linker flags for exe/shared linking
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /machine:ARM64")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /machine:ARM64")
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /machine:ARM64")
set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_STATIC_LINKER_FLAGS} /machine:ARM64")
endif()
# Windows Code Signing Option
if(WIN32)
optionx(ENABLE_WINDOWS_CODESIGNING BOOL "Enable Windows code signing with DigiCert KeyLocker" DEFAULT OFF)
if(ENABLE_WINDOWS_CODESIGNING)
message(STATUS "Windows code signing: ENABLED")
# Check for required environment variables
if(NOT DEFINED ENV{SM_API_KEY})
message(WARNING "SM_API_KEY not set - code signing may fail")
endif()
if(NOT DEFINED ENV{SM_CLIENT_CERT_FILE})
message(WARNING "SM_CLIENT_CERT_FILE not set - code signing may fail")
endif()
endif()
endif()
if(LINUX)
if(EXISTS "/etc/alpine-release")
set(DEFAULT_ABI "musl")

View File

@@ -1,8 +0,0 @@
# This file is included after project() via CMAKE_PROJECT_INCLUDE
# It fixes the static library creation command to use ARM64 machine type
if(WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL \"aarch64\")
# Override the static library creation commands to avoid spurious /machine:x64 flags
set(CMAKE_C_CREATE_STATIC_LIBRARY \"<CMAKE_AR> /nologo /machine:ARM64 /out:<TARGET> <OBJECTS>\" CACHE STRING \"\" FORCE)
set(CMAKE_CXX_CREATE_STATIC_LIBRARY \"<CMAKE_AR> /nologo /machine:ARM64 /out:<TARGET> <OBJECTS>\" CACHE STRING \"\" FORCE)
endif()

View File

@@ -21,12 +21,7 @@ if(NOT DEFINED CMAKE_HOST_SYSTEM_PROCESSOR)
endif()
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "arm64|ARM64|aarch64|AARCH64")
# Windows ARM64 can run x86_64 via emulation, and no native ARM64 Zig build exists yet
if(CMAKE_HOST_WIN32)
set(ZIG_ARCH "x86_64")
else()
set(ZIG_ARCH "aarch64")
endif()
set(ZIG_ARCH "aarch64")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "amd64|AMD64|x86_64|X86_64|x64|X64")
set(ZIG_ARCH "x86_64")
else()

View File

@@ -1,34 +0,0 @@
@echo off
setlocal enabledelayedexpansion
REM Wrapper for llvm-lib that strips conflicting /machine:x64 flag for ARM64 builds
REM This is a workaround for CMake 4.1.0 bug
REM Find llvm-lib.exe - check LLVM_LIB env var, then PATH, then known locations
if defined LLVM_LIB (
set "LLVM_LIB_EXE=!LLVM_LIB!"
) else (
where llvm-lib.exe >nul 2>&1
if !ERRORLEVEL! equ 0 (
for /f "delims=" %%i in ('where llvm-lib.exe') do set "LLVM_LIB_EXE=%%i"
) else if exist "C:\Program Files\LLVM\bin\llvm-lib.exe" (
set "LLVM_LIB_EXE=C:\Program Files\LLVM\bin\llvm-lib.exe"
) else (
echo Error: Cannot find llvm-lib.exe. Set LLVM_LIB environment variable or add LLVM to PATH.
exit /b 1
)
)
set "ARGS="
for %%a in (%*) do (
set "ARG=%%a"
if /i "!ARG!"=="/machine:x64" (
REM Skip this argument
) else (
set "ARGS=!ARGS! %%a"
)
)
"!LLVM_LIB_EXE!" %ARGS%
exit /b %ERRORLEVEL%

View File

@@ -1,18 +0,0 @@
# Wrapper for llvm-lib that strips conflicting /machine:x64 flag for ARM64 builds
# This is a workaround for CMake 4.1.0 bug where both /machine:ARM64 and /machine:x64 are added
# Find llvm-lib.exe - check LLVM_LIB env var, then PATH, then known locations
if ($env:LLVM_LIB) {
$llvmLib = $env:LLVM_LIB
} elseif (Get-Command llvm-lib.exe -ErrorAction SilentlyContinue) {
$llvmLib = (Get-Command llvm-lib.exe).Source
} elseif (Test-Path "C:\Program Files\LLVM\bin\llvm-lib.exe") {
$llvmLib = "C:\Program Files\LLVM\bin\llvm-lib.exe"
} else {
Write-Error "Cannot find llvm-lib.exe. Set LLVM_LIB environment variable or add LLVM to PATH."
exit 1
}
$filteredArgs = $args | Where-Object { $_ -ne "/machine:x64" }
& $llvmLib @filteredArgs
exit $LASTEXITCODE

View File

@@ -1,34 +0,0 @@
@echo off
setlocal enabledelayedexpansion
REM Wrapper for llvm-lib that strips conflicting /machine:x64 flag for ARM64 builds
REM This is a workaround for CMake 4.1.0 bug
REM Find llvm-lib.exe - check LLVM_LIB env var, then PATH, then known locations
if defined LLVM_LIB (
set "LLVM_LIB_EXE=!LLVM_LIB!"
) else (
where llvm-lib.exe >nul 2>&1
if !ERRORLEVEL! equ 0 (
for /f "delims=" %%i in ('where llvm-lib.exe') do set "LLVM_LIB_EXE=%%i"
) else if exist "C:\Program Files\LLVM\bin\llvm-lib.exe" (
set "LLVM_LIB_EXE=C:\Program Files\LLVM\bin\llvm-lib.exe"
) else (
echo Error: Cannot find llvm-lib.exe. Set LLVM_LIB environment variable or add LLVM to PATH.
exit /b 1
)
)
set NEWARGS=
for %%a in (%*) do (
set "ARG=%%a"
if /i "!ARG!"=="/machine:x64" (
REM Skip /machine:x64 argument
) else (
set "NEWARGS=!NEWARGS! %%a"
)
)
"!LLVM_LIB_EXE!" %NEWARGS%
exit /b %ERRORLEVEL%

View File

@@ -7,13 +7,6 @@ register_repository(
4f4f5ef8ebc6e23cbf393428f0ab1b526773f7ac
)
set(BORINGSSL_CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF)
# Disable ASM on Windows ARM64 to avoid mixing non-ARM object files into ARM64 libs
if(WIN32 AND CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64|AARCH64")
list(APPEND BORINGSSL_CMAKE_ARGS -DOPENSSL_NO_ASM=1)
endif()
register_cmake_command(
TARGET
boringssl
@@ -22,7 +15,7 @@ register_cmake_command(
ssl
decrepit
ARGS
${BORINGSSL_CMAKE_ARGS}
-DBUILD_SHARED_LIBS=OFF
INCLUDES
include
)

View File

@@ -341,6 +341,7 @@ register_command(
SOURCES
${BUN_JAVASCRIPT_CODEGEN_SOURCES}
${BUN_CXX_SOURCES}
${ESBUILD_EXECUTABLE}
OUTPUTS
${BUN_CPP_OUTPUTS}
)
@@ -362,7 +363,7 @@ register_command(
)
if(SKIP_CODEGEN)
# Skip JavaScript codegen - useful for Windows ARM64 debug builds where bun crashes
# Skip JavaScript codegen - useful for bootstrapping new platforms
message(STATUS "SKIP_CODEGEN is ON - skipping bun-js-modules codegen")
foreach(output ${BUN_JAVASCRIPT_OUTPUTS})
if(NOT EXISTS ${output})
@@ -546,6 +547,7 @@ set(BUN_OBJECT_LUT_SOURCES
${CWD}/src/bun.js/bindings/ProcessBindingHTTPParser.cpp
${CWD}/src/bun.js/modules/NodeModuleModule.cpp
${CODEGEN_PATH}/ZigGeneratedClasses.lut.txt
${CWD}/src/bun.js/bindings/webcore/JSEvent.cpp
)
set(BUN_OBJECT_LUT_OUTPUTS
@@ -560,6 +562,7 @@ set(BUN_OBJECT_LUT_OUTPUTS
${CODEGEN_PATH}/ProcessBindingHTTPParser.lut.h
${CODEGEN_PATH}/NodeModuleModule.lut.h
${CODEGEN_PATH}/ZigGeneratedClasses.lut.h
${CODEGEN_PATH}/JSEvent.lut.h
)
macro(WEBKIT_ADD_SOURCE_DEPENDENCIES _source _deps)
@@ -593,6 +596,7 @@ foreach(i RANGE 0 ${BUN_OBJECT_LUT_SOURCES_MAX_INDEX})
"Generating ${filename}.lut.h"
DEPENDS
${BUN_OBJECT_LUT_SOURCE}
${CWD}/src/codegen/create_hash_table
COMMAND
${BUN_EXECUTABLE}
${BUN_FLAGS}
@@ -602,6 +606,7 @@ foreach(i RANGE 0 ${BUN_OBJECT_LUT_SOURCES_MAX_INDEX})
${BUN_OBJECT_LUT_OUTPUT}
SOURCES
${BUN_OBJECT_LUT_SCRIPT}
${CWD}/src/codegen/create_hash_table
${BUN_OBJECT_LUT_SOURCE}
OUTPUTS
${BUN_OBJECT_LUT_OUTPUT}
@@ -680,8 +685,7 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|ARM|arm64|ARM64|aarch64|AARCH64")
if(APPLE)
set(ZIG_CPU "apple_m1")
elseif(WIN32)
# Windows ARM64: use a specific CPU with NEON support
# Zig running under x64 emulation would detect wrong CPU with "native"
# Windows ARM64: use a specific CPU target for consistent builds
set(ZIG_CPU "cortex_a76")
else()
set(ZIG_CPU "native")
@@ -1405,47 +1409,8 @@ if(NOT BUN_CPP_ONLY)
${BUILD_PATH}/${bunStripExe}
)
# Then sign both executables on Windows
if(WIN32 AND ENABLE_WINDOWS_CODESIGNING)
set(SIGN_SCRIPT "${CMAKE_SOURCE_DIR}/.buildkite/scripts/sign-windows.ps1")
# Verify signing script exists
if(NOT EXISTS "${SIGN_SCRIPT}")
message(FATAL_ERROR "Windows signing script not found: ${SIGN_SCRIPT}")
endif()
# Use PowerShell for Windows code signing (native Windows, no path issues)
find_program(POWERSHELL_EXECUTABLE
NAMES pwsh.exe powershell.exe
PATHS
"C:/Program Files/PowerShell/7"
"C:/Program Files (x86)/PowerShell/7"
"C:/Windows/System32/WindowsPowerShell/v1.0"
DOC "Path to PowerShell executable"
)
if(NOT POWERSHELL_EXECUTABLE)
set(POWERSHELL_EXECUTABLE "powershell.exe")
endif()
message(STATUS "Using PowerShell executable: ${POWERSHELL_EXECUTABLE}")
# Sign both bun-profile.exe and bun.exe after stripping
register_command(
TARGET
${bun}
TARGET_PHASE
POST_BUILD
COMMENT
"Code signing bun-profile.exe and bun.exe with DigiCert KeyLocker"
COMMAND
"${POWERSHELL_EXECUTABLE}" "-NoProfile" "-ExecutionPolicy" "Bypass" "-File" "${SIGN_SCRIPT}" "-BunProfileExe" "${BUILD_PATH}/${bunExe}" "-BunExe" "${BUILD_PATH}/${bunStripExe}"
CWD
${CMAKE_SOURCE_DIR}
SOURCES
${BUILD_PATH}/${bunStripExe}
)
endif()
# Windows code signing happens in a dedicated Buildkite step after all
# Windows builds complete. See .buildkite/scripts/sign-windows-artifacts.ps1
endif()
# somehow on some Linux systems we need to disable ASLR for ASAN-instrumented binaries to run
@@ -1457,8 +1422,6 @@ if(NOT BUN_CPP_ONLY)
# ==856230==See https://github.com/google/sanitizers/issues/856 for possible workarounds.
# the linked issue refers to very old kernels but this still happens to us on modern ones.
# disabling ASLR to run the binary works around it
# Skip post-build test/features when cross-compiling (can't run the target binary on the host)
if(NOT CMAKE_CROSSCOMPILING)
set(TEST_BUN_COMMAND_BASE ${BUILD_PATH}/${bunExe} --revision)
set(TEST_BUN_COMMAND_ENV_WRAP
${CMAKE_COMMAND} -E env BUN_DEBUG_QUIET_LOGS=1)
@@ -1507,7 +1470,6 @@ if(NOT BUN_CPP_ONLY)
${BUILD_PATH}/features.json
)
endif()
endif() # NOT CMAKE_CROSSCOMPILING
if(CMAKE_HOST_APPLE AND bunStrip)
register_command(
@@ -1554,10 +1516,7 @@ if(NOT BUN_CPP_ONLY)
string(REPLACE bun ${bunTriplet} bunPath ${bun})
endif()
set(bunFiles ${bunExe})
if(NOT CMAKE_CROSSCOMPILING)
list(APPEND bunFiles features.json)
endif()
set(bunFiles ${bunExe} features.json)
if(WIN32)
list(APPEND bunFiles ${bun}.pdb)
elseif(APPLE)

View File

@@ -26,7 +26,7 @@ if(RELEASE)
list(APPEND LOLHTML_BUILD_ARGS --release)
endif()
# Cross-compilation: tell cargo to target ARM64
# Explicitly tell cargo to target ARM64 on Windows ARM64
if(WIN32 AND CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64|AARCH64")
list(APPEND LOLHTML_BUILD_ARGS --target aarch64-pc-windows-msvc)
set(LOLHTML_LIBRARY ${LOLHTML_BUILD_PATH}/aarch64-pc-windows-msvc/${LOLHTML_BUILD_TYPE}/${CMAKE_STATIC_LIBRARY_PREFIX}lolhtml${CMAKE_STATIC_LIBRARY_SUFFIX})
@@ -57,11 +57,11 @@ if(WIN32)
if(MSVC_VERSIONS)
list(GET MSVC_VERSIONS -1 MSVC_LATEST) # Get the latest version
if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64")
# Use Hostx64/arm64 for cross-compilation from x64, fall back to native
if(EXISTS "${MSVC_LATEST}/bin/Hostx64/arm64/link.exe")
set(MSVC_LINK_PATH "${MSVC_LATEST}/bin/Hostx64/arm64/link.exe")
else()
# Prefer native HostARM64, fall back to Hostx64/arm64
if(EXISTS "${MSVC_LATEST}/bin/HostARM64/arm64/link.exe")
set(MSVC_LINK_PATH "${MSVC_LATEST}/bin/HostARM64/arm64/link.exe")
else()
set(MSVC_LINK_PATH "${MSVC_LATEST}/bin/Hostx64/arm64/link.exe")
endif()
set(CARGO_LINKER_VAR "CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER")
set(MSVC_LIB_ARCH "arm64")

View File

@@ -13,6 +13,11 @@ else()
set(LSHPACK_INCLUDES .)
endif()
# Suppress all warnings from vendored lshpack on Windows (clang-cl)
if(WIN32)
set(LSHPACK_CMAKE_ARGS "-DCMAKE_C_FLAGS=-w")
endif()
register_cmake_command(
TARGET
lshpack
@@ -28,6 +33,7 @@ register_cmake_command(
# _lshpack_enc_get_static_name in libls-hpack.a(lshpack.c.o)
# _update_hash in libls-hpack.a(lshpack.c.o)
-DCMAKE_BUILD_TYPE=Release
${LSHPACK_CMAKE_ARGS}
INCLUDES
${LSHPACK_INCLUDES}
)

View File

@@ -79,12 +79,22 @@ endif()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64|AARCH64" AND NOT APPLE)
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_NO_OPT_ARCH=ON)
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_OPT_SIMD=ON)
list(APPEND MIMALLOC_CMAKE_ARGS "-DCMAKE_C_FLAGS=-moutline-atomics")
if(NOT WIN32)
list(APPEND MIMALLOC_CMAKE_ARGS "-DCMAKE_C_FLAGS=-moutline-atomics")
endif()
elseif(NOT ENABLE_BASELINE)
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_OPT_ARCH=ON)
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_OPT_SIMD=ON)
endif()
# Suppress all warnings from mimalloc on Windows — it's vendored C code compiled
# as C++ (MI_USE_CXX=ON) which triggers many clang-cl warnings (-Wold-style-cast,
# -Wzero-as-null-pointer-constant, -Wc++98-compat-pedantic, etc.)
if(WIN32)
list(APPEND MIMALLOC_CMAKE_ARGS "-DCMAKE_C_FLAGS=-w")
list(APPEND MIMALLOC_CMAKE_ARGS "-DCMAKE_CXX_FLAGS=-w")
endif()
if(WIN32)
if(DEBUG)
set(MIMALLOC_LIBRARY mimalloc-static-debug)

View File

@@ -7,9 +7,16 @@ register_repository(
12882eee073cfe5c7621bcfadf679e1372d4537b
)
# Suppress all warnings from vendored tinycc on Windows (clang-cl)
if(WIN32)
set(TINYCC_CMAKE_ARGS "-DCMAKE_C_FLAGS=-w")
endif()
register_cmake_command(
TARGET
tinycc
ARGS
${TINYCC_CMAKE_ARGS}
LIBRARIES
tcc
)

View File

@@ -7,6 +7,32 @@ register_repository(
886098f3f339617b4243b286f5ed364b9989e245
)
# cloudflare/zlib hardcodes STATIC_LIBRARY_FLAGS "/machine:x64" for 64-bit MSVC,
# which conflicts with ARM64 object files. Patch it after clone to use the correct
# machine type based on CMAKE_SYSTEM_PROCESSOR.
if(WIN32 AND CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64|AARCH64")
set(ZLIB_PATCH_SCRIPT "${BUILD_PATH}/zlib-arm64-patch.cmake")
file(WRITE ${ZLIB_PATCH_SCRIPT} "
file(READ \"\${ZLIB_CMAKELISTS}\" content)
string(REPLACE \"/machine:x64\" \"/machine:ARM64\" content \"\${content}\")
file(WRITE \"\${ZLIB_CMAKELISTS}\" \"\${content}\")
file(TOUCH \"\${ZLIB_PATCH_MARKER}\")
")
register_command(
COMMENT "Patching zlib for ARM64"
TARGET patch-zlib
COMMAND ${CMAKE_COMMAND}
-DZLIB_CMAKELISTS=${VENDOR_PATH}/zlib/CMakeLists.txt
-DZLIB_PATCH_MARKER=${VENDOR_PATH}/zlib/.arm64-patched
-P ${ZLIB_PATCH_SCRIPT}
SOURCES ${VENDOR_PATH}/zlib/.ref
OUTPUTS ${VENDOR_PATH}/zlib/.arm64-patched
)
if(TARGET clone-zlib)
add_dependencies(patch-zlib clone-zlib)
endif()
endif()
# https://gitlab.kitware.com/cmake/cmake/-/issues/25755
if(APPLE)
set(ZLIB_CMAKE_C_FLAGS "-fno-define-target-os-macros")
@@ -38,3 +64,8 @@ register_cmake_command(
INCLUDES
.
)
# Ensure zlib is patched before configure
if(TARGET patch-zlib)
add_dependencies(configure-zlib patch-zlib)
endif()

View File

@@ -4,34 +4,3 @@ set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER_WORKS ON)
set(CMAKE_CXX_COMPILER_WORKS ON)
set(CMAKE_CROSSCOMPILING ON)
# The rest only applies when building on Windows (C++ and link steps).
# The Zig step runs on Linux and only needs CMAKE_SYSTEM_NAME/PROCESSOR above.
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
# Ensure clang/clang-cl targets Windows ARM64 (otherwise ARM64-specific flags like
# -march=armv8-a are rejected as x86-only).
set(CMAKE_C_COMPILER_TARGET aarch64-pc-windows-msvc CACHE STRING "" FORCE)
set(CMAKE_CXX_COMPILER_TARGET aarch64-pc-windows-msvc CACHE STRING "" FORCE)
# ARM64 has lock-free atomics (highway's FindAtomics check can't run ARM64 test binary on x64)
set(ATOMICS_LOCK_FREE_INSTRUCTIONS TRUE CACHE BOOL "" FORCE)
set(HAVE_CXX_ATOMICS_WITHOUT_LIB TRUE CACHE BOOL "" FORCE)
set(HAVE_CXX_ATOMICS64_WITHOUT_LIB TRUE CACHE BOOL "" FORCE)
# Force ARM64 architecture ID - this is what CMake uses to determine /machine: flag
set(MSVC_C_ARCHITECTURE_ID ARM64 CACHE INTERNAL "")
set(MSVC_CXX_ARCHITECTURE_ID ARM64 CACHE INTERNAL "")
# CMake 4.0+ policy CMP0197 controls how MSVC machine type flags are handled
set(CMAKE_POLICY_DEFAULT_CMP0197 NEW CACHE INTERNAL "")
# Clear any inherited static linker flags that might have wrong machine types
set(CMAKE_STATIC_LINKER_FLAGS "" CACHE STRING "" FORCE)
# Use wrapper script for llvm-lib that strips /machine:x64 flags
# This works around CMake 4.1.0 bug where both ARM64 and x64 machine flags are added
get_filename_component(_TOOLCHAIN_DIR "${CMAKE_CURRENT_LIST_DIR}" DIRECTORY)
set(CMAKE_AR "${_TOOLCHAIN_DIR}/scripts/llvm-lib-wrapper.bat" CACHE FILEPATH "" FORCE)
endif()

View File

@@ -17,13 +17,7 @@ if (NOT CI)
set(BUN_EXECUTABLE ${BUN_EXECUTABLE} CACHE FILEPATH "Bun executable" FORCE)
endif()
# On Windows ARM64, we need to add --smol flag to avoid crashes when running
# x64 bun under WoW64 emulation
if(WIN32 AND ARCH STREQUAL "aarch64")
set(BUN_FLAGS "--smol" CACHE STRING "Extra flags for bun executable")
else()
set(BUN_FLAGS "" CACHE STRING "Extra flags for bun executable")
endif()
set(BUN_FLAGS "" CACHE STRING "Extra flags for bun executable")
# If this is not set, some advanced features are not checked.
# https://github.com/oven-sh/bun/blob/cd7f6a1589db7f1e39dc4e3f4a17234afbe7826c/src/bun.js/javascript.zig#L1069-L1072

View File

@@ -51,7 +51,7 @@ if(APPLE)
endif()
if(WIN32)
# Prefer standalone LLVM over VS-bundled (standalone supports cross-compilation)
# Prefer standalone LLVM over VS-bundled
list(APPEND LLVM_PATHS "C:/Program Files/LLVM/bin")
endif()

View File

@@ -6,11 +6,9 @@ option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of down
option(WEBKIT_BUILD_TYPE "The build type for local WebKit (defaults to CMAKE_BUILD_TYPE)")
if(NOT WEBKIT_VERSION)
set(WEBKIT_VERSION 8af7958ff0e2a4787569edf64641a1ae7cfe074a)
set(WEBKIT_VERSION 4a6a32c32c11ffb9f5a94c310b10f50130bfe6de)
endif()
# Use preview build URL for Windows ARM64 until the fix is merged to main
set(WEBKIT_PREVIEW_PR 140)
string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX)
string(SUBSTRING ${WEBKIT_VERSION} 0 8 WEBKIT_VERSION_SHORT)
@@ -95,6 +93,9 @@ if(WEBKIT_LOCAL)
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
-DENABLE_REMOTE_INSPECTOR=ON
-DENABLE_MEDIA_SOURCE=OFF
-DENABLE_MEDIA_STREAM=OFF
-DENABLE_WEB_RTC=OFF
)
if(WIN32)

View File

@@ -20,7 +20,7 @@ else()
unsupported(CMAKE_SYSTEM_NAME)
endif()
set(ZIG_COMMIT "c1423ff3fc7064635773a4a4616c5bf986eb00fe")
set(ZIG_COMMIT "c031cbebf5b063210473ff5204a24ebfb2492c72")
optionx(ZIG_TARGET STRING "The zig target to use" DEFAULT ${DEFAULT_ZIG_TARGET})
if(CMAKE_BUILD_TYPE STREQUAL "Release")
@@ -55,7 +55,14 @@ optionx(ZIG_OBJECT_FORMAT "obj|bc" "Output file format for Zig object files" DEF
optionx(ZIG_LOCAL_CACHE_DIR FILEPATH "The path to local the zig cache directory" DEFAULT ${CACHE_PATH}/zig/local)
optionx(ZIG_GLOBAL_CACHE_DIR FILEPATH "The path to the global zig cache directory" DEFAULT ${CACHE_PATH}/zig/global)
optionx(ZIG_COMPILER_SAFE BOOL "Download a ReleaseSafe build of the Zig compiler." DEFAULT ${CI})
# The ReleaseSafe Zig compiler for Windows ARM64 has an LLVM SEH epilogue bug
# (incorrect size for compiler_rt.rem_pio2_large epilogue). Use the default build instead.
if(CI AND WIN32 AND DEFAULT_ZIG_ARCH STREQUAL "aarch64")
set(DEFAULT_ZIG_COMPILER_SAFE OFF)
else()
set(DEFAULT_ZIG_COMPILER_SAFE ${CI})
endif()
optionx(ZIG_COMPILER_SAFE BOOL "Download a ReleaseSafe build of the Zig compiler." DEFAULT ${DEFAULT_ZIG_COMPILER_SAFE})
setenv(ZIG_LOCAL_CACHE_DIR ${ZIG_LOCAL_CACHE_DIR})
setenv(ZIG_GLOBAL_CACHE_DIR ${ZIG_GLOBAL_CACHE_DIR})

View File

@@ -148,6 +148,9 @@ _bun_completions() {
upgrade)
COMPREPLY=( $(compgen -W "--version --cwd --help -v -h") );
return;;
repl)
COMPREPLY=( $(compgen -W "--help -h --eval -e --print -p --preload -r --smol --config -c --cwd --env-file --no-env-file" -- "${cur_word}") );
return;;
run)
_file_arguments "!(*.@(js|ts|jsx|tsx|mjs|cjs)?($|))";
COMPREPLY+=( $(compgen -W "--version --cwd --help --silent -v -h" -- "${cur_word}" ) );

View File

@@ -35,7 +35,7 @@ end
set -l bun_install_boolean_flags yarn production optional development no-save dry-run force no-cache silent verbose global
set -l bun_install_boolean_flags_descriptions "Write a yarn.lock file (yarn v1)" "Don't install devDependencies" "Add dependency to optionalDependencies" "Add dependency to devDependencies" "Don't update package.json or save a lockfile" "Don't install anything" "Always request the latest versions from the registry & reinstall all dependencies" "Ignore manifest cache entirely" "Don't output anything" "Excessively verbose logging" "Use global folder"
set -l bun_builtin_cmds_without_run dev create help bun upgrade discord install remove add update init pm x
set -l bun_builtin_cmds_without_run dev create help bun upgrade discord install remove add update init pm x repl
set -l bun_builtin_cmds_accepting_flags create help bun upgrade discord run init link unlink pm x update
function __bun_complete_bins_scripts --inherit-variable bun_builtin_cmds_without_run -d "Emit bun completions for bins and scripts"
@@ -185,3 +185,12 @@ complete -c bun -n "__fish_use_subcommand" -a "x" -d "Execute a package binary,
complete -c bun -n "__fish_use_subcommand" -a "outdated" -d "Display the latest versions of outdated dependencies" -f
complete -c bun -n "__fish_use_subcommand" -a "update" -d "Update dependencies to their latest versions" -f
complete -c bun -n "__fish_use_subcommand" -a "publish" -d "Publish your package from local to npm" -f
complete -c bun -n "__fish_use_subcommand" -a "repl" -d "Start a REPL session with Bun" -f
complete -c bun -n "__fish_seen_subcommand_from repl" -s "e" -l "eval" -r -d "Evaluate argument as a script, then exit" -f
complete -c bun -n "__fish_seen_subcommand_from repl" -s "p" -l "print" -r -d "Evaluate argument as a script, print the result, then exit" -f
complete -c bun -n "__fish_seen_subcommand_from repl" -s "r" -l "preload" -r -d "Import a module before other modules are loaded"
complete -c bun -n "__fish_seen_subcommand_from repl" -l "smol" -d "Use less memory, but run garbage collection more often" -f
complete -c bun -n "__fish_seen_subcommand_from repl" -s "c" -l "config" -r -d "Specify path to Bun config file"
complete -c bun -n "__fish_seen_subcommand_from repl" -l "cwd" -r -d "Absolute path to resolve files & entry points from"
complete -c bun -n "__fish_seen_subcommand_from repl" -l "env-file" -r -d "Load environment variables from the specified file(s)"
complete -c bun -n "__fish_seen_subcommand_from repl" -l "no-env-file" -d "Disable automatic loading of .env files" -f

View File

@@ -524,6 +524,33 @@ _bun_upgrade_completion() {
}
_bun_repl_completion() {
_arguments -s -C \
'1: :->cmd' \
'--help[Print this help menu]' \
'-h[Print this help menu]' \
'(-p --print)--eval[Evaluate argument as a script, then exit]:script' \
'(-p --print)-e[Evaluate argument as a script, then exit]:script' \
'(-e --eval)--print[Evaluate argument as a script, print the result, then exit]:script' \
'(-e --eval)-p[Evaluate argument as a script, print the result, then exit]:script' \
'--preload[Import a module before other modules are loaded]:preload' \
'-r[Import a module before other modules are loaded]:preload' \
'--smol[Use less memory, but run garbage collection more often]' \
'--config[Specify path to Bun config file]: :->config' \
'-c[Specify path to Bun config file]: :->config' \
'--cwd[Absolute path to resolve files & entry points from]:cwd' \
'--env-file[Load environment variables from the specified file(s)]:env-file' \
'--no-env-file[Disable automatic loading of .env files]' &&
ret=0
case $state in
config)
_bun_list_bunfig_toml
;;
esac
}
_bun_build_completion() {
_arguments -s -C \
'1: :->cmd' \
@@ -787,6 +814,10 @@ _bun() {
upgrade)
_bun_upgrade_completion
;;
repl)
_bun_repl_completion
;;
build)
_bun_build_completion
@@ -870,6 +901,10 @@ _bun() {
upgrade)
_bun_upgrade_completion
;;
repl)
_bun_repl_completion
;;
build)
_bun_build_completion

View File

@@ -157,6 +157,31 @@ To build for Windows x64:
</Tab>
</Tabs>
To build for Windows arm64:
<Tabs>
<Tab title="CLI">
```bash icon="terminal" terminal
bun build --compile --target=bun-windows-arm64 ./path/to/my/app.ts --outfile myapp
# note: if no .exe extension is provided, Bun will automatically add it for Windows executables
```
</Tab>
<Tab title="JavaScript">
```ts build.ts icon="/icons/typescript.svg"
await Bun.build({
entrypoints: ["./path/to/my/app.ts"],
compile: {
target: "bun-windows-arm64",
outfile: "./myapp", // .exe added automatically
},
});
```
</Tab>
</Tabs>
To build for macOS arm64:
<Tabs>
@@ -203,16 +228,16 @@ To build for macOS x64:
The order of the `--target` flag does not matter, as long as they're delimited by a `-`.
| --target | Operating System | Architecture | Modern | Baseline | Libc |
| --------------------- | ---------------- | ------------ | ------ | -------- | ----- |
| bun-linux-x64 | Linux | x64 | ✅ | ✅ | glibc |
| bun-linux-arm64 | Linux | arm64 | ✅ | N/A | glibc |
| bun-windows-x64 | Windows | x64 | ✅ | ✅ | - |
| ~~bun-windows-arm64~~ | ~~Windows~~ | ~~arm64~~ | | | - |
| bun-darwin-x64 | macOS | x64 | ✅ | ✅ | - |
| bun-darwin-arm64 | macOS | arm64 | ✅ | N/A | - |
| bun-linux-x64-musl | Linux | x64 | ✅ | ✅ | musl |
| bun-linux-arm64-musl | Linux | arm64 | ✅ | N/A | musl |
| --target | Operating System | Architecture | Modern | Baseline | Libc |
| -------------------- | ---------------- | ------------ | ------ | -------- | ----- |
| bun-linux-x64 | Linux | x64 | ✅ | ✅ | glibc |
| bun-linux-arm64 | Linux | arm64 | ✅ | N/A | glibc |
| bun-windows-x64 | Windows | x64 | ✅ | ✅ | - |
| bun-windows-arm64 | Windows | arm64 | | N/A | - |
| bun-darwin-x64 | macOS | x64 | ✅ | ✅ | - |
| bun-darwin-arm64 | macOS | arm64 | ✅ | N/A | - |
| bun-linux-x64-musl | Linux | x64 | ✅ | ✅ | musl |
| bun-linux-arm64-musl | Linux | arm64 | ✅ | N/A | musl |
<Warning>
On x64 platforms, Bun uses SIMD optimizations which require a modern CPU supporting AVX2 instructions. The `-baseline`
@@ -1252,7 +1277,8 @@ type Target =
| "bun-linux-arm64-musl"
| "bun-windows-x64"
| "bun-windows-x64-baseline"
| "bun-windows-x64-modern";
| "bun-windows-x64-modern"
| "bun-windows-arm64";
```
### Complete example

View File

@@ -1291,6 +1291,28 @@ declare module "bun:bundle" {
Ensure the file is included in your `tsconfig.json` (e.g., `"include": ["src", "env.d.ts"]`). Now `feature()` only accepts those flags, and invalid strings like `feature("TYPO")` become type errors.
### optimizeImports
Skip parsing unused submodules of barrel files (re-export index files). When you import only a few named exports from a large library, normally the bundler parses every file the barrel re-exports. With `optimizeImports`, only the submodules you actually use are parsed.
```ts title="build.ts" icon="/icons/typescript.svg"
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./out",
optimizeImports: ["antd", "@mui/material", "lodash-es"],
});
```
For example, `import { Button } from 'antd'` normally parses all ~3000 modules that `antd/index.js` re-exports. With `optimizeImports: ['antd']`, only the `Button` submodule is parsed.
This works for **pure barrel files** — files where every named export is a re-export (`export { X } from './x'`). If a barrel file has any local exports (`export const foo = ...`), or if any importer uses `import *`, all submodules are loaded.
`export *` re-exports are always loaded (never deferred) to avoid circular resolution issues. Only named re-exports (`export { X } from './x'`) that aren't used by any importer are deferred.
**Automatic mode:** Packages with `"sideEffects": false` in their `package.json` get barrel optimization automatically — no `optimizeImports` config needed. Use `optimizeImports` for packages that don't have this field.
**Plugins:** Resolve and load plugins work correctly with barrel optimization. Deferred submodules go through the plugin pipeline when they are eventually loaded.
### metafile
Generate metadata about the build in a structured format. The metafile contains information about all input files, output files, their sizes, imports, and exports. This is useful for:

View File

@@ -75,7 +75,7 @@
{
"group": "Core Runtime",
"icon": "cog",
"pages": ["/runtime/index", "/runtime/watch-mode", "/runtime/debugger", "/runtime/bunfig"]
"pages": ["/runtime/index", "/runtime/watch-mode", "/runtime/debugger", "/runtime/repl", "/runtime/bunfig"]
},
{
"group": "File & Module System",

View File

@@ -33,7 +33,7 @@ const stream = await renderToReadableStream(<Component message="Hello from serve
Combining this with `Bun.serve()`, we get a simple SSR HTTP server:
```tsx server.ts icon="/icons/typescript.svg"
```tsx server.tsx icon="/icons/typescript.svg"
Bun.serve({
async fetch() {
const stream = await renderToReadableStream(<Component message="Hello from server!" />);

View File

@@ -13,7 +13,7 @@ mode: center
Use the interactive CLI to create a new TanStack Start app.
```sh terminal icon="terminal"
bun create @tanstack/start@latest my-tanstack-app
bunx @tanstack/cli create my-tanstack-app
```
</Step>

View File

@@ -260,6 +260,13 @@ To download Bun binaries directly, visit the [releases page on GitHub](https://g
>
For older CPUs without AVX2
</Card>
<Card
icon="/icons/windows.svg"
title="Windows ARM64"
href="https://github.com/oven-sh/bun/releases/latest/download/bun-windows-aarch64.zip"
>
Windows on ARM (Snapdragon, etc.)
</Card>
<Card
icon="/icons/apple.svg"
title="macOS ARM64"

176
docs/runtime/repl.mdx Normal file
View File

@@ -0,0 +1,176 @@
---
title: "REPL"
description: "An interactive JavaScript and TypeScript REPL with syntax highlighting, history, and tab completion"
---
`bun repl` starts an interactive Read-Eval-Print Loop (REPL) for evaluating JavaScript and TypeScript expressions. It's useful for quickly testing code snippets, exploring APIs, and debugging.
```sh terminal icon="terminal"
bun repl
```
```txt
Welcome to Bun v1.3.3
Type .copy [code] to copy to clipboard. .help for more info.
> 1 + 1
2
> const greeting = "Hello, Bun!"
undefined
> greeting
'Hello, Bun!'
```
---
## Features
- **TypeScript & JSX** — Write TypeScript and JSX directly. Bun transpiles everything on the fly.
- **Top-level `await`** — Await promises directly at the prompt without wrapping in an async function.
- **Syntax highlighting** — Input is highlighted as you type.
- **Persistent history** — History is saved to `~/.bun_repl_history` and persists across sessions.
- **Tab completion** — Press `Tab` to complete property names and REPL commands.
- **Multi-line input** — Unclosed brackets, braces, and parentheses automatically continue on the next line.
- **Node.js globals** — `require`, `module`, `__dirname`, and `__filename` are available, resolved relative to your current working directory.
---
## Special variables
The REPL exposes two special variables that update after each evaluation.
| Variable | Description |
| -------- | --------------------------------- |
| `_` | The result of the last expression |
| `_error` | The last error that was thrown |
```txt
> 2 + 2
4
> _ * 10
40
> JSON.parse("oops")
SyntaxError: JSON Parse error: Unexpected identifier "oops"
> _error
SyntaxError: JSON Parse error: Unexpected identifier "oops"
```
---
## Top-level `await`
Promises are automatically awaited. You can `await` any expression directly at the prompt.
```txt
> await fetch("https://api.github.com/repos/oven-sh/bun").then(r => r.json()).then(r => r.stargazers_count)
81234
> const response = await fetch("https://example.com")
undefined
> response.status
200
```
---
## Importing modules
Just like Bun's runtime, you can use either `require` or `import` in the REPL and it Just Works — mix ESM and CommonJS freely at the prompt. Module resolution uses the same rules as `bun run`, so you can import from `node_modules`, relative paths, or `node:` builtins.
```txt
> import { z } from "zod"
undefined
> const path = require("path")
undefined
> z.string().parse(path.join("/tmp", "file.txt"))
'/tmp/file.txt'
```
Declarations persist for the rest of the session, and `const`/`let` can be redeclared across evaluations (unlike in regular scripts) so you can re-run `import` and `require` statements while iterating.
---
## Multi-line input
When you press `Enter` on a line with unclosed brackets, braces, or parentheses, the REPL automatically continues on the next line. The prompt changes to `...` to indicate continuation.
```txt
> function add(a, b) {
... return a + b;
... }
undefined
> add(2, 3)
5
```
For longer multi-line entries, use `.editor` to enter editor mode, which buffers all input until you press `Ctrl+D`.
---
## REPL commands
Type `.help` at the prompt to see all available REPL commands.
| Command | Description |
| ---------- | ------------------------------------------------------------------------------------------------ |
| `.help` | Print the help message listing commands and keybindings |
| `.exit` | Exit the REPL |
| `.clear` | Clear the screen |
| `.copy` | Copy the last result to the clipboard. Pass an expression to evaluate and copy it: `.copy 1 + 1` |
| `.load` | Load a file into the REPL session: `.load ./script.ts` |
| `.save` | Save the current REPL history to a file: `.save ./session.txt` |
| `.editor` | Enter multi-line editor mode (press `Ctrl+D` to evaluate, `Ctrl+C` to cancel) |
| `.break` | Cancel the current multi-line input |
| `.history` | Print the command history |
---
## Keybindings
The REPL supports Emacs-style line editing.
| Keybinding | Action |
| ------------------- | -------------------------------------------------------- |
| `Ctrl+A` | Move to start of line |
| `Ctrl+E` | Move to end of line |
| `Ctrl+B` / `Ctrl+F` | Move backward/forward one character |
| `Alt+B` / `Alt+F` | Move backward/forward one word |
| `Ctrl+U` | Delete to start of line |
| `Ctrl+K` | Delete to end of line |
| `Ctrl+W` | Delete word backward |
| `Ctrl+D` | Delete character (or exit if line is empty) |
| `Ctrl+L` | Clear screen |
| `Ctrl+T` | Swap the two characters before the cursor |
| `Up` / `Down` | Navigate history |
| `Tab` | Auto-complete |
| `Ctrl+C` | Cancel current input (press twice on empty line to exit) |
---
## History
REPL history is automatically saved to `~/.bun_repl_history` (up to 1000 entries) and loaded at the start of each session. Use `Up`/`Down` to navigate.
To export your history to a different file, use `.save`:
```txt
> .save ./my-session.txt
```
---
## Non-interactive mode
Use `-e` / `--eval` to evaluate a script with REPL semantics and exit. Use `-p` / `--print` to additionally print the result.
```sh terminal icon="terminal"
bun repl -e "const x: number = 42; console.log(x)"
# 42
bun repl -p "await fetch('https://example.com').then(r => r.status)"
# 200
bun repl -p "{ a: 1, b: 2 }"
# { a: 1, b: 2 }
```
This uses the same transforms as the interactive REPL, so a bare object literal like `{ a: 1 }` is treated as an object expression instead of a block statement. The process exits after the event loop drains (pending timers and I/O complete first). On error, the process exits with code `1`.

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "bun",
"version": "1.3.10",
"version": "1.3.11",
"workspaces": [
"./packages/bun-types",
"./packages/@types/bun"

View File

@@ -22,8 +22,9 @@ bun upgrade
- [Linux, arm64](https://www.npmjs.com/package/@oven/bun-linux-aarch64)
- [Linux, x64](https://www.npmjs.com/package/@oven/bun-linux-x64)
- [Linux, x64 (without AVX2 instructions)](https://www.npmjs.com/package/@oven/bun-linux-x64-baseline)
- [Windows](https://www.npmjs.com/package/@oven/bun-windows-x64)
- [Windows (without AVX2 instructions)](https://www.npmjs.com/package/@oven/bun-windows-x64-baseline)
- [Windows, x64](https://www.npmjs.com/package/@oven/bun-windows-x64)
- [Windows, x64 (without AVX2 instructions)](https://www.npmjs.com/package/@oven/bun-windows-x64-baseline)
- [Windows ARM64](https://www.npmjs.com/package/@oven/bun-windows-aarch64)
### Future Platforms

View File

@@ -95,12 +95,12 @@ export const platforms: Platform[] = [
bin: "bun-windows-x64-baseline",
exe: "bin/bun.exe",
},
// {
// os: "win32",
// arch: "arm64",
// bin: "bun-windows-aarch64",
// exe: "bin/bun.exe",
// },
{
os: "win32",
arch: "arm64",
bin: "bun-windows-aarch64",
exe: "bin/bun.exe",
},
];
export const supportedPlatforms: Platform[] = platforms

View File

@@ -610,6 +610,83 @@ declare module "bun" {
*/
function stripANSI(input: string): string;
interface SliceAnsiOptions {
/**
* If set, and content was cut at either edge of the requested range,
* insert this string at the cut edge(s). The ellipsis is counted against
* the visible-width budget and is emitted *inside* any active SGR styles
* (color, bold, etc.) so it inherits them, but *outside* any active OSC 8
* hyperlink.
*
* This turns `sliceAnsi` into a drop-in `cli-truncate` replacement:
* - truncate-end: `sliceAnsi(str, 0, max, { ellipsis: "\u2026" })`
* - truncate-start: `sliceAnsi(str, -max, undefined, { ellipsis: "\u2026" })`
*/
ellipsis?: string;
/**
* Count characters with East Asian Width "Ambiguous" as 1 column (narrow)
* instead of 2 (wide). Affects Greek, Cyrillic, some symbols, etc. that
* render wide in CJK-encoded terminals but narrow in Western ones.
*
* Matches the option of the same name in {@link stringWidth} and
* {@link wrapAnsi}.
*
* @default true
*/
ambiguousIsNarrow?: boolean;
}
/**
* Slice a string by visible column width, preserving ANSI escape codes.
*
* Like `String.prototype.slice`, but indices are terminal column widths
* (accounting for wide CJK characters, emoji grapheme clusters, and
* zero-width joiners), and ANSI escape sequences (SGR colors, OSC 8
* hyperlinks, etc.) are preserved and correctly re-opened/closed at the
* slice boundaries.
*
* @category Utilities
*
* @param input The string to slice
* @param start Starting column (default 0). Negative counts from end.
* @param end Ending column, exclusive (default end of string). Negative counts from end.
* @param options Optional behavior flags (e.g. `ellipsis` for truncation)
* @returns The sliced string with ANSI codes intact
*
* @example
* ```ts
* import { sliceAnsi } from "bun";
*
* // Plain slice (replaces the `slice-ansi` npm package)
* sliceAnsi("hello", 1, 4); // "ell"
* sliceAnsi("\u001b[31mhello\u001b[39m", 1, 4); // "\u001b[31mell\u001b[39m"
* sliceAnsi("\u5b89\u5b81\u54c8", 0, 4); // "\u5b89\u5b81" (CJK: width 2 each)
*
* // Truncation (replaces the `cli-truncate` npm package)
* sliceAnsi("unicorn", 0, 4, "\u2026"); // "uni\u2026"
* sliceAnsi("unicorn", -4, undefined, "\u2026"); // "\u2026orn"
* ```
*/
function sliceAnsi(
input: string,
start?: number,
end?: number,
/**
* Shorthand for common options (avoids `{}` allocation):
* - `string` → ellipsis (equivalent to `{ ellipsis: string }`)
* - `boolean` → ambiguousIsNarrow (equivalent to `{ ambiguousIsNarrow: boolean }`)
* - `SliceAnsiOptions` → full options object
*/
options?: string | boolean | SliceAnsiOptions,
/**
* ambiguousIsNarrow as a positional arg, usable when the 4th arg is an
* ellipsis string (or `undefined`). Lets you pass both options without
* an object: `sliceAnsi(s, 0, n, "\u2026", false)`.
*/
ambiguousIsNarrow?: boolean,
): string;
interface WrapAnsiOptions {
/**
* If `true`, break words in the middle if they don't fit on a line.
@@ -2428,7 +2505,7 @@ declare module "bun" {
}
namespace Build {
type Architecture = "x64" | "arm64";
type Architecture = "x64" | "arm64" | "aarch64";
type Libc = "glibc" | "musl";
type SIMD = "baseline" | "modern";
type CompileTarget =
@@ -2644,6 +2721,25 @@ declare module "bun" {
*/
features?: string[];
/**
* List of package names whose barrel files (re-export index files) should
* be optimized. When a named import comes from one of these packages,
* only the submodules actually used are parsed — unused re-exports are
* skipped entirely.
*
* This is also enabled automatically for any package with
* `"sideEffects": false` in its `package.json`.
*
* @example
* ```ts
* await Bun.build({
* entrypoints: ['./app.ts'],
* optimizeImports: ['antd', '@mui/material', 'lodash-es'],
* });
* ```
*/
optimizeImports?: string[];
/**
* - When set to `true`, the returned promise rejects with an AggregateError when a build failure happens.
* - When set to `false`, returns a {@link BuildOutput} with `{success: false}`
@@ -4567,6 +4663,20 @@ declare module "bun" {
*/
function generateHeapSnapshot(format: "v8"): string;
/**
* Show precise statistics about memory usage of your application
*
* Generate a V8 Heap Snapshot as an ArrayBuffer.
*
* This avoids the overhead of creating a JavaScript string for large heap snapshots.
* The ArrayBuffer contains the UTF-8 encoded JSON.
* ```ts
* const snapshot = Bun.generateHeapSnapshot("v8", "arraybuffer");
* await Bun.write("heap.heapsnapshot", snapshot);
* ```
*/
function generateHeapSnapshot(format: "v8", encoding: "arraybuffer"): ArrayBuffer;
/**
* The next time JavaScriptCore is idle, clear unused memory and attempt to reduce the heap size.
*

View File

@@ -828,7 +828,7 @@ struct us_socket_t *us_socket_context_adopt_socket(int ssl, struct us_socket_con
struct us_socket_t *new_s = s;
if (ext_size != -1) {
struct us_poll_t *pool_ref = &s->p;
new_s = (struct us_socket_t *) us_poll_resize(pool_ref, loop, sizeof(struct us_socket_t) + old_ext_size, sizeof(struct us_socket_t) + ext_size);
new_s = (struct us_socket_t *) us_poll_resize(pool_ref, loop, sizeof(struct us_socket_t) - sizeof(struct us_poll_t) + old_ext_size, sizeof(struct us_socket_t) - sizeof(struct us_poll_t) + ext_size);
if(new_s != s) {
/* Mark the old socket as closed */
s->flags.is_closed = 1;

View File

@@ -462,7 +462,7 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
#ifdef _WIN32
const int recv_flags = MSG_PUSH_IMMEDIATE;
#else
const int recv_flags = MSG_DONTWAIT | MSG_NOSIGNAL;
const int recv_flags = MSG_DONTWAIT;
#endif
int length;

View File

@@ -54,15 +54,19 @@ namespace uWS {
while (data.length() && data[0] > 32 && data[0] != ';') {
unsigned char digit = (unsigned char)data[0];
if (digit >= 'a') {
digit = (unsigned char) (digit - ('a' - ':'));
} else if (digit >= 'A') {
digit = (unsigned char) (digit - ('A' - ':'));
unsigned int number;
if (digit >= '0' && digit <= '9') {
number = digit - '0';
} else if (digit >= 'a' && digit <= 'f') {
number = digit - 'a' + 10;
} else if (digit >= 'A' && digit <= 'F') {
number = digit - 'A' + 10;
} else {
state = STATE_IS_ERROR;
return;
}
unsigned int number = ((unsigned int) digit - (unsigned int) '0');
if (number > 16 || (chunkSize(state) & STATE_SIZE_OVERFLOW)) {
if ((chunkSize(state) & STATE_SIZE_OVERFLOW)) {
state = STATE_IS_ERROR;
return;
}

View File

@@ -721,7 +721,8 @@ namespace uWS
/* Check for empty headers (no headers, just \r\n) */
if (postPaddedBuffer[0] == '\r' && postPaddedBuffer[1] == '\n') {
/* Valid request with no headers */
/* Valid request with no headers - write null terminator like the normal path */
headers[1].key = std::string_view(nullptr, 0);
return HttpParserResult::success((unsigned int) ((postPaddedBuffer + 2) - start));
}

View File

@@ -467,7 +467,9 @@ public:
/* Write mark on first call to write */
writeMark();
writeHeader("Transfer-Encoding", "chunked");
if (!(httpResponseData->state & HttpResponseData<SSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER)) {
writeHeader("Transfer-Encoding", "chunked");
}
Super::write("\r\n", 2);
httpResponseData->state |= HttpResponseData<SSL>::HTTP_WRITE_CALLED;
}
@@ -489,7 +491,9 @@ public:
/* Write mark on first call to write */
writeMark();
writeHeader("Transfer-Encoding", "chunked");
if (!(httpResponseData->state & HttpResponseData<SSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER)) {
writeHeader("Transfer-Encoding", "chunked");
}
Super::write("\r\n", 2);
httpResponseData->state |= HttpResponseData<SSL>::HTTP_WRITE_CALLED;
}
@@ -558,7 +562,9 @@ public:
/* Write mark on first call to write */
writeMark();
writeHeader("Transfer-Encoding", "chunked");
if (!(httpResponseData->state & HttpResponseData<SSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER)) {
writeHeader("Transfer-Encoding", "chunked");
}
Super::write("\r\n", 2);
httpResponseData->state |= HttpResponseData<SSL>::HTTP_WRITE_CALLED;
}

View File

@@ -87,6 +87,7 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
HTTP_CONNECTION_CLOSE = 16, // used
HTTP_WROTE_CONTENT_LENGTH_HEADER = 32, // used
HTTP_WROTE_DATE_HEADER = 64, // used
HTTP_WROTE_TRANSFER_ENCODING_HEADER = 128, // used
};
/* Shared context pointer */

665
scripts/azure.mjs Normal file
View File

@@ -0,0 +1,665 @@
// Azure REST API client for machine.mjs
// Used by the [build images] pipeline to create Windows VM images (x64 and ARM64)
import { getSecret, isCI } from "./utils.mjs";
/**
* @typedef {Object} AzureConfig
* @property {string} tenantId
* @property {string} clientId
* @property {string} clientSecret
* @property {string} subscriptionId
* @property {string} resourceGroup
* @property {string} location
* @property {string} galleryName
*/
/** @returns {AzureConfig} */
function getConfig() {
const env = (name, fallback) => {
if (isCI) {
try {
return getSecret(name, { required: !fallback }) || fallback;
} catch {
if (fallback) return fallback;
throw new Error(`Azure secret not found: ${name}`);
}
}
return process.env[name] || fallback;
};
return {
tenantId: env("AZURE_TENANT_ID"),
clientId: env("AZURE_CLIENT_ID"),
clientSecret: env("AZURE_CLIENT_SECRET"),
subscriptionId: env("AZURE_SUBSCRIPTION_ID"),
resourceGroup: env("AZURE_RESOURCE_GROUP", "BUN-CI"),
location: env("AZURE_LOCATION", "eastus2"),
galleryName: env("AZURE_GALLERY_NAME", "bunCIGallery2"),
};
}
let _config;
function config() {
return (_config ??= getConfig());
}
// ============================================================================
// Authentication
// ============================================================================
let _accessToken = null;
let _tokenExpiry = 0;
async function getAccessToken() {
if (_accessToken && Date.now() < _tokenExpiry - 300_000) {
return _accessToken;
}
const { tenantId, clientId, clientSecret } = config();
const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: clientId,
client_secret: clientSecret,
scope: "https://management.azure.com/.default",
}),
});
if (!response.ok) {
throw new Error(`[azure] Auth failed: ${response.status} ${await response.text()}`);
}
const data = await response.json();
_accessToken = data.access_token;
_tokenExpiry = Date.now() + data.expires_in * 1000;
return _accessToken;
}
// ============================================================================
// REST Client
// ============================================================================
/**
* @param {"GET"|"PUT"|"POST"|"PATCH"|"DELETE"} method
* @param {string} path - Relative path under management.azure.com, or absolute URL
* @param {object} [body]
* @param {string} [apiVersion]
*/
async function azureFetch(method, path, body, apiVersion = "2024-07-01") {
const token = await getAccessToken();
const url = path.startsWith("http") ? new URL(path) : new URL(`https://management.azure.com${path}`);
if (!url.searchParams.has("api-version")) {
url.searchParams.set("api-version", apiVersion);
}
const options = {
method,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
};
if (body && method !== "GET" && method !== "DELETE") {
options.body = JSON.stringify(body);
}
for (let attempt = 0; attempt < 3; attempt++) {
const response = await fetch(url, options);
if (response.status === 429 || response.status >= 500) {
const wait = Math.pow(2, attempt) * 1000;
console.warn(`[azure] ${method} ${path} returned ${response.status}, retrying in ${wait}ms...`);
await new Promise(r => setTimeout(r, wait));
continue;
}
// 202 Accepted — async operation, poll for completion
if (response.status === 202) {
const operationUrl = response.headers.get("Azure-AsyncOperation") || response.headers.get("Location");
if (operationUrl) {
return waitForOperation(operationUrl);
}
}
if (response.status === 204) {
return null;
}
if (!response.ok) {
const text = await response.text();
throw new Error(`[azure] ${method} ${path} failed: ${response.status} ${text}`);
}
const text = await response.text();
return text ? JSON.parse(text) : null;
}
throw new Error(`[azure] ${method} ${path} failed after 3 retries`);
}
async function waitForOperation(operationUrl, maxWaitMs = 3_600_000) {
const start = Date.now();
let fetchErrors = 0;
while (Date.now() - start < maxWaitMs) {
const token = await getAccessToken();
let response;
try {
response = await fetch(operationUrl, {
headers: { Authorization: `Bearer ${token}` },
});
} catch (err) {
fetchErrors++;
if (fetchErrors > 10) {
throw new Error(`[azure] Operation poll failed after ${fetchErrors} fetch errors`, { cause: err });
}
console.warn(`[azure] Operation poll fetch error (${fetchErrors}), retrying...`);
await new Promise(r => setTimeout(r, 10_000));
continue;
}
if (!response.ok) {
throw new Error(`[azure] Operation poll failed: ${response.status} ${await response.text()}`);
}
const data = await response.json();
if (data.status === "Succeeded") {
return data.properties?.output ?? data;
}
if (data.status === "Failed" || data.status === "Canceled") {
throw new Error(`[azure] Operation ${data.status}: ${data.error?.message ?? "unknown"}`);
}
await new Promise(r => setTimeout(r, 5000));
}
throw new Error(`[azure] Operation timed out after ${maxWaitMs}ms`);
}
// ============================================================================
// Resource helpers
// ============================================================================
function rgPath() {
const { subscriptionId, resourceGroup } = config();
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}`;
}
// ============================================================================
// Public IP
// ============================================================================
async function createPublicIp(name) {
const { location } = config();
console.log(`[azure] Creating public IP: ${name}`);
const result = await azureFetch("PUT", `${rgPath()}/providers/Microsoft.Network/publicIPAddresses/${name}`, {
location,
sku: { name: "Standard" },
properties: {
publicIPAllocationMethod: "Static",
deleteOption: "Delete",
},
});
return result?.properties?.ipAddress;
}
async function deletePublicIp(name) {
await azureFetch("DELETE", `${rgPath()}/providers/Microsoft.Network/publicIPAddresses/${name}`).catch(() => {});
}
// ============================================================================
// Network Security Group
// ============================================================================
// ============================================================================
// Network Interface
// ============================================================================
async function createNic(name, publicIpName, subnetId, nsgId) {
const { location } = config();
console.log(`[azure] Creating NIC: ${name}`);
const publicIpId = `${rgPath()}/providers/Microsoft.Network/publicIPAddresses/${publicIpName}`;
await azureFetch("PUT", `${rgPath()}/providers/Microsoft.Network/networkInterfaces/${name}`, {
location,
properties: {
ipConfigurations: [
{
name: "ipconfig1",
properties: {
privateIPAllocationMethod: "Dynamic",
publicIPAddress: { id: publicIpId, properties: { deleteOption: "Delete" } },
subnet: { id: subnetId },
},
},
],
...(nsgId ? { networkSecurityGroup: { id: nsgId } } : {}),
},
});
return `${rgPath()}/providers/Microsoft.Network/networkInterfaces/${name}`;
}
async function deleteNic(name) {
await azureFetch("DELETE", `${rgPath()}/providers/Microsoft.Network/networkInterfaces/${name}`).catch(() => {});
}
// ============================================================================
// Virtual Machines
// ============================================================================
/**
* @param {object} opts
* @param {string} opts.name
* @param {string} opts.vmSize
* @param {object} opts.imageReference
* @param {number} opts.osDiskSizeGB
* @param {string} opts.nicId
* @param {string} opts.adminUsername
* @param {string} opts.adminPassword
* @param {Record<string, string>} [opts.tags]
*/
async function createVm(opts) {
const { location } = config();
console.log(`[azure] Creating VM: ${opts.name} (${opts.vmSize})`);
const result = await azureFetch("PUT", `${rgPath()}/providers/Microsoft.Compute/virtualMachines/${opts.name}`, {
location,
tags: opts.tags,
properties: {
hardwareProfile: { vmSize: opts.vmSize },
storageProfile: {
imageReference: opts.imageReference,
osDisk: {
createOption: "FromImage",
diskSizeGB: opts.osDiskSizeGB,
deleteOption: "Delete",
managedDisk: { storageAccountType: "Premium_LRS" },
},
},
osProfile: {
computerName: opts.name.substring(0, 15),
adminUsername: opts.adminUsername,
adminPassword: opts.adminPassword,
},
securityProfile: {
securityType: "TrustedLaunch",
},
networkProfile: {
networkInterfaces: [{ id: opts.nicId, properties: { deleteOption: "Delete" } }],
},
},
});
return result;
}
async function getVm(name) {
try {
return await azureFetch(
"GET",
`${rgPath()}/providers/Microsoft.Compute/virtualMachines/${name}?$expand=instanceView`,
);
} catch {
return null;
}
}
async function getVmPowerState(name) {
const vm = await getVm(name);
const statuses = vm?.properties?.instanceView?.statuses ?? [];
const powerStatus = statuses.find(s => s.code?.startsWith("PowerState/"));
return powerStatus?.code;
}
async function stopVm(name) {
console.log(`[azure] Stopping VM: ${name}`);
await azureFetch("POST", `${rgPath()}/providers/Microsoft.Compute/virtualMachines/${name}/deallocate`);
}
async function generalizeVm(name) {
console.log(`[azure] Generalizing VM: ${name}`);
await azureFetch("POST", `${rgPath()}/providers/Microsoft.Compute/virtualMachines/${name}/generalize`);
}
async function deleteVm(name) {
console.log(`[azure] Deleting VM: ${name}`);
await azureFetch("DELETE", `${rgPath()}/providers/Microsoft.Compute/virtualMachines/${name}?forceDeletion=true`);
}
async function getPublicIpAddress(publicIpName) {
const result = await azureFetch("GET", `${rgPath()}/providers/Microsoft.Network/publicIPAddresses/${publicIpName}`);
return result?.properties?.ipAddress;
}
/**
* Run a PowerShell script on a Windows VM via Azure Run Command.
* This works even without SSH installed on the VM.
*/
async function runCommand(vmName, script) {
console.log(`[azure] Running command on VM: ${vmName}`);
return azureFetch("POST", `${rgPath()}/providers/Microsoft.Compute/virtualMachines/${vmName}/runCommand`, {
commandId: "RunPowerShellScript",
script: Array.isArray(script) ? script : [script],
});
}
/**
* Install OpenSSH server and configure authorized keys on a Windows VM.
*/
// SSH is not used — all remote operations go through Azure Run Command API.
// ============================================================================
// Virtual Network
// ============================================================================
// ============================================================================
// Compute Gallery
// ============================================================================
const GALLERY_API_VERSION = "2024-03-03";
async function ensureImageDefinition(name, os, arch) {
const { location, galleryName } = config();
const path = `${rgPath()}/providers/Microsoft.Compute/galleries/${galleryName}/images/${name}`;
try {
const def = await azureFetch("GET", path, undefined, GALLERY_API_VERSION);
if (def) return;
} catch {}
console.log(`[azure] Creating image definition: ${name}`);
await azureFetch(
"PUT",
path,
{
location,
properties: {
osType: os === "windows" ? "Windows" : "Linux",
osState: "Generalized",
hyperVGeneration: "V2",
architecture: arch === "aarch64" ? "Arm64" : "x64",
identifier: {
publisher: "bun",
offer: `${os}-${arch}-ci`,
sku: name,
},
features: [
{ name: "DiskControllerTypes", value: "SCSI, NVMe" },
{ name: "SecurityType", value: "TrustedLaunch" },
],
},
},
GALLERY_API_VERSION,
);
}
async function createImageVersion(imageDefName, version, vmId) {
const { location, galleryName } = config();
const path = `${rgPath()}/providers/Microsoft.Compute/galleries/${galleryName}/images/${imageDefName}/versions/${version}`;
console.log(`[azure] Creating image version: ${imageDefName}/${version}`);
const result = await azureFetch(
"PUT",
path,
{
location,
properties: {
storageProfile: {
source: { virtualMachineId: vmId },
},
},
},
GALLERY_API_VERSION,
);
return result;
}
// ============================================================================
// Base Images
// ============================================================================
function getBaseImageReference(os, arch) {
if (os === "windows") {
if (arch === "aarch64") {
return {
publisher: "MicrosoftWindowsDesktop",
offer: "windows11preview-arm64",
sku: "win11-24h2-pro",
version: "latest",
};
}
// Windows Server 2019 x64 — oldest supported version
return {
publisher: "MicrosoftWindowsServer",
offer: "WindowsServer",
sku: "2019-datacenter-gensecond",
version: "latest",
};
}
throw new Error(`[azure] Unsupported OS: ${os}`);
}
function getVmSize(arch) {
return arch === "aarch64" ? "Standard_D4ps_v6" : "Standard_D4ds_v6";
}
// ============================================================================
// Exports
// ============================================================================
export const azure = {
get name() {
return "azure";
},
config,
/**
* @param {import("./machine.mjs").MachineOptions} options
* @returns {Promise<import("./machine.mjs").Machine>}
*/
async createMachine(options) {
const { os, arch, tags, sshKeys } = options;
const vmName = `bun-${os}-${arch}-${Date.now()}`;
const publicIpName = `${vmName}-ip`;
const nicName = `${vmName}-nic`;
const vmSize = options.instanceType || getVmSize(arch);
const diskSizeGB = options.diskSizeGb || (os === "windows" ? 150 : 40);
// Generate a random password for the admin account
const adminPassword = `P@${crypto.randomUUID().replace(/-/g, "").substring(0, 20)}!`;
const subnetId = `${rgPath()}/providers/Microsoft.Network/virtualNetworks/bun-ci-vnet/subnets/default`;
const nsgId = `${rgPath()}/providers/Microsoft.Network/networkSecurityGroups/bun-ci-ssh-nsg`;
await createPublicIp(publicIpName);
const nicId = await createNic(nicName, publicIpName, subnetId, nsgId);
// Create VM
const imageReference = options.imageId ? { id: options.imageId } : getBaseImageReference(os, arch);
await createVm({
name: vmName,
vmSize,
imageReference,
osDiskSizeGB: diskSizeGB,
nicId,
adminUsername: "bunadmin",
adminPassword,
tags: tags
? Object.fromEntries(
Object.entries(tags)
.filter(([_, v]) => v != null)
.map(([k, v]) => [k, String(v)]),
)
: undefined,
});
// Wait for public IP to be assigned
let publicIp;
for (let i = 0; i < 30; i++) {
publicIp = await getPublicIpAddress(publicIpName);
if (publicIp) break;
await new Promise(r => setTimeout(r, 5000));
}
if (!publicIp) {
throw new Error(`[azure] Failed to get public IP for ${vmName}`);
}
console.log(`[azure] VM created: ${vmName} at ${publicIp}`);
// Use Azure Run Command for all remote operations instead of SSH.
// This avoids the sshd startup issues on Azure Windows VMs.
const spawnFn = async (command, opts) => {
const script = command.join(" ");
console.log(`[azure] Run: ${script}`);
// Note: Azure Run Command output is limited to the last 4096 bytes.
// Full output is not available — only the tail is returned.
// value[0] = stdout (ComponentStatus/StdOut), value[1] = stderr (ComponentStatus/StdErr)
const result = await runCommand(vmName, [script]);
const values = result?.value ?? [];
const stdout = values[0]?.message ?? "";
const stderr = values[1]?.message ?? "";
if (opts?.stdio === "inherit") {
if (stdout) process.stdout.write(stdout);
if (stderr) process.stderr.write(stderr);
}
// Only use displayStatus to detect errors — stderr often contains non-error
// output (rustup progress, cargo warnings, PowerShell Write-Warning, etc.)
const hasError = values.some(v => v?.displayStatus === "Provisioning failed");
const exitCode = hasError ? 1 : 0;
return { exitCode, stdout, stderr };
};
const spawnSafeFn = async (command, opts) => {
const result = await spawnFn(command, opts);
if (result.exitCode !== 0) {
const msg = result.stderr || result.stdout || "Unknown error";
throw new Error(`[azure] Command failed (exit ${result.exitCode}): ${command.join(" ")}\n${msg}`);
}
return result;
};
const upload = async (source, destination) => {
// Read the file locally and write it on the VM via Run Command
const { readFileSync } = await import("node:fs");
const content = readFileSync(source, "utf-8");
// Escape for PowerShell — use base64 to avoid escaping issues
const b64 = Buffer.from(content).toString("base64");
const script = [
`$bytes = [Convert]::FromBase64String('${b64}')`,
`$dir = Split-Path '${destination}' -Parent`,
`if (-not (Test-Path $dir)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null }`,
`[IO.File]::WriteAllBytes('${destination}', $bytes)`,
`Write-Host "Uploaded to ${destination} ($($bytes.Length) bytes)"`,
];
console.log(`[azure] Uploading ${source} -> ${destination}`);
await runCommand(vmName, script);
};
const attach = async () => {
console.log(`[azure] Attach not supported via Run Command (VM: ${vmName}, IP: ${publicIp})`);
};
const waitForSsh = async () => {
// No SSH needed — Run Command works immediately after VM is provisioned
// Just verify the VM is responsive
console.log(`[azure] Verifying VM is responsive...`);
await runCommand(vmName, ["Write-Host 'VM is ready'"]);
console.log(`[azure] VM is responsive`);
};
const snapshot = async label => {
const vmId = `${rgPath()}/providers/Microsoft.Compute/virtualMachines/${vmName}`;
// Run sysprep inside the VM before deallocating.
// This prepares Windows for generalization so the gallery image
// can be used to create new VMs with OS provisioning.
console.log(`[azure] Running sysprep on ${vmName}...`);
await runCommand(vmName, ["C:\\Windows\\System32\\Sysprep\\sysprep.exe /generalize /oobe /shutdown /quiet"]);
// Wait for VM to shut down after sysprep (sysprep triggers shutdown)
for (let i = 0; i < 60; i++) {
const state = await getVmPowerState(vmName);
if (state === "PowerState/stopped" || state === "PowerState/deallocated") break;
await new Promise(r => setTimeout(r, 10000));
}
// Deallocate the VM
await stopVm(vmName);
// Wait for VM to be deallocated
for (let i = 0; i < 60; i++) {
const state = await getVmPowerState(vmName);
if (state === "PowerState/deallocated") break;
await new Promise(r => setTimeout(r, 5000));
}
await generalizeVm(vmName);
// Ensure gallery and image definition exist.
// Use the label as the image definition name — this matches what ci.mjs
// emits as the image-name agent tag, so robobun can look it up directly.
const imageDefName = label;
await ensureImageDefinition(imageDefName, os, arch);
// Create a single version "1.0.0" under this definition.
await createImageVersion(imageDefName, "1.0.0", vmId);
// Wait for image replication to complete before returning.
// Single-region replication typically takes 5-15 minutes.
const { galleryName } = config();
const versionPath = `${rgPath()}/providers/Microsoft.Compute/galleries/${galleryName}/images/${imageDefName}/versions/1.0.0`;
console.log(`[azure] Waiting for image replication...`);
for (let i = 0; i < 120; i++) {
const ver = await azureFetch("GET", versionPath, undefined, GALLERY_API_VERSION);
const state = ver?.properties?.provisioningState;
if (state === "Succeeded") {
console.log(`[azure] Image ready: ${imageDefName}/1.0.0`);
break;
}
if (state === "Failed") {
throw new Error(`[azure] Image replication failed: ${JSON.stringify(ver?.properties)}`);
}
if (i % 6 === 0) {
console.log(`[azure] Image replicating... (${i}m elapsed)`);
}
await new Promise(r => setTimeout(r, 10_000));
}
return label;
};
const terminate = async () => {
await deleteVm(vmName);
// Resources with deleteOption=Delete are cleaned up automatically
// But clean up anything that might be left
await deleteNic(nicName);
await deletePublicIp(publicIpName);
};
return {
cloud: "azure",
id: vmName,
imageId: options.imageId,
instanceType: vmSize,
region: config().location,
get publicIp() {
return publicIp;
},
spawn: spawnFn,
spawnSafe: spawnSafeFn,
upload,
attach,
snapshot,
waitForSsh,
close: terminate,
[Symbol.asyncDispose]: terminate,
};
},
};

View File

@@ -1,6 +1,7 @@
# Version: 12
# A script that installs the dependencies needed to build and test Bun.
# This should work on Windows 10 or newer with PowerShell.
# Version: 14
# A script that installs the dependencies needed to build and test Bun on Windows.
# Supports both x64 and ARM64 using Scoop for package management.
# Used by Azure [build images] pipeline.
# If this script does not work on your machine, please open an issue:
# https://github.com/oven-sh/bun/issues
@@ -11,7 +12,7 @@
param (
[Parameter(Mandatory = $false)]
[switch]$CI = $false,
[switch]$CI = ($env:CI -eq "true"),
[Parameter(Mandatory = $false)]
[switch]$Optimize = $CI
)
@@ -19,17 +20,26 @@ param (
$ErrorActionPreference = "Stop"
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
function Execute-Command {
$command = $args -join ' '
Write-Output "$ $command"
# Detect ARM64 from registry (works even under x64 emulation)
$realArch = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment').PROCESSOR_ARCHITECTURE
$script:IsARM64 = $realArch -eq "ARM64"
& $args[0] $args[1..$args.Length]
if ((-not $?) -or ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE)) {
throw "Command failed: $command"
# If we're on ARM64 but running under x64 emulation, re-launch as native ARM64.
# Azure Run Command uses x64-emulated PowerShell which breaks package installs.
if ($script:IsARM64 -and $env:PROCESSOR_ARCHITECTURE -ne "ARM64") {
$nativePS = "$env:SystemRoot\Sysnative\WindowsPowerShell\v1.0\powershell.exe"
if (Test-Path $nativePS) {
Write-Output "Re-launching bootstrap as native ARM64 PowerShell..."
& $nativePS -NoProfile -ExecutionPolicy Bypass -File $MyInvocation.MyCommand.Path @PSBoundParameters
exit $LASTEXITCODE
}
}
# ============================================================================
# Utility functions
# ============================================================================
function Which {
param ([switch]$Required = $false)
@@ -46,16 +56,6 @@ function Which {
}
}
function Execute-Script {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Path
)
$pwsh = Which pwsh powershell -Required
Execute-Command $pwsh $Path
}
function Download-File {
param (
[Parameter(Mandatory = $true, Position = 0)]
@@ -87,18 +87,6 @@ function Download-File {
return $Path
}
function Install-Chocolatey {
if (Which choco) {
return
}
Write-Output "Installing Chocolatey..."
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
$installScript = Download-File "https://community.chocolatey.org/install.ps1"
Execute-Script $installScript
Refresh-Path
}
function Refresh-Path {
$paths = @(
[System.Environment]::GetEnvironmentVariable("Path", "Machine"),
@@ -111,15 +99,19 @@ function Refresh-Path {
Where-Object { $_ -and (Test-Path $_) } |
Select-Object -Unique
$env:Path = ($uniquePaths -join ';').TrimEnd(';')
if ($env:ChocolateyInstall) {
Import-Module $env:ChocolateyInstall\helpers\chocolateyProfile.psm1 -ErrorAction SilentlyContinue
}
}
function Add-To-Path {
$absolutePath = Resolve-Path $args[0]
$currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$PathToAdd,
[Parameter(Mandatory = $false)]
[ValidateSet("Machine", "User")]
[string]$Scope = "Machine"
)
$absolutePath = Resolve-Path $PathToAdd
$currentPath = [Environment]::GetEnvironmentVariable("Path", $Scope)
if ($currentPath -like "*$absolutePath*") {
return
}
@@ -140,8 +132,8 @@ function Add-To-Path {
}
}
Write-Output "Adding $absolutePath to PATH..."
[Environment]::SetEnvironmentVariable("Path", "$newPath", "Machine")
Write-Output "Adding $absolutePath to PATH ($Scope)..."
[Environment]::SetEnvironmentVariable("Path", "$newPath", $Scope)
Refresh-Path
}
@@ -158,104 +150,374 @@ function Set-Env {
[System.Environment]::SetEnvironmentVariable("$Name", "$Value", "Process")
}
function Install-Package {
# ============================================================================
# Scoop — ARM64-native package manager
# ============================================================================
function Install-Scoop {
if (Which scoop) {
return
}
Write-Output "Installing Scoop..."
# Scoop blocks admin installs unless -RunAsAdmin is passed.
# Install to a known global location so all users can access it.
$env:SCOOP = "C:\Scoop"
[Environment]::SetEnvironmentVariable("SCOOP", $env:SCOOP, "Machine")
iex "& {$(irm get.scoop.sh)} -RunAsAdmin -ScoopDir C:\Scoop"
Add-To-Path "C:\Scoop\shims"
Refresh-Path
Write-Output "Scoop version: $(scoop --version)"
}
function Install-Scoop-Package {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Name,
[Parameter(Mandatory = $false)]
[string]$Command = $Name,
[Parameter(Mandatory = $false)]
[string]$Version,
[Parameter(Mandatory = $false)]
[switch]$Force = $false,
[Parameter(Mandatory = $false)]
[string[]]$ExtraArgs = @()
[string]$Command = $Name
)
if (-not $Force `
-and (Which $Command) `
-and (-not $Version -or (& $Command --version) -like "*$Version*")) {
if (Which $Command) {
return
}
Write-Output "Installing $Name..."
$flags = @(
"--yes",
"--accept-license",
"--no-progress",
"--force"
)
if ($Version) {
$flags += "--version=$Version"
}
Execute-Command choco install $Name @flags @ExtraArgs
Write-Output "Installing $Name (via Scoop)..."
# Scoop post_install scripts can have non-fatal Remove-Item errors
# (e.g. 7zip ARM64 7zr.exe locked, llvm-arm64 missing Uninstall.exe).
# Suppress all error streams so they don't kill the bootstrap or Packer.
$prevErrorPref = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
scoop install $Name *>&1 | ForEach-Object { "$_" } | Write-Host
$ErrorActionPreference = $prevErrorPref
Refresh-Path
}
function Install-Packages {
foreach ($package in $args) {
Install-Package $package
}
}
function Install-Common-Software {
Install-Chocolatey
Install-Pwsh
Install-Git
Install-Packages curl 7zip nssm
Install-NodeJs
Install-Bun
Install-Cygwin
if ($CI) {
# FIXME: Installing tailscale causes the AWS metadata server to become unreachable
# Install-Tailscale
Install-Buildkite
}
}
function Install-Pwsh {
Install-Package powershell-core -Command pwsh
if ($CI) {
$shellPath = (Which pwsh -Required)
New-ItemProperty `
-Path "HKLM:\\SOFTWARE\\OpenSSH" `
-Name DefaultShell `
-Value $shellPath `
-PropertyType String `
-Force
}
}
# ============================================================================
# Scoop packages (native ARM64 binaries)
# ============================================================================
function Install-Git {
Install-Packages git
Install-Scoop-Package git
# Git for Windows ships Unix tools (cat, head, tail, etc.) in usr\bin
$gitUsrBin = "C:\Scoop\apps\git\current\usr\bin"
if (Test-Path $gitUsrBin) {
Add-To-Path $gitUsrBin
}
if ($CI) {
Execute-Command git config --system --add safe.directory "*"
Execute-Command git config --system core.autocrlf false
Execute-Command git config --system core.eol lf
Execute-Command git config --system core.longpaths true
git config --system --add safe.directory "*"
git config --system core.autocrlf false
git config --system core.eol lf
git config --system core.longpaths true
}
}
function Install-NodeJs {
Install-Package nodejs -Command node -Version "24.3.0"
# Pin to match the ABI version Bun expects (NODE_MODULE_VERSION 137).
# Latest Node (25.x) uses ABI 141 which breaks node-gyp tests.
Install-Scoop-Package "nodejs@24.3.0" -Command node
}
function Install-Bun {
Install-Package bun -Version "1.3.1"
function Install-CMake {
Install-Scoop-Package cmake
}
function Install-Llvm {
$LLVM_VERSION = "21.1.8"
if (Which clang-cl) {
return
}
if ($script:IsARM64) {
Install-Scoop-Package "llvm-arm64@$LLVM_VERSION" -Command clang-cl
} else {
Install-Scoop-Package "llvm@$LLVM_VERSION" -Command clang-cl
}
}
function Install-Ninja {
Install-Scoop-Package ninja
}
function Install-Python {
Install-Scoop-Package python
}
function Install-Go {
Install-Scoop-Package go
}
function Install-Ruby {
Install-Scoop-Package ruby
}
function Install-7zip {
Install-Scoop-Package 7zip -Command 7z
}
function Install-Make {
Install-Scoop-Package make
}
function Install-Cygwin {
Install-Package cygwin
Add-To-Path "C:\tools\cygwin\bin"
# Cygwin's default mirror (mirrors.kernel.org) can be unreachable from Azure.
# Make this non-fatal — the build will fail later if cygwin is actually needed.
try {
Install-Scoop-Package cygwin
# Cygwin binaries are at <scoop>/apps/cygwin/current/root/bin
$cygwinBin = "C:\Scoop\apps\cygwin\current\root\bin"
if (Test-Path $cygwinBin) {
Add-To-Path $cygwinBin # Machine scope (default) — survives Sysprep
}
} catch {
Write-Warning "Cygwin installation failed (non-fatal): $_"
}
}
function Install-Tailscale {
Install-Package tailscale
# ============================================================================
# Manual installs (not available or not ideal via Scoop)
# ============================================================================
function Install-Pwsh {
if (Which pwsh) {
return
}
$pwshArch = if ($script:IsARM64) { "arm64" } else { "x64" }
Write-Output "Installing PowerShell Core ($pwshArch)..."
$msi = Download-File "https://github.com/PowerShell/PowerShell/releases/download/v7.5.2/PowerShell-7.5.2-win-$pwshArch.msi" -Name "pwsh-$pwshArch.msi"
$process = Start-Process msiexec -ArgumentList "/i `"$msi`" /quiet /norestart ADD_PATH=1" -Wait -PassThru -NoNewWindow
if ($process.ExitCode -ne 0) {
throw "Failed to install PowerShell: code $($process.ExitCode)"
}
Remove-Item $msi -ErrorAction SilentlyContinue
Refresh-Path
}
function Install-OpenSSH {
$sshdService = Get-Service -Name sshd -ErrorAction SilentlyContinue
if ($sshdService) {
return
}
Write-Output "Installing OpenSSH Server..."
# Add-WindowsCapability requires DISM elevation which isn't available in Packer's
# WinRM session. Download and install from GitHub releases instead.
$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq "Arm64") { "Arm64" } else { "Win64" }
$url = "https://github.com/PowerShell/Win32-OpenSSH/releases/download/v9.8.1.0p1-Preview/OpenSSH-${arch}.zip"
$zip = "$env:TEMP\OpenSSH.zip"
$dest = "$env:ProgramFiles\OpenSSH"
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
Expand-Archive -Path $zip -DestinationPath "$env:TEMP\OpenSSH" -Force
New-Item -Path $dest -ItemType Directory -Force | Out-Null
$extractedDir = Get-ChildItem -Path "$env:TEMP\OpenSSH" -Directory | Select-Object -First 1
Get-ChildItem -Path $extractedDir.FullName -Recurse | Move-Item -Destination $dest -Force
& "$dest\install-sshd.ps1"
& "$dest\FixHostFilePermissions.ps1" -Confirm:$false
Remove-Item $zip, "$env:TEMP\OpenSSH" -Recurse -Force -ErrorAction SilentlyContinue
# Configure sshd to start on boot (don't start now — host keys may not exist yet during image build)
Set-Service -Name sshd -StartupType Automatic
# Set default shell to pwsh
$pwshPath = (Which pwsh -ErrorAction SilentlyContinue)
if (-not $pwshPath) { $pwshPath = (Which powershell) }
if ($pwshPath) {
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
-Value $pwshPath -PropertyType String -Force
}
# Firewall rule for port 22
$rule = Get-NetFirewallRule -Name "OpenSSH-Server" -ErrorAction SilentlyContinue
if (-not $rule) {
New-NetFirewallRule -Profile Any -Name "OpenSSH-Server" `
-DisplayName "OpenSSH Server (sshd)" -Enabled True `
-Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
}
# Configure sshd_config for key-based auth
$sshdConfigPath = "C:\ProgramData\ssh\sshd_config"
if (Test-Path $sshdConfigPath) {
$config = Get-Content $sshdConfigPath
$config = $config -replace '#PubkeyAuthentication yes', 'PubkeyAuthentication yes'
$config = $config -replace 'PasswordAuthentication yes', 'PasswordAuthentication no'
Set-Content -Path $sshdConfigPath -Value $config
}
Write-Output "OpenSSH Server installed and configured"
# Register a startup task that fetches oven-sh GitHub org members' SSH keys
# on every boot so any bun dev can SSH in.
$fetchScript = @'
try {
$members = Invoke-RestMethod -Uri "https://api.github.com/orgs/oven-sh/members" -Headers @{ "User-Agent" = "bun-ci" }
$keys = @()
foreach ($member in $members) {
if ($member.type -ne "User" -or -not $member.login) { continue }
try {
$userKeys = (Invoke-WebRequest -Uri "https://github.com/$($member.login).keys" -UseBasicParsing).Content
if ($userKeys) { $keys += $userKeys.Trim() }
} catch { }
}
if ($keys.Count -gt 0) {
$keysPath = "C:\ProgramData\ssh\administrators_authorized_keys"
Set-Content -Path $keysPath -Value ($keys -join "`n") -Force
icacls $keysPath /inheritance:r /grant "SYSTEM:(F)" /grant "Administrators:(R)" | Out-Null
}
} catch { }
'@
$scriptPath = "C:\ProgramData\ssh\fetch-ssh-keys.ps1"
Set-Content -Path $scriptPath -Value $fetchScript -Force
$action = New-ScheduledTaskAction -Execute "pwsh.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""
$trigger = New-ScheduledTaskTrigger -AtStartup
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName "FetchSshKeys" -Action $action -Trigger $trigger `
-Settings $settings -User "SYSTEM" -RunLevel Highest -Force
Write-Output "Registered FetchSshKeys startup task"
}
function Install-Ccache {
if (Which ccache) {
return
}
$version = "4.12.2"
$archSuffix = if ($script:IsARM64) { "aarch64" } else { "x86_64" }
Write-Output "Installing ccache $version ($archSuffix)..."
$zip = Download-File "https://github.com/ccache/ccache/releases/download/v$version/ccache-$version-windows-$archSuffix.zip" -Name "ccache-$archSuffix.zip"
$extractDir = "$env:TEMP\ccache-extract"
Expand-Archive $zip $extractDir -Force
$installDir = "$env:ProgramFiles\ccache"
New-Item -Path $installDir -ItemType Directory -Force | Out-Null
Copy-Item "$extractDir\ccache-$version-windows-$archSuffix\*" $installDir -Recurse -Force
Remove-Item $zip -ErrorAction SilentlyContinue
Remove-Item $extractDir -Recurse -ErrorAction SilentlyContinue
Add-To-Path $installDir
}
function Install-Bun {
if (Which bun) {
return
}
if ($script:IsARM64) {
# ARM64 bun binary from blob storage (faster than GitHub releases for CI)
Write-Output "Installing Bun (ARM64)..."
$zip = Download-File "https://buncistore.blob.core.windows.net/artifacts/bun-windows-aarch64.zip" -Name "bun-arm64.zip"
$extractDir = "$env:TEMP\bun-arm64"
Expand-Archive -Path $zip -DestinationPath $extractDir -Force
$bunExe = Get-ChildItem $extractDir -Recurse -Filter "*.exe" | Where-Object { $_.Name -match "bun" } | Select-Object -First 1
if ($bunExe) {
Copy-Item $bunExe.FullName "C:\Windows\System32\bun.exe" -Force
Write-Output "Bun ARM64 installed to C:\Windows\System32\bun.exe"
} else {
throw "Failed to find bun executable in ARM64 zip"
}
} else {
Write-Output "Installing Bun..."
$installScript = Download-File "https://bun.sh/install.ps1" -Name "bun-install.ps1"
$pwsh = Which pwsh powershell -Required
& $pwsh $installScript
Refresh-Path
# Copy to System32 so it survives Sysprep (user profile PATH is lost)
$bunPath = Which bun
if ($bunPath) {
Copy-Item $bunPath "C:\Windows\System32\bun.exe" -Force
Write-Output "Bun copied to C:\Windows\System32\bun.exe"
}
}
}
function Install-Rust {
if (Which rustc) {
return
}
$rustPath = Join-Path $env:ProgramFiles "Rust"
if (-not (Test-Path $rustPath)) {
New-Item -Path $rustPath -ItemType Directory | Out-Null
}
# Set install paths before running rustup so it installs directly
# to Program Files (avoids issues with SYSTEM user profile path)
$env:CARGO_HOME = "$rustPath\cargo"
$env:RUSTUP_HOME = "$rustPath\rustup"
Write-Output "Installing Rustup..."
$rustupInit = Download-File "https://win.rustup.rs/" -Name "rustup-init.exe"
Write-Output "Installing Rust..."
& $rustupInit -y
Write-Output "Setting environment variables for Rust..."
Set-Env "CARGO_HOME" "$rustPath\cargo"
Set-Env "RUSTUP_HOME" "$rustPath\rustup"
Add-To-Path "$rustPath\cargo\bin"
}
function Install-Visual-Studio {
param (
[Parameter(Mandatory = $false)]
[string]$Edition = "community"
)
Write-Output "Downloading Visual Studio installer..."
$vsInstaller = Download-File "https://aka.ms/vs/17/release/vs_$Edition.exe"
Write-Output "Installing Visual Studio..."
$vsInstallArgs = @(
"--passive",
"--norestart",
"--wait",
"--force",
"--locale en-US",
"--add Microsoft.VisualStudio.Workload.NativeDesktop",
"--includeRecommended"
)
$process = Start-Process $vsInstaller -ArgumentList ($vsInstallArgs -join ' ') -Wait -PassThru -NoNewWindow
# Exit code 3010 means "reboot required" which is not a real error
if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) {
throw "Failed to install Visual Studio: code $($process.ExitCode)"
}
}
function Install-PdbAddr2line {
cargo install --examples "pdb-addr2line@0.11.2"
# Also copy to System32 so it's always on PATH (like bun.exe)
$src = Join-Path $env:CARGO_HOME "bin\pdb-addr2line.exe"
if (Test-Path $src) {
Copy-Item $src "C:\Windows\System32\pdb-addr2line.exe" -Force
Write-Output "pdb-addr2line copied to C:\Windows\System32"
}
}
function Install-Nssm {
if (Which nssm) {
return
}
# Try Scoop first, fall back to our mirror if nssm.cc is down (503 errors)
Install-Scoop-Package nssm
if (-not (Which nssm)) {
Write-Output "Scoop install of nssm failed, downloading from mirror..."
$zip = Download-File "https://buncistore.blob.core.windows.net/artifacts/nssm-2.24-103-gdee49fc.zip" -Name "nssm.zip"
Expand-Archive -Path $zip -DestinationPath "C:\Windows\Temp\nssm" -Force
$nssm = Get-ChildItem "C:\Windows\Temp\nssm" -Recurse -Filter "nssm.exe" | Where-Object { $_.DirectoryName -like "*win64*" } | Select-Object -First 1
if ($nssm) {
Copy-Item $nssm.FullName "C:\Windows\System32\nssm.exe" -Force
Write-Output "nssm installed to C:\Windows\System32\nssm.exe"
} else {
throw "Failed to install nssm"
}
}
}
# ============================================================================
# Buildkite
# ============================================================================
function Create-Buildkite-Environment-Hooks {
param (
[Parameter(Mandatory = $true)]
@@ -278,6 +540,17 @@ function Create-Buildkite-Environment-Hooks {
"@ | Set-Content -Path $environmentHook -Encoding UTF8
Write-Output "Environment hook created at $environmentHook"
# pre-exit hook: logout from Tailscale so ephemeral nodes are removed
# instantly instead of waiting 30-60 minutes. This runs after the job
# finishes, which is after the SSH user wait loop in runner.node.mjs.
$preExitHook = Join-Path $hooksDir "pre-exit.ps1"
@"
if (Test-Path "C:\Program Files\Tailscale\tailscale.exe") {
& "C:\Program Files\Tailscale\tailscale.exe" logout 2>`$null
}
"@ | Set-Content -Path $preExitHook -Encoding UTF8
Write-Output "Pre-exit hook created at $preExitHook"
}
function Install-Buildkite {
@@ -288,7 +561,8 @@ function Install-Buildkite {
Write-Output "Installing Buildkite agent..."
$env:buildkiteAgentToken = "xxx"
$installScript = Download-File "https://raw.githubusercontent.com/buildkite/agent/main/install.ps1"
Execute-Script $installScript
$pwsh = Which pwsh powershell -Required
& $pwsh $installScript
Refresh-Path
if ($CI) {
@@ -300,96 +574,9 @@ function Install-Buildkite {
}
}
function Install-Build-Essentials {
Install-Visual-Studio
Install-Packages `
cmake `
make `
ninja `
python `
golang `
nasm `
ruby `
strawberryperl `
mingw
Install-Rust
Install-Ccache
# Needed to remap stack traces
Install-PdbAddr2line
Install-Llvm
}
function Install-Visual-Studio {
param (
[Parameter(Mandatory = $false)]
[string]$Edition = "community"
)
Write-Output "Downloading Visual Studio installer..."
$vsInstaller = Download-File "https://aka.ms/vs/17/release/vs_$Edition.exe"
Write-Output "Installing Visual Studio..."
$vsInstallArgs = @(
"--passive",
"--norestart",
"--wait",
"--force",
"--locale en-US",
"--add Microsoft.VisualStudio.Workload.NativeDesktop",
"--includeRecommended"
)
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $vsInstaller
$startInfo.Arguments = $vsInstallArgs -join ' '
$startInfo.CreateNoWindow = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
$process.Start()
$process.WaitForExit()
if ($process.ExitCode -ne 0) {
throw "Failed to install Visual Studio: code $($process.ExitCode)"
}
}
function Install-Rust {
if (Which rustc) {
return
}
Write-Output "Installing Rustup..."
$rustupInit = Download-File "https://win.rustup.rs/" -Name "rustup-init.exe"
Write-Output "Installing Rust..."
Execute-Command $rustupInit -y
Write-Output "Moving Rust to $env:ProgramFiles..."
$rustPath = Join-Path $env:ProgramFiles "Rust"
if (-not (Test-Path $rustPath)) {
New-Item -Path $rustPath -ItemType Directory
}
Move-Item "$env:UserProfile\.cargo" "$rustPath\cargo" -Force
Move-Item "$env:UserProfile\.rustup" "$rustPath\rustup" -Force
Write-Output "Setting environment variables for Rust..."
Set-Env "CARGO_HOME" "$rustPath\cargo"
Set-Env "RUSTUP_HOME" "$rustPath\rustup"
Add-To-Path "$rustPath\cargo\bin"
}
function Install-Ccache {
Install-Package ccache
}
function Install-PdbAddr2line {
Execute-Command cargo install --examples "pdb-addr2line@0.11.2"
}
function Install-Llvm {
Install-Package llvm `
-Command clang-cl `
-Version "21.1.8"
Add-To-Path "$env:ProgramFiles\LLVM\bin"
}
# ============================================================================
# System optimization
# ============================================================================
function Optimize-System {
Disable-Windows-Defender
@@ -417,8 +604,11 @@ function Disable-Windows-Threat-Protection {
}
function Uninstall-Windows-Defender {
Write-Output "Uninstalling Windows Defender..."
Uninstall-WindowsFeature -Name Windows-Defender
# Requires a reboot — run before the windows-restart Packer provisioner.
if (Get-Command Uninstall-WindowsFeature -ErrorAction SilentlyContinue) {
Write-Output "Uninstalling Windows Defender..."
Uninstall-WindowsFeature -Name Windows-Defender
}
}
function Disable-Windows-Services {
@@ -432,8 +622,12 @@ function Disable-Windows-Services {
)
foreach ($service in $services) {
Stop-Service $service -Force
Set-Service $service -StartupType Disabled
try {
Stop-Service $service -Force -ErrorAction SilentlyContinue
Set-Service $service -StartupType Disabled -ErrorAction SilentlyContinue
} catch {
Write-Warning "Could not disable service: $service"
}
}
}
@@ -448,13 +642,98 @@ function Disable-Power-Management {
powercfg /change hibernate-timeout-dc 0
}
# ============================================================================
# Main
# ============================================================================
if ($Optimize) {
Optimize-System
}
Install-Common-Software
Install-Build-Essentials
# Scoop package manager
Install-Scoop
# Packages via Scoop (native ARM64 or x64 depending on architecture)
# 7zip must be installed before git — git depends on 7zip via Scoop,
# and 7zip's post_install has a cleanup error on ARM64 SYSTEM context.
Install-7zip
Install-Git
Install-NodeJs
Install-CMake
Install-Ninja
Install-Python
Install-Go
Install-Ruby
Install-Make
Install-Llvm
Install-Cygwin
Install-Nssm
Install-Scoop-Package perl
# x64-only packages (not needed on ARM64)
if (-not $script:IsARM64) {
Install-Scoop-Package nasm
Install-Scoop-Package mingw -Command gcc
}
function Install-Tailscale {
if (Which tailscale -ErrorAction SilentlyContinue) {
return
}
Write-Output "Installing Tailscale..."
$msi = "$env:TEMP\tailscale-setup.msi"
$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq "Arm64") { "arm64" } else { "amd64" }
Invoke-WebRequest -Uri "https://pkgs.tailscale.com/stable/tailscale-setup-latest-${arch}.msi" -OutFile $msi -UseBasicParsing
Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /quiet /norestart" -Wait
Remove-Item $msi -ErrorAction SilentlyContinue
Refresh-Path
Write-Output "Tailscale installed"
# Register a startup task that reads the tailscale authkey from Azure IMDS
# tags and joins the tailnet. The key is set by robobun as a VM tag.
$joinScript = @'
try {
$headers = @{ "Metadata" = "true" }
$response = Invoke-RestMethod -Uri "http://169.254.169.254/metadata/instance/compute/tagsList?api-version=2021-02-01" -Headers $headers
$authkey = ($response | Where-Object { $_.name -eq "tailscale:authkey" }).value
if ($authkey) {
$stepKey = ($response | Where-Object { $_.name -eq "buildkite:step-key" }).value
$buildNumber = ($response | Where-Object { $_.name -eq "buildkite:build-number" }).value
if ($stepKey) {
$hostname = "azure-${stepKey}"
if ($buildNumber) { $hostname += "-${buildNumber}" }
} else {
$hostname = (Invoke-RestMethod -Uri "http://169.254.169.254/metadata/instance/compute/name?api-version=2021-02-01&format=text" -Headers $headers)
}
& "C:\Program Files\Tailscale\tailscale.exe" up --authkey=$authkey --hostname=$hostname --unattended
}
} catch { }
'@
$scriptPath = "C:\ProgramData\tailscale-join.ps1"
Set-Content -Path $scriptPath -Value $joinScript -Force
$action = New-ScheduledTaskAction -Execute "pwsh.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""
$trigger = New-ScheduledTaskTrigger -AtStartup
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName "TailscaleJoin" -Action $action -Trigger $trigger `
-Settings $settings -User "SYSTEM" -RunLevel Highest -Force
Write-Output "Registered TailscaleJoin startup task"
}
# Manual installs (not in Scoop or need special handling)
Install-Pwsh
Install-OpenSSH
#Install-Tailscale # Disabled — Tailscale adapter interferes with IPv6 multicast tests (node-dgram)
Install-Bun
Install-Ccache
Install-Rust
Install-Visual-Studio
Install-PdbAddr2line
if ($CI) {
Install-Buildkite
}
if ($Optimize) {
Optimize-System-Needs-Reboot
}
}

View File

@@ -4,7 +4,6 @@ import { basename, join, relative, resolve } from "node:path";
import {
formatAnnotationToHtml,
getEnv,
getSecret,
isCI,
isWindows,
parseAnnotations,
@@ -14,14 +13,7 @@ import {
startGroup,
} from "./utils.mjs";
// Detect Windows ARM64 - bun may run under x64 emulation (WoW64), so check multiple indicators
const isWindowsARM64 =
isWindows &&
(process.env.PROCESSOR_ARCHITECTURE === "ARM64" ||
process.env.VSCMD_ARG_HOST_ARCH === "arm64" ||
process.env.MSYSTEM_CARCH === "aarch64" ||
(process.env.PROCESSOR_IDENTIFIER || "").includes("ARMv8") ||
process.arch === "arm64");
const isWindowsARM64 = isWindows && process.arch === "arm64";
if (globalThis.Bun) {
await import("./glob-sources.mjs");
@@ -57,11 +49,7 @@ async function build(args) {
if (process.platform === "win32" && !process.env["VSINSTALLDIR"]) {
const shellPath = join(import.meta.dirname, "vs-shell.ps1");
const scriptPath = import.meta.filename;
// When cross-compiling to ARM64, tell vs-shell.ps1 to set up the x64_arm64 VS environment
const toolchainIdx = args.indexOf("--toolchain");
const requestedVsArch = toolchainIdx !== -1 && args[toolchainIdx + 1] === "windows-aarch64" ? "arm64" : undefined;
const env = requestedVsArch ? { ...process.env, BUN_VS_ARCH: requestedVsArch } : undefined;
return spawn("pwsh", ["-NoProfile", "-NoLogo", "-File", shellPath, process.argv0, scriptPath, ...args], { env });
return spawn("pwsh", ["-NoProfile", "-NoLogo", "-File", shellPath, process.argv0, scriptPath, ...args]);
}
if (isCI) {
@@ -176,35 +164,6 @@ async function spawn(command, args, options, label) {
const pipe = process.env.CI === "true";
if (isBuildkite()) {
if (process.env.BUN_LINK_ONLY && isWindows) {
env ||= options?.env || { ...process.env };
// Pass signing secrets directly to the build process
// The PowerShell signing script will handle certificate decoding
env.SM_CLIENT_CERT_PASSWORD = getSecret("SM_CLIENT_CERT_PASSWORD", {
redact: true,
required: true,
});
env.SM_CLIENT_CERT_FILE = getSecret("SM_CLIENT_CERT_FILE", {
redact: true,
required: true,
});
env.SM_API_KEY = getSecret("SM_API_KEY", {
redact: true,
required: true,
});
env.SM_KEYPAIR_ALIAS = getSecret("SM_KEYPAIR_ALIAS", {
redact: true,
required: true,
});
env.SM_HOST = getSecret("SM_HOST", {
redact: true,
required: true,
});
}
}
const subprocess = nodeSpawn(command, effectiveArgs, {
stdio: pipe ? "pipe" : "inherit",
...options,

View File

@@ -1,9 +1,11 @@
#!/usr/bin/env node
import { existsSync, mkdtempSync, readdirSync } from "node:fs";
import { chmodSync, existsSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, extname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { inspect, parseArgs } from "node:util";
import { azure } from "./azure.mjs";
import { docker } from "./docker.mjs";
import { tart } from "./tart.mjs";
import {
@@ -35,7 +37,6 @@ import {
spawnSshSafe,
spawnSyncSafe,
startGroup,
tmpdir,
waitForPort,
which,
writeFile,
@@ -1047,16 +1048,14 @@ function getRdpFile(hostname, username) {
* @property {(options: MachineOptions) => Promise<Machine>} createMachine
*/
/**
* @param {string} name
* @returns {Cloud}
*/
function getCloud(name) {
switch (name) {
case "docker":
return docker;
case "aws":
return aws;
case "azure":
return azure;
case "tart":
return tart;
}
@@ -1127,6 +1126,173 @@ function getCloud(name) {
* @property {SshKey[]} sshKeys
*/
async function getAzureToken(tenantId, clientId, clientSecret) {
const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `grant_type=client_credentials&client_id=${clientId}&client_secret=${encodeURIComponent(clientSecret)}&scope=https://management.azure.com/.default`,
});
if (!response.ok) throw new Error(`Azure auth failed: ${response.status}`);
const data = await response.json();
return data.access_token;
}
/**
* Build a Windows image using Packer (Azure only).
* Packer handles VM creation, bootstrap, sysprep, and gallery capture via WinRM.
* This eliminates all the Azure Run Command issues (output truncation, x64 emulation,
* PATH not refreshing, stderr false positives, quote escaping).
*/
async function buildWindowsImageWithPacker({ os, arch, release, command, ci, agentPath, bootstrapPath }) {
const { getSecret } = await import("./utils.mjs");
// Determine Packer template
const templateName = arch === "aarch64" ? "windows-arm64" : "windows-x64";
const templateDir = resolve(import.meta.dirname, "packer");
const templateFile = join(templateDir, `${templateName}.pkr.hcl`);
if (!existsSync(templateFile)) {
throw new Error(`Packer template not found: ${templateFile}`);
}
// Get Azure credentials from Buildkite secrets
const clientId = await getSecret("AZURE_CLIENT_ID");
const clientSecret = await getSecret("AZURE_CLIENT_SECRET");
const subscriptionId = await getSecret("AZURE_SUBSCRIPTION_ID");
const tenantId = await getSecret("AZURE_TENANT_ID");
const resourceGroup = await getSecret("AZURE_RESOURCE_GROUP");
const location = (await getSecret("AZURE_LOCATION")) || "eastus2";
const galleryName = (await getSecret("AZURE_GALLERY_NAME")) || "bunCIGallery2";
// Image naming must match getImageName() in ci.mjs:
// [publish images] / normal CI: "windows-x64-2019-v13"
// [build images]: "windows-x64-2019-build-37194"
const imageKey = arch === "aarch64" ? "windows-aarch64-11" : "windows-x64-2019";
const imageDefName =
command === "publish-image"
? `${imageKey}-v${getBootstrapVersion(os)}`
: ci
? `${imageKey}-build-${getBuildNumber()}`
: `${imageKey}-build-draft-${Date.now()}`;
const galleryArch = arch === "aarch64" ? "Arm64" : "x64";
console.log(`[packer] Ensuring gallery image definition: ${imageDefName}`);
const galleryPath = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Compute/galleries/${galleryName}/images/${imageDefName}`;
const token = await getAzureToken(tenantId, clientId, clientSecret);
const defResponse = await fetch(`https://management.azure.com${galleryPath}?api-version=2024-03-03`, {
method: "PUT",
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
location: location,
properties: {
osType: "Windows",
osState: "Generalized",
hyperVGeneration: "V2",
architecture: galleryArch,
identifier: { publisher: "bun", offer: `${os}-${arch}-ci`, sku: imageDefName },
features: [
{ name: "DiskControllerTypes", value: "SCSI, NVMe" },
{ name: "SecurityType", value: "TrustedLaunch" },
],
},
}),
});
if (!defResponse.ok && defResponse.status !== 409) {
throw new Error(`Failed to create gallery image definition: ${defResponse.status} ${await defResponse.text()}`);
}
// Install Packer if not available
const packerBin = await ensurePacker();
// Initialize plugins
console.log("[packer] Initializing plugins...");
await spawnSafe([packerBin, "init", templateDir], { stdio: "inherit" });
// Build the image
console.log(`[packer] Building ${templateName} image: ${imageDefName}`);
const packerArgs = [
packerBin,
"build",
"-only",
`azure-arm.${templateName}`,
"-var",
`client_id=${clientId}`,
"-var",
`client_secret=${clientSecret}`,
"-var",
`subscription_id=${subscriptionId}`,
"-var",
`tenant_id=${tenantId}`,
"-var",
`resource_group=${resourceGroup}-EASTUS2`,
"-var",
`gallery_resource_group=${resourceGroup}`,
"-var",
`location=${location}`,
"-var",
`gallery_name=${galleryName}`,
"-var",
`image_name=${imageDefName}`,
"-var",
`bootstrap_script=${bootstrapPath}`,
"-var",
`agent_script=${agentPath}`,
templateDir,
];
await spawnSafe(packerArgs, {
stdio: "inherit",
env: {
...process.env,
// Packer also reads these env vars
ARM_CLIENT_ID: clientId,
ARM_CLIENT_SECRET: clientSecret,
ARM_SUBSCRIPTION_ID: subscriptionId,
ARM_TENANT_ID: tenantId,
},
});
console.log(`[packer] Image built successfully: ${imageDefName}`);
}
/**
* Download and install Packer if not already available.
*/
async function ensurePacker() {
// Check if packer is already in PATH
const packerPath = which("packer");
if (packerPath) {
console.log("[packer] Found:", packerPath);
return packerPath;
}
// Check if we have a local copy
const localPacker = join(tmpdir(), "packer");
if (existsSync(localPacker)) {
return localPacker;
}
// Download Packer
const version = "1.15.0";
const platform = process.platform === "win32" ? "windows" : process.platform;
const packerArch = process.arch === "arm64" ? "arm64" : "amd64";
const url = `https://releases.hashicorp.com/packer/${version}/packer_${version}_${platform}_${packerArch}.zip`;
console.log(`[packer] Downloading Packer ${version}...`);
const zipPath = join(tmpdir(), "packer.zip");
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to download Packer: ${response.status}`);
const buffer = Buffer.from(await response.arrayBuffer());
writeFileSync(zipPath, buffer);
// Extract
await spawnSafe(["unzip", "-o", zipPath, "-d", tmpdir()], { stdio: "inherit" });
chmodSync(localPacker, 0o755);
console.log(`[packer] Installed Packer ${version}`);
return localPacker;
}
async function main() {
const { positionals } = parseArgs({
allowPositionals: true,
@@ -1269,6 +1435,13 @@ async function main() {
}
}
// Use Packer for Windows Azure image builds — it handles VM creation,
// bootstrap, sysprep, and gallery capture via WinRM (no Run Command hacks).
if (args["cloud"] === "azure" && os === "windows" && (command === "create-image" || command === "publish-image")) {
await buildWindowsImageWithPacker({ os, arch, release, command, ci, agentPath, bootstrapPath });
return;
}
/** @type {Machine} */
const machine = await startGroup("Creating machine...", async () => {
console.log("Creating machine:");
@@ -1342,7 +1515,7 @@ async function main() {
});
}
await startGroup("Connecting with SSH...", async () => {
await startGroup(`Connecting${options.cloud === "azure" ? "" : " with SSH"}...`, async () => {
const command = os === "windows" ? ["cmd", "/c", "ver"] : ["uname", "-a"];
await machine.spawnSafe(command, { stdio: "inherit" });
});
@@ -1392,7 +1565,12 @@ async function main() {
if (cloud.name === "docker" || features?.includes("docker")) {
return;
}
await machine.spawnSafe(["node", remotePath, "install"], { stdio: "inherit" });
// Refresh PATH from registry before running agent.mjs — bootstrap added
// buildkite-agent to PATH but Azure Run Command sessions have stale PATH.
const cmd = `$env:PATH = [Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('PATH', 'User'); C:\\Scoop\\apps\\nodejs\\current\\node.exe ${remotePath} install`;
await machine.spawnSafe(["powershell", "-NoProfile", "-Command", cmd], {
stdio: "inherit",
});
});
} else {
const tmpPath = "/tmp/agent.mjs";

View File

@@ -0,0 +1,74 @@
packer {
required_plugins {
azure = {
source = "github.com/hashicorp/azure"
version = "= 2.5.0"
}
}
}
// Shared variables for all Windows image builds
variable "client_id" {
type = string
default = env("AZURE_CLIENT_ID")
}
variable "client_secret" {
type = string
sensitive = true
default = env("AZURE_CLIENT_SECRET")
}
variable "subscription_id" {
type = string
default = env("AZURE_SUBSCRIPTION_ID")
}
variable "tenant_id" {
type = string
default = env("AZURE_TENANT_ID")
}
variable "resource_group" {
type = string
default = env("AZURE_RESOURCE_GROUP")
}
variable "location" {
type = string
default = "eastus2"
}
variable "gallery_name" {
type = string
default = "bunCIGallery2"
}
variable "build_number" {
type = string
default = "0"
}
variable "image_name" {
type = string
default = ""
description = "Gallery image definition name. If empty, derived from build_number."
}
variable "bootstrap_script" {
type = string
default = "scripts/bootstrap.ps1"
}
variable "agent_script" {
type = string
default = ""
description = "Path to bundled agent.mjs. If empty, agent install is skipped."
}
variable "gallery_resource_group" {
type = string
default = "BUN-CI"
description = "Resource group containing the Compute Gallery (may differ from build RG)"
}

View File

@@ -0,0 +1,143 @@
source "azure-arm" "windows-arm64" {
// Authentication
client_id = var.client_id
client_secret = var.client_secret
subscription_id = var.subscription_id
tenant_id = var.tenant_id
// Source image Windows 11 ARM64 (no Windows Server ARM64 exists)
os_type = "Windows"
image_publisher = "MicrosoftWindowsDesktop"
image_offer = "windows11preview-arm64"
image_sku = "win11-24h2-pro"
image_version = "latest"
// Build VM only used during image creation, not for CI runners.
// CI runner VM sizes are set in ci.mjs (azureVmSizes).
vm_size = "Standard_D4ps_v6"
// Use existing resource group instead of creating a temp one
build_resource_group_name = var.resource_group
os_disk_size_gb = 150
// Security
security_type = "TrustedLaunch"
secure_boot_enabled = true
vtpm_enabled = true
// Networking Packer creates a temp VNet + public IP + NSG automatically.
// WinRM communicator
communicator = "winrm"
winrm_use_ssl = true
winrm_insecure = true
winrm_timeout = "15m"
winrm_username = "packer"
// CRITICAL: No managed_image_name ARM64 doesn't support Managed Images.
// Packer publishes directly from the VM to the gallery (PR #242 feature).
shared_image_gallery_destination {
subscription = var.subscription_id
resource_group = var.gallery_resource_group
gallery_name = var.gallery_name
image_name = var.image_name != "" ? var.image_name : "windows-aarch64-11-build-${var.build_number}"
image_version = "1.0.0"
storage_account_type = "Standard_LRS"
target_region { name = var.location }
target_region { name = "australiaeast" }
target_region { name = "brazilsouth" }
target_region { name = "canadacentral" }
target_region { name = "canadaeast" }
target_region { name = "centralindia" }
target_region { name = "centralus" }
target_region { name = "francecentral" }
target_region { name = "germanywestcentral" }
target_region { name = "italynorth" }
target_region { name = "japaneast" }
target_region { name = "japanwest" }
target_region { name = "koreacentral" }
target_region { name = "mexicocentral" }
target_region { name = "northcentralus" }
target_region { name = "northeurope" }
target_region { name = "southcentralus" }
target_region { name = "southeastasia" }
target_region { name = "spaincentral" }
target_region { name = "swedencentral" }
target_region { name = "switzerlandnorth" }
target_region { name = "uaenorth" }
target_region { name = "ukwest" }
target_region { name = "westeurope" }
target_region { name = "westus" }
target_region { name = "westus2" }
target_region { name = "westus3" }
}
azure_tags = {
os = "windows"
arch = "aarch64"
build = var.build_number
}
}
build {
sources = ["source.azure-arm.windows-arm64"]
// Step 1: Run bootstrap installs all build dependencies
provisioner "powershell" {
script = var.bootstrap_script
valid_exit_codes = [0, 3010]
environment_vars = ["CI=true"]
}
// Step 2: Upload agent.mjs
provisioner "file" {
source = var.agent_script
destination = "C:\\buildkite-agent\\agent.mjs"
}
// Step 3: Install agent service via nssm
provisioner "powershell" {
inline = [
"C:\\Scoop\\apps\\nodejs\\current\\node.exe C:\\buildkite-agent\\agent.mjs install"
]
valid_exit_codes = [0]
}
// Step 4: Reboot to clear pending updates (VS Build Tools, Windows Updates)
provisioner "windows-restart" {
restart_timeout = "10m"
}
// Step 5: Sysprep MUST be last provisioner
provisioner "powershell" {
inline = [
"Remove-Item -Recurse -Force C:\\Windows\\Panther -ErrorAction SilentlyContinue",
"Write-Output '>>> Clearing pending reboot flags...'",
"Remove-Item 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending' -Recurse -Force -ErrorAction SilentlyContinue",
"Remove-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update' -Name 'RebootRequired' -Force -ErrorAction SilentlyContinue",
"Remove-Item 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired' -Recurse -Force -ErrorAction SilentlyContinue",
"Remove-ItemProperty 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager' -Name 'PendingFileRenameOperations' -Force -ErrorAction SilentlyContinue",
"Write-Output '>>> Waiting for Azure Guest Agent...'",
"while ((Get-Service RdAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
"while ((Get-Service WindowsAzureGuestAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
"Write-Output '>>> Running Sysprep...'",
"$global:LASTEXITCODE = 0",
"& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit /mode:vm",
"$timeout = 300; $elapsed = 0",
"while ($true) {",
" $imageState = (Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State).ImageState",
" Write-Output \"ImageState: $imageState ($${elapsed}s)\"",
" if ($imageState -eq 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { break }",
" if ($elapsed -ge $timeout) {",
" Write-Error \"Timed out after $${timeout}s -- stuck at $imageState\"",
" Get-Content \"$env:SystemRoot\\System32\\Sysprep\\Panther\\setupact.log\" -Tail 100 -ErrorAction SilentlyContinue",
" exit 1",
" }",
" Start-Sleep -s 10",
" $elapsed += 10",
"}",
"Write-Output '>>> Sysprep complete.'"
]
}
}

View File

@@ -0,0 +1,144 @@
source "azure-arm" "windows-x64" {
// Authentication (from env vars or -var flags)
client_id = var.client_id
client_secret = var.client_secret
subscription_id = var.subscription_id
tenant_id = var.tenant_id
// Source image Windows Server 2019 Gen2
os_type = "Windows"
image_publisher = "MicrosoftWindowsServer"
image_offer = "WindowsServer"
image_sku = "2019-datacenter-gensecond"
image_version = "latest"
// Build VM only used during image creation, not for CI runners.
// CI runner VM sizes are set in ci.mjs (azureVmSizes).
vm_size = "Standard_D4ds_v6"
// Use existing resource group instead of creating a temp one
build_resource_group_name = var.resource_group
os_disk_size_gb = 150
// Security
security_type = "TrustedLaunch"
secure_boot_enabled = true
vtpm_enabled = true
// Networking Packer creates a temp VNet + public IP + NSG automatically.
// WinRM needs the public IP to connect from CI runners.
// WinRM communicator Packer auto-configures via temp Key Vault
communicator = "winrm"
winrm_use_ssl = true
winrm_insecure = true
winrm_timeout = "15m"
winrm_username = "packer"
// Output Managed Image (x64 supports this)
// Also publish to Compute Gallery
shared_image_gallery_destination {
subscription = var.subscription_id
resource_group = var.gallery_resource_group
gallery_name = var.gallery_name
image_name = var.image_name != "" ? var.image_name : "windows-x64-2019-build-${var.build_number}"
image_version = "1.0.0"
storage_account_type = "Standard_LRS"
target_region { name = var.location }
target_region { name = "australiaeast" }
target_region { name = "brazilsouth" }
target_region { name = "canadacentral" }
target_region { name = "canadaeast" }
target_region { name = "centralindia" }
target_region { name = "centralus" }
target_region { name = "francecentral" }
target_region { name = "germanywestcentral" }
target_region { name = "italynorth" }
target_region { name = "japaneast" }
target_region { name = "japanwest" }
target_region { name = "koreacentral" }
target_region { name = "mexicocentral" }
target_region { name = "northcentralus" }
target_region { name = "northeurope" }
target_region { name = "southcentralus" }
target_region { name = "southeastasia" }
target_region { name = "spaincentral" }
target_region { name = "swedencentral" }
target_region { name = "switzerlandnorth" }
target_region { name = "uaenorth" }
target_region { name = "ukwest" }
target_region { name = "westeurope" }
target_region { name = "westus" }
target_region { name = "westus2" }
target_region { name = "westus3" }
}
azure_tags = {
os = "windows"
arch = "x64"
build = var.build_number
}
}
build {
sources = ["source.azure-arm.windows-x64"]
// Step 1: Run bootstrap installs all build dependencies
provisioner "powershell" {
script = var.bootstrap_script
valid_exit_codes = [0, 3010]
environment_vars = ["CI=true"]
}
// Step 2: Upload agent.mjs
provisioner "file" {
source = var.agent_script
destination = "C:\\buildkite-agent\\agent.mjs"
}
// Step 3: Install agent service via nssm
provisioner "powershell" {
inline = [
"C:\\Scoop\\apps\\nodejs\\current\\node.exe C:\\buildkite-agent\\agent.mjs install"
]
valid_exit_codes = [0]
}
// Step 4: Reboot to clear pending updates (VS Build Tools, Windows Updates)
provisioner "windows-restart" {
restart_timeout = "10m"
}
// Step 5: Sysprep MUST be last provisioner
provisioner "powershell" {
inline = [
"Remove-Item -Recurse -Force C:\\Windows\\Panther -ErrorAction SilentlyContinue",
"Write-Output '>>> Clearing pending reboot flags...'",
"Remove-Item 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending' -Recurse -Force -ErrorAction SilentlyContinue",
"Remove-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update' -Name 'RebootRequired' -Force -ErrorAction SilentlyContinue",
"Remove-Item 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired' -Recurse -Force -ErrorAction SilentlyContinue",
"Remove-ItemProperty 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager' -Name 'PendingFileRenameOperations' -Force -ErrorAction SilentlyContinue",
"Write-Output '>>> Waiting for Azure Guest Agent...'",
"while ((Get-Service RdAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
"while ((Get-Service WindowsAzureGuestAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
"Write-Output '>>> Running Sysprep...'",
"$global:LASTEXITCODE = 0",
"& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit /mode:vm",
"$timeout = 300; $elapsed = 0",
"while ($true) {",
" $imageState = (Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State).ImageState",
" Write-Output \"ImageState: $imageState ($${elapsed}s)\"",
" if ($imageState -eq 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { break }",
" if ($elapsed -ge $timeout) {",
" Write-Error \"Timed out after $${timeout}s -- stuck at $imageState\"",
" Get-Content \"$env:SystemRoot\\System32\\Sysprep\\Panther\\setupact.log\" -Tail 100 -ErrorAction SilentlyContinue",
" exit 1",
" }",
" Start-Sleep -s 10",
" $elapsed += 10",
"}",
"Write-Output '>>> Sysprep complete.'"
]
}
}

View File

@@ -1837,6 +1837,13 @@ export function getTailscale() {
}
}
if (isWindows) {
const tailscaleExe = "C:\\Program Files\\Tailscale\\tailscale.exe";
if (existsSync(tailscaleExe)) {
return tailscaleExe;
}
}
return "tailscale";
}
@@ -2043,7 +2050,7 @@ export function getShell() {
}
/**
* @typedef {"aws" | "google"} Cloud
* @typedef {"aws" | "google" | "azure"} Cloud
*/
/** @type {Cloud | undefined} */
@@ -2136,6 +2143,37 @@ export async function isGoogleCloud() {
}
}
/**
* @returns {Promise<boolean | undefined>}
*/
export async function isAzure() {
if (typeof detectedCloud === "string") {
return detectedCloud === "azure";
}
async function detectAzure() {
// Azure IMDS (Instance Metadata Service) — the official way to detect Azure VMs.
// https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service
const { error, body } = await curl("http://169.254.169.254/metadata/instance?api-version=2021-02-01", {
headers: { "Metadata": "true" },
retries: 1,
});
if (!error && body) {
try {
const metadata = JSON.parse(body);
if (metadata?.compute?.azEnvironment) {
return true;
}
} catch {}
}
}
if (await detectAzure()) {
detectedCloud = "azure";
return true;
}
}
/**
* @returns {Promise<Cloud | undefined>}
*/
@@ -2151,6 +2189,10 @@ export async function getCloud() {
if (await isGoogleCloud()) {
return "google";
}
if (await isAzure()) {
return "azure";
}
}
/**
@@ -2175,6 +2217,10 @@ export async function getCloudMetadata(name, cloud) {
} else if (cloud === "google") {
url = new URL(name, "http://metadata.google.internal/computeMetadata/v1/instance/");
headers = { "Metadata-Flavor": "Google" };
} else if (cloud === "azure") {
// Azure IMDS uses a single JSON endpoint; individual fields are extracted by the caller.
url = new URL("http://169.254.169.254/metadata/instance?api-version=2021-02-01");
headers = { "Metadata": "true" };
} else {
throw new Error(`Unsupported cloud: ${inspect(cloud)}`);
}
@@ -2193,7 +2239,25 @@ export async function getCloudMetadata(name, cloud) {
* @param {Cloud} [cloud]
* @returns {Promise<string | undefined>}
*/
export function getCloudMetadataTag(tag, cloud) {
export async function getCloudMetadataTag(tag, cloud) {
cloud ??= await getCloud();
if (cloud === "azure") {
// Azure IMDS returns all tags in a single JSON response.
// Tags are in compute.tagsList as [{name, value}, ...].
const body = await getCloudMetadata("", cloud);
if (!body) return;
try {
const metadata = JSON.parse(body);
const tags = metadata?.compute?.tagsList;
if (Array.isArray(tags)) {
const entry = tags.find(t => t.name === tag);
return entry?.value;
}
} catch {}
return;
}
const metadata = {
"aws": `tags/instance/${tag}`,
"google": `labels/${tag.replace(":", "-")}`,

239
scripts/verify-baseline.ts Normal file
View File

@@ -0,0 +1,239 @@
// Verify that a Bun binary doesn't use CPU instructions beyond its baseline target.
//
// Detects the platform and chooses the appropriate emulator:
// Linux x64: QEMU with Nehalem CPU (no AVX)
// Linux arm64: QEMU with Cortex-A53 (no LSE/SVE)
// 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
import { readdirSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
const { parseArgs } = require("node:util");
const { values } = parseArgs({
args: process.argv.slice(2),
options: {
binary: { type: "string" },
emulator: { type: "string" },
"jit-stress": { type: "boolean", default: false },
},
strict: true,
});
const binary = resolve(values.binary!);
function resolveEmulator(name: string): string {
const found = Bun.which(name);
if (found) return found;
// Try without -static suffix (e.g. qemu-aarch64 instead of qemu-aarch64-static)
if (name.endsWith("-static")) {
const fallback = Bun.which(name.slice(0, -"-static".length));
if (fallback) return fallback;
}
// Last resort: resolve as a relative path (e.g. sde-external/sde.exe)
return resolve(name);
}
const emulatorPath = resolveEmulator(values.emulator!);
const scriptDir = dirname(import.meta.path);
const repoRoot = resolve(scriptDir, "..");
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
const isWindows = process.platform === "win32";
const isAarch64 = process.arch === "arm64";
// SDE outputs this when a chip-check violation occurs
const SDE_VIOLATION_PATTERN = /SDE-ERROR:.*not valid for specified chip/i;
// Configure emulator based on platform
const config = isWindows
? {
runnerCmd: [emulatorPath, "-nhm", "--"],
cpuDesc: "Nehalem (SSE4.2, no AVX/AVX2/AVX512)",
// SDE must run from its own directory for Pin DLL resolution
cwd: dirname(emulatorPath),
}
: isAarch64
? {
runnerCmd: [emulatorPath, "-cpu", "cortex-a53"],
cpuDesc: "Cortex-A53 (ARMv8.0-A+CRC, no LSE/SVE)",
cwd: undefined,
}
: {
runnerCmd: [emulatorPath, "-cpu", "Nehalem"],
cpuDesc: "Nehalem (SSE4.2, no AVX/AVX2/AVX512)",
cwd: undefined,
};
function isInstructionViolation(exitCode: number, output: string): boolean {
if (isWindows) return SDE_VIOLATION_PATTERN.test(output);
return exitCode === 132; // SIGILL = 128 + signal 4
}
console.log(`--- Verifying ${basename(binary)} on ${config.cpuDesc}`);
console.log(` Binary: ${binary}`);
console.log(` Emulator: ${config.runnerCmd.join(" ")}`);
console.log();
let instructionFailures = 0;
let otherFailures = 0;
let passed = 0;
const failedTests: string[] = [];
interface RunTestOptions {
cwd?: string;
/** Tee output live to the console while still capturing it for analysis */
live?: boolean;
}
/** Read a stream, write each chunk to a writable, and return the full text. */
async function teeStream(stream: ReadableStream<Uint8Array>, output: NodeJS.WriteStream): Promise<string> {
const chunks: Uint8Array[] = [];
for await (const chunk of stream) {
chunks.push(chunk);
output.write(chunk);
}
return Buffer.concat(chunks).toString();
}
async function runTest(label: string, binaryArgs: string[], options?: RunTestOptions): Promise<boolean> {
console.log(`+++ ${label}`);
const start = performance.now();
const live = options?.live ?? false;
const proc = Bun.spawn([...config.runnerCmd, binary, ...binaryArgs], {
// config.cwd takes priority — SDE on Windows must run from its own directory for Pin DLL resolution
cwd: config.cwd ?? options?.cwd,
stdout: "pipe",
stderr: "pipe",
});
let stdout: string;
let stderr: string;
if (live) {
[stdout, stderr] = await Promise.all([
teeStream(proc.stdout as ReadableStream<Uint8Array>, process.stdout),
teeStream(proc.stderr as ReadableStream<Uint8Array>, process.stderr),
proc.exited,
]);
} else {
[stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
}
const exitCode = proc.exitCode!;
const elapsed = ((performance.now() - start) / 1000).toFixed(1);
const output = stdout + "\n" + stderr;
if (exitCode === 0) {
if (!live && stdout.trim()) console.log(stdout.trim());
console.log(` PASS (${elapsed}s)`);
passed++;
return true;
}
if (isInstructionViolation(exitCode, output)) {
if (!live && output.trim()) console.log(output.trim());
console.log();
console.log(` FAIL: CPU instruction violation detected (${elapsed}s)`);
if (isAarch64) {
console.log(" The aarch64 build targets Cortex-A53 (ARMv8.0-A+CRC).");
console.log(" LSE atomics, SVE, and dotprod instructions are not allowed.");
} else {
console.log(" The baseline x64 build targets Nehalem (SSE4.2).");
console.log(" AVX, AVX2, and AVX512 instructions are not allowed.");
}
instructionFailures++;
failedTests.push(label);
} else {
if (!live && output.trim()) console.log(output.trim());
console.log(` WARN: exit code ${exitCode} (${elapsed}s, not a CPU instruction issue)`);
otherFailures++;
}
return false;
}
// Phase 1: SIMD code path verification (always runs)
const simdTestPath = join(repoRoot, "test", "js", "bun", "jsc-stress", "fixtures", "simd-baseline.test.ts");
await runTest("SIMD baseline tests", ["test", simdTestPath], { live: true });
// Phase 2: JIT stress fixtures (only with --jit-stress, e.g. on WebKit changes)
if (values["jit-stress"]) {
const jsFixtures = readdirSync(fixturesDir)
.filter(f => f.endsWith(".js"))
.sort();
console.log();
console.log(`--- JS fixtures (DFG/FTL) — ${jsFixtures.length} tests`);
for (let i = 0; i < jsFixtures.length; i++) {
const fixture = jsFixtures[i];
await runTest(`[${i + 1}/${jsFixtures.length}] ${fixture}`, ["--preload", preloadPath, join(fixturesDir, fixture)]);
}
const wasmFixtures = readdirSync(wasmFixturesDir)
.filter(f => f.endsWith(".js"))
.sort();
console.log();
console.log(`--- Wasm fixtures (BBQ/OMG) — ${wasmFixtures.length} tests`);
for (let i = 0; i < wasmFixtures.length; i++) {
const fixture = wasmFixtures[i];
await runTest(
`[${i + 1}/${wasmFixtures.length}] ${fixture}`,
["--preload", preloadPath, join(wasmFixturesDir, fixture)],
{ cwd: wasmFixturesDir },
);
}
} else {
console.log();
console.log("--- Skipping JIT stress fixtures (pass --jit-stress to enable)");
}
// Summary
console.log();
console.log("--- Summary");
console.log(` Passed: ${passed}`);
console.log(` Instruction failures: ${instructionFailures}`);
console.log(` Other failures: ${otherFailures} (warnings, not CPU instruction issues)`);
console.log();
if (instructionFailures > 0) {
console.error(" FAILED: Code uses unsupported CPU instructions.");
// Report to Buildkite annotations tab
const platform = isWindows
? isAarch64
? "Windows aarch64"
: "Windows x64"
: isAarch64
? "Linux aarch64"
: "Linux x64";
const annotation = [
`<details>`,
`<summary>CPU instruction violation on ${platform}${instructionFailures} failed</summary>`,
`<p>The baseline build uses instructions not available on <code>${config.cpuDesc}</code>.</p>`,
`<ul>${failedTests.map(t => `<li><code>${t}</code></li>`).join("")}</ul>`,
`</details>`,
].join("\n");
Bun.spawnSync(["buildkite-agent", "annotate", "--append", "--style", "error", "--context", "verify-baseline"], {
stdin: new Blob([annotation]),
});
process.exit(1);
}
if (otherFailures > 0) {
console.log(" Some tests failed for reasons unrelated to CPU instructions.");
}
console.log(` All baseline verification passed on ${config.cpuDesc}.`);

View File

@@ -5,22 +5,7 @@ $ErrorActionPreference = "Stop"
# Detect system architecture
$script:IsARM64 = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64
# Allow overriding the target arch (useful for cross-compiling on x64 -> ARM64)
$script:VsArch = $null
if ($env:BUN_VS_ARCH) {
switch ($env:BUN_VS_ARCH.ToLowerInvariant()) {
"arm64" { $script:VsArch = "arm64" }
"aarch64" { $script:VsArch = "arm64" }
"amd64" { $script:VsArch = "amd64" }
"x64" { $script:VsArch = "amd64" }
default { throw "Invalid BUN_VS_ARCH: $env:BUN_VS_ARCH (expected arm64|amd64)" }
}
}
if (-not $script:VsArch) {
$script:VsArch = if ($script:IsARM64) { "arm64" } else { "amd64" }
}
$script:VsArch = if ($script:IsARM64) { "arm64" } else { "amd64" }
if($env:VSINSTALLDIR -eq $null) {
Write-Host "Loading Visual Studio environment, this may take a second..."
@@ -51,10 +36,15 @@ if($env:VSINSTALLDIR -eq $null) {
Push-Location $vsDir
try {
$vsShell = (Join-Path -Path $vsDir -ChildPath "Common7\Tools\Launch-VsDevShell.ps1")
# Visual Studio's Launch-VsDevShell.ps1 only supports x86/amd64 for HostArch
# For ARM64 builds, use amd64 as HostArch since it can cross-compile to ARM64
# -HostArch only accepts "x86" or "amd64" — even on native ARM64, use "amd64"
$hostArch = if ($script:VsArch -eq "arm64") { "amd64" } else { $script:VsArch }
. $vsShell -Arch $script:VsArch -HostArch $hostArch
# VS dev shell with -HostArch amd64 sets PROCESSOR_ARCHITECTURE=AMD64,
# which causes CMake to misdetect the system as x64. Restore it on ARM64.
if ($script:IsARM64) {
$env:PROCESSOR_ARCHITECTURE = "ARM64"
}
} finally {
Pop-Location
}

View File

@@ -17,14 +17,15 @@ Conventions:
| Instead of | Use |
| ------------------------------------------------------------ | ------------------------------------ |
| `std.fs.File` | `bun.sys.File` |
| `std.base64` | `bun.base64` |
| `std.crypto.sha{...}` | `bun.sha.Hashers.{...}` |
| `std.fs.cwd()` | `bun.FD.cwd()` |
| `std.posix.open/read/write/stat/mkdir/unlink/rename/symlink` | `bun.sys.*` equivalents |
| `std.fs.File` | `bun.sys.File` |
| `std.fs.path.join/dirname/basename` | `bun.path.join/dirname/basename` |
| `std.mem.eql/indexOf/startsWith` (for strings) | `bun.strings.eql/indexOf/startsWith` |
| `std.posix.O` / `std.posix.mode_t` / `std.posix.fd_t` | `bun.O` / `bun.Mode` / `bun.FD` |
| `std.posix.open/read/write/stat/mkdir/unlink/rename/symlink` | `bun.sys.*` equivalents |
| `std.process.Child` | `bun.spawnSync` |
| `catch bun.outOfMemory()` | `bun.handleOom(...)` |
## `bun.sys` — System Calls (`src/sys.zig`)

View File

@@ -12,6 +12,11 @@ const ImportRef = struct {
stmt_index: u32,
};
const DeduplicatedImportResult = struct {
namespace_ref: Ref,
import_record_index: u32,
};
pub fn convertStmt(ctx: *ConvertESMExportsForHmr, p: anytype, stmt: Stmt) !void {
const new_stmt = switch (stmt.data) {
else => brk: {
@@ -195,7 +200,7 @@ pub fn convertStmt(ctx: *ConvertESMExportsForHmr, p: anytype, stmt: Stmt) !void
return; // do not emit a statement here
},
.s_export_from => |st| {
const namespace_ref = try ctx.deduplicatedImport(
const deduped = try ctx.deduplicatedImport(
p,
st.import_record_index,
st.namespace_ref,
@@ -207,13 +212,17 @@ pub fn convertStmt(ctx: *ConvertESMExportsForHmr, p: anytype, stmt: Stmt) !void
for (st.items) |*item| {
const ref = item.name.ref.?;
const symbol = &p.symbols.items[ref.innerIndex()];
if (symbol.namespace_alias == null) {
symbol.namespace_alias = .{
.namespace_ref = namespace_ref,
.alias = item.original_name,
.import_record_index = st.import_record_index,
};
}
// Always set the namespace alias using the deduplicated import
// record. When two `export { ... } from` statements reference
// the same source, the second import record is marked unused
// and its items are merged into the first. The symbols may
// already have a namespace_alias from ImportScanner pointing at
// the now-unused record, so we must update it.
symbol.namespace_alias = .{
.namespace_ref = deduped.namespace_ref,
.alias = item.original_name,
.import_record_index = deduped.import_record_index,
};
try ctx.visitRefToExport(
p,
ref,
@@ -234,7 +243,7 @@ pub fn convertStmt(ctx: *ConvertESMExportsForHmr, p: anytype, stmt: Stmt) !void
return;
},
.s_export_star => |st| {
const namespace_ref = try ctx.deduplicatedImport(
const deduped = try ctx.deduplicatedImport(
p,
st.import_record_index,
st.namespace_ref,
@@ -248,13 +257,13 @@ pub fn convertStmt(ctx: *ConvertESMExportsForHmr, p: anytype, stmt: Stmt) !void
// 'export * as ns from' creates one named property.
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{ .data = alias.original_name }, stmt.loc),
.value = Expr.initIdentifier(namespace_ref, stmt.loc),
.value = Expr.initIdentifier(deduped.namespace_ref, stmt.loc),
});
} else {
// 'export * from' creates a spread, hoisted at the top.
try ctx.export_star_props.append(p.allocator, .{
.kind = .spread,
.value = Expr.initIdentifier(namespace_ref, stmt.loc),
.value = Expr.initIdentifier(deduped.namespace_ref, stmt.loc),
});
}
return;
@@ -279,7 +288,8 @@ pub fn convertStmt(ctx: *ConvertESMExportsForHmr, p: anytype, stmt: Stmt) !void
try ctx.stmts.append(p.allocator, new_stmt);
}
/// Deduplicates imports, returning a previously used Ref if present.
/// Deduplicates imports, returning a previously used Ref and import record
/// index if present.
fn deduplicatedImport(
ctx: *ConvertESMExportsForHmr,
p: anytype,
@@ -289,7 +299,7 @@ fn deduplicatedImport(
star_name_loc: ?logger.Loc,
default_name: ?js_ast.LocRef,
loc: logger.Loc,
) !Ref {
) !DeduplicatedImportResult {
const ir = &p.import_records.items[import_record_index];
const gop = try ctx.imports_seen.getOrPut(p.allocator, ir.path.text);
if (gop.found_existing) {
@@ -299,6 +309,12 @@ fn deduplicatedImport(
ir.flags.is_unused = true;
const stmt = ctx.stmts.items[gop.value_ptr.stmt_index].data.s_import;
// The surviving record may have been marked is_unused by barrel
// optimization (when the first export-from statement's exports
// were all deferred). Since we are merging new items into it,
// clear is_unused so the import is actually emitted.
p.import_records.items[stmt.import_record_index].flags.is_unused = false;
if (items.len > 0) {
if (stmt.items.len == 0) {
stmt.items = items;
@@ -309,7 +325,7 @@ fn deduplicatedImport(
if (namespace_ref.isValid()) {
if (!stmt.namespace_ref.isValid()) {
stmt.namespace_ref = namespace_ref;
return namespace_ref;
return .{ .namespace_ref = namespace_ref, .import_record_index = stmt.import_record_index };
} else {
// Erase this namespace ref, but since it may be used in
// existing AST trees, a link must be established.
@@ -327,7 +343,7 @@ fn deduplicatedImport(
if (stmt.default_name == null) if (default_name) |dn| {
stmt.default_name = dn;
};
return stmt.namespace_ref;
return .{ .namespace_ref = stmt.namespace_ref, .import_record_index = stmt.import_record_index };
}
try ctx.stmts.append(p.allocator, Stmt.alloc(S.Import, .{
@@ -340,7 +356,7 @@ fn deduplicatedImport(
}, loc));
gop.value_ptr.* = .{ .stmt_index = @intCast(ctx.stmts.items.len - 1) };
return namespace_ref;
return .{ .namespace_ref = namespace_ref, .import_record_index = import_record_index };
}
fn visitBindingToExport(ctx: *ConvertESMExportsForHmr, p: anytype, binding: Binding) !void {

View File

@@ -28,9 +28,6 @@ pub fn ReplTransforms(comptime P: type) type {
return;
}
// Check if there's top-level await
const has_top_level_await = p.top_level_await_keyword.len > 0;
// Collect all statements into a single array
var all_stmts = bun.handleOom(allocator.alloc(Stmt, total_stmts_count));
var stmt_idx: usize = 0;
@@ -41,6 +38,17 @@ pub fn ReplTransforms(comptime P: type) type {
}
}
// Check if there's top-level await or imports (imports become dynamic awaited imports)
var has_top_level_await = p.top_level_await_keyword.len > 0;
if (!has_top_level_await) {
for (all_stmts) |stmt| {
if (stmt.data == .s_import) {
has_top_level_await = true;
break;
}
}
}
// Apply transform with is_async based on presence of top-level await
try transformWithHoisting(p, parts, all_stmts, allocator, has_top_level_await);
}
@@ -154,6 +162,86 @@ pub fn ReplTransforms(comptime P: type) type {
try inner_stmts.append(stmt);
}
},
.s_import => |import_data| {
// Convert static imports to dynamic imports for REPL evaluation:
// import X from 'mod' -> var X = (await import('mod')).default
// import { a, b } from 'mod' -> var {a, b} = await import('mod')
// import * as X from 'mod' -> var X = await import('mod')
// import 'mod' -> await import('mod')
const path_str = p.import_records.items[import_data.import_record_index].path.text;
const import_expr = p.newExpr(E.Import{
.expr = p.newExpr(E.String{ .data = path_str }, stmt.loc),
.import_record_index = std.math.maxInt(u32),
}, stmt.loc);
const await_expr = p.newExpr(E.Await{ .value = import_expr }, stmt.loc);
if (import_data.star_name_loc) |_| {
// import * as X from 'mod' -> var X = await import('mod')
try hoisted_stmts.append(p.s(S.Local{
.kind = .k_var,
.decls = Decl.List.fromOwnedSlice(bun.handleOom(allocator.dupe(G.Decl, &.{
G.Decl{
.binding = p.b(B.Identifier{ .ref = import_data.namespace_ref }, stmt.loc),
.value = null,
},
}))),
}, stmt.loc));
const assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = import_data.namespace_ref }, stmt.loc),
.right = await_expr,
}, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = assign }, stmt.loc));
} else if (import_data.default_name) |default_name| {
// import X from 'mod' -> var X = (await import('mod')).default
// import X, { a } from 'mod' -> var __ns = await import('mod'); var X = __ns.default; var a = __ns.a;
try hoisted_stmts.append(p.s(S.Local{
.kind = .k_var,
.decls = Decl.List.fromOwnedSlice(bun.handleOom(allocator.dupe(G.Decl, &.{
G.Decl{
.binding = p.b(B.Identifier{ .ref = default_name.ref.? }, default_name.loc),
.value = null,
},
}))),
}, stmt.loc));
if (import_data.items.len > 0) {
// Share a single await import() between default and named imports.
// namespace_ref is synthesized by processImportStatement for all non-star imports.
try convertNamedImports(p, import_data, await_expr, &hoisted_stmts, &inner_stmts, allocator, stmt.loc);
const ns_ref_expr = p.newExpr(E.Identifier{ .ref = import_data.namespace_ref }, stmt.loc);
const dot_default = p.newExpr(E.Dot{
.target = ns_ref_expr,
.name = "default",
.name_loc = stmt.loc,
}, stmt.loc);
const assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = default_name.ref.? }, default_name.loc),
.right = dot_default,
}, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = assign }, stmt.loc));
} else {
const dot_default = p.newExpr(E.Dot{
.target = await_expr,
.name = "default",
.name_loc = stmt.loc,
}, stmt.loc);
const assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = default_name.ref.? }, default_name.loc),
.right = dot_default,
}, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = assign }, stmt.loc));
}
} else if (import_data.items.len > 0) {
// import { a, b } from 'mod' -> destructure from await import('mod')
try convertNamedImports(p, import_data, await_expr, &hoisted_stmts, &inner_stmts, allocator, stmt.loc);
} else {
// import 'mod' (side-effect only) -> await import('mod')
try inner_stmts.append(p.s(S.SExpr{ .value = await_expr }, stmt.loc));
}
},
.s_directive => |directive| {
// In REPL mode, treat directives (string literals) as expressions
const str_expr = p.newExpr(E.String{ .data = directive.value }, stmt.loc);
@@ -195,6 +283,63 @@ pub fn ReplTransforms(comptime P: type) type {
}
}
/// Convert named imports to individual var assignments from the dynamic import
/// import { a, b as c } from 'mod' ->
/// var a; var c; (hoisted)
/// var __mod = await import('mod'); a = __mod.a; c = __mod.b; (inner)
fn convertNamedImports(
p: *P,
import_data: *const S.Import,
await_expr: Expr,
hoisted_stmts: *ListManaged(Stmt),
inner_stmts: *ListManaged(Stmt),
allocator: Allocator,
loc: logger.Loc,
) !void {
// Store the module in the namespace ref: var __ns = await import('mod')
try hoisted_stmts.append(p.s(S.Local{
.kind = .k_var,
.decls = Decl.List.fromOwnedSlice(bun.handleOom(allocator.dupe(G.Decl, &.{
G.Decl{
.binding = p.b(B.Identifier{ .ref = import_data.namespace_ref }, loc),
.value = null,
},
}))),
}, loc));
const ns_assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = import_data.namespace_ref }, loc),
.right = await_expr,
}, loc);
try inner_stmts.append(p.s(S.SExpr{ .value = ns_assign }, loc));
// For each named import: var name; name = __ns.originalName;
for (import_data.items) |item| {
try hoisted_stmts.append(p.s(S.Local{
.kind = .k_var,
.decls = Decl.List.fromOwnedSlice(bun.handleOom(allocator.dupe(G.Decl, &.{
G.Decl{
.binding = p.b(B.Identifier{ .ref = item.name.ref.? }, item.name.loc),
.value = null,
},
}))),
}, loc));
const ns_ref_expr = p.newExpr(E.Identifier{ .ref = import_data.namespace_ref }, loc);
const prop_access = p.newExpr(E.Dot{
.target = ns_ref_expr,
.name = item.alias,
.name_loc = item.name.loc,
}, loc);
const item_assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = item.name.ref.? }, item.name.loc),
.right = prop_access,
}, loc);
try inner_stmts.append(p.s(S.SExpr{ .value = item_assign }, loc));
}
}
/// Wrap the last expression in return { value: expr }
fn wrapLastExpressionWithReturn(p: *P, inner_stmts: *ListManaged(Stmt), allocator: Allocator) void {
if (inner_stmts.items.len > 0) {

View File

@@ -67,6 +67,15 @@ route_bundles: ArrayListUnmanaged(RouteBundle),
graph_safety_lock: bun.safety.ThreadLock,
client_graph: IncrementalGraph(.client),
server_graph: IncrementalGraph(.server),
/// Barrel files with deferred (is_unused) import records. These files must
/// be re-parsed on every incremental build because the set of needed exports
/// may have changed. Populated by applyBarrelOptimization.
barrel_files_with_deferrals: bun.StringArrayHashMapUnmanaged(void) = .{},
/// Accumulated barrel export requests across all builds. Maps barrel file
/// path → set of export names that have been requested. This ensures that
/// when a barrel is re-parsed in an incremental build, exports requested
/// by non-stale files (from previous builds) are still kept.
barrel_needed_exports: bun.StringArrayHashMapUnmanaged(bun.StringHashMapUnmanaged(void)) = .{},
/// State populated during bundling and hot updates. Often cleared
incremental_result: IncrementalResult,
/// Quickly retrieve a framework route's index from its entry point file. These
@@ -616,6 +625,23 @@ pub fn deinit(dev: *DevServer) void {
},
.server_graph = dev.server_graph.deinit(),
.client_graph = dev.client_graph.deinit(),
.barrel_files_with_deferrals = {
for (dev.barrel_files_with_deferrals.keys()) |key| {
alloc.free(key);
}
dev.barrel_files_with_deferrals.deinit(alloc);
},
.barrel_needed_exports = {
var it = dev.barrel_needed_exports.iterator();
while (it.next()) |entry| {
var inner = entry.value_ptr.*;
var inner_it = inner.keyIterator();
while (inner_it.next()) |k| alloc.free(k.*);
inner.deinit(alloc);
alloc.free(entry.key_ptr.*);
}
dev.barrel_needed_exports.deinit(alloc);
},
.assets = dev.assets.deinit(alloc),
.incremental_result = useAllFields(IncrementalResult, .{
.had_adjusted_edges = {},
@@ -2192,6 +2218,7 @@ pub fn finalizeBundle(
) bun.JSError!void {
assert(dev.magic == .valid);
var had_sent_hmr_event = false;
defer {
var heap = bv2.graph.heap;
bv2.deinitWithoutFreeingArena();
@@ -3070,6 +3097,11 @@ const CacheEntry = struct {
};
pub fn isFileCached(dev: *DevServer, path: []const u8, side: bake.Graph) ?CacheEntry {
// Barrel files with deferred records must always be re-parsed so the
// barrel optimization can evaluate updated requested_exports.
if (dev.barrel_files_with_deferrals.contains(path))
return null;
dev.graph_safety_lock.lock();
defer dev.graph_safety_lock.unlock();

View File

@@ -79,6 +79,8 @@ pub fn memoryCostDetailed(dev: *DevServer) MemoryCost {
js_code += cost.code;
source_maps += cost.source_maps;
},
.barrel_files_with_deferrals = {},
.barrel_needed_exports = {},
.assets = {
assets += dev.assets.memoryCost();
},

View File

@@ -3,6 +3,17 @@ pub const webcore = @import("./bun.js/webcore.zig");
pub const api = @import("./bun.js/api.zig");
pub const bindgen = @import("./bun.js/bindgen.zig");
pub fn applyStandaloneRuntimeFlags(b: *bun.Transpiler, graph: *const bun.StandaloneModuleGraph) void {
b.options.env.disable_default_env_files = graph.flags.disable_default_env_files;
b.options.env.behavior = if (graph.flags.disable_default_env_files)
.disable
else
.load_all_without_inlining;
b.resolver.opts.load_tsconfig_json = !graph.flags.disable_autoload_tsconfig;
b.resolver.opts.load_package_json = !graph.flags.disable_autoload_package_json;
}
pub const Run = struct {
ctx: Command.Context,
vm: *VirtualMachine,
@@ -82,18 +93,7 @@ pub const Run = struct {
.unspecified => {},
}
// If .env loading is disabled, only load process env vars
// Otherwise, load all .env files
if (graph_ptr.flags.disable_default_env_files) {
b.options.env.behavior = .disable;
} else {
b.options.env.behavior = .load_all_without_inlining;
}
// Control loading of tsconfig.json and package.json at runtime
// By default, these are disabled for standalone executables
b.resolver.opts.load_tsconfig_json = !graph_ptr.flags.disable_autoload_tsconfig;
b.resolver.opts.load_package_json = !graph_ptr.flags.disable_autoload_package_json;
applyStandaloneRuntimeFlags(b, graph_ptr);
b.configureDefines() catch {
failWithBuildError(vm);

View File

@@ -1684,11 +1684,20 @@ fn _resolve(
const buster_name = name: {
if (std.fs.path.isAbsolute(normalized_specifier)) {
if (std.fs.path.dirname(normalized_specifier)) |dir| {
if (dir.len > specifier_cache_resolver_buf.len) {
return error.ModuleNotFound;
}
// Normalized without trailing slash
break :name bun.strings.normalizeSlashesOnly(&specifier_cache_resolver_buf, dir, std.fs.path.sep);
}
}
// If the specifier is too long to join, it can't name a real
// directory — skip the cache bust and fail.
if (source_to_use.len + normalized_specifier.len + 4 >= specifier_cache_resolver_buf.len) {
return error.ModuleNotFound;
}
var parts = [_]string{
source_to_use,
normalized_specifier,
@@ -2669,7 +2678,7 @@ pub fn remapZigException(
allow_source_code_preview: bool,
) void {
error_instance.toZigException(this.global, exception);
const enable_source_code_preview = allow_source_code_preview and
var enable_source_code_preview = allow_source_code_preview and
!(bun.feature_flag.BUN_DISABLE_SOURCE_CODE_PREVIEW.get() or
bun.feature_flag.BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW.get());
@@ -2764,6 +2773,12 @@ pub fn remapZigException(
}
}
// Don't show source code preview for REPL frames - it would show the
// transformed IIFE wrapper code, not what the user typed.
if (top.source_url.eqlComptime("[repl]")) {
enable_source_code_preview = false;
}
var top_source_url = top.source_url.toUTF8(bun.default_allocator);
defer top_source_url.deinit();
@@ -2815,7 +2830,6 @@ pub fn remapZigException(
// Avoid printing "export default 'native'"
break :code ZigString.Slice.empty;
}
var log = logger.Log.init(bun.default_allocator);
defer log.deinit();

View File

@@ -21,16 +21,3 @@ export const gc = fn({
},
ret: t.usize,
});
export const StringWidthOptions = t.dictionary({
countAnsiEscapeCodes: t.boolean.default(false),
ambiguousIsNarrow: t.boolean.default(true),
});
export const stringWidth = fn({
args: {
str: t.DOMString.default(""),
opts: StringWidthOptions.default({}),
},
ret: t.usize,
});

View File

@@ -34,6 +34,7 @@ pub const BunObject = struct {
pub const sha = toJSCallback(host_fn.wrapStaticMethod(Crypto.SHA512_256, "hash_", true));
pub const shellEscape = toJSCallback(Bun.shellEscape);
pub const shrink = toJSCallback(Bun.shrink);
pub const stringWidth = toJSCallback(Bun.stringWidth);
pub const sleepSync = toJSCallback(Bun.sleepSync);
pub const spawn = toJSCallback(host_fn.wrapStaticMethod(api.Subprocess, "spawn", false));
pub const spawnSync = toJSCallback(host_fn.wrapStaticMethod(api.Subprocess, "spawnSync", false));
@@ -179,6 +180,7 @@ pub const BunObject = struct {
@export(&BunObject.sha, .{ .name = callbackName("sha") });
@export(&BunObject.shellEscape, .{ .name = callbackName("shellEscape") });
@export(&BunObject.shrink, .{ .name = callbackName("shrink") });
@export(&BunObject.stringWidth, .{ .name = callbackName("stringWidth") });
@export(&BunObject.sleepSync, .{ .name = callbackName("sleepSync") });
@export(&BunObject.spawn, .{ .name = callbackName("spawn") });
@export(&BunObject.spawnSync, .{ .name = callbackName("spawnSync") });
@@ -1382,14 +1384,8 @@ pub fn getUnsafe(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue
return UnsafeObject.create(globalThis);
}
pub fn stringWidth(str: bun.String, opts: gen.StringWidthOptions) usize {
if (str.length() == 0)
return 0;
if (opts.count_ansi_escape_codes)
return str.visibleWidth(!opts.ambiguous_is_narrow);
return str.visibleWidthExcludeANSIColors(!opts.ambiguous_is_narrow);
pub fn stringWidth(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!jsc.JSValue {
return bun.String.jsGetStringWidth(globalObject, callFrame);
}
/// EnvironmentVariables is runtime defined.

View File

@@ -260,6 +260,10 @@ pub const JSBundler = struct {
files: FileMap = .{},
/// Generate metafile (JSON module graph)
metafile: bool = false,
/// Package names whose barrel files should be optimized.
/// Named imports from these packages will only load the submodules
/// that are actually used instead of parsing all re-exported submodules.
optimize_imports: bun.StringSet = bun.StringSet.init(bun.default_allocator),
pub const CompileOptions = struct {
compile_target: CompileTarget = .{},
@@ -435,6 +439,7 @@ pub const JSBundler = struct {
var this = Config{
.entry_points = bun.StringSet.init(allocator),
.external = bun.StringSet.init(allocator),
.optimize_imports = bun.StringSet.init(allocator),
.define = bun.StringMap.init(allocator, true),
.dir = OwnedString.initEmpty(allocator),
.outdir = OwnedString.initEmpty(allocator),
@@ -819,6 +824,15 @@ pub const JSBundler = struct {
}
}
if (try config.getOwnArray(globalThis, "optimizeImports")) |optimize_imports| {
var iter = try optimize_imports.arrayIterator(globalThis);
while (try iter.next()) |entry| {
var slice = try entry.toSliceOrNull(globalThis);
defer slice.deinit();
try this.optimize_imports.insert(slice.slice());
}
}
// if (try config.getOptional(globalThis, "dir", ZigString.Slice)) |slice| {
// defer slice.deinit();
// this.appendSliceExact(slice.slice()) catch unreachable;
@@ -1129,6 +1143,7 @@ pub const JSBundler = struct {
self.env_prefix.deinit();
self.footer.deinit();
self.tsconfig_override.deinit();
self.optimize_imports.deinit();
self.files.deinitAndUnprotect();
self.metafile_json_path.deinit();
self.metafile_markdown_path.deinit();

View File

@@ -698,8 +698,7 @@ pub fn setRawMode(
if (comptime Environment.isPosix) {
// Use the existing TTY mode function
const mode: c_int = if (enabled) 1 else 0;
const tty_result = Bun__ttySetMode(this.master_fd.cast(), mode);
const tty_result = bun.tty.setMode(this.master_fd.cast(), if (enabled) .raw else .normal);
if (tty_result != 0) {
return globalObject.throw("Failed to set raw mode", .{});
}
@@ -708,9 +707,6 @@ pub fn setRawMode(
this.flags.raw_mode = enabled;
return .js_undefined;
}
extern fn Bun__ttySetMode(fd: c_int, mode: c_int) c_int;
/// POSIX termios struct for terminal flags manipulation
const Termios = if (Environment.isPosix) std.posix.termios else void;

View File

@@ -50,23 +50,28 @@ fn getArgv0(globalThis: *jsc.JSGlobalObject, PATH: []const u8, cwd: []const u8,
/// `argv` for `Bun.spawn` & `Bun.spawnSync`
fn getArgv(globalThis: *jsc.JSGlobalObject, args: JSValue, PATH: []const u8, cwd: []const u8, argv0: *?[*:0]const u8, allocator: std.mem.Allocator, argv: *std.array_list.Managed(?[*:0]const u8)) bun.JSError!void {
var cmds_array = try args.arrayIterator(globalThis);
// + 1 for argv0
// + 1 for null terminator
argv.* = try @TypeOf(argv.*).initCapacity(allocator, cmds_array.len + 2);
if (args.isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArguments("cmd must be an array of strings", .{});
}
var cmds_array = try args.arrayIterator(globalThis);
if (cmds_array.len == 0) {
return globalThis.throwInvalidArguments("cmd must not be empty", .{});
}
if (cmds_array.len > std.math.maxInt(u32) - 2) {
return globalThis.throwInvalidArguments("cmd array is too large", .{});
}
// + 1 for argv0
// + 1 for null terminator
argv.* = try @TypeOf(argv.*).initCapacity(allocator, @as(usize, cmds_array.len) + 2);
const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0.*, (try cmds_array.next()).?, allocator);
argv0.* = argv0_result.argv0.ptr;
argv.appendAssumeCapacity(argv0_result.arg0.ptr);
try argv.append(argv0_result.arg0.ptr);
var arg_index: usize = 1;
while (try cmds_array.next()) |value| {
@@ -78,7 +83,7 @@ fn getArgv(globalThis: *jsc.JSGlobalObject, args: JSValue, PATH: []const u8, cwd
return globalThis.ERR(.INVALID_ARG_VALUE, "The argument 'args[{d}]' must be a string without null bytes. Received \"{f}\"", .{ arg_index, arg.toZigString() }).throw();
}
argv.appendAssumeCapacity(try arg.toOwnedSliceZ(allocator));
try argv.append(try arg.toOwnedSliceZ(allocator));
arg_index += 1;
}

View File

@@ -1087,8 +1087,10 @@ pub const WindowsSpawnOptions = struct {
dup2: struct { out: bun.jsc.Subprocess.StdioKind, to: bun.jsc.Subprocess.StdioKind },
pub fn deinit(this: *const Stdio) void {
if (this.* == .buffer) {
bun.default_allocator.destroy(this.buffer);
switch (this.*) {
.buffer => |pipe| pipe.closeAndDestroy(),
.ipc => |pipe| pipe.closeAndDestroy(),
else => {},
}
}
};
@@ -1377,36 +1379,22 @@ pub fn spawnProcessPosix(
break :brk .{ pair[if (i == 0) 1 else 0], pair[if (i == 0) 0 else 1] };
};
if (i == 0) {
// their copy of stdin should be readable
_ = std.c.shutdown(@intCast(fds[1].cast()), std.posix.SHUT.WR);
// our copy of stdin should be writable
_ = std.c.shutdown(@intCast(fds[0].cast()), std.posix.SHUT.RD);
if (comptime Environment.isMac) {
// macOS seems to default to around 8 KB for the buffer size
// this is comically small.
// TODO: investigate if this should be adjusted on Linux.
const so_recvbuf: c_int = 1024 * 512;
const so_sendbuf: c_int = 1024 * 512;
// Note: we intentionally do NOT call shutdown() on the
// socketpair fds. On SOCK_STREAM socketpairs, shutdown(fd, SHUT_WR)
// sends a FIN to the peer, which causes programs that poll the
// write end for readability (e.g. Python's asyncio connect_write_pipe)
// to interpret it as "connection closed" and tear down their transport.
// The socketpair is already used unidirectionally by convention.
if (comptime Environment.isMac) {
// macOS seems to default to around 8 KB for the buffer size
// this is comically small.
// TODO: investigate if this should be adjusted on Linux.
const so_recvbuf: c_int = 1024 * 512;
const so_sendbuf: c_int = 1024 * 512;
if (i == 0) {
_ = std.c.setsockopt(fds[1].cast(), std.posix.SOL.SOCKET, std.posix.SO.RCVBUF, &so_recvbuf, @sizeOf(c_int));
_ = std.c.setsockopt(fds[0].cast(), std.posix.SOL.SOCKET, std.posix.SO.SNDBUF, &so_sendbuf, @sizeOf(c_int));
}
} else {
// their copy of stdout or stderr should be writable
_ = std.c.shutdown(@intCast(fds[1].cast()), std.posix.SHUT.RD);
// our copy of stdout or stderr should be readable
_ = std.c.shutdown(@intCast(fds[0].cast()), std.posix.SHUT.WR);
if (comptime Environment.isMac) {
// macOS seems to default to around 8 KB for the buffer size
// this is comically small.
// TODO: investigate if this should be adjusted on Linux.
const so_recvbuf: c_int = 1024 * 512;
const so_sendbuf: c_int = 1024 * 512;
} else {
_ = std.c.setsockopt(fds[0].cast(), std.posix.SOL.SOCKET, std.posix.SO.RCVBUF, &so_recvbuf, @sizeOf(c_int));
_ = std.c.setsockopt(fds[1].cast(), std.posix.SOL.SOCKET, std.posix.SO.SNDBUF, &so_sendbuf, @sizeOf(c_int));
}
@@ -1629,9 +1617,10 @@ pub fn spawnProcessWindows(
stdio.flags = uv.UV_INHERIT_FD;
stdio.data.fd = fd_i;
},
.ipc => |my_pipe| {
// ipc option inside stdin, stderr or stdout are not supported
bun.default_allocator.destroy(my_pipe);
.ipc => {
// ipc option inside stdin, stderr or stdout is not supported.
// Don't free the pipe here — the caller owns it and will
// clean it up via WindowsSpawnOptions.deinit().
stdio.flags = uv.UV_IGNORE;
},
.ignore => {
@@ -1829,7 +1818,7 @@ pub const sync = struct {
.ignore => .ignore,
.buffer => .{
.buffer = if (Environment.isWindows)
bun.handleOom(bun.default_allocator.create(bun.windows.libuv.Pipe)),
bun.new(bun.windows.libuv.Pipe, std.mem.zeroes(bun.windows.libuv.Pipe)),
},
};
}

View File

@@ -851,6 +851,9 @@ pub fn getsockname(this: *Listener, globalThis: *jsc.JSGlobalObject, callFrame:
}
const out = callFrame.argumentsAsArray(1)[0];
if (!out.isObject()) {
return globalThis.throwInvalidArguments("Expected object", .{});
}
const socket = this.listener.uws;
var buf: [64]u8 = [_]u8{0} ** 64;

View File

@@ -1,5 +1,6 @@
const WindowsNamedPipeContext = @This();
ref_count: RefCount,
named_pipe: uws.WindowsNamedPipe,
socket: SocketType,
@@ -10,6 +11,14 @@ task: jsc.AnyTask,
task_event: EventState = .none,
is_open: bool = false,
const RefCount = bun.ptr.RefCount(@This(), "ref_count", scheduleDeinit, .{});
pub const ref = RefCount.ref;
pub const deref = RefCount.deref;
fn scheduleDeinit(this: *WindowsNamedPipeContext) void {
this.deinitInNextTick();
}
pub const EventState = enum(u8) {
deinit,
none,
@@ -148,7 +157,7 @@ fn onClose(this: *WindowsNamedPipeContext) void {
.none => {},
}
this.deinitInNextTick();
this.deref();
}
fn runEvent(this: *WindowsNamedPipeContext) void {
@@ -169,6 +178,7 @@ fn deinitInNextTick(this: *WindowsNamedPipeContext) void {
pub fn create(globalThis: *jsc.JSGlobalObject, socket: SocketType) *WindowsNamedPipeContext {
const vm = globalThis.bunVM();
const this = WindowsNamedPipeContext.new(.{
.ref_count = .init(),
.vm = vm,
.globalThis = globalThis,
.task = undefined,
@@ -177,8 +187,10 @@ pub fn create(globalThis: *jsc.JSGlobalObject, socket: SocketType) *WindowsNamed
});
// named_pipe owns the pipe (PipeWriter owns the pipe and will close and deinit it)
this.named_pipe = uws.WindowsNamedPipe.from(bun.handleOom(bun.default_allocator.create(uv.Pipe)), .{
this.named_pipe = uws.WindowsNamedPipe.from(bun.new(uv.Pipe, std.mem.zeroes(uv.Pipe)), .{
.ctx = this,
.ref_ctx = @ptrCast(&WindowsNamedPipeContext.ref),
.deref_ctx = @ptrCast(&WindowsNamedPipeContext.deref),
.onOpen = @ptrCast(&WindowsNamedPipeContext.onOpen),
.onData = @ptrCast(&WindowsNamedPipeContext.onData),
.onHandshake = @ptrCast(&WindowsNamedPipeContext.onHandshake),
@@ -218,7 +230,7 @@ pub fn open(globalThis: *jsc.JSGlobalObject, fd: bun.FileDescriptor, ssl_config:
},
.none => {},
}
this.deinitInNextTick();
this.deref();
}
try this.named_pipe.open(fd, ssl_config).unwrap();
return &this.named_pipe;
@@ -238,7 +250,7 @@ pub fn connect(globalThis: *jsc.JSGlobalObject, path: []const u8, ssl_config: ?j
},
.none => {},
}
this.deinitInNextTick();
this.deref();
}
if (path[path.len - 1] == 0) {
@@ -276,6 +288,8 @@ pub fn deinit(this: *WindowsNamedPipeContext) void {
bun.destroy(this);
}
const std = @import("std");
const bun = @import("bun");
const Output = bun.Output;
const jsc = bun.jsc;

View File

@@ -235,10 +235,10 @@ pub const Stdio = union(enum) {
return .{ .err = .blob_used_as_out };
}
break :brk .{ .buffer = bun.handleOom(bun.default_allocator.create(uv.Pipe)) };
break :brk .{ .buffer = createZeroedPipe() };
},
.ipc => .{ .ipc = bun.handleOom(bun.default_allocator.create(uv.Pipe)) },
.capture, .pipe, .array_buffer, .readable_stream => .{ .buffer = bun.handleOom(bun.default_allocator.create(uv.Pipe)) },
.ipc => .{ .ipc = createZeroedPipe() },
.capture, .pipe, .array_buffer, .readable_stream => .{ .buffer = createZeroedPipe() },
.fd => |fd| .{ .pipe = fd },
.dup2 => .{ .dup2 = .{ .out = stdio.dup2.out, .to = stdio.dup2.to } },
.path => |pathlike| .{ .path = pathlike.slice() },
@@ -487,12 +487,18 @@ pub const Stdio = union(enum) {
}
};
/// Allocate a zero-initialized uv.Pipe. Zero-init ensures `pipe.loop` is null
/// for pipes that never reach `uv_pipe_init`, so `closeAndDestroy` can tell
/// whether `uv_close` is needed.
fn createZeroedPipe() *uv.Pipe {
return bun.new(uv.Pipe, std.mem.zeroes(uv.Pipe));
}
const std = @import("std");
const bun = @import("bun");
const Environment = bun.Environment;
const Output = bun.Output;
const default_allocator = bun.default_allocator;
const uv = bun.windows.libuv;
const jsc = bun.jsc;

View File

@@ -24,6 +24,12 @@ client_renegotiation_window: u32 = 0,
requires_custom_request_ctx: bool = false,
is_using_default_ciphers: bool = true,
low_memory_mode: bool = false,
ref_count: RC = .init(),
cached_hash: u64 = 0,
const RC = bun.ptr.ThreadSafeRefCount(@This(), "ref_count", destroy, .{});
pub const ref = RC.ref;
pub const deref = RC.deref;
const ReadFromBlobError = bun.JSError || error{
NullStore,
@@ -113,6 +119,7 @@ pub fn forClientVerification(this: SSLConfig) SSLConfig {
pub fn isSame(this: *const SSLConfig, other: *const SSLConfig) bool {
inline for (comptime std.meta.fields(SSLConfig)) |field| {
if (comptime std.mem.eql(u8, field.name, "ref_count") or std.mem.eql(u8, field.name, "cached_hash")) continue;
const first = @field(this, field.name);
const second = @field(other, field.name);
switch (field.type) {
@@ -185,6 +192,8 @@ pub fn deinit(this: *SSLConfig) void {
.requires_custom_request_ctx = {},
.is_using_default_ciphers = {},
.low_memory_mode = {},
.ref_count = {},
.cached_hash = {},
});
}
@@ -222,9 +231,97 @@ pub fn clone(this: *const SSLConfig) SSLConfig {
.requires_custom_request_ctx = this.requires_custom_request_ctx,
.is_using_default_ciphers = this.is_using_default_ciphers,
.low_memory_mode = this.low_memory_mode,
.ref_count = .init(),
.cached_hash = 0,
};
}
pub fn contentHash(this: *SSLConfig) u64 {
if (this.cached_hash != 0) return this.cached_hash;
var hasher = std.hash.Wyhash.init(0);
inline for (comptime std.meta.fields(SSLConfig)) |field| {
if (comptime std.mem.eql(u8, field.name, "ref_count") or std.mem.eql(u8, field.name, "cached_hash")) continue;
const value = @field(this, field.name);
switch (field.type) {
?[*:0]const u8 => {
if (value) |s| {
hasher.update(bun.asByteSlice(s));
}
hasher.update(&.{0});
},
?[][*:0]const u8 => {
if (value) |slice| {
for (slice) |s| {
hasher.update(bun.asByteSlice(s));
hasher.update(&.{0});
}
}
hasher.update(&.{0});
},
else => {
hasher.update(std.mem.asBytes(&value));
},
}
}
const hash = hasher.final();
// Avoid 0 since it's the sentinel for "not computed"
this.cached_hash = if (hash == 0) 1 else hash;
return this.cached_hash;
}
/// Called by the RC mixin when refcount reaches 0.
fn destroy(this: *SSLConfig) void {
GlobalRegistry.remove(this);
this.deinit();
bun.default_allocator.destroy(this);
}
pub const GlobalRegistry = struct {
const MapContext = struct {
pub fn hash(_: @This(), key: *SSLConfig) u32 {
return @truncate(key.contentHash());
}
pub fn eql(_: @This(), a: *SSLConfig, b: *SSLConfig, _: usize) bool {
return a.isSame(b);
}
};
var mutex: bun.Mutex = .{};
var configs: std.ArrayHashMapUnmanaged(*SSLConfig, void, MapContext, true) = .empty;
/// Takes ownership of a heap-allocated SSLConfig.
/// If an identical config already exists in the registry, the new one is freed
/// and the existing one is returned (with refcount incremented).
/// If no match, the new config is registered and returned.
pub fn intern(new_config: *SSLConfig) *SSLConfig {
mutex.lock();
defer mutex.unlock();
// Look up by content hash/equality
const gop = bun.handleOom(configs.getOrPutContext(bun.default_allocator, new_config, .{}));
if (gop.found_existing) {
// Identical config already exists - free the new one, return existing
const existing = gop.key_ptr.*;
new_config.ref_count.clearWithoutDestructor();
new_config.deinit();
bun.default_allocator.destroy(new_config);
existing.ref();
return existing;
}
// New config - it's already inserted by getOrPut
// refcount is already 1 from initialization
return new_config;
}
/// Remove a config from the registry. Called when refcount reaches 0.
fn remove(config: *SSLConfig) void {
mutex.lock();
defer mutex.unlock();
_ = configs.swapRemoveContext(config, .{});
}
};
pub const zero = SSLConfig{};
pub fn fromJS(
@@ -294,9 +391,9 @@ pub fn fromGenerated(
const protocols = switch (generated.alpn_protocols) {
.none => null,
.string => |*ref| ref.get().toOwnedSliceZ(bun.default_allocator),
.buffer => |*ref| blk: {
const buffer: jsc.ArrayBuffer = ref.get().asArrayBuffer();
.string => |*val| val.get().toOwnedSliceZ(bun.default_allocator),
.buffer => |*val| blk: {
const buffer: jsc.ArrayBuffer = val.get().asArrayBuffer();
break :blk try bun.default_allocator.dupeZ(u8, buffer.byteSlice());
},
};
@@ -366,9 +463,9 @@ fn handleFile(
) ReadFromBlobError!?[][*:0]const u8 {
const single = try handleSingleFile(global, switch (file.*) {
.none => return null,
.string => |*ref| .{ .string = ref.get() },
.buffer => |*ref| .{ .buffer = ref.get() },
.file => |*ref| .{ .file = ref.get() },
.string => |*val| .{ .string = val.get() },
.buffer => |*val| .{ .buffer = val.get() },
.file => |*val| .{ .file = val.get() },
.array => |*list| return try handleFileArray(global, list.items()),
});
errdefer bun.freeSensitive(bun.default_allocator, single);
@@ -391,9 +488,9 @@ fn handleFileArray(
}
for (elements) |*elem| {
result.appendAssumeCapacity(try handleSingleFile(global, switch (elem.*) {
.string => |*ref| .{ .string = ref.get() },
.buffer => |*ref| .{ .buffer = ref.get() },
.file => |*ref| .{ .file = ref.get() },
.string => |*val| .{ .string = val.get() },
.buffer => |*val| .{ .buffer = val.get() },
.file => |*val| .{ .file = val.get() },
}));
}
return try result.toOwnedSlice();

View File

@@ -2,6 +2,8 @@
#include "root.h"
#include <wtf/SIMDHelpers.h>
#include <span>
#include <unicode/utf16.h>
namespace Bun {
namespace ANSI {
@@ -26,14 +28,16 @@ static inline bool isEscapeCharacter(Char c)
// SIMD comparison against exact escape character values. Used to refine
// the broad range match (0x10-0x1F / 0x90-0x9F) to only actual escape
// introducers: 0x1B, 0x90, 0x98, 0x9B, 0x9D, 0x9E, 0x9F.
// introducers: 0x1B, 0x90, 0x98, 0x9B, 0x9D, 0x9E, 0x9F. Also includes 0x9C
// (C1 ST — a terminator, not an introducer) so callers tokenizing ANSI by
// skipping to the next interesting byte will stop at standalone ST too.
template<typename SIMDType>
static auto exactEscapeMatch(std::conditional_t<sizeof(SIMDType) == 1, simde_uint8x16_t, simde_uint16x8_t> chunk)
{
if constexpr (sizeof(SIMDType) == 1)
return SIMD::equal<0x1b, 0x90, 0x98, 0x9b, 0x9d, 0x9e, 0x9f>(chunk);
return SIMD::equal<0x1b, 0x90, 0x98, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f>(chunk);
else
return SIMD::equal<u'\x1b', u'\x90', u'\x98', u'\x9b', u'\x9d', u'\x9e', u'\x9f'>(chunk);
return SIMD::equal<u'\x1b', u'\x90', u'\x98', u'\x9b', u'\x9c', u'\x9d', u'\x9e', u'\x9f'>(chunk);
}
// Find the first escape character in a string using SIMD
@@ -64,9 +68,9 @@ static const Char* findEscapeCharacter(const Char* start, const Char* end)
}
}
// Check remaining characters
// Check remaining characters (include 0x9c to match SIMD behavior)
for (; it != end; ++it) {
if (isEscapeCharacter(*it))
if (isEscapeCharacter(*it) || *it == 0x9c)
return it;
}
return nullptr;
@@ -203,5 +207,145 @@ static const Char* consumeANSI(const Char* start, const Char* end)
return end;
}
// ============================================================================
// UTF-16 surrogate pair decoding — thin wrapper over ICU's U16_NEXT
// ============================================================================
static inline char32_t decodeUTF16(const UChar* p, size_t available, size_t& outLen)
{
size_t i = 0;
char32_t cp;
U16_NEXT(p, i, available, cp);
outLen = i;
return cp;
}
// ============================================================================
// SIMD: index of first code unit NOT in [0x20, 0x7E] (or span.size() if none)
// ============================================================================
// Range check via wrapping subtract + unsigned compare:
// c in [0x20, 0x7E] <=> (c - 0x20) <= 0x5E unsigned
// Any lane with (c - 0x20) > 0x5E is out of range.
//
// Returns an index rather than a bool so callers can:
// 1. Take a fast path if the whole string qualifies (index == size)
// 2. Take a fast path if the requested operation lies inside the prefix
// 3. Fast-forward past the proven-ASCII prefix without re-checking each byte
//
// Lane = uint8_t for Latin-1, uint16_t for UTF-16.
template<typename Lane>
static size_t firstNonAsciiPrintable(std::span<const Lane> input)
{
static_assert(sizeof(Lane) == 1 || sizeof(Lane) == 2);
constexpr size_t stride = SIMD::stride<Lane>;
const auto v20 = SIMD::splat<Lane>(static_cast<Lane>(0x20));
const auto v5E = SIMD::splat<Lane>(static_cast<Lane>(0x5E));
const Lane* const data = input.data();
const Lane* const end = data + input.size();
const Lane* it = data;
for (; static_cast<size_t>(end - it) >= stride; it += stride) {
auto chunk = SIMD::load(it);
auto shifted = SIMD::sub(chunk, v20);
auto oob = SIMD::greaterThan(shifted, v5E);
if (auto idx = SIMD::findFirstNonZeroIndex(oob))
return static_cast<size_t>(it - data) + *idx;
}
for (; it != end; ++it) {
Lane c = *it;
if (static_cast<Lane>(c - 0x20) > 0x5E)
return static_cast<size_t>(it - data);
}
return input.size();
}
// ============================================================================
// SGR (Select Graphic Rendition) open → close code mapping
// ============================================================================
// Shared by sliceAnsi and wrapAnsi for ANSI style tracking across boundaries.
// Returns the SGR reset code for a given open code, or 0 if unknown.
static inline uint32_t sgrCloseCode(uint32_t openCode)
{
// Densely-packed case ranges — LLVM lowers this to a jump table.
switch (openCode) {
case 1:
case 2:
return 22; // bold, dim
case 3:
return 23; // italic
case 4:
return 24; // underline
case 5:
case 6:
return 25; // blink
case 7:
return 27; // inverse
case 8:
return 28; // hidden
case 9:
return 29; // strikethrough
// Foreground colors (basic + extended + bright)
case 30:
case 31:
case 32:
case 33:
case 34:
case 35:
case 36:
case 37:
case 38: // 256/truecolor foreground introducer
case 90:
case 91:
case 92:
case 93:
case 94:
case 95:
case 96:
case 97:
return 39;
// Background colors (basic + extended + bright)
case 40:
case 41:
case 42:
case 43:
case 44:
case 45:
case 46:
case 47:
case 48: // 256/truecolor background introducer
case 100:
case 101:
case 102:
case 103:
case 104:
case 105:
case 106:
case 107:
return 49;
case 53:
return 55; // overline
default:
return 0; // Unknown → caller uses full reset
}
}
static inline bool isSgrEndCode(uint32_t code)
{
switch (code) {
case 0:
case 22:
case 23:
case 24:
case 25:
case 27:
case 28:
case 29:
case 39:
case 49:
case 55:
return true;
default:
return false;
}
}
} // namespace ANSI
} // namespace Bun

View File

@@ -77,6 +77,8 @@ BUN_DECLARE_HOST_FUNCTION(Bun__fetchPreconnect);
BUN_DECLARE_HOST_FUNCTION(Bun__randomUUIDv7);
BUN_DECLARE_HOST_FUNCTION(Bun__randomUUIDv5);
#include "sliceAnsi.h"
namespace Bun {
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunStripANSI);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunWrapAnsi);
@@ -797,6 +799,7 @@ JSC_DEFINE_HOST_FUNCTION(functionGenerateHeapSnapshot, (JSC::JSGlobalObject * gl
JSValue arg0 = callFrame->argument(0);
auto throwScope = DECLARE_THROW_SCOPE(vm);
bool useV8 = false;
bool useArrayBuffer = false;
if (!arg0.isUndefined()) {
if (arg0.isString()) {
auto str = arg0.toWTFString(globalObject);
@@ -813,6 +816,31 @@ JSC_DEFINE_HOST_FUNCTION(functionGenerateHeapSnapshot, (JSC::JSGlobalObject * gl
}
if (useV8) {
JSValue arg1 = callFrame->argument(1);
if (!arg1.isUndefined()) {
if (arg1.isString()) {
auto str = arg1.toWTFString(globalObject);
RETURN_IF_EXCEPTION(throwScope, {});
if (str == "arraybuffer"_s) {
useArrayBuffer = true;
} else {
throwTypeError(globalObject, throwScope, "Expected 'arraybuffer' or undefined as second argument"_s);
return {};
}
}
}
if (useArrayBuffer) {
JSC::BunV8HeapSnapshotBuilder builder(heapProfiler);
auto bytes = builder.jsonBytes();
auto released = bytes.releaseBuffer();
auto span = released.leakSpan();
auto buffer = ArrayBuffer::createFromBytes(std::span<const uint8_t> { span.data(), span.size() }, createSharedTask<void(void*)>([](void* p) {
fastFree(p);
}));
return JSC::JSValue::encode(JSC::JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(), WTF::move(buffer)));
}
JSC::BunV8HeapSnapshotBuilder builder(heapProfiler);
return JSC::JSValue::encode(jsString(vm, builder.json()));
}
@@ -947,7 +975,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
file BunObject_callback_file DontDelete|Function 1
fileURLToPath functionFileURLToPath DontDelete|Function 1
gc Generated::BunObject::jsGc DontDelete|Function 1
generateHeapSnapshot functionGenerateHeapSnapshot DontDelete|Function 1
generateHeapSnapshot functionGenerateHeapSnapshot DontDelete|Function 2
gunzipSync BunObject_callback_gunzipSync DontDelete|Function 1
gzipSync BunObject_callback_gzipSync DontDelete|Function 1
hash BunObject_lazyPropCb_wrap_hash DontDelete|PropertyCallback
@@ -988,6 +1016,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
serve BunObject_callback_serve DontDelete|Function 1
sha BunObject_callback_sha DontDelete|Function 1
shrink BunObject_callback_shrink DontDelete|Function 1
sliceAnsi jsFunctionBunSliceAnsi DontDelete|Function 5
sleep functionBunSleep DontDelete|Function 1
sleepSync BunObject_callback_sleepSync DontDelete|Function 1
spawn BunObject_callback_spawn DontDelete|Function 1
@@ -995,7 +1024,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
stderr BunObject_lazyPropCb_wrap_stderr DontDelete|PropertyCallback
stdin BunObject_lazyPropCb_wrap_stdin DontDelete|PropertyCallback
stdout BunObject_lazyPropCb_wrap_stdout DontDelete|PropertyCallback
stringWidth Generated::BunObject::jsStringWidth DontDelete|Function 2
stringWidth BunObject_callback_stringWidth DontDelete|Function 2
stripANSI jsFunctionBunStripANSI DontDelete|Function 1
wrapAnsi jsFunctionBunWrapAnsi DontDelete|Function 3
Terminal BunObject_lazyPropCb_wrap_Terminal DontDelete|PropertyCallback

View File

@@ -2812,7 +2812,11 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionsetgroups, (JSGlobalObject * globalObje
auto groups = callFrame->argument(0);
Bun::V::validateArray(scope, globalObject, groups, "groups"_s, jsUndefined());
RETURN_IF_EXCEPTION(scope, {});
auto groupsArray = JSC::jsDynamicCast<JSC::JSArray*>(groups);
auto* groupsArray = JSC::jsDynamicCast<JSC::JSArray*>(groups);
if (!groupsArray) [[unlikely]] {
// validateArray uses JSC::isArray() which accepts Proxy->Array, but jsDynamicCast returns null.
return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "groups"_s, "Array"_s, groups);
}
auto count = groupsArray->length();
gid_t groupsStack[64];
if (count > 64) return Bun::ERR::OUT_OF_RANGE(scope, globalObject, "groups.length"_s, 0, 64, groups);

View File

@@ -1,10 +1,15 @@
// clang-format off
#include "ModuleLoader.h"
#include "root.h"
#include "ModuleLoader.h"
#include "headers-handwritten.h"
#include "PathInlines.h"
#include "JSCommonJSModule.h"
#include <JavaScriptCore/JSBoundFunction.h>
#include <JavaScriptCore/PropertySlot.h>
#include <JavaScriptCore/JSMap.h>
#include <JavaScriptCore/JSString.h>
#include <JavaScriptCore/SourceCode.h>
#include "ZigGlobalObject.h"
#include "InternalModuleRegistry.h"
@@ -85,3 +90,44 @@ extern "C" [[ZIG_EXPORT(nothrow)]] void Bun__ExposeNodeModuleGlobals(Zig::Global
FOREACH_EXPOSED_BUILTIN_IMR(PUT_CUSTOM_GETTER_SETTER)
#undef PUT_CUSTOM_GETTER_SETTER
}
// Set up require(), module, __filename, __dirname on globalThis for the REPL.
// Creates a CommonJS module object rooted at the given directory so require() resolves correctly.
extern "C" [[ZIG_EXPORT(check_slow)]] void Bun__REPL__setupGlobalRequire(
Zig::GlobalObject* globalObject,
const unsigned char* cwdPtr,
size_t cwdLen)
{
using namespace JSC;
auto& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto cwdStr = WTF::String::fromUTF8(std::span { cwdPtr, cwdLen });
auto* filename = jsString(vm, makeString(cwdStr, PLATFORM_SEP_s, "[repl]"_s));
auto* dirname = jsString(vm, WTF::String(cwdStr));
auto* moduleObject = Bun::JSCommonJSModule::create(vm,
globalObject->CommonJSModuleObjectStructure(),
filename, filename, dirname, SourceCode());
moduleObject->hasEvaluated = true;
auto* resolveFunction = JSBoundFunction::create(vm, globalObject,
globalObject->requireResolveFunctionUnbound(), filename,
ArgList(), 1, globalObject->commonStrings().resolveString(globalObject),
makeSource("resolve"_s, SourceOrigin(), SourceTaintedOrigin::Untainted));
RETURN_IF_EXCEPTION(scope, );
auto* requireFunction = JSBoundFunction::create(vm, globalObject,
globalObject->requireFunctionUnbound(), moduleObject,
ArgList(), 1, globalObject->commonStrings().requireString(globalObject),
makeSource("require"_s, SourceOrigin(), SourceTaintedOrigin::Untainted));
RETURN_IF_EXCEPTION(scope, );
requireFunction->putDirect(vm, vm.propertyNames->resolve, resolveFunction, 0);
moduleObject->putDirect(vm, WebCore::clientData(vm)->builtinNames().requirePublicName(), requireFunction, 0);
globalObject->putDirect(vm, WebCore::builtinNames(vm).requirePublicName(), requireFunction, 0);
globalObject->putDirect(vm, Identifier::fromString(vm, "module"_s), moduleObject, 0);
globalObject->putDirect(vm, Identifier::fromString(vm, "__filename"_s), filename, 0);
globalObject->putDirect(vm, Identifier::fromString(vm, "__dirname"_s), dirname, 0);
}

View File

@@ -144,9 +144,15 @@ static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES functionFuzzilli(JSC::JSGlob
WTF::String output = arg1.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
FILE* f = fdopen(REPRL_DWFD, "w");
fprintf(f, "%s\n", output.utf8().data());
fflush(f);
// Use a static FILE* to avoid repeatedly calling fdopen (which
// duplicates the descriptor and leaks) and to gracefully handle
// the case where REPRL_DWFD is not open (i.e. running outside
// the fuzzer harness).
static FILE* f = fdopen(REPRL_DWFD, "w");
if (f) {
fprintf(f, "%s\n", output.utf8().data());
fflush(f);
}
}
}

View File

@@ -837,11 +837,27 @@ static JSC::EncodedJSValue jsBufferConstructorFunction_concatBody(JSC::JSGlobalO
Bun::V::validateArray(throwScope, lexicalGlobalObject, listValue, "list"_s, jsUndefined());
RETURN_IF_EXCEPTION(throwScope, {});
auto array = JSC::jsDynamicCast<JSC::JSArray*>(listValue);
size_t arrayLength = array->length();
if (arrayLength < 1) {
// Note: `validateArray` uses `JSC::isArray()` which returns true for Proxy->Array.
// `jsDynamicCast<JSArray*>` returns nullptr for Proxy, so we must fall back to
// the generic get() path to match Node.js behavior.
auto* array = JSC::jsDynamicCast<JSC::JSArray*>(listValue);
uint64_t arrayLength64;
if (array) [[likely]] {
arrayLength64 = array->length();
} else {
JSValue lengthValue = listValue.get(lexicalGlobalObject, vm.propertyNames->length);
RETURN_IF_EXCEPTION(throwScope, {});
arrayLength64 = lengthValue.toLength(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
}
if (arrayLength64 < 1) {
RELEASE_AND_RETURN(throwScope, constructBufferEmpty(lexicalGlobalObject));
}
if (arrayLength64 > std::numeric_limits<unsigned>::max()) [[unlikely]] {
throwOutOfMemoryError(lexicalGlobalObject, throwScope);
return {};
}
unsigned arrayLength = static_cast<unsigned>(arrayLength64);
JSValue totalLengthValue = callFrame->argument(1);
@@ -857,7 +873,7 @@ static JSC::EncodedJSValue jsBufferConstructorFunction_concatBody(JSC::JSGlobalO
}
for (unsigned i = 0; i < arrayLength; i++) {
JSValue element = array->getIndex(lexicalGlobalObject, i);
JSValue element = array ? array->getIndex(lexicalGlobalObject, i) : listValue.get(lexicalGlobalObject, i);
RETURN_IF_EXCEPTION(throwScope, {});
auto* typedArray = JSC::jsDynamicCast<JSC::JSUint8Array*>(element);
@@ -1092,21 +1108,23 @@ static JSC::EncodedJSValue jsBufferPrototypeFunction_compareBody(JSC::JSGlobalOb
break;
}
if (targetStart > targetEndInit && targetStart <= targetEnd) {
return Bun::ERR::OUT_OF_RANGE(throwScope, lexicalGlobalObject, "targetStart"_s, 0, targetEndInit, targetStartValue);
}
if (targetEnd > targetEndInit && targetEnd >= targetStart) {
// Validate end values against their respective buffer lengths to prevent OOB access.
// This matches Node.js behavior where targetEnd is validated against target.length
// and sourceEnd is validated against source.length.
if (targetEnd > targetEndInit) {
return Bun::ERR::OUT_OF_RANGE(throwScope, lexicalGlobalObject, "targetEnd"_s, 0, targetEndInit, targetEndValue);
}
if (sourceStart > sourceEndInit && sourceStart <= sourceEnd) {
return Bun::ERR::OUT_OF_RANGE(throwScope, lexicalGlobalObject, "sourceStart"_s, 0, sourceEndInit, sourceStartValue);
}
if (sourceEnd > sourceEndInit && sourceEnd >= sourceStart) {
if (sourceEnd > sourceEndInit) {
return Bun::ERR::OUT_OF_RANGE(throwScope, lexicalGlobalObject, "sourceEnd"_s, 0, sourceEndInit, sourceEndValue);
}
targetStart = std::min(targetStart, std::min(targetEnd, targetEndInit));
sourceStart = std::min(sourceStart, std::min(sourceEnd, sourceEndInit));
// When start >= end for either side, return early per Node.js semantics.
// This must be checked before validating start against buffer length, because
// Node.js allows start > buffer.length when it forms a zero-length range.
if (sourceStart >= sourceEnd)
RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(JSC::jsNumber(targetStart >= targetEnd ? 0 : -1)));
if (targetStart >= targetEnd)
RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(JSC::jsNumber(1)));
auto sourceLength = sourceEnd - sourceStart;
auto targetLength = targetEnd - targetStart;

View File

@@ -1164,6 +1164,7 @@ void JSCommonJSModule::visitChildrenImpl(JSCell* cell, Visitor& visitor)
visitor.appendHidden(thisObject->m_dirname);
visitor.appendHidden(thisObject->m_paths);
visitor.appendHidden(thisObject->m_overriddenParent);
visitor.appendHidden(thisObject->m_overriddenCompile);
visitor.appendHidden(thisObject->m_childrenValue);
visitor.appendValues(thisObject->m_children.begin(), thisObject->m_children.size());
}

View File

@@ -202,20 +202,23 @@ pub const JSType = enum(u8) {
/// Global context for Promise.all() (new in recent WebKit).
PromiseAllGlobalContext = 27,
/// Microtask dispatcher for promise/microtask queue management.
JSMicrotaskDispatcher = 28,
/// Base JavaScript object type.
/// ```js
/// {}
/// new Object()
/// ```
Object = 28,
Object = 29,
/// Optimized object type for object literals with fixed properties.
/// ```js
/// { a: 1, b: 2 }
/// ```
FinalObject = 29,
FinalObject = 30,
JSCallee = 30,
JSCallee = 31,
/// JavaScript function object created from JavaScript source code.
/// ```js
@@ -225,7 +228,7 @@ pub const JSType = enum(u8) {
/// method() {}
/// }
/// ```
JSFunction = 31,
JSFunction = 32,
/// Built-in function implemented in native code.
/// ```js
@@ -234,23 +237,23 @@ pub const JSType = enum(u8) {
/// parseInt
/// console.log
/// ```
InternalFunction = 32,
InternalFunction = 33,
NullSetterFunction = 33,
NullSetterFunction = 34,
/// Boxed Boolean object.
/// ```js
/// new Boolean(true)
/// new Boolean(false)
/// ```
BooleanObject = 34,
BooleanObject = 35,
/// Boxed Number object.
/// ```js
/// new Number(42)
/// new Number(3.14)
/// ```
NumberObject = 35,
NumberObject = 36,
/// JavaScript Error object and its subclasses.
/// ```js
@@ -258,9 +261,9 @@ pub const JSType = enum(u8) {
/// new TypeError()
/// throw new RangeError()
/// ```
ErrorInstance = 36,
ErrorInstance = 37,
GlobalProxy = 37,
GlobalProxy = 38,
/// Arguments object for function parameters.
/// ```js
@@ -269,10 +272,10 @@ pub const JSType = enum(u8) {
/// console.log(arguments.length);
/// }
/// ```
DirectArguments = 38,
DirectArguments = 39,
ScopedArguments = 39,
ClonedArguments = 40,
ScopedArguments = 40,
ClonedArguments = 41,
/// JavaScript Array object.
/// ```js
@@ -281,94 +284,94 @@ pub const JSType = enum(u8) {
/// new Array(10)
/// Array.from(iterable)
/// ```
Array = 41,
Array = 42,
/// Array subclass created through class extension.
/// ```js
/// class MyArray extends Array {}
/// const arr = new MyArray();
/// ```
DerivedArray = 42,
DerivedArray = 43,
/// ArrayBuffer for binary data storage.
/// ```js
/// new ArrayBuffer(1024)
/// ```
ArrayBuffer = 43,
ArrayBuffer = 44,
/// Typed array for 8-bit signed integers.
/// ```js
/// new Int8Array(buffer)
/// new Int8Array([1, -1, 127])
/// ```
Int8Array = 44,
Int8Array = 45,
/// Typed array for 8-bit unsigned integers.
/// ```js
/// new Uint8Array(buffer)
/// new Uint8Array([0, 255])
/// ```
Uint8Array = 45,
Uint8Array = 46,
/// Typed array for 8-bit unsigned integers with clamping.
/// ```js
/// new Uint8ClampedArray([0, 300]) // 300 becomes 255
/// ```
Uint8ClampedArray = 46,
Uint8ClampedArray = 47,
/// Typed array for 16-bit signed integers.
/// ```js
/// new Int16Array(buffer)
/// ```
Int16Array = 47,
Int16Array = 48,
/// Typed array for 16-bit unsigned integers.
/// ```js
/// new Uint16Array(buffer)
/// ```
Uint16Array = 48,
Uint16Array = 49,
/// Typed array for 32-bit signed integers.
/// ```js
/// new Int32Array(buffer)
/// ```
Int32Array = 49,
Int32Array = 50,
/// Typed array for 32-bit unsigned integers.
/// ```js
/// new Uint32Array(buffer)
/// ```
Uint32Array = 50,
Uint32Array = 51,
/// Typed array for 16-bit floating point numbers.
/// ```js
/// new Float16Array(buffer)
/// ```
Float16Array = 51,
Float16Array = 52,
/// Typed array for 32-bit floating point numbers.
/// ```js
/// new Float32Array(buffer)
/// ```
Float32Array = 52,
Float32Array = 53,
/// Typed array for 64-bit floating point numbers.
/// ```js
/// new Float64Array(buffer)
/// ```
Float64Array = 53,
Float64Array = 54,
/// Typed array for 64-bit signed BigInt values.
/// ```js
/// new BigInt64Array([123n, -456n])
/// ```
BigInt64Array = 54,
BigInt64Array = 55,
/// Typed array for 64-bit unsigned BigInt values.
/// ```js
/// new BigUint64Array([123n, 456n])
/// ```
BigUint64Array = 55,
BigUint64Array = 56,
/// DataView for flexible binary data access.
/// ```js
@@ -376,7 +379,7 @@ pub const JSType = enum(u8) {
/// view.getInt32(0)
/// view.setFloat64(8, 3.14)
/// ```
DataView = 56,
DataView = 57,
/// Global object containing all global variables and functions.
/// ```js
@@ -384,12 +387,12 @@ pub const JSType = enum(u8) {
/// window // in browsers
/// global // in Node.js
/// ```
GlobalObject = 57,
GlobalObject = 58,
GlobalLexicalEnvironment = 58,
LexicalEnvironment = 59,
ModuleEnvironment = 60,
StrictEvalActivation = 61,
GlobalLexicalEnvironment = 59,
LexicalEnvironment = 60,
ModuleEnvironment = 61,
StrictEvalActivation = 62,
/// Scope object for with statements.
/// ```js
@@ -397,19 +400,19 @@ pub const JSType = enum(u8) {
/// prop; // looks up prop in obj first
/// }
/// ```
WithScope = 62,
WithScope = 63,
AsyncDisposableStack = 63,
DisposableStack = 64,
AsyncDisposableStack = 64,
DisposableStack = 65,
/// Namespace object for ES6 modules.
/// ```js
/// import * as ns from 'module';
/// ns.exportedFunction()
/// ```
ModuleNamespaceObject = 65,
ModuleNamespaceObject = 66,
ShadowRealm = 66,
ShadowRealm = 67,
/// Regular expression object.
/// ```js
@@ -417,7 +420,7 @@ pub const JSType = enum(u8) {
/// new RegExp('pattern', 'flags')
/// /abc/gi
/// ```
RegExpObject = 67,
RegExpObject = 68,
/// JavaScript Date object for date/time operations.
/// ```js
@@ -425,7 +428,7 @@ pub const JSType = enum(u8) {
/// new Date('2023-01-01')
/// Date.now()
/// ```
JSDate = 68,
JSDate = 69,
/// Proxy object that intercepts operations on another object.
/// ```js
@@ -433,7 +436,7 @@ pub const JSType = enum(u8) {
/// get(obj, prop) { return obj[prop]; }
/// })
/// ```
ProxyObject = 69,
ProxyObject = 70,
/// Generator object created by generator functions.
/// ```js
@@ -441,7 +444,7 @@ pub const JSType = enum(u8) {
/// const g = gen();
/// g.next()
/// ```
Generator = 70,
Generator = 71,
/// Async generator object for asynchronous iteration.
/// ```js
@@ -449,17 +452,17 @@ pub const JSType = enum(u8) {
/// yield await promise;
/// }
/// ```
AsyncGenerator = 71,
AsyncGenerator = 72,
/// Iterator for Array objects.
/// ```js
/// [1,2,3][Symbol.iterator]()
/// for (const x of array) {}
/// ```
JSArrayIterator = 72,
JSArrayIterator = 73,
Iterator = 73,
IteratorHelper = 74,
Iterator = 74,
IteratorHelper = 75,
/// Iterator for Map objects.
/// ```js
@@ -468,32 +471,32 @@ pub const JSType = enum(u8) {
/// map.entries()
/// for (const [k,v] of map) {}
/// ```
MapIterator = 75,
MapIterator = 76,
/// Iterator for Set objects.
/// ```js
/// set.values()
/// for (const value of set) {}
/// ```
SetIterator = 76,
SetIterator = 77,
/// Iterator for String objects.
/// ```js
/// 'hello'[Symbol.iterator]()
/// for (const char of string) {}
/// ```
StringIterator = 77,
StringIterator = 78,
WrapForValidIterator = 78,
WrapForValidIterator = 79,
/// Iterator for RegExp string matching.
/// ```js
/// 'abc'.matchAll(/./g)
/// for (const match of string.matchAll(regex)) {}
/// ```
RegExpStringIterator = 79,
RegExpStringIterator = 80,
AsyncFromSyncIterator = 80,
AsyncFromSyncIterator = 81,
/// JavaScript Promise object for asynchronous operations.
/// ```js
@@ -501,7 +504,7 @@ pub const JSType = enum(u8) {
/// Promise.resolve(42)
/// async function foo() { await promise; }
/// ```
JSPromise = 81,
JSPromise = 82,
/// JavaScript Map object for key-value storage.
/// ```js
@@ -509,7 +512,7 @@ pub const JSType = enum(u8) {
/// map.set(key, value)
/// map.get(key)
/// ```
Map = 82,
Map = 83,
/// JavaScript Set object for unique value storage.
/// ```js
@@ -517,34 +520,34 @@ pub const JSType = enum(u8) {
/// set.add(value)
/// set.has(value)
/// ```
Set = 83,
Set = 84,
/// WeakMap for weak key-value references.
/// ```js
/// new WeakMap()
/// weakMap.set(object, value)
/// ```
WeakMap = 84,
WeakMap = 85,
/// WeakSet for weak value references.
/// ```js
/// new WeakSet()
/// weakSet.add(object)
/// ```
WeakSet = 85,
WeakSet = 86,
WebAssemblyModule = 86,
WebAssemblyInstance = 87,
WebAssemblyGCObject = 88,
WebAssemblyModule = 87,
WebAssemblyInstance = 88,
WebAssemblyGCObject = 89,
/// Boxed String object.
/// ```js
/// new String("hello")
/// ```
StringObject = 89,
StringObject = 90,
DerivedStringObject = 90,
InternalFieldTuple = 91,
DerivedStringObject = 91,
InternalFieldTuple = 92,
MaxJS = 0b11111111,
Event = 0b11101111,

View File

@@ -570,6 +570,11 @@ static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::
if (header.key == WebCore::HTTPHeaderName::Date) {
data->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_DATE_HEADER;
}
// Prevent automatic Transfer-Encoding: chunked insertion when user provides one
if (header.key == WebCore::HTTPHeaderName::TransferEncoding) {
data->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER;
}
writeResponseHeader<isSSL>(res, name, value);
}
@@ -642,6 +647,7 @@ static void NodeHTTPServer__writeHead(
String key = propertyNames[i].string();
String value = headerValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, void());
writeResponseHeader<isSSL>(response, key, value);
}
}
@@ -922,8 +928,9 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFr
if (valueValue.isUndefined())
return JSValue::encode(jsUndefined());
if (isArray(globalObject, valueValue)) {
auto* array = jsCast<JSArray*>(valueValue);
// Note: isArray() accepts Proxy->Array, but jsDynamicCast returns null for Proxy.
// Fall through to the single-value path in that case.
if (auto* array = jsDynamicCast<JSArray*>(valueValue)) {
unsigned length = array->length();
if (length > 0) {
JSValue item = array->getIndex(globalObject, 0);

View File

@@ -703,6 +703,17 @@ void NodeVMSpecialSandbox::finishCreation(VM& vm)
const JSC::ClassInfo NodeVMSpecialSandbox::s_info = { "NodeVMSpecialSandbox"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMSpecialSandbox) };
template<typename Visitor>
void NodeVMSpecialSandbox::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{
auto* thisObject = jsCast<NodeVMSpecialSandbox*>(cell);
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
Base::visitChildren(thisObject, visitor);
visitor.append(thisObject->m_parentGlobal);
}
DEFINE_VISIT_CHILDREN(NodeVMSpecialSandbox);
NodeVMGlobalObject::NodeVMGlobalObject(JSC::VM& vm, JSC::Structure* structure, NodeVMContextOptions contextOptions, JSValue importer)
: Base(vm, structure, &globalObjectMethodTable())
, m_dynamicImportCallback(vm, this, importer)
@@ -1209,10 +1220,15 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject
return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "params"_s, "Array"_s, paramsArg);
}
auto* paramsArray = jsCast<JSArray*>(paramsArg);
auto* paramsArray = jsDynamicCast<JSArray*>(paramsArg);
if (!paramsArray) [[unlikely]] {
// isArray() accepts Proxy->Array, but jsDynamicCast returns null.
return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "params"_s, "Array"_s, paramsArg);
}
unsigned length = paramsArray->length();
for (unsigned i = 0; i < length; i++) {
JSValue param = paramsArray->getIndexQuickly(i);
JSValue param = paramsArray->getIndex(globalObject, i);
RETURN_IF_EXCEPTION(scope, {});
if (!param.isString()) {
return ERR::INVALID_ARG_TYPE(scope, globalObject, "params"_s, "Array<string>"_s, paramsArg);
}
@@ -1256,8 +1272,8 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject
JSScope* functionScope = options.parsingContext ? options.parsingContext : globalObject;
if (!options.contextExtensions.isUndefinedOrNull() && !options.contextExtensions.isEmpty() && options.contextExtensions.isObject() && isArray(globalObject, options.contextExtensions)) {
auto* contextExtensionsArray = jsCast<JSArray*>(options.contextExtensions);
unsigned length = contextExtensionsArray->length();
auto* contextExtensionsArray = jsDynamicCast<JSArray*>(options.contextExtensions);
unsigned length = contextExtensionsArray ? contextExtensionsArray->length() : 0;
if (length > 0) {
// Get the global scope from the parsing context
@@ -1265,7 +1281,8 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject
// Create JSWithScope objects for each context extension
for (unsigned i = 0; i < length; i++) {
JSValue extension = contextExtensionsArray->getIndexQuickly(i);
JSValue extension = contextExtensionsArray->getIndex(globalObject, i);
RETURN_IF_EXCEPTION(scope, {});
if (extension.isObject()) {
JSObject* extensionObject = asObject(extension);
currentScope = JSWithScope::create(vm, options.parsingContext, currentScope, extensionObject);
@@ -1769,10 +1786,15 @@ bool CompileFunctionOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM&
return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue);
// Validate that all items in the array are objects
auto* contextExtensionsArray = jsCast<JSArray*>(contextExtensionsValue);
auto* contextExtensionsArray = jsDynamicCast<JSArray*>(contextExtensionsValue);
if (!contextExtensionsArray) [[unlikely]] {
// isArray() accepts Proxy->Array, but jsDynamicCast returns null.
return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue);
}
unsigned length = contextExtensionsArray->length();
for (unsigned i = 0; i < length; i++) {
JSValue extension = contextExtensionsArray->getIndexQuickly(i);
JSValue extension = contextExtensionsArray->getIndex(globalObject, i);
RETURN_IF_EXCEPTION(scope, {});
if (!extension.isObject())
return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions[0]"_s, "object"_s, extension);
}

View File

@@ -85,6 +85,7 @@ public:
static NodeVMSpecialSandbox* create(VM& vm, Structure* structure, NodeVMGlobalObject* globalObject);
DECLARE_INFO;
DECLARE_VISIT_CHILDREN;
template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm);
static Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype);

View File

@@ -1061,9 +1061,7 @@ JSC_DEFINE_HOST_FUNCTION(functionQueueMicrotask,
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
JSC::JSValue asyncContext = globalObject->m_asyncContextData.get()->getInternalField(0);
auto function = globalObject->performMicrotaskFunction();
#if ASSERT_ENABLED
ASSERT_WITH_MESSAGE(function, "Invalid microtask function");
ASSERT_WITH_MESSAGE(!callback.isEmpty(), "Invalid microtask callback");
#endif
@@ -1071,10 +1069,8 @@ JSC_DEFINE_HOST_FUNCTION(functionQueueMicrotask,
asyncContext = JSC::jsUndefined();
}
// BunPerformMicrotaskJob accepts a variable number of arguments (up to: performMicrotask, job, asyncContext, arg0, arg1).
// The runtime inspects argumentCount to determine which arguments are present, so callers may pass only the subset they need.
// Here we pass: function, callback, asyncContext.
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, function, callback, asyncContext };
// BunPerformMicrotaskJob: callback, asyncContext
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, callback, asyncContext };
globalObject->vm().queueMicrotask(WTF::move(task));
return JSC::JSValue::encode(JSC::jsUndefined());
@@ -1554,63 +1550,6 @@ extern "C" napi_env ZigGlobalObject__makeNapiEnvForFFI(Zig::GlobalObject* global
return globalObject->makeNapiEnvForFFI();
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionPerformMicrotask, (JSGlobalObject * globalObject, CallFrame* callframe))
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
auto job = callframe->argument(0);
if (!job || job.isUndefinedOrNull()) [[unlikely]] {
return JSValue::encode(jsUndefined());
}
auto callData = JSC::getCallData(job);
MarkedArgumentBuffer arguments;
if (callData.type == CallData::Type::None) [[unlikely]] {
return JSValue::encode(jsUndefined());
}
JSValue result;
WTF::NakedPtr<JSC::Exception> exceptionPtr;
JSValue restoreAsyncContext = {};
InternalFieldTuple* asyncContextData = nullptr;
auto setAsyncContext = callframe->argument(1);
if (!setAsyncContext.isUndefined()) {
asyncContextData = globalObject->m_asyncContextData.get();
restoreAsyncContext = asyncContextData->getInternalField(0);
asyncContextData->putInternalField(vm, 0, setAsyncContext);
}
size_t argCount = callframe->argumentCount();
switch (argCount) {
case 3: {
arguments.append(callframe->uncheckedArgument(2));
break;
}
case 4: {
arguments.append(callframe->uncheckedArgument(2));
arguments.append(callframe->uncheckedArgument(3));
break;
}
default:
break;
}
JSC::profiledCall(globalObject, ProfilingReason::API, job, callData, jsUndefined(), arguments, exceptionPtr);
if (asyncContextData) {
asyncContextData->putInternalField(vm, 0, restoreAsyncContext);
}
if (auto* exception = exceptionPtr.get()) {
Bun__reportUnhandledError(globalObject, JSValue::encode(exception));
}
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionPerformMicrotaskVariadic, (JSGlobalObject * globalObject, CallFrame* callframe))
{
auto& vm = JSC::getVM(globalObject);
@@ -1940,11 +1879,6 @@ void GlobalObject::finishCreation(VM& vm)
scope.assertNoExceptionExceptTermination();
init.set(subclassStructure);
});
m_performMicrotaskFunction.initLater(
[](const Initializer<JSFunction>& init) {
init.set(JSFunction::create(init.vm, init.owner, 4, "performMicrotask"_s, jsFunctionPerformMicrotask, ImplementationVisibility::Public));
});
m_performMicrotaskVariadicFunction.initLater(
[](const Initializer<JSFunction>& init) {
init.set(JSFunction::create(init.vm, init.owner, 4, "performMicrotaskVariadic"_s, jsFunctionPerformMicrotaskVariadic, ImplementationVisibility::Public));

View File

@@ -272,7 +272,6 @@ public:
JSC::JSObject* performanceObject() const { return m_performanceObject.getInitializedOnMainThread(this); }
JSC::JSFunction* performMicrotaskFunction() const { return m_performMicrotaskFunction.getInitializedOnMainThread(this); }
JSC::JSFunction* performMicrotaskVariadicFunction() const { return m_performMicrotaskVariadicFunction.getInitializedOnMainThread(this); }
JSC::Structure* utilInspectOptionsStructure() const { return m_utilInspectOptionsStructure.getInitializedOnMainThread(this); }
@@ -569,7 +568,6 @@ public:
V(private, LazyPropertyOfGlobalObject<Structure>, m_jsonlParseResultStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_pathParsedObjectStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_pendingVirtualModuleResultStructure) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_performMicrotaskFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_nativeMicrotaskTrampoline) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_performMicrotaskVariadicFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_utilInspectFunction) \

View File

@@ -462,43 +462,41 @@ AsymmetricMatcherResult matchAsymmetricMatcherAndGetFlags(JSGlobalObject* global
JSValue expectedArrayValue = expectArrayContaining->m_arrayValue.get();
if (JSC::isArray(globalObject, otherProp)) {
if (JSC::isArray(globalObject, expectedArrayValue)) {
JSArray* expectedArray = jsDynamicCast<JSArray*>(expectedArrayValue);
JSArray* otherArray = jsDynamicCast<JSArray*>(otherProp);
unsigned expectedLength = expectedArray->length();
unsigned otherLength = otherArray->length();
// A empty array is all array's subset
if (expectedLength == 0) {
return AsymmetricMatcherResult::PASS;
}
// O(m*n) but works for now
for (unsigned m = 0; m < expectedLength; m++) {
JSValue expectedValue = expectedArray->getIndex(globalObject, m);
bool found = false;
for (unsigned n = 0; n < otherLength; n++) {
JSValue otherValue = otherArray->getIndex(globalObject, n);
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer gcBuffer;
bool foundNow = Bun__deepEquals<false, true>(globalObject, expectedValue, otherValue, gcBuffer, stack, throwScope, true);
RETURN_IF_EXCEPTION(throwScope, AsymmetricMatcherResult::FAIL);
if (foundNow) {
found = true;
break;
}
}
if (!found) {
return AsymmetricMatcherResult::FAIL;
}
}
// Note: isArray() accepts Proxy->Array, but jsDynamicCast returns null for Proxy.
JSArray* expectedArray = jsDynamicCast<JSArray*>(expectedArrayValue);
JSArray* otherArray = jsDynamicCast<JSArray*>(otherProp);
if (expectedArray && otherArray) {
unsigned expectedLength = expectedArray->length();
unsigned otherLength = otherArray->length();
// A empty array is all array's subset
if (expectedLength == 0) {
return AsymmetricMatcherResult::PASS;
}
// O(m*n) but works for now
for (unsigned m = 0; m < expectedLength; m++) {
JSValue expectedValue = expectedArray->getIndex(globalObject, m);
bool found = false;
for (unsigned n = 0; n < otherLength; n++) {
JSValue otherValue = otherArray->getIndex(globalObject, n);
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer gcBuffer;
bool foundNow = Bun__deepEquals<false, true>(globalObject, expectedValue, otherValue, gcBuffer, stack, throwScope, true);
RETURN_IF_EXCEPTION(throwScope, AsymmetricMatcherResult::FAIL);
if (foundNow) {
found = true;
break;
}
}
if (!found) {
return AsymmetricMatcherResult::FAIL;
}
}
return AsymmetricMatcherResult::PASS;
}
return AsymmetricMatcherResult::FAIL;
@@ -2450,13 +2448,20 @@ void JSC__JSObject__putRecord(JSC::JSObject* object, JSC::JSGlobalObject* global
descriptor.setValue(JSC::jsString(global->vm(), Zig::toStringCopy(values[0])));
} else {
// Pre-convert all strings to JSValues before entering ObjectInitializationScope,
// since jsString() allocates GC cells which is not allowed inside the scope.
MarkedArgumentBuffer strings;
for (size_t i = 0; i < valuesLen; ++i) {
strings.append(JSC::jsString(global->vm(), Zig::toStringCopy(values[i])));
}
JSC::JSArray* array = nullptr;
{
JSC::ObjectInitializationScope initializationScope(global->vm());
if ((array = JSC::JSArray::tryCreateUninitializedRestricted(initializationScope, nullptr, global->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), valuesLen))) {
for (size_t i = 0; i < valuesLen; ++i) {
array->initializeIndexWithoutBarrier(initializationScope, i, JSC::jsString(global->vm(), Zig::toStringCopy(values[i])));
array->initializeIndexWithoutBarrier(initializationScope, i, strings.at(i));
}
}
}
@@ -2490,6 +2495,13 @@ void JSC__JSValue__putRecord(JSC::EncodedJSValue objectValue, JSC::JSGlobalObjec
descriptor.setValue(JSC::jsString(global->vm(), Zig::toString(values[0])));
} else {
// Pre-convert all strings to JSValues before entering ObjectInitializationScope,
// since jsString() allocates GC cells which is not allowed inside the scope.
MarkedArgumentBuffer strings;
for (size_t i = 0; i < valuesLen; ++i) {
strings.append(JSC::jsString(global->vm(), Zig::toString(values[i])));
}
JSC::JSArray* array = nullptr;
{
JSC::ObjectInitializationScope initializationScope(global->vm());
@@ -2500,7 +2512,7 @@ void JSC__JSValue__putRecord(JSC::EncodedJSValue objectValue, JSC::JSGlobalObjec
for (size_t i = 0; i < valuesLen; ++i) {
array->initializeIndexWithoutBarrier(
initializationScope, i, JSC::jsString(global->vm(), Zig::toString(values[i])));
initializationScope, i, strings.at(i));
}
}
}
@@ -3536,13 +3548,11 @@ void JSC__JSPromise__rejectOnNextTickWithHandled(JSC::JSPromise* promise, JSC::J
promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, jsNumber(flags | JSC::JSPromise::isFirstResolvingFunctionCalledFlag));
auto* globalObject = jsCast<Zig::GlobalObject*>(promise->globalObject());
auto microtaskFunction = globalObject->performMicrotaskFunction();
auto rejectPromiseFunction = globalObject->rejectPromiseFunction();
auto asyncContext = globalObject->m_asyncContextData.get()->getInternalField(0);
#if ASSERT_ENABLED
ASSERT_WITH_MESSAGE(microtaskFunction, "Invalid microtask function");
ASSERT_WITH_MESSAGE(rejectPromiseFunction, "Invalid microtask callback");
ASSERT_WITH_MESSAGE(!value.isEmpty(), "Invalid microtask value");
#endif
@@ -3555,7 +3565,8 @@ void JSC__JSPromise__rejectOnNextTickWithHandled(JSC::JSPromise* promise, JSC::J
value = jsUndefined();
}
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, microtaskFunction, rejectPromiseFunction, globalObject->m_asyncContextData.get()->getInternalField(0), promise, value };
// BunPerformMicrotaskJob: rejectPromiseFunction, asyncContext, promise, value
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, rejectPromiseFunction, globalObject->m_asyncContextData.get()->getInternalField(0), promise, value };
globalObject->vm().queueMicrotask(WTF::move(task));
RETURN_IF_EXCEPTION(scope, );
}
@@ -5436,9 +5447,7 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskJob(JSC::JSGlobalObject* arg0
if (microtaskArgs[3].isEmpty()) {
microtaskArgs[3] = jsUndefined();
}
JSC::JSFunction* microTaskFunction = globalObject->performMicrotaskFunction();
#if ASSERT_ENABLED
ASSERT_WITH_MESSAGE(microTaskFunction, "Invalid microtask function");
auto& vm = globalObject->vm();
if (microtaskArgs[0].isCell()) {
JSC::Integrity::auditCellFully(vm, microtaskArgs[0].asCell());
@@ -5458,7 +5467,8 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskJob(JSC::JSGlobalObject* arg0
#endif
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, microTaskFunction, WTF::move(microtaskArgs[0]), WTF::move(microtaskArgs[1]), WTF::move(microtaskArgs[2]), WTF::move(microtaskArgs[3]) };
// BunPerformMicrotaskJob: job, asyncContext, arg0, arg1
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, WTF::move(microtaskArgs[0]), WTF::move(microtaskArgs[1]), WTF::move(microtaskArgs[2]), WTF::move(microtaskArgs[3]) };
globalObject->vm().queueMicrotask(WTF::move(task));
}
@@ -6139,6 +6149,166 @@ CPP_DECL [[ZIG_EXPORT(nothrow)]] unsigned int Bun__CallFrame__getLineNumber(JSC:
return lineColumn.line;
}
// REPL evaluation function - evaluates JavaScript code in the global scope
// Returns the result value, or undefined if an exception was thrown
// If an exception is thrown, the exception value is stored in *exception
extern "C" JSC::EncodedJSValue Bun__REPL__evaluate(
JSC::JSGlobalObject* globalObject,
const unsigned char* sourcePtr,
size_t sourceLen,
const unsigned char* filenamePtr,
size_t filenameLen,
JSC::EncodedJSValue* exception)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
WTF::String source = WTF::String::fromUTF8(std::span { sourcePtr, sourceLen });
WTF::String filename = filenameLen > 0
? WTF::String::fromUTF8(std::span { filenamePtr, filenameLen })
: "[repl]"_s;
JSC::SourceCode sourceCode = JSC::makeSource(
source,
JSC::SourceOrigin {},
JSC::SourceTaintedOrigin::Untainted,
filename,
WTF::TextPosition(),
JSC::SourceProviderSourceType::Program);
WTF::NakedPtr<JSC::Exception> evalException;
JSC::JSValue result = JSC::evaluate(globalObject, sourceCode, globalObject->globalThis(), evalException);
if (evalException) {
*exception = JSC::JSValue::encode(evalException->value());
// Set _error on the globalObject directly (not globalThis proxy)
globalObject->putDirect(vm, JSC::Identifier::fromString(vm, "_error"_s), evalException->value());
scope.clearException();
return JSC::JSValue::encode(JSC::jsUndefined());
}
if (scope.exception()) {
*exception = JSC::JSValue::encode(scope.exception()->value());
// Set _error on the globalObject directly (not globalThis proxy)
globalObject->putDirect(vm, JSC::Identifier::fromString(vm, "_error"_s), scope.exception()->value());
scope.clearException();
return JSC::JSValue::encode(JSC::jsUndefined());
}
// Note: _ is now set in Zig code (repl.zig) after extracting the value from
// the REPL transform wrapper. We don't set it here anymore.
return JSC::JSValue::encode(result);
}
// REPL completion function - gets completions for a partial property access
// Returns an array of completion strings, or undefined if no completions
extern "C" JSC::EncodedJSValue Bun__REPL__getCompletions(
JSC::JSGlobalObject* globalObject,
JSC::EncodedJSValue targetValue,
const unsigned char* prefixPtr,
size_t prefixLen)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSC::JSValue target = JSC::JSValue::decode(targetValue);
if (!target || target.isUndefined() || target.isNull()) {
target = globalObject->globalThis();
}
if (!target.isObject()) {
JSObject* boxed = target.toObject(globalObject);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
target = boxed;
}
WTF::String prefix = prefixLen > 0
? WTF::String::fromUTF8(std::span { prefixPtr, prefixLen })
: WTF::String();
JSC::JSObject* object = target.getObject();
JSC::PropertyNameArrayBuilder propertyNames(vm, JSC::PropertyNameMode::Strings, JSC::PrivateSymbolMode::Exclude);
object->getPropertyNames(globalObject, propertyNames, DontEnumPropertiesMode::Include);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
JSC::JSArray* completions = JSC::constructEmptyArray(globalObject, nullptr, 0);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
unsigned completionIndex = 0;
for (const auto& propertyName : propertyNames) {
WTF::String name = propertyName.string();
if (prefix.isEmpty() || name.startsWith(prefix)) {
completions->putDirectIndex(globalObject, completionIndex++, JSC::jsString(vm, name));
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
}
}
// Also check the prototype chain
JSC::JSValue proto = object->getPrototype(globalObject);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(completions));
while (proto && proto.isObject()) {
JSC::JSObject* protoObj = proto.getObject();
JSC::PropertyNameArrayBuilder protoNames(vm, JSC::PropertyNameMode::Strings, JSC::PrivateSymbolMode::Exclude);
protoObj->getPropertyNames(globalObject, protoNames, DontEnumPropertiesMode::Include);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(completions));
for (const auto& propertyName : protoNames) {
WTF::String name = propertyName.string();
if (prefix.isEmpty() || name.startsWith(prefix)) {
completions->putDirectIndex(globalObject, completionIndex++, JSC::jsString(vm, name));
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(completions));
}
}
proto = protoObj->getPrototype(globalObject);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(completions));
}
return JSC::JSValue::encode(completions);
}
// Format a value for REPL output using util.inspect style
extern "C" JSC::EncodedJSValue Bun__REPL__formatValue(
JSC::JSGlobalObject* globalObject,
JSC::EncodedJSValue valueEncoded,
int32_t depth,
bool colors)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
// Get the util.inspect function from the global object
auto* bunGlobal = jsCast<Zig::GlobalObject*>(globalObject);
JSC::JSValue inspectFn = bunGlobal->utilInspectFunction();
if (!inspectFn || !inspectFn.isCallable()) {
// Fallback to toString if util.inspect is not available
JSC::JSValue value = JSC::JSValue::decode(valueEncoded);
JSString* str = value.toString(globalObject);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
return JSC::JSValue::encode(str);
}
// Create options object
JSC::JSObject* options = JSC::constructEmptyObject(globalObject);
options->putDirect(vm, JSC::Identifier::fromString(vm, "depth"_s), JSC::jsNumber(depth));
options->putDirect(vm, JSC::Identifier::fromString(vm, "colors"_s), JSC::jsBoolean(colors));
options->putDirect(vm, JSC::Identifier::fromString(vm, "maxArrayLength"_s), JSC::jsNumber(100));
options->putDirect(vm, JSC::Identifier::fromString(vm, "maxStringLength"_s), JSC::jsNumber(10000));
options->putDirect(vm, JSC::Identifier::fromString(vm, "breakLength"_s), JSC::jsNumber(80));
JSC::MarkedArgumentBuffer args;
args.append(JSC::JSValue::decode(valueEncoded));
args.append(options);
JSC::JSValue result = JSC::call(globalObject, inspectFn, JSC::ArgList(args), "util.inspect"_s);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
return JSC::JSValue::encode(result);
}
extern "C" void JSC__ArrayBuffer__ref(JSC::ArrayBuffer* self) { self->ref(); }
extern "C" void JSC__ArrayBuffer__deref(JSC::ArrayBuffer* self) { self->deref(); }
extern "C" void JSC__ArrayBuffer__asBunArrayBuffer(JSC::ArrayBuffer* self, Bun__ArrayBuffer* out)

View File

@@ -168,6 +168,12 @@ CPP_DECL uint32_t JSC__JSInternalPromise__status(const JSC::JSInternalPromise* a
CPP_DECL void JSC__JSFunction__optimizeSoon(JSC::EncodedJSValue JSValue0);
#pragma mark - REPL Functions
CPP_DECL JSC::EncodedJSValue Bun__REPL__evaluate(JSC::JSGlobalObject* globalObject, const unsigned char* sourcePtr, size_t sourceLen, const unsigned char* filenamePtr, size_t filenameLen, JSC::EncodedJSValue* exception);
CPP_DECL JSC::EncodedJSValue Bun__REPL__getCompletions(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue targetValue, const unsigned char* prefixPtr, size_t prefixLen);
CPP_DECL JSC::EncodedJSValue Bun__REPL__formatValue(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue valueEncoded, int32_t depth, bool colors);
#pragma mark - JSC::JSGlobalObject
CPP_DECL VirtualMachine* JSC__JSGlobalObject__bunVM(JSC::JSGlobalObject* arg0);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
#pragma once
#include "root.h"
namespace Bun {
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunSliceAnsi);
}

View File

@@ -759,8 +759,6 @@ static void initializeColumnNames(JSC::JSGlobalObject* lexicalGlobalObject, JSSQ
// Slow path:
JSC::ObjectInitializationScope initializationScope(vm);
// 64 is the maximum we can preallocate here
// see https://github.com/oven-sh/bun/issues/987
JSObject* prototype = castedThis->userPrototype ? castedThis->userPrototype.get() : lexicalGlobalObject->objectPrototype();
@@ -2022,7 +2020,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSetPrototypeFunction, (JSGlobalObject * l
return {};
}
castedThis->userPrototype.set(vm, classObject, prototype.getObject());
castedThis->userPrototype.set(vm, castedThis, prototype.getObject());
// Force the prototypes to be re-created
if (castedThis->version_db) {

View File

@@ -57,6 +57,46 @@ static std::optional<WTF::String> stripANSI(const std::span<const Char> input)
return result.toString();
}
struct BunANSIIterator {
const unsigned char* input;
size_t input_len;
size_t cursor;
const unsigned char* slice_ptr;
size_t slice_len;
};
extern "C" bool Bun__ANSI__next(BunANSIIterator* it)
{
auto start = it->input + it->cursor;
const auto end = it->input + it->input_len;
// Skip past any ANSI sequences at current position
while (start < end) {
const auto escPos = ANSI::findEscapeCharacter(start, end);
if (escPos != start) break;
const auto after = ANSI::consumeANSI(start, end);
if (after == start) {
start++;
break;
}
start = after;
}
if (start >= end) {
it->cursor = it->input_len;
it->slice_ptr = nullptr;
it->slice_len = 0;
return false;
}
const auto escPos = ANSI::findEscapeCharacter(start, end);
const auto slice_end = escPos ? escPos : end;
it->slice_ptr = start;
it->slice_len = slice_end - start;
it->cursor = slice_end - it->input;
return true;
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunStripANSI, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = globalObject->vm();

View File

@@ -31,8 +31,17 @@
#include <utility>
#include <vector>
#ifdef _WIN32
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmicrosoft-include"
#endif
#define v8 real_v8
#define private public
#include "node/v8.h"
#undef private
#undef v8
#ifdef _WIN32
#pragma clang diagnostic pop
#endif

View File

@@ -113,8 +113,9 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSCookieMapDOMConstructo
} else if (initValue.isObject()) {
auto* object = initValue.getObject();
if (isArray(lexicalGlobalObject, object)) {
auto* array = jsCast<JSArray*>(object);
// Note: isArray() accepts Proxy->Array, but jsDynamicCast returns null for Proxy.
auto* array = jsDynamicCast<JSArray*>(object);
if (array) {
Vector<Vector<String>> seqSeq;
uint32_t length = array->length();

Some files were not shown because too many files have changed in this diff Show More