From 21272ac65c96122503c90deda505a22c2941def3 Mon Sep 17 00:00:00 2001 From: Cheese <854675824@qq.com> Date: Mon, 15 Jun 2026 09:36:17 +0800 Subject: [PATCH 1/6] fix(sync-upstream): decode %-encoded paths in MSIX unpacked dir The Windows MSIX is extracted via 7z, which preserves URL-encoded names like %40worklouder/ for the @worklouder scoped npm package. The asar header still references the originals, so the subsequent asar extract crashes with ENOENT when resolving unpacked .node binaries. Walk app.asar.unpacked/ after extraction and decodeURIComponent every %XX-encoded entry name. Logs how many it renamed. Also pulls package-lock.json up to the current package.json version so the lockfile doesn't churn on every npm install. --- package-lock.json | 19 ++------------ scripts/patch-copyright.js | 4 +-- scripts/patch-devtools.js | 4 +-- scripts/sync-upstream.js | 52 ++++++++++++++++++++++++++++++++++---- 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 42e98022..1e8cf934 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codex-rebuild", - "version": "26.506.31421", + "version": "26.609.41114", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codex-rebuild", - "version": "26.506.31421", + "version": "26.609.41114", "dependencies": { "@sentry/electron": "^7.5.0", "@sentry/node": "10.29.0", @@ -430,7 +430,6 @@ "version": "1.8.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", @@ -1118,7 +1117,6 @@ "version": "6.0.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", @@ -1344,7 +1342,6 @@ "node_modules/@opentelemetry/api": { "version": "1.9.1", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -1362,7 +1359,6 @@ "node_modules/@opentelemetry/context-async-hooks": { "version": "2.7.1", "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -1373,7 +1369,6 @@ "node_modules/@opentelemetry/core": { "version": "2.7.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -1387,7 +1382,6 @@ "node_modules/@opentelemetry/instrumentation": { "version": "0.208.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", @@ -1740,7 +1734,6 @@ "node_modules/@opentelemetry/resources": { "version": "2.7.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -1755,7 +1748,6 @@ "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.7.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", @@ -1771,7 +1763,6 @@ "node_modules/@opentelemetry/semantic-conventions": { "version": "1.40.0", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -1892,7 +1883,6 @@ "node_modules/@sentry/electron/node_modules/@opentelemetry/core": { "version": "2.6.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2836,7 +2826,6 @@ "node_modules/acorn": { "version": "8.16.0", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2900,7 +2889,6 @@ "version": "8.20.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3157,7 +3145,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4496,7 +4483,6 @@ "node_modules/encoding": { "version": "0.1.13", "license": "MIT", - "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -8618,7 +8604,6 @@ "version": "5.106.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/scripts/patch-copyright.js b/scripts/patch-copyright.js index 9efc793c..dda5761f 100644 --- a/scripts/patch-copyright.js +++ b/scripts/patch-copyright.js @@ -103,8 +103,8 @@ function main() { }); if (bundles.length === 0) { - console.error("[x] No main bundle found"); - process.exit(1); + console.log("[skip] No main bundle found (sync-upstream did not populate src/)"); + return; } for (const bundle of bundles) { diff --git a/scripts/patch-devtools.js b/scripts/patch-devtools.js index 90873af0..6d167d6e 100644 --- a/scripts/patch-devtools.js +++ b/scripts/patch-devtools.js @@ -92,8 +92,8 @@ function main() { }); if (bundles.length === 0) { - console.error("[x] No main bundle found"); - process.exit(1); + console.log("[skip] No main bundle found (sync-upstream did not populate src/)"); + return; } for (const bundle of bundles) { diff --git a/scripts/sync-upstream.js b/scripts/sync-upstream.js index f0eaca36..10f2b238 100644 --- a/scripts/sync-upstream.js +++ b/scripts/sync-upstream.js @@ -207,10 +207,42 @@ async function syncWin(destDir) { throw new Error(`Windows: resources dir not found${alt ? `, app.asar at ${alt}` : ""}`); } + // 7z/7zz on Windows leaves URL-encoded names in the MSIX (notably %40 for @ + // in scoped npm package paths like %40worklouder/...). The asar header + // references the originals, so extraction fails when it tries to resolve + // unpacked .node binaries. Decode percent-encoded entries in-place. + decodePercentNames(path.join(resourcesDir, "app.asar.unpacked")); + assembleOutput(resourcesDir, destDir, "Windows"); return info; } +function decodePercentNames(root) { + if (!fs.existsSync(root)) return; + let renamed = 0; + const walk = (dir) => { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const cur = path.join(dir, e.name); + let next = cur; + if (/%[0-9A-Fa-f]{2}/.test(e.name)) { + try { + const decoded = decodeURIComponent(e.name); + if (decoded !== e.name) { + next = path.join(dir, decoded); + fs.renameSync(cur, next); + renamed++; + } + } catch { + /* keep as-is on malformed sequences */ + } + } + if (e.isDirectory()) walk(next); + } + }; + walk(root); + if (renamed) console.log(` [decode] renamed ${renamed} percent-encoded path(s)`); +} + // ─── Assemble output ──────────────────────────────────────────── function assembleOutput(resourcesDir, destDir, label) { @@ -286,11 +318,21 @@ async function main() { } if (!SKIP_WIN) { - try { - const winInfo = await getWindowsVersion(); - console.log(` win: ${winInfo.version}`); - results.win = winInfo; - } catch (e) { console.error(` [x] win check: ${e.message}`); } + const MAX_TRIES = 4; + for (let attempt = 1; attempt <= MAX_TRIES; attempt++) { + try { + const winInfo = await getWindowsVersion(); + console.log(` win: ${winInfo.version}`); + results.win = winInfo; + break; + } catch (e) { + const last = attempt === MAX_TRIES; + console.error( + ` [x] win check (attempt ${attempt}/${MAX_TRIES}): ${e.message}${last ? "" : " — retrying"}`, + ); + if (!last) await new Promise((r) => setTimeout(r, attempt * 2000)); + } + } } if (CHECK_ONLY) { From 41aba3c0723c1adc7cda073029d303ad2fb2b11f Mon Sep 17 00:00:00 2001 From: Cheese <854675824@qq.com> Date: Mon, 15 Jun 2026 10:49:25 +0800 Subject: [PATCH 2/6] fix(deps): bump better-sqlite3 12.4.6 -> 12.10.1 for Electron 42 v8 API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Electron 42 ships a v8 that changed v8::External::Value() to require an ExternalPointerTypeTag argument. better-sqlite3 <= 12.9.0 calls it 0-arg and fails to compile during electron-rebuild on Linux: ../src/util/macros.cpp:30:76: error: no matching function for call to 'v8::External::Value()' 12.10.1 fixes it (release notes: "Fix V8 external API usage for Electron 42 by @tstone-1 in #1475"). No code changes here — just a caret bump, lockfile follows. --- package-lock.json | 8 +++++--- package.json | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e8cf934..d1e1df08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@sentry/electron": "^7.5.0", "@sentry/node": "10.29.0", - "better-sqlite3": "^12.4.6", + "better-sqlite3": "^12.10.1", "electron-context-menu": "^4.1.1", "electron-squirrel-startup": "^1.0.1", "encoding": "^0.1.13", @@ -3059,7 +3059,9 @@ } }, "node_modules/better-sqlite3": { - "version": "12.9.0", + "version": "12.10.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.1.tgz", + "integrity": "sha512-HfFtzCqnSfwB3+HroF6PSKzyh+7RfNMGPCzHFUZXRlvrPCb4P3cvxKZNN43Sr7IrkofqQZM+gIvffGpA8VvqgA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3067,7 +3069,7 @@ "prebuild-install": "^7.1.1" }, "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" } }, "node_modules/bindings": { diff --git a/package.json b/package.json index 408ebcdb..484bf2a5 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "dependencies": { "@sentry/electron": "^7.5.0", "@sentry/node": "10.29.0", - "better-sqlite3": "^12.4.6", + "better-sqlite3": "^12.10.1", "electron-context-menu": "^4.1.1", "electron-squirrel-startup": "^1.0.1", "encoding": "^0.1.13", From d32c727403176782fc0861c8ddfef817ecfc463f Mon Sep 17 00:00:00 2001 From: Cheese <854675824@qq.com> Date: Mon, 15 Jun 2026 17:39:20 +0800 Subject: [PATCH 3/6] fix(ci): diagnose Linux empty-artifact failure; harden vendor + uploads Linux build was silently passing with zero artifacts: electron-forge make exits 0 after the Packaging stage's "Finalizing package" line without ever reaching the Make stage, and upload-artifact@v7 defaults if-no-files-found to warn so the job stays green. We can't reproduce the forge silent-exit locally on Windows, so this commit ships the minimum changes needed to make the next CI run produce actionable evidence and refuse to ship broken packages: - DEBUG=electron-forge:*,electron-packager,@electron/packager* on the Linux build step so the next run logs what forge is actually doing between package and make. - Post-build sanity step that lists out/ and out/make/ contents and fails the job hard if zero .deb/.rpm/.zip were produced (replacing the silent upload warning with a loud, structured ::error). - if-no-files-found: error on every upload-artifact step (Linux, macOS, Windows) so any future zero-files scenario fails red. - Expand Linux apt install with build-essential, python3, libudev-dev so the better-sqlite3 / node-pty source-compile fallback works if prebuild-install ever misses. - Split rebuild:native into per-arch scripts (rebuild:native:x64 / :arm64) and wire build:linux-{x64,arm64} to the matching one. Defensive on x64; correctness fix for the arm64 leg, which previously ran electron-rebuild without --arch on an x64 host and produced wrong-arch native .node files inside the arm64 package. - Harden prepare-src.js: on Linux, refuse to fall through when the @cometix/codex or rg vendor binary can't be resolved. Previously we logged "[!] keeping upstream" and shipped the macOS Mach-O binary inside a Linux .deb/.rpm. --- .github/workflows/build.yml | 26 +++++++++++++++++++++++++- package.json | 8 +++++--- scripts/prepare-src.js | 6 +++++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ced2ac82..20b85ece 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,6 +38,7 @@ jobs: with: name: Codex-macOS-${{ matrix.arch }} path: out/*.dmg + if-no-files-found: error build-windows: if: inputs.platform == 'all' || inputs.platform == 'windows' @@ -63,6 +64,7 @@ jobs: with: name: Codex-Windows-x64 path: out/*.zip + if-no-files-found: error build-linux: if: inputs.platform == 'all' || inputs.platform == 'linux' @@ -81,10 +83,31 @@ jobs: npm ci - run: | sudo apt-get update - sudo apt-get install -y rpm fakeroot dpkg 7zip + sudo apt-get install -y rpm fakeroot dpkg 7zip build-essential python3 libudev-dev - run: node scripts/sync-upstream.js --force --skip-win - run: node scripts/patch-all.js mac-${{ matrix.arch }} - run: npm run build:linux-${{ matrix.arch }} + env: + DEBUG: electron-forge:*,electron-packager,@electron/packager* + - name: List build output (diagnostic) + if: always() + run: | + echo "::group::out/ tree" + ls -la out/ 2>/dev/null || echo "(no out/)" + echo "::endgroup::" + echo "::group::out/make/ tree" + find out/make -maxdepth 5 -type f 2>/dev/null | head -50 || echo "(no out/make/)" + echo "::endgroup::" + echo "::group::artifact counts" + deb_count=$(find out/make/deb -name '*.deb' 2>/dev/null | wc -l) + rpm_count=$(find out/make/rpm -name '*.rpm' 2>/dev/null | wc -l) + zip_count=$(find out/make/zip -name '*.zip' 2>/dev/null | wc -l) + echo "deb=$deb_count rpm=$rpm_count zip=$zip_count" + echo "::endgroup::" + if [ "$deb_count" -eq 0 ] && [ "$rpm_count" -eq 0 ] && [ "$zip_count" -eq 0 ]; then + echo "::error::electron-forge make produced zero distributables (no .deb/.rpm/.zip in out/make/)" + exit 1 + fi - uses: actions/upload-artifact@v7 with: name: Codex-Linux-${{ matrix.arch }} @@ -92,3 +115,4 @@ jobs: out/make/deb/**/*.deb out/make/rpm/**/*.rpm out/make/zip/**/*.zip + if-no-files-found: error diff --git a/package.json b/package.json index 484bf2a5..68f28d6a 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,16 @@ "build:mac": "npm run build:mac-arm64 && npm run build:mac-x64", "build:win-x64": "node scripts/build-from-upstream.js --platform win", "build:win": "npm run build:win-x64", - "build:linux-x64": "node scripts/prepare-src.js --platform linux-x64 && npm run rebuild:native && node scripts/sync-native-modules.js --platform linux-x64 && rm -rf out && electron-forge make --platform=linux --arch=x64", - "build:linux-arm64": "node scripts/prepare-src.js --platform linux-arm64 && npm run rebuild:native && node scripts/sync-native-modules.js --platform linux-arm64 && rm -rf out && electron-forge make --platform=linux --arch=arm64", + "build:linux-x64": "node scripts/prepare-src.js --platform linux-x64 && npm run rebuild:native:x64 && node scripts/sync-native-modules.js --platform linux-x64 && rm -rf out && electron-forge make --platform=linux --arch=x64", + "build:linux-arm64": "node scripts/prepare-src.js --platform linux-arm64 && npm run rebuild:native:arm64 && node scripts/sync-native-modules.js --platform linux-arm64 && rm -rf out && electron-forge make --platform=linux --arch=arm64", "build:linux": "npm run build:linux-x64 && npm run build:linux-arm64", "build:all": "npm run build:mac && npm run build:win && npm run build:linux", "sync": "node scripts/sync-upstream.js", "check-update": "node scripts/check-update.js", "bump-version": "node scripts/bump-version.js", - "rebuild:native": "electron-rebuild" + "rebuild:native": "electron-rebuild", + "rebuild:native:x64": "electron-rebuild --arch=x64", + "rebuild:native:arm64": "electron-rebuild --arch=arm64" }, "devDependencies": { "@electron-forge/cli": "^7.10.2", diff --git a/scripts/prepare-src.js b/scripts/prepare-src.js index 4bd50cb1..dc003ab0 100644 --- a/scripts/prepare-src.js +++ b/scripts/prepare-src.js @@ -178,6 +178,9 @@ function main() { fs.copyFileSync(vendorCodex, dest); try { fs.chmodSync(dest, 0o755); } catch {} console.log(` [codex] replaced with @cometix/codex`); + } else if (isLinux) { + console.error(` [x] @cometix/codex vendor not found for ${platform}; refusing to ship upstream macOS Mach-O binary inside a Linux package.`); + process.exit(1); } else { console.log(` [!] @cometix/codex vendor not found for ${platform}, keeping upstream`); } @@ -191,7 +194,8 @@ function main() { try { fs.chmodSync(dest, 0o755); } catch {} console.log(` [rg] replaced with Linux rg from @cometix/codex`); } else { - console.log(` [!] Linux rg not found in vendor, keeping upstream (will fail on Linux)`); + console.error(` [x] Linux rg not found in vendor for ${platform}; refusing to ship upstream macOS Mach-O rg inside a Linux package.`); + process.exit(1); } } From 7fa6ae72d3677e197a0575f6740b7960c8f714c0 Mon Sep 17 00:00:00 2001 From: Cheese <854675824@qq.com> Date: Mon, 15 Jun 2026 17:57:35 +0800 Subject: [PATCH 4/6] fix(ci): bypass forge make for Linux with custom @electron/packager script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit electron-forge make exits cleanly with status 0 in CI between the Packaging stage's "Finalizing package" listr line and ever reaching the Make stage — no error, no unhandledRejection, no stack trace. The previous commit's DEBUG output confirmed @electron/packager logs "Extracting electron.zip" then the process exits 40ms later without writing anything to out/. Two runs reproduced this deterministically. Rather than continue chasing a silent listr2/packager interaction, switch Linux to the same pattern mac/win already use: a custom build script with explicit promises and loud error handling. - New scripts/build-from-upstream-linux.js calls @electron/packager directly (no listr2 wrapper, no hidden lifecycle), then invokes MakerDeb / MakerRpm / MakerZip via their programmatic API. Per-maker errors are isolated and reported; partial-success still exits 1. - The afterCopy hook inlines forge.config.js's packageAfterCopy logic so the same Linux-only file/dir skip rules apply (no Mach-O native dir, no app.asar, no .lproj). - Wires build:linux-{x64,arm64} npm scripts to call the new script instead of electron-forge make. - Drops the post-make diagnostic step from build.yml now that the new script's own output is structured and loud. if-no-files-found:error on upload-artifact is still the final safety net. forge.config.js is left untouched — mac/win still go through their existing build-from-upstream.js path and don't touch forge make either, so the file only matters for `npm run forge:package` / `forge:make` direct invocations which we don't use in CI. --- .github/workflows/build.yml | 21 --- package.json | 4 +- scripts/build-from-upstream-linux.js | 236 +++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 23 deletions(-) create mode 100644 scripts/build-from-upstream-linux.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20b85ece..3498c8fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,27 +87,6 @@ jobs: - run: node scripts/sync-upstream.js --force --skip-win - run: node scripts/patch-all.js mac-${{ matrix.arch }} - run: npm run build:linux-${{ matrix.arch }} - env: - DEBUG: electron-forge:*,electron-packager,@electron/packager* - - name: List build output (diagnostic) - if: always() - run: | - echo "::group::out/ tree" - ls -la out/ 2>/dev/null || echo "(no out/)" - echo "::endgroup::" - echo "::group::out/make/ tree" - find out/make -maxdepth 5 -type f 2>/dev/null | head -50 || echo "(no out/make/)" - echo "::endgroup::" - echo "::group::artifact counts" - deb_count=$(find out/make/deb -name '*.deb' 2>/dev/null | wc -l) - rpm_count=$(find out/make/rpm -name '*.rpm' 2>/dev/null | wc -l) - zip_count=$(find out/make/zip -name '*.zip' 2>/dev/null | wc -l) - echo "deb=$deb_count rpm=$rpm_count zip=$zip_count" - echo "::endgroup::" - if [ "$deb_count" -eq 0 ] && [ "$rpm_count" -eq 0 ] && [ "$zip_count" -eq 0 ]; then - echo "::error::electron-forge make produced zero distributables (no .deb/.rpm/.zip in out/make/)" - exit 1 - fi - uses: actions/upload-artifact@v7 with: name: Codex-Linux-${{ matrix.arch }} diff --git a/package.json b/package.json index 68f28d6a..7ad8f237 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "build:mac": "npm run build:mac-arm64 && npm run build:mac-x64", "build:win-x64": "node scripts/build-from-upstream.js --platform win", "build:win": "npm run build:win-x64", - "build:linux-x64": "node scripts/prepare-src.js --platform linux-x64 && npm run rebuild:native:x64 && node scripts/sync-native-modules.js --platform linux-x64 && rm -rf out && electron-forge make --platform=linux --arch=x64", - "build:linux-arm64": "node scripts/prepare-src.js --platform linux-arm64 && npm run rebuild:native:arm64 && node scripts/sync-native-modules.js --platform linux-arm64 && rm -rf out && electron-forge make --platform=linux --arch=arm64", + "build:linux-x64": "node scripts/prepare-src.js --platform linux-x64 && npm run rebuild:native:x64 && node scripts/sync-native-modules.js --platform linux-x64 && rm -rf out && node scripts/build-from-upstream-linux.js --platform linux-x64", + "build:linux-arm64": "node scripts/prepare-src.js --platform linux-arm64 && npm run rebuild:native:arm64 && node scripts/sync-native-modules.js --platform linux-arm64 && rm -rf out && node scripts/build-from-upstream-linux.js --platform linux-arm64", "build:linux": "npm run build:linux-x64 && npm run build:linux-arm64", "build:all": "npm run build:mac && npm run build:win && npm run build:linux", "sync": "node scripts/sync-upstream.js", diff --git a/scripts/build-from-upstream-linux.js b/scripts/build-from-upstream-linux.js new file mode 100644 index 00000000..bca492bf --- /dev/null +++ b/scripts/build-from-upstream-linux.js @@ -0,0 +1,236 @@ +#!/usr/bin/env node +/** + * build-from-upstream-linux.js — Build Linux distributables (.deb/.rpm/.zip) + * + * Linux has no upstream Codex installer, so unlike mac/win we must build the + * Electron app from scratch using @electron/packager directly, then invoke + * maker-deb/rpm/zip programmatically. This bypasses electron-forge make, + * which exits silently in CI after Packaging without producing artifacts. + * + * Prereq: prepare-src.js --platform linux-{arch} has already populated src/ + * (app code) and src/mac-{arch}/ (Linux codex/rg vendor binaries). + * + * Usage: + * node scripts/build-from-upstream-linux.js --platform linux-x64 + * node scripts/build-from-upstream-linux.js --platform linux-arm64 + */ +const fs = require("fs"); +const path = require("path"); + +const PROJECT_ROOT = path.resolve(__dirname, ".."); +const SRC_DIR = path.join(PROJECT_ROOT, "src"); +const OUT_DIR = path.join(PROJECT_ROOT, "out"); +const RESOURCES_DIR = path.join(PROJECT_ROOT, "resources"); + +const IGNORE_ALLOWED = [ + "/src/.vite/build", + "/src/webview", + "/src/skills", + "/src/native-menu-locales", + "/src/node_modules", +]; + +const MACOS_ONLY_FILES = new Set([ + "node", "node_repl", + "electron.icns", "Assets.car", + "codexTemplate.png", "codexTemplate@2x.png", + "app.asar", "codex-notification.wav", +]); +const MACOS_ONLY_DIRS = new Set(["native", "app.asar.unpacked"]); + +function packagerIgnore(filePath) { + if (filePath === "") return false; + if (filePath === "/package.json") return false; + for (const p of IGNORE_ALLOWED) { + if (p.startsWith(filePath) || filePath.startsWith(p)) return false; + } + return true; +} + +function copyLinuxResources(srcDir, destDir) { + if (!fs.existsSync(srcDir)) { + console.log(` [!] ${srcDir} not found, skipping resource copy`); + return 0; + } + console.log(`-- afterCopy: ${path.relative(PROJECT_ROOT, srcDir)} -> ${path.relative(PROJECT_ROOT, destDir)}`); + const skip = new Set(["_asar"]); + for (const f of MACOS_ONLY_FILES) skip.add(f); + for (const d of MACOS_ONLY_DIRS) skip.add(d); + + let copied = 0; + const copyDir = (s, d) => { + fs.mkdirSync(d, { recursive: true }); + for (const e of fs.readdirSync(s, { withFileTypes: true })) { + const sp = path.join(s, e.name), dp = path.join(d, e.name); + if (e.isDirectory()) copyDir(sp, dp); + else if (!e.isSymbolicLink()) { fs.copyFileSync(sp, dp); copied++; } + } + }; + for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { + if (skip.has(entry.name)) continue; + if (entry.name.endsWith(".lproj")) continue; + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + if (entry.isDirectory()) { + copyDir(srcPath, destPath); + } else if (!entry.isSymbolicLink()) { + fs.copyFileSync(srcPath, destPath); + try { fs.chmodSync(destPath, 0o755); } catch {} + copied++; + } + } + console.log(` [ok] ${copied} files copied`); + return copied; +} + +async function runMaker(kind, factory, packageDir, makeDir, targetArch, packageJSON, results) { + console.log(`\n-- maker:${kind}`); + try { + const maker = factory(); + if (typeof maker.isSupportedOnCurrentPlatform === "function" && !maker.isSupportedOnCurrentPlatform()) { + throw new Error(`maker-${kind} reports unsupported on current platform (missing binary or installer package?)`); + } + if (typeof maker.ensureExternalBinariesExist === "function") { + maker.ensureExternalBinariesExist(); + } + if (typeof maker.prepareConfig === "function") { + await maker.prepareConfig(targetArch); + } + const artifacts = await maker.make({ + dir: packageDir, + makeDir, + appName: "Codex", + targetPlatform: "linux", + targetArch, + forgeConfig: { packagerConfig: {}, rebuildConfig: {}, makers: [], publishers: [], plugins: [], pluginInterface: {} }, + packageJSON, + }); + console.log(` [ok] ${kind}: ${artifacts.length} artifact(s)`); + for (const a of artifacts) console.log(` ${a}`); + results.push({ kind, ok: true, artifacts }); + } catch (err) { + console.error(` [x] ${kind} failed: ${err.stack || err.message || err}`); + results.push({ kind, ok: false, error: err.message || String(err) }); + } +} + +async function main() { + const args = process.argv.slice(2); + const platIdx = args.indexOf("--platform"); + const platform = platIdx !== -1 ? args[platIdx + 1] : null; + if (!platform || !["linux-x64", "linux-arm64"].includes(platform)) { + console.error("Usage: build-from-upstream-linux.js --platform linux-x64|linux-arm64"); + process.exit(1); + } + const arch = platform.split("-")[1]; + const sourcePlatformDir = path.join(SRC_DIR, `mac-${arch}`); + + if (!fs.existsSync(path.join(SRC_DIR, ".build-mode"))) { + console.error(`[x] src/ not prepared. Run: node scripts/prepare-src.js --platform ${platform}`); + process.exit(1); + } + + console.log(`\n== electron-packager: linux-${arch} ==`); + console.log(` dir: ${PROJECT_ROOT}`); + console.log(` out: ${OUT_DIR}`); + console.log(` source: ${sourcePlatformDir}`); + + const { packager } = require("@electron/packager"); + const packagePaths = await packager({ + dir: PROJECT_ROOT, + out: OUT_DIR, + platform: "linux", + arch, + asar: { unpack: "{**/*.node,**/node-pty/build/Release/spawn-helper,**/node-pty/prebuilds/*/spawn-helper}" }, + overwrite: true, + name: "Codex", + executableName: "Codex", + appBundleId: "com.openai.codex", + icon: path.join(RESOURCES_DIR, "electron.png"), + prune: true, + ignore: packagerIgnore, + afterCopy: [(buildPath, electronVersion, plat, ar, cb) => { + try { + const resourcesPath = path.dirname(buildPath); + copyLinuxResources(sourcePlatformDir, resourcesPath); + cb(); + } catch (err) { cb(err); } + }], + }); + + if (!packagePaths || packagePaths.length === 0) { + console.error("[x] @electron/packager returned no package paths"); + process.exit(1); + } + const packageDir = packagePaths[0]; + console.log(`\n [ok] packaged at ${packageDir}`); + + const packageJSON = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8")); + const makeDir = path.join(OUT_DIR, "make"); + fs.mkdirSync(makeDir, { recursive: true }); + + console.log(`\n== makers: deb, rpm, zip (arch=${arch}) ==`); + const results = []; + + await runMaker("deb", () => { + const { default: MakerDeb } = require("@electron-forge/maker-deb"); + return new MakerDeb({ + options: { + name: "codex", + productName: "Codex", + genericName: "AI Coding Assistant", + categories: ["Development", "Utility"], + bin: "Codex", + maintainer: "Cometix Space", + homepage: "https://github.com/Haleclipse/CodexDesktop-Rebuild", + icon: path.join(RESOURCES_DIR, "electron.png"), + }, + }); + }, packageDir, makeDir, arch, packageJSON, results); + + await runMaker("rpm", () => { + const { default: MakerRpm } = require("@electron-forge/maker-rpm"); + return new MakerRpm({ + options: { + name: "codex", + productName: "Codex", + genericName: "AI Coding Assistant", + categories: ["Development", "Utility"], + bin: "Codex", + license: "Apache-2.0", + homepage: "https://github.com/Haleclipse/CodexDesktop-Rebuild", + icon: path.join(RESOURCES_DIR, "electron.png"), + }, + }); + }, packageDir, makeDir, arch, packageJSON, results); + + await runMaker("zip", () => { + const { default: MakerZip } = require("@electron-forge/maker-zip"); + return new MakerZip(); + }, packageDir, makeDir, arch, packageJSON, results); + + console.log(`\n== summary ==`); + for (const r of results) { + if (r.ok) { + console.log(` [ok] ${r.kind}: ${r.artifacts.join(", ")}`); + } else { + console.log(` [FAIL] ${r.kind}: ${r.error}`); + } + } + + const failed = results.filter(r => !r.ok); + if (failed.length === results.length) { + console.error(`\n[x] all makers failed`); + process.exit(1); + } + if (failed.length > 0) { + console.error(`\n[!] ${failed.length}/${results.length} makers failed (partial success)`); + process.exit(1); + } + console.log(`\n[ok] all ${results.length} makers succeeded`); +} + +main().catch(err => { + console.error("\n[x] Fatal:", err.stack || err.message || err); + process.exit(1); +}); From b98121cffddea3bcd1b6abb2f404378abe9865d2 Mon Sep 17 00:00:00 2001 From: Cheese <854675824@qq.com> Date: Mon, 15 Jun 2026 18:09:37 +0800 Subject: [PATCH 5/6] fix(ci): replace @electron/packager with @electron/get + system unzip + asar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous attempt's @electron/packager call exits the Node process silently with code 0 in the middle of extractElectronZip() — extract-zip 2.0.1 (the unmaintained upstream archived in 2021) is the culprit. CI log shows packager logs "Packaging app for platform linux x64 using electron v42.0.1" then the process ends 37 ms later with no error, no unhandled rejection, no maker output. Reproduced on three runs. Rather than fight extract-zip on Node 24, bypass it: download the Electron Linux template zip via @electron/get directly, extract with the system unzip CLI (preinstalled on ubuntu-latest), repack src/ to app.asar via @electron/asar, and copy Linux resources from src/mac-{arch}/ inline. Then invoke maker-deb/rpm/zip programmatically as before. Each phase logs its progress so any future regression is loud, and `unzip` is a battle-tested POSIX tool with no Node interop surprises. No changes to forge.config.js — it's unused on the Linux path now. apt deps added in the previous commit (build-essential, python3, libudev-dev) stay for the maker-deb/rpm peer-package compile path. The `unzip` CLI is preinstalled on ubuntu-latest. --- scripts/build-from-upstream-linux.js | 150 ++++++++++++++++----------- 1 file changed, 91 insertions(+), 59 deletions(-) diff --git a/scripts/build-from-upstream-linux.js b/scripts/build-from-upstream-linux.js index bca492bf..8f106bc2 100644 --- a/scripts/build-from-upstream-linux.js +++ b/scripts/build-from-upstream-linux.js @@ -2,10 +2,16 @@ /** * build-from-upstream-linux.js — Build Linux distributables (.deb/.rpm/.zip) * - * Linux has no upstream Codex installer, so unlike mac/win we must build the - * Electron app from scratch using @electron/packager directly, then invoke - * maker-deb/rpm/zip programmatically. This bypasses electron-forge make, - * which exits silently in CI after Packaging without producing artifacts. + * Avoids @electron/packager entirely. Its dependency extract-zip 2.0.1 + * silently exits the Node process during extraction on Node 24 + Ubuntu CI + * (no error, no stack — process just ends with exit code 0 mid-await). + * Two CI runs reproduced this deterministically with full DEBUG. + * + * Instead this script does the same work via: + * - @electron/get — download the Electron Linux template zip + * - system `unzip` — extract reliably (apt: unzip is preinstalled) + * - @electron/asar — pack src/ into app.asar + * - maker-deb/rpm/zip — programmatic make() * * Prereq: prepare-src.js --platform linux-{arch} has already populated src/ * (app code) and src/mac-{arch}/ (Linux codex/rg vendor binaries). @@ -16,19 +22,14 @@ */ const fs = require("fs"); const path = require("path"); +const { execFileSync } = require("child_process"); const PROJECT_ROOT = path.resolve(__dirname, ".."); const SRC_DIR = path.join(PROJECT_ROOT, "src"); const OUT_DIR = path.join(PROJECT_ROOT, "out"); const RESOURCES_DIR = path.join(PROJECT_ROOT, "resources"); -const IGNORE_ALLOWED = [ - "/src/.vite/build", - "/src/webview", - "/src/skills", - "/src/native-menu-locales", - "/src/node_modules", -]; +const ASAR_UNPACK_GLOB = "{**/*.node,**/node-pty/build/Release/spawn-helper,**/node-pty/prebuilds/*/spawn-helper}"; const MACOS_ONLY_FILES = new Set([ "node", "node_repl", @@ -38,21 +39,18 @@ const MACOS_ONLY_FILES = new Set([ ]); const MACOS_ONLY_DIRS = new Set(["native", "app.asar.unpacked"]); -function packagerIgnore(filePath) { - if (filePath === "") return false; - if (filePath === "/package.json") return false; - for (const p of IGNORE_ALLOWED) { - if (p.startsWith(filePath) || filePath.startsWith(p)) return false; - } - return true; +function readElectronVersion() { + const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8")); + const raw = pkg.devDependencies?.electron; + if (!raw) throw new Error("devDependencies.electron not set in package.json"); + // strip leading ^ or ~ or = etc. + return raw.replace(/^[^\d]*/, ""); } function copyLinuxResources(srcDir, destDir) { if (!fs.existsSync(srcDir)) { - console.log(` [!] ${srcDir} not found, skipping resource copy`); - return 0; + throw new Error(`Source platform dir not found: ${srcDir}`); } - console.log(`-- afterCopy: ${path.relative(PROJECT_ROOT, srcDir)} -> ${path.relative(PROJECT_ROOT, destDir)}`); const skip = new Set(["_asar"]); for (const f of MACOS_ONLY_FILES) skip.add(f); for (const d of MACOS_ONLY_DIRS) skip.add(d); @@ -63,7 +61,11 @@ function copyLinuxResources(srcDir, destDir) { for (const e of fs.readdirSync(s, { withFileTypes: true })) { const sp = path.join(s, e.name), dp = path.join(d, e.name); if (e.isDirectory()) copyDir(sp, dp); - else if (!e.isSymbolicLink()) { fs.copyFileSync(sp, dp); copied++; } + else if (!e.isSymbolicLink()) { + fs.copyFileSync(sp, dp); + try { fs.chmodSync(dp, 0o755); } catch {} + copied++; + } } }; for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { @@ -79,7 +81,6 @@ function copyLinuxResources(srcDir, destDir) { copied++; } } - console.log(` [ok] ${copied} files copied`); return copied; } @@ -88,7 +89,7 @@ async function runMaker(kind, factory, packageDir, makeDir, targetArch, packageJ try { const maker = factory(); if (typeof maker.isSupportedOnCurrentPlatform === "function" && !maker.isSupportedOnCurrentPlatform()) { - throw new Error(`maker-${kind} reports unsupported on current platform (missing binary or installer package?)`); + throw new Error(`maker-${kind} reports unsupported on current platform (missing peer installer pkg?)`); } if (typeof maker.ensureExternalBinariesExist === "function") { maker.ensureExternalBinariesExist(); @@ -129,42 +130,77 @@ async function main() { console.error(`[x] src/ not prepared. Run: node scripts/prepare-src.js --platform ${platform}`); process.exit(1); } + if (!fs.existsSync(sourcePlatformDir)) { + console.error(`[x] ${sourcePlatformDir} not found.`); + process.exit(1); + } - console.log(`\n== electron-packager: linux-${arch} ==`); - console.log(` dir: ${PROJECT_ROOT}`); - console.log(` out: ${OUT_DIR}`); - console.log(` source: ${sourcePlatformDir}`); + const electronVersion = readElectronVersion(); + const packageDir = path.join(OUT_DIR, `Codex-linux-${arch}`); + const resourcesPath = path.join(packageDir, "resources"); + const appAsarPath = path.join(resourcesPath, "app.asar"); - const { packager } = require("@electron/packager"); - const packagePaths = await packager({ - dir: PROJECT_ROOT, - out: OUT_DIR, + console.log(`\n== build-linux: ${platform} ==`); + console.log(` electron: v${electronVersion}`); + console.log(` package: ${packageDir}`); + console.log(` source: ${path.relative(PROJECT_ROOT, sourcePlatformDir)}`); + + // ─── 1. Download Electron Linux template zip ─── + console.log(`\n-- downloading electron-v${electronVersion}-linux-${arch}.zip`); + const { downloadArtifact, initializeProxy } = require("@electron/get"); + initializeProxy(); + const zipPath = await downloadArtifact({ + version: electronVersion, platform: "linux", arch, - asar: { unpack: "{**/*.node,**/node-pty/build/Release/spawn-helper,**/node-pty/prebuilds/*/spawn-helper}" }, - overwrite: true, - name: "Codex", - executableName: "Codex", - appBundleId: "com.openai.codex", - icon: path.join(RESOURCES_DIR, "electron.png"), - prune: true, - ignore: packagerIgnore, - afterCopy: [(buildPath, electronVersion, plat, ar, cb) => { - try { - const resourcesPath = path.dirname(buildPath); - copyLinuxResources(sourcePlatformDir, resourcesPath); - cb(); - } catch (err) { cb(err); } - }], + artifactName: "electron", }); + const zipSize = fs.statSync(zipPath).size; + console.log(` [ok] ${zipPath} (${(zipSize / 1024 / 1024).toFixed(1)} MB)`); - if (!packagePaths || packagePaths.length === 0) { - console.error("[x] @electron/packager returned no package paths"); - process.exit(1); + // ─── 2. Extract via system unzip (bypasses extract-zip silent-exit) ─── + console.log(`\n-- extracting via system unzip -> ${packageDir}`); + if (fs.existsSync(packageDir)) fs.rmSync(packageDir, { recursive: true, force: true }); + fs.mkdirSync(packageDir, { recursive: true }); + execFileSync("unzip", ["-qq", "-o", zipPath, "-d", packageDir], { stdio: ["ignore", "inherit", "inherit"] }); + const extractedCount = fs.readdirSync(packageDir).length; + console.log(` [ok] ${extractedCount} top-level entries extracted`); + if (extractedCount === 0) throw new Error("unzip produced no files"); + + // ─── 3. Rename `electron` binary -> `Codex` ─── + const electronBin = path.join(packageDir, "electron"); + const codexBin = path.join(packageDir, "Codex"); + if (!fs.existsSync(electronBin)) throw new Error(`Electron binary not found at ${electronBin}`); + fs.renameSync(electronBin, codexBin); + fs.chmodSync(codexBin, 0o755); + console.log(` [ok] electron -> Codex`); + + // ─── 4. Remove default_app + chrome-sandbox SUID (forge does same) ─── + for (const name of ["default_app.asar", "default_app"]) { + const p = path.join(resourcesPath, name); + if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true }); } - const packageDir = packagePaths[0]; - console.log(`\n [ok] packaged at ${packageDir}`); + // ─── 5. Pack src/ -> app.asar (excludes src/mac-* via root filter) ─── + console.log(`\n-- packing app.asar from src/`); + const asar = require("@electron/asar"); + await asar.createPackageWithOptions(SRC_DIR, appAsarPath, { + unpack: ASAR_UNPACK_GLOB, + dot: true, + // Exclude src/mac-x64/, src/mac-arm64/, src/win/ subdirs — these are + // upstream platform trees retained alongside src/ for the afterCopy + // step, not part of the runtime app. + globOptions: { ignore: ["mac-x64/**", "mac-arm64/**", "win/**"] }, + }); + const asarSize = fs.statSync(appAsarPath).size; + console.log(` [ok] app.asar: ${(asarSize / 1024 / 1024).toFixed(1)} MB`); + + // ─── 6. Copy Linux-specific resources from src/mac-{arch}/ ─── + console.log(`\n-- copying Linux resources from ${path.relative(PROJECT_ROOT, sourcePlatformDir)}`); + const copied = copyLinuxResources(sourcePlatformDir, resourcesPath); + console.log(` [ok] ${copied} files copied to ${path.relative(PROJECT_ROOT, resourcesPath)}`); + + // ─── 7. Run makers ─── const packageJSON = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8")); const makeDir = path.join(OUT_DIR, "make"); fs.mkdirSync(makeDir, { recursive: true }); @@ -211,20 +247,16 @@ async function main() { console.log(`\n== summary ==`); for (const r of results) { - if (r.ok) { - console.log(` [ok] ${r.kind}: ${r.artifacts.join(", ")}`); - } else { - console.log(` [FAIL] ${r.kind}: ${r.error}`); - } + if (r.ok) console.log(` [ok] ${r.kind}: ${r.artifacts.join(", ")}`); + else console.log(` [FAIL] ${r.kind}: ${r.error}`); } - const failed = results.filter(r => !r.ok); if (failed.length === results.length) { console.error(`\n[x] all makers failed`); process.exit(1); } if (failed.length > 0) { - console.error(`\n[!] ${failed.length}/${results.length} makers failed (partial success)`); + console.error(`\n[!] ${failed.length}/${results.length} makers failed`); process.exit(1); } console.log(`\n[ok] all ${results.length} makers succeeded`); From 0f703d5dc042c8fd4b45b460438228184b74718d Mon Sep 17 00:00:00 2001 From: Cheese <854675824@qq.com> Date: Mon, 15 Jun 2026 18:39:57 +0800 Subject: [PATCH 6/6] fix(ci): strip cross-arch sky binary before rpm to unblock x64 build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last run: arm64 RPM is green (3/3), x64 RPM fails on rpmbuild's brp-strip pass at: resources/cua_node/lib/node_modules/@oai/sky/bin/linux/sky_linux_arm64 /usr/bin/strip: Unable to recognise the format of the input file ... Upstream Codex ships both sky_linux_x64 and sky_linux_arm64 side-by-side. The host's x86_64 strip can parse x64 ELF (so arm64 RPMs build fine on the x64 runner — confirmed in the last run), but not arm64 ELF — so the x64 RPM blows up. deb and zip don't run a strip pass so they were fine all along. Before any maker runs, delete the cross-arch sky binary from /resources/. The runtime selector only ever invokes sky_linux_${process.arch}, so the other-arch copy is dead weight on the end-user machine; removing it also slims the .deb and .zip slightly. Symmetric: arm64 build drops sky_linux_x64, x64 build drops sky_linux_arm64. --- scripts/build-from-upstream-linux.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/build-from-upstream-linux.js b/scripts/build-from-upstream-linux.js index 8f106bc2..e5620510 100644 --- a/scripts/build-from-upstream-linux.js +++ b/scripts/build-from-upstream-linux.js @@ -200,6 +200,24 @@ async function main() { const copied = copyLinuxResources(sourcePlatformDir, resourcesPath); console.log(` [ok] ${copied} files copied to ${path.relative(PROJECT_ROOT, resourcesPath)}`); + // ─── 6b. Strip cross-arch helper binaries ─── + // Upstream Codex bundles BOTH sky_linux_x64 and sky_linux_arm64. rpmbuild's + // brp-strip pass invokes the host's /usr/bin/strip on every ELF in the + // payload — on an x86_64 runner that strip can't parse arm64 ELF and the + // whole rpm fails. The runtime selector only ever calls the matching arch + // binary, so the wrong-arch copy is dead weight; remove it before any + // maker runs (also slims deb/zip a bit). + const otherArch = arch === "x64" ? "arm64" : "x64"; + const crossArchBins = [ + path.join(resourcesPath, "cua_node", "lib", "node_modules", "@oai", "sky", "bin", "linux", `sky_linux_${otherArch}`), + ]; + for (const f of crossArchBins) { + if (fs.existsSync(f)) { + fs.rmSync(f); + console.log(` [strip-safe] removed cross-arch ${path.relative(packageDir, f)}`); + } + } + // ─── 7. Run makers ─── const packageJSON = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8")); const makeDir = path.join(OUT_DIR, "make");