ext-apps/scripts/setup-bun.mjs
Olivier Chafik 974862237b
fix: Windows compatibility - bun as optional dep, cross-env for examples (#145)
* fix: Windows compatibility for npm scripts and cross-platform CI

Fixes #142 - Windows PowerShell install bug

Changes:
- Replace single quotes with escaped double quotes in generate:schemas
  (cmd.exe passes single quotes literally, breaking prettier glob matching)
- Simplify prettier scripts: remove redundant --ignore-path flags
  (prettier auto-loads .gitignore and .prettierignore by default)
- Remove $(pwd) from prettier scripts (bash-only syntax)
- Add cross-platform CI matrix: Linux x64, Windows x64, macOS ARM64, macOS x64
- Use shell: bash for grep commands in CI (not available in Windows cmd.exe)

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: remove bun npm package from all workspaces

The bun npm package tries to install platform-specific binaries which
fails on Windows CI. Since bun is installed via oven-sh/setup-bun action
in CI (and developers install it globally), we don't need the npm package.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: skip examples:build on Windows (uses Unix-only INPUT=... syntax)

The examples use `INPUT=mcp-app.html vite build` which is bash syntax
for setting environment variables. This doesn't work on Windows cmd.exe.
Since examples aren't published to npm, we can skip building them on Windows.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: force LF line endings for cross-platform prettier consistency

Git on Windows auto-converts line endings to CRLF, causing prettier to
report formatting issues. Force LF for all text files.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: add test for git-based npm install (cross-platform)

This verifies that `npm install git+https://...` works on all platforms,
which triggers the prepare script and requires bun to be available.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: add Linux ARM64 and Windows ARM64 to CI matrix

- Added ubuntu-24.04-arm (Linux ARM64)
- Added windows-11-arm (Windows ARM64)

Note: WSL is not available on GitHub-hosted runners (requires nested
virtualization which isn't supported).

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

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: disable Windows ARM64 (Bun lacks stable ARM64 binaries)

The oven-sh/setup-bun action fails with 404 when trying to download
Bun for Windows ARM64. Re-enable when Bun adds proper support.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: add Windows WSL build test

Uses Vampire/setup-wsl action to install Ubuntu-24.04 in WSL and run
the full build/test cycle. This tests the Linux-in-Windows scenario
that was reported in issue #142.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: fix WSL build - install unzip for Bun installer

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

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: add setup-bun to publish.yml workflow

The pkg-pr-new publish workflow was missing bun setup, causing
"bun: not found" errors during npm ci (which triggers prepare script).

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

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: enable Windows ARM64 with x64-baseline Bun (via emulation)

Windows ARM64 can run x64 binaries via emulation. Use bun-download-url
to force x64-baseline build since native ARM64 Bun isn't available yet.

Reference: https://github.com/oven-sh/bun/issues/9824

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

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: remove macOS x64 (macos-13 runner retired)

The macos-13 based runner images are now retired.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add Bun installation instructions per platform

Documents that Bun is required for development, with platform-specific
install commands in collapsible sections.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: note Bun requirement in README examples section

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

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: add bun as optional dependency (no global install required)

- Add @oven/bun-* platform packages as optionalDependencies
- Create postinstall script to set up bun binary from optional deps
- Handle Windows ARM64 by downloading x64-baseline for emulation
- Remove setup-bun from CI workflows (npm install handles it now)
- Remove manual Bun install instructions from docs

This alternative approach eliminates the need for global Bun installation
or setup-bun action in CI. npm install automatically installs the
platform-appropriate bun binary.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: add debug logging to setup-bun script

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: force stdout flush in setup-bun for CI debugging

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: add more debugging to setup-bun

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: remove accidental test artifacts

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: run setup-bun in prepare script (before build)

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

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: use cross-env for Windows-compatible example builds

- Add cross-env to devDependencies
- Update all example package.json scripts to use cross-env for INPUT and NODE_ENV
- Enable examples:build on Windows in CI (no longer Unix-only)
- Add examples:build to WSL CI job

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: remove unused imports in setup-bun.mjs

Address github-code-quality bot comments.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: use run-bun.mjs wrapper for prepare hook compatibility

During npm install's prepare hook, node_modules/.bin is not in PATH,
so calling 'bun' directly fails. The new run-bun.mjs wrapper finds
bun at node_modules/.bin/bun and runs it with the correct path.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: restore setup-bun action for build jobs

- Add setup-bun action to build and test-git-install jobs
- Disable Windows ARM64 (Bun doesn't support this platform)
- Add bun installation to WSL build

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: remove bun devDep, add libc detection for Linux ARM64

- Remove 'bun' from devDependencies (its postinstall fails on Windows ARM64)
- Add libc detection (glibc vs musl) to setup-bun.mjs
- Only try compatible binaries based on detected libc
- Re-enable Windows ARM64 in CI (now uses x64 emulation fallback)
- Remove setup-bun action from CI (no longer needed)

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

Co-Authored-By: Claude <noreply@anthropic.com>

* ci: simplify WSL build - let setup-bun.mjs handle bun installation

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 14:07:38 +00:00

262 lines
7.1 KiB
JavaScript

#!/usr/bin/env node
// Immediate log to verify script execution
console.log("[setup-bun] Script loaded");
/**
* Postinstall script to set up bun from platform-specific optional dependencies.
* Handles Windows ARM64 by downloading x64-baseline via emulation.
*/
import {
existsSync,
mkdirSync,
symlinkSync,
unlinkSync,
copyFileSync,
chmodSync,
writeFileSync,
} from "fs";
import { join, dirname } from "path";
import { spawnSync } from "child_process";
import { fileURLToPath } from "url";
import { get } from "https";
import { createGunzip } from "zlib";
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, "..");
const nodeModules = join(projectRoot, "node_modules");
const binDir = join(nodeModules, ".bin");
const os = process.platform;
const arch = process.arch;
const isWindows = os === "win32";
const bunExe = isWindows ? "bun.exe" : "bun";
// Detect libc type on Linux (glibc vs musl)
function detectLibc() {
if (os !== "linux") return null;
// Check for musl-specific loader
const muslLoaders = [
`/lib/ld-musl-${arch === "arm64" ? "aarch64" : "x86_64"}.so.1`,
"/lib/ld-musl-x86_64.so.1",
"/lib/ld-musl-aarch64.so.1",
];
for (const loader of muslLoaders) {
if (existsSync(loader)) {
console.log(` Detected musl libc (found ${loader})`);
return "musl";
}
}
// Default to glibc on Linux
console.log(" Detected glibc (no musl loader found)");
return "glibc";
}
// Platform to package mapping (matches @oven/bun-* package names)
// For Linux, separate glibc and musl packages
const platformPackages = {
darwin: {
arm64: ["bun-darwin-aarch64"],
x64: ["bun-darwin-x64", "bun-darwin-x64-baseline"],
},
linux: {
arm64: {
glibc: ["bun-linux-aarch64"],
musl: ["bun-linux-aarch64-musl"],
},
x64: {
glibc: ["bun-linux-x64", "bun-linux-x64-baseline"],
musl: ["bun-linux-x64-musl", "bun-linux-x64-musl-baseline"],
},
},
win32: {
x64: ["bun-windows-x64", "bun-windows-x64-baseline"],
arm64: ["bun-windows-x64-baseline"], // x64 runs via emulation on ARM64
},
};
function findBunBinary() {
let packages = platformPackages[os]?.[arch];
// For Linux, select packages based on libc type
if (os === "linux" && packages && typeof packages === "object") {
const libc = detectLibc();
packages = packages[libc] || [];
}
packages = packages || [];
console.log(
`Looking for bun packages: ${packages.join(", ") || "(none for this platform)"}`,
);
for (const pkg of packages) {
const binPath = join(nodeModules, "@oven", pkg, "bin", bunExe);
console.log(` Checking: ${binPath}`);
if (existsSync(binPath)) {
console.log(` Found bun at: ${binPath}`);
return binPath;
} else {
console.log(` Not found`);
}
}
return null;
}
async function downloadBunForWindowsArm64() {
// Windows ARM64 can run x64 binaries via emulation
const pkg = "bun-windows-x64-baseline";
const version = "1.3.4";
const url = `https://registry.npmjs.org/@oven/${pkg}/-/${pkg}-${version}.tgz`;
const destDir = join(nodeModules, "@oven", pkg);
console.log(`Downloading ${pkg} for Windows ARM64 emulation...`);
return new Promise((resolve, reject) => {
get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
get(response.headers.location, handleResponse).on("error", reject);
} else {
handleResponse(response);
}
function handleResponse(res) {
if (res.statusCode !== 200) {
reject(new Error(`Failed to download: ${res.statusCode}`));
return;
}
const chunks = [];
const gunzip = createGunzip();
res.pipe(gunzip);
gunzip.on("data", (chunk) => chunks.push(chunk));
gunzip.on("end", () => {
try {
extractTar(Buffer.concat(chunks), destDir);
const binPath = join(destDir, "bin", bunExe);
if (existsSync(binPath)) {
resolve(binPath);
} else {
reject(new Error("Binary not found after extraction"));
}
} catch (err) {
reject(err);
}
});
gunzip.on("error", reject);
}
}).on("error", reject);
});
}
function extractTar(buffer, destDir) {
// Simple tar extraction (512-byte blocks)
let offset = 0;
while (offset < buffer.length) {
const name = buffer
.toString("utf-8", offset, offset + 100)
.replace(/\0.*$/, "")
.replace("package/", "");
const size = parseInt(
buffer.toString("utf-8", offset + 124, offset + 136).trim(),
8,
);
offset += 512;
if (!isNaN(size) && size > 0 && name) {
const filePath = join(destDir, name);
const fileDir = dirname(filePath);
if (!existsSync(fileDir)) {
mkdirSync(fileDir, { recursive: true });
}
const content = buffer.subarray(offset, offset + size);
writeFileSync(filePath, content);
// Make executable
if (name.endsWith(bunExe) || name === "bin/bun") {
try {
chmodSync(filePath, 0o755);
} catch {}
}
offset += Math.ceil(size / 512) * 512;
}
}
}
function setupBinLink(bunPath) {
if (!existsSync(binDir)) {
mkdirSync(binDir, { recursive: true });
}
const bunLink = join(binDir, bunExe);
const bunxLink = join(binDir, isWindows ? "bunx.exe" : "bunx");
// Remove existing links
for (const link of [bunLink, bunxLink]) {
try {
unlinkSync(link);
} catch {}
}
if (isWindows) {
// On Windows, copy the binary (symlinks may not work without admin)
copyFileSync(bunPath, bunLink);
copyFileSync(bunPath, bunxLink);
} else {
// On Unix, use symlinks
symlinkSync(bunPath, bunLink);
symlinkSync(bunPath, bunxLink);
}
console.log(`Bun linked to: ${bunLink}`);
}
// Force immediate output
process.stdout.write("[setup-bun] Script starting...\n");
async function main() {
process.stdout.write(`[setup-bun] Setting up bun for ${os} ${arch}...\n`);
process.stdout.write(`[setup-bun] Project root: ${projectRoot}\n`);
process.stdout.write(`[setup-bun] Node modules: ${nodeModules}\n`);
let bunPath = findBunBinary();
if (!bunPath && os === "win32" && arch === "arm64") {
try {
bunPath = await downloadBunForWindowsArm64();
} catch (err) {
console.error("Failed to download bun for Windows ARM64:", err.message);
}
}
if (!bunPath) {
console.log("No bun binary found in optional dependencies.");
console.log("Bun will need to be installed separately.");
console.log("See: https://bun.sh/docs/installation");
process.exit(0); // Don't fail the install
}
try {
setupBinLink(bunPath);
// Verify installation
const result = spawnSync(bunPath, ["--version"], { encoding: "utf-8" });
if (result.status === 0) {
console.log(`Bun ${result.stdout.trim()} installed successfully!`);
}
} catch (err) {
console.error("Failed to set up bun:", err.message);
process.exit(0); // Don't fail the install
}
}
main().catch((err) => {
console.error(err);
process.exit(0); // Don't fail the install
});