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>
This commit is contained in:
parent
ce95bb2559
commit
974862237b
2 changed files with 555 additions and 158 deletions
51
scripts/run-bun.mjs
Normal file
51
scripts/run-bun.mjs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Wrapper script to run bun with the correct path.
|
||||
* This is needed because during npm install's prepare hook,
|
||||
* node_modules/.bin is not in PATH yet.
|
||||
*/
|
||||
import { spawn } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, "..");
|
||||
const isWindows = process.platform === "win32";
|
||||
const bunExe = isWindows ? "bun.exe" : "bun";
|
||||
|
||||
// Find bun binary
|
||||
const bunPaths = [
|
||||
join(projectRoot, "node_modules", ".bin", bunExe),
|
||||
// Fallback to system bun
|
||||
"bun",
|
||||
];
|
||||
|
||||
let bunPath = null;
|
||||
for (const p of bunPaths) {
|
||||
if (p === "bun" || existsSync(p)) {
|
||||
bunPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bunPath) {
|
||||
console.error("Could not find bun binary");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Run bun with the provided arguments
|
||||
const args = process.argv.slice(2);
|
||||
const child = spawn(bunPath, args, {
|
||||
stdio: "inherit",
|
||||
shell: isWindows,
|
||||
});
|
||||
|
||||
child.on("exit", (code) => {
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
console.error("Failed to run bun:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
262
scripts/setup-bun.mjs
Normal file
262
scripts/setup-bun.mjs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
#!/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
|
||||
});
|
||||
Loading…
Reference in a new issue