From b216ea26b57b75dac31ba7e19c46d27d2c22eb63 Mon Sep 17 00:00:00 2001 From: Liu Juncheng <2350106512@qq.com> Date: Sun, 17 May 2026 01:38:00 +0800 Subject: [PATCH] fix: make Bun build work on Windows shell Move the long package build pipeline into a bash script so Bun's Windows script parser no longer sees subshell redirections like '( git ... ) > dist/.version'. Write version files via a dedicated helper and add regression coverage for the shell pattern. When setup runs on Windows from a non-ASCII bun.exe path, use a temporary ASCII-path bun copy for --compile and clean it up afterward. --- package.json | 2 +- scripts/build.sh | 38 +++++++++++++++++++++ scripts/write-version-files.sh | 13 +++++++ setup | 47 +++++++++++++++++++++----- test/build-script-shell-compat.test.ts | 30 +++++++++++----- 5 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 scripts/build.sh create mode 100644 scripts/write-version-files.sh diff --git a/package.json b/package.json index 601eb963c9..bc09b9f6c2 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "make-pdf": "./make-pdf/dist/pdf" }, "scripts": { - "build": "bun run vendor:xterm && bun run gen:skill-docs --host all; bun build --compile browse/src/cli.ts --outfile browse/dist/browse && bun build --compile browse/src/find-browse.ts --outfile browse/dist/find-browse && bun build --compile design/src/cli.ts --outfile design/dist/design && bun build --compile make-pdf/src/cli.ts --outfile make-pdf/dist/pdf && bun build --compile bin/gstack-global-discover.ts --outfile bin/gstack-global-discover && bash browse/scripts/build-node-server.sh && ( git rev-parse HEAD 2>/dev/null || true ) > browse/dist/.version && ( git rev-parse HEAD 2>/dev/null || true ) > design/dist/.version && ( git rev-parse HEAD 2>/dev/null || true ) > make-pdf/dist/.version && chmod +x browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover && (rm -f .*.bun-build || true)", + "build": "bash scripts/build.sh", "vendor:xterm": "mkdir -p extension/lib && cp node_modules/xterm/lib/xterm.js extension/lib/xterm.js && cp node_modules/xterm/css/xterm.css extension/lib/xterm.css && cp node_modules/xterm-addon-fit/lib/xterm-addon-fit.js extension/lib/xterm-addon-fit.js", "dev:make-pdf": "bun run make-pdf/src/cli.ts", "dev:design": "bun run design/src/cli.ts", diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000000..67acf6dc06 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -e + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +cd "$ROOT" + +BUN_CMD="${BUN_CMD:-bun}" +BUN_CMD_WAS_COPIED=0 + +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*|Windows_NT) + bun_path="$(command -v "$BUN_CMD" 2>/dev/null || true)" + case "$bun_path" in + *[![:ascii:]]*) + bun_copy_dir="$ROOT/.tmp-bun-bin" + mkdir -p "$bun_copy_dir" + cp -f "$bun_path" "$bun_copy_dir/bun.exe" + BUN_CMD="$bun_copy_dir/bun.exe" + BUN_CMD_WAS_COPIED=1 + ;; + esac + ;; +esac + +"$BUN_CMD" run vendor:xterm +"$BUN_CMD" run gen:skill-docs --host all +"$BUN_CMD" build --compile browse/src/cli.ts --outfile browse/dist/browse +"$BUN_CMD" build --compile browse/src/find-browse.ts --outfile browse/dist/find-browse +"$BUN_CMD" build --compile design/src/cli.ts --outfile design/dist/design +"$BUN_CMD" build --compile make-pdf/src/cli.ts --outfile make-pdf/dist/pdf +"$BUN_CMD" build --compile bin/gstack-global-discover.ts --outfile bin/gstack-global-discover +bash browse/scripts/build-node-server.sh +bash scripts/write-version-files.sh browse/dist/.version design/dist/.version make-pdf/dist/.version +chmod +x browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover +rm -f .*.bun-build +if [ "$BUN_CMD_WAS_COPIED" -eq 1 ]; then + rm -rf "$ROOT/.tmp-bun-bin" +fi diff --git a/scripts/write-version-files.sh b/scripts/write-version-files.sh new file mode 100644 index 0000000000..c4932171c9 --- /dev/null +++ b/scripts/write-version-files.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e + +if git_head="$(git rev-parse HEAD 2>/dev/null)"; then + : +else + git_head="" +fi + +for version_file in "$@"; do + mkdir -p "$(dirname "$version_file")" + printf '%s\n' "$git_head" > "$version_file" +done diff --git a/setup b/setup index b51fed83df..631b84003c 100755 --- a/setup +++ b/setup @@ -261,6 +261,37 @@ ensure_playwright_browser() { fi } +prepare_bun_for_windows_compile() { + BUN_CMD="bun" + BUN_CMD_WAS_COPIED=0 + [ "$IS_WINDOWS" -eq 1 ] || return 0 + + local bun_path + bun_path="$(command -v bun 2>/dev/null || true)" + case "$bun_path" in + *[![:ascii:]]*) + local bun_copy_dir="$SOURCE_GSTACK_DIR/.tmp-bun-bin" + mkdir -p "$bun_copy_dir" + cp -f "$bun_path" "$bun_copy_dir/bun.exe" + BUN_CMD="$bun_copy_dir/bun.exe" + BUN_CMD_WAS_COPIED=1 + ;; + esac +} + +bun_cmd() { + "$BUN_CMD" "$@" +} + +cleanup_copied_bun() { + if [ "${BUN_CMD_WAS_COPIED:-0}" -eq 1 ]; then + rm -rf "$SOURCE_GSTACK_DIR/.tmp-bun-bin" + fi +} + +prepare_bun_for_windows_compile +trap cleanup_copied_bun EXIT + # 1. Build browse binary if needed (smart rebuild: stale sources, package.json, lock) NEEDS_BUILD=0 if [ ! -x "$BROWSE_BIN" ]; then @@ -277,8 +308,8 @@ if [ "$NEEDS_BUILD" -eq 1 ]; then log "Building browse binary..." ( cd "$SOURCE_GSTACK_DIR" - bun install --frozen-lockfile 2>/dev/null || bun install - bun run build + bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install + bun_cmd run build ) # Safety net: write .version if build script didn't (e.g., git not available during build) if [ ! -f "$SOURCE_GSTACK_DIR/browse/dist/.version" ]; then @@ -337,8 +368,8 @@ if [ "$NEEDS_AGENTS_GEN" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then log "Generating .agents/ skill docs..." ( cd "$SOURCE_GSTACK_DIR" - bun install --frozen-lockfile 2>/dev/null || bun install - bun run gen:skill-docs --host codex + bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install + bun_cmd run gen:skill-docs --host codex ) fi @@ -347,8 +378,8 @@ if [ "$INSTALL_FACTORY" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then log "Generating .factory/ skill docs..." ( cd "$SOURCE_GSTACK_DIR" - bun install --frozen-lockfile 2>/dev/null || bun install - bun run gen:skill-docs --host factory + bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install + bun_cmd run gen:skill-docs --host factory ) fi @@ -357,8 +388,8 @@ if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then log "Generating .opencode/ skill docs..." ( cd "$SOURCE_GSTACK_DIR" - bun install --frozen-lockfile 2>/dev/null || bun install - bun run gen:skill-docs --host opencode + bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install + bun_cmd run gen:skill-docs --host opencode ) fi diff --git a/test/build-script-shell-compat.test.ts b/test/build-script-shell-compat.test.ts index ee13fb709e..6b39f5b3e8 100644 --- a/test/build-script-shell-compat.test.ts +++ b/test/build-script-shell-compat.test.ts @@ -6,6 +6,7 @@ const ROOT = path.resolve(import.meta.dir, '..'); const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8')) as { scripts: Record; }; +const BUILD_SCRIPT = fs.readFileSync(path.join(ROOT, 'scripts', 'build.sh'), 'utf-8'); // Strip single-quoted strings so JS code emitted as `echo '{ ... }'` doesn't // trip the shell-brace-group check. Conservative: only `'...'` segments. @@ -15,7 +16,8 @@ function stripSingleQuoted(s: string): string { describe('package.json build scripts — POSIX shell compat (D-1460)', () => { // Bun's Windows shell parser doesn't grok bash brace groups `{ cmd; }`. - // Subshells `( cmd )` are POSIX-universal. This test prevents regression. + // Bun 1.3.x on Windows also rejects subshells when the subshell or the + // command inside it uses redirection, so redirected commands must be direct. test('no bash brace groups in any npm script', () => { const offending: { script: string; pattern: string }[] = []; for (const [name, body] of Object.entries(PKG.scripts)) { @@ -28,13 +30,25 @@ describe('package.json build scripts — POSIX shell compat (D-1460)', () => { expect(offending).toEqual([]); }); - test('every `> path/.version` redirect is preceded by a subshell, not a brace group', () => { - // The original PR #1460 target: package.json line 12 had three of these. - const build = PKG.scripts.build ?? ''; - const versionRedirects = [...build.matchAll(/(\([^)]*\)|\{[^}]*\})\s*>\s*\S+\/\.version/g)]; - expect(versionRedirects.length).toBeGreaterThan(0); - for (const m of versionRedirects) { - expect(m[1].startsWith('(')).toBe(true); + test('build script has no subshells with redirections', () => { + const offending: { script: string; pattern: string }[] = []; + for (const [name, body] of Object.entries({ build: PKG.scripts.build ?? '' })) { + const matches = [ + ...body.matchAll(/\([^)]*[<>][^)]*\)/g), + ...body.matchAll(/\([^)]*\)\s*[<>]/g), + ]; + for (const match of matches) { + offending.push({ script: name, pattern: match[0] }); + } } + expect(offending).toEqual([]); + }); + + test('build script delegates .version writes to a shell script', () => { + // Bun rejects `( git ... ) > path/.version`. + const build = PKG.scripts.build ?? ''; + expect(build).not.toMatch(/>\s*\S+\/\.version/); + expect(build).toBe('bash scripts/build.sh'); + expect(BUILD_SCRIPT).toContain('bash scripts/write-version-files.sh'); }); });