Skip to content

feat: ship bun runtime instead of compiled binary#367

Open
shadowfax92 wants to merge 5 commits intomainfrom
feat/new-bun-runtime
Open

feat: ship bun runtime instead of compiled binary#367
shadowfax92 wants to merge 5 commits intomainfrom
feat/new-bun-runtime

Conversation

@shadowfax92
Copy link
Contributor

Summary

  • Adds scripts/build/server-new.ts — a new build script that ships the Bun runtime alongside a bundled index.js instead of compiling to a standalone executable via bun build --compile
  • Downloads platform-specific Bun binaries from GitHub releases (version sourced from packageManager field)
  • Outputs dist/server/browseros-server-{platform}/resources/ matching the directory structure Chromium expects after the bun-binary-based-start migration (resources/bin/bun + resources/index.js)

What changed

Old approach (server.ts): Bun.build()bun build --compile → single executable per platform

New approach (server-new.ts): Bun.build() → download bun runtime → package resources/ directory per platform

Output structure:

dist/server/browseros-server-darwin-arm64/
  resources/
    bin/bun           ← official bun binary for darwin-aarch64
    index.js          ← bundled server (WASM inlined, env vars baked)
    index.js.map      ← linked sourcemap

This aligns with:

  • GetBrowserOSServerExecutablePath()resources/bin/bun
  • ProcessControllerImpl::Launch()bun index.js --config=...
  • OTA updater structure → versions/{v}/resources/

Key design decisions

  • Bun version: Read from packageManager in root package.json (single source of truth)
  • Downloads: Parallel per-platform with caching in dist/server/.cache/bun-v{version}/
  • WASM: Still inlined via wasmBinaryPlugin (user request: put everything in index.js)
  • CLI interface: Unchanged (--mode=prod/dev, --target=...)
  • Sourcemaps: Same Sentry upload pipeline as before

Test plan

  • Dev build tested for darwin-arm64 and darwin-x64
  • Output structure verified (matches Chromium expectations)
  • Bun binary runs correctly (bun --version → 1.3.6)
  • Cache hit works on repeated builds (instant)
  • Biome lint passes
  • Server typecheck passes
  • CI: build for all 5 platform targets
  • Integration: copy resources into Chromium build, verify server starts

🤖 Generated with Claude Code

New build script that ships the Bun runtime alongside a bundled index.js
instead of compiling to a standalone executable via `bun build --compile`.

Output per platform: dist/server/browseros-server-{platform}/resources/
  - bin/bun      (platform-specific Bun runtime from GitHub releases)
  - index.js     (bundled server with WASM inlined)
  - index.js.map (linked source map)

This matches the directory structure Chromium expects after the
bun-binary-based-start migration (resources/bin/bun + resources/index.js).

Key changes from server.ts:
- Eliminates `bun build --compile` step entirely
- Downloads official Bun binaries per platform from GitHub releases
- Reads bun version from root package.json packageManager field
- Parallel downloads with per-version caching in dist/server/.cache/
- Same bundle step (Bun.build with WASM plugin, env injection)
- Same sourcemap/Sentry upload pipeline
- Same CLI interface (--mode=prod/dev --target=...)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 26, 2026

Greptile Summary

Changes build pipeline from standalone executable (bun build --compile) to shipping Bun runtime + bundled index.js. Output structure matches Chromium expectations: resources/bin/bun + resources/index.js.

Key improvements from previous feedback:

  • Added SHA256 checksum verification for downloaded binaries (line 357-361)
  • Fixed cross-platform zip extraction using PowerShell on Windows (line 319-328)
  • Added validation for empty Bun version (line 202-208)

New R2 upload script (scripts/upload/server.ts) zips and uploads artifacts to Cloudflare R2 for distribution.

Issues found:

  • Security: cached Bun binaries are reused without verification — if cache is tampered with, malicious binary will execute
  • Cross-platform: upload script uses zip command which isn't available on Windows by default

Confidence Score: 3/5

  • Has security vulnerability in cache handling and cross-platform compatibility issue
  • Two critical issues prevent safe merge: (1) cached binaries returned without verification allows cache poisoning attack, (2) Windows upload will fail due to missing zip command. Both issues have clear fixes.
  • Pay close attention to scripts/build/server-new.ts (cache verification) and scripts/upload/server.ts (Windows compatibility)

Important Files Changed

Filename Overview
scripts/build/server-new.ts New build script that ships Bun runtime + bundled index.js instead of compiled binary. Security issue: cached binaries returned without verification.
scripts/upload/server.ts New R2 upload script for server artifacts. Cross-platform issue: uses zip command which isn't available on Windows by default.
package.json Added AWS SDK dependency and new build/upload scripts

Last reviewed commit: 9ad1f99

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 7 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 271 to 293
async function downloadBunRuntime(
bunVersion: string,
target: BuildTarget,
): Promise<string> {
const cacheDir = join(CACHE_DIR, `bun-v${bunVersion}`, target.bunReleaseName)
const cachedBinary = join(cacheDir, target.bunBinaryName)

if (existsSync(cachedBinary)) {
log.info(`Cached: ${target.name}`)
return cachedBinary
}

const url = `${BUN_GITHUB_URL}/bun-v${bunVersion}/${target.bunReleaseName}.zip`
log.info(`Downloading: ${target.name}`)

mkdirSync(CACHE_DIR, { recursive: true })
const zipPath = join(CACHE_DIR, `${target.bunReleaseName}.zip`)

await runCommand(
'curl',
['-fSL', '--retry', '3', '-o', zipPath, url],
process.env,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Downloading binaries without integrity verification creates a security risk. If the download is intercepted (MITM) or GitHub is compromised, malicious binaries could be executed.

Consider adding SHA256 checksum verification against known-good hashes published by Bun, or use GPG signature verification if available.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/build/server-new.ts
Line: 271-293

Comment:
Downloading binaries without integrity verification creates a security risk. If the download is intercepted (MITM) or GitHub is compromised, malicious binaries could be executed.

Consider adding SHA256 checksum verification against known-good hashes published by Bun, or use GPG signature verification if available.

How can I resolve this? If you propose a fix, please make it concise.

rmSync(extractDir, { recursive: true, force: true })
mkdirSync(extractDir, { recursive: true })

await runCommandQuiet('unzip', ['-q', '-o', zipPath, '-d', extractDir])
Copy link
Contributor

Choose a reason for hiding this comment

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

unzip command is not available by default on Windows, causing builds to fail on Windows.

Consider using a JavaScript-based unzip library like adm-zip for cross-platform compatibility.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/build/server-new.ts
Line: 299

Comment:
`unzip` command is not available by default on Windows, causing builds to fail on Windows.

Consider using a JavaScript-based unzip library like `adm-zip` for cross-platform compatibility.

How can I resolve this? If you propose a fix, please make it concise.

Address review feedback:
- Verify downloaded bun binaries against SHASUMS256.txt from official releases
- Use platform-aware extraction (unzip on Unix, Expand-Archive on Windows)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines 351 to 357
const expectedHash = sha256Sums.get(zipName)
if (expectedHash) {
verifySha256(zipPath, expectedHash)
log.info(`Verified: ${target.name}`)
} else {
log.info(`Warning: no checksum found for ${zipName}, skipping verification`)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

missing checksum allows unverified binary to be used, defeating the security check

Suggested change
const expectedHash = sha256Sums.get(zipName)
if (expectedHash) {
verifySha256(zipPath, expectedHash)
log.info(`Verified: ${target.name}`)
} else {
log.info(`Warning: no checksum found for ${zipName}, skipping verification`)
}
const expectedHash = sha256Sums.get(zipName)
if (!expectedHash) {
throw new Error(`No checksum found for ${zipName} in SHASUMS256.txt`)
}
verifySha256(zipPath, expectedHash)
log.info(`Verified: ${target.name}`)
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/build/server-new.ts
Line: 351-357

Comment:
missing checksum allows unverified binary to be used, defeating the security check

```suggestion
  const expectedHash = sha256Sums.get(zipName)
  if (!expectedHash) {
    throw new Error(`No checksum found for ${zipName} in SHASUMS256.txt`)
  }
  verifySha256(zipPath, expectedHash)
  log.info(`Verified: ${target.name}`)
```

How can I resolve this? If you propose a fix, please make it concise.

'Root package.json must have packageManager set to bun@{version}',
)
}
return pm.slice(4)
Copy link
Contributor

Choose a reason for hiding this comment

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

empty version not validated - if packageManager is 'bun@', returns empty string causing invalid URLs

Suggested change
return pm.slice(4)
const version = pm.slice(4)
if (!version) {
throw new Error(
'Root package.json packageManager must include version (e.g., bun@1.3.6)',
)
}
return version
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/build/server-new.ts
Line: 202

Comment:
empty version not validated - if `packageManager` is 'bun@', returns empty string causing invalid URLs

```suggestion
  const version = pm.slice(4)
  if (!version) {
    throw new Error(
      'Root package.json packageManager must include version (e.g., bun@1.3.6)',
    )
  }
  return version
```

How can I resolve this? If you propose a fix, please make it concise.

shadowfax92 and others added 2 commits February 26, 2026 15:21
- Fail hard when no SHA256 checksum is found instead of warning
- Validate that packageManager version string is non-empty

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +339 to +342
if (existsSync(cachedBinary)) {
log.info(`Cached: ${target.name}`)
return cachedBinary
}
Copy link
Contributor

Choose a reason for hiding this comment

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

cached binary returned without checksum verification — if cache is compromised, malicious binary will be used

Suggested change
if (existsSync(cachedBinary)) {
log.info(`Cached: ${target.name}`)
return cachedBinary
}
if (existsSync(cachedBinary)) {
const expectedHash = sha256Sums.get(`${target.bunReleaseName}.zip`)
if (expectedHash) {
try {
verifySha256(cachedBinary, expectedHash)
log.info(`Cached: ${target.name}`)
return cachedBinary
} catch {
log.info(`Cache invalid for ${target.name}, re-downloading...`)
rmSync(cachedBinary, { force: true })
}
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/build/server-new.ts
Line: 339-342

Comment:
cached binary returned without checksum verification — if cache is compromised, malicious binary will be used

```suggestion
  if (existsSync(cachedBinary)) {
    const expectedHash = sha256Sums.get(`${target.bunReleaseName}.zip`)
    if (expectedHash) {
      try {
        verifySha256(cachedBinary, expectedHash)
        log.info(`Cached: ${target.name}`)
        return cachedBinary
      } catch {
        log.info(`Cache invalid for ${target.name}, re-downloading...`)
        rmSync(cachedBinary, { force: true })
      }
    }
  }
```

How can I resolve this? If you propose a fix, please make it concise.

- scripts/upload/server.ts: zips and uploads per-platform resources/ to R2
- scripts/.env.example: R2 credential template
- Adds @aws-sdk/client-s3 for S3-compatible R2 uploads
- New command: bun run upload:server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +339 to +341
if (existsSync(cachedBinary)) {
log.info(`Cached: ${target.name}`)
return cachedBinary
Copy link
Contributor

Choose a reason for hiding this comment

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

cached binary used without checksum verification — if cache directory is compromised, malicious binary will execute without detection

Suggested change
if (existsSync(cachedBinary)) {
log.info(`Cached: ${target.name}`)
return cachedBinary
if (existsSync(cachedBinary)) {
const expectedHash = sha256Sums.get(zipName)
if (expectedHash) {
const binaryHash = createHash('sha256').update(readFileSync(cachedBinary)).digest('hex')
const zipPath = join(CACHE_DIR, zipName)
if (existsSync(zipPath)) {
try {
verifySha256(zipPath, expectedHash)
log.info(`Cached: ${target.name}`)
return cachedBinary
} catch {
// Cache corrupted, re-download
rmSync(cacheDir, { recursive: true, force: true })
}
}
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/build/server-new.ts
Line: 339-341

Comment:
cached binary used without checksum verification — if cache directory is compromised, malicious binary will execute without detection

```suggestion
  if (existsSync(cachedBinary)) {
    const expectedHash = sha256Sums.get(zipName)
    if (expectedHash) {
      const binaryHash = createHash('sha256').update(readFileSync(cachedBinary)).digest('hex')
      const zipPath = join(CACHE_DIR, zipName)
      if (existsSync(zipPath)) {
        try {
          verifySha256(zipPath, expectedHash)
          log.info(`Cached: ${target.name}`)
          return cachedBinary
        } catch {
          // Cache corrupted, re-download
          rmSync(cacheDir, { recursive: true, force: true })
        }
      }
    }
  }
```

How can I resolve this? If you propose a fix, please make it concise.

outputPath: string,
): Promise<void> {
const absOutput = resolve(outputPath)
const proc = Bun.spawn(['zip', '-r', '-q', absOutput, 'resources'], {
Copy link
Contributor

Choose a reason for hiding this comment

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

zip command not available on Windows by default — will cause upload failures on Windows

follow the pattern from server-new.ts:319-328 which uses PowerShell's Compress-Archive on Windows

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/upload/server.ts
Line: 186

Comment:
`zip` command not available on Windows by default — will cause upload failures on Windows

follow the pattern from `server-new.ts:319-328` which uses PowerShell's `Compress-Archive` on Windows

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant