Created
June 10, 2026 04:21
-
-
Save Yubo-Cao/8b21ac616db0199f98676b41e49d3a8e to your computer and use it in GitHub Desktop.
Codex Patch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/.SRCINFO b/.SRCINFO | |
| index fb17634..d996c01 100644 | |
| --- a/.SRCINFO | |
| +++ b/.SRCINFO | |
| @@ -5,6 +5,7 @@ pkgbase = codex-app-unofficial | |
| url = https://github.com/better-slop/codex-app-linux | |
| arch = x86_64 | |
| license = custom | |
| + makedepends = asar | |
| depends = alsa-lib | |
| depends = gtk3 | |
| depends = libnotify | |
| @@ -18,7 +19,9 @@ pkgbase = codex-app-unofficial | |
| install = codex-app-unofficial.install | |
| source = codex-app-linux-26.608.12217-launcher.27-x64-linux-unpacked.tar.gz::https://github.com/better-slop/codex-app-linux/releases/download/v26.608.12217-launcher.27/codex-app-linux-26.608.12217-launcher.27-x64-linux-unpacked.tar.gz | |
| source = codex-app-linux-26.608.12217-launcher.27-x64.png::https://github.com/better-slop/codex-app-linux/releases/download/v26.608.12217-launcher.27/codex-app-linux-26.608.12217-launcher.27-x64.png | |
| + source = patch-settings-overlap.mjs | |
| sha256sums = 066d104a85c87cf5058b1ae24e04f4dbe63460f0be264c5b8caf8b90162b60ae | |
| sha256sums = 1c926e380bfe6a50f40648dd9bc5de88da7271546491adf99ec72172e17df6a0 | |
| + sha256sums = 074a9f4c874172786bc465f95f5a41c7130e33cb5585177517bb7bd7584ff7d4 | |
| pkgname = codex-app-unofficial | |
| diff --git a/PKGBUILD b/PKGBUILD | |
| index 3704bfa..73db752 100644 | |
| --- a/PKGBUILD | |
| +++ b/PKGBUILD | |
| @@ -5,6 +5,7 @@ pkgdesc='Unofficial Linux build of Codex from OpenAI'\''s Codex appcast feed.' | |
| arch=('x86_64') | |
| url='https://github.com/better-slop/codex-app-linux' | |
| license=('custom') | |
| +makedepends=('asar') | |
| depends=('alsa-lib' 'gtk3' 'libnotify' 'libsecret' 'libxss' 'nss' 'xdg-utils') | |
| provides=('codex-app-linux-bin') | |
| conflicts=('codex-app-linux-bin') | |
| @@ -13,12 +14,23 @@ install='codex-app-unofficial.install' | |
| source=( | |
| 'codex-app-linux-26.608.12217-launcher.27-x64-linux-unpacked.tar.gz::https://github.com/better-slop/codex-app-linux/releases/download/v26.608.12217-launcher.27/codex-app-linux-26.608.12217-launcher.27-x64-linux-unpacked.tar.gz' | |
| 'codex-app-linux-26.608.12217-launcher.27-x64.png::https://github.com/better-slop/codex-app-linux/releases/download/v26.608.12217-launcher.27/codex-app-linux-26.608.12217-launcher.27-x64.png' | |
| + 'patch-settings-overlap.mjs' | |
| ) | |
| sha256sums=( | |
| '066d104a85c87cf5058b1ae24e04f4dbe63460f0be264c5b8caf8b90162b60ae' | |
| '1c926e380bfe6a50f40648dd9bc5de88da7271546491adf99ec72172e17df6a0' | |
| + '074a9f4c874172786bc465f95f5a41c7130e33cb5585177517bb7bd7584ff7d4' | |
| ) | |
| +# 2026-06-09 Yubo-Cao | |
| +# Purpose: patch Codex's packaged Electron app before installation so Linux | |
| +# windows and renderer surfaces no longer render transparent flickering panes. | |
| +# Parameters: none; makepkg provides srcdir and the extracted app bundle. | |
| +# Returns: exits non-zero if the expected Electron/CSS/settings anchors are missing. | |
| +prepare() { | |
| + node "${srcdir}/patch-settings-overlap.mjs" "${srcdir}/linux-unpacked/resources/app.asar" | |
| +} | |
| + | |
| package() { | |
| install -dm755 "${pkgdir}/opt/codex-app-linux" | |
| cp -a "${srcdir}/linux-unpacked/." "${pkgdir}/opt/codex-app-linux/" | |
| diff --git a/patch-settings-overlap.mjs b/patch-settings-overlap.mjs | |
| new file mode 100644 | |
| index 0000000..571632f | |
| --- /dev/null | |
| +++ b/patch-settings-overlap.mjs | |
| @@ -0,0 +1,247 @@ | |
| +#!/usr/bin/env node | |
| + | |
| +import { execFileSync } from "node:child_process"; | |
| +import { createHash } from "node:crypto"; | |
| +import { existsSync, mkdtempSync, readFileSync, rmSync, renameSync, writeFileSync } from "node:fs"; | |
| +import { readdir } from "node:fs/promises"; | |
| +import { basename, dirname, join } from "node:path"; | |
| + | |
| +const SETTINGS_BRANCH_PATTERN = /if\([A-Za-z_$][A-Za-z0-9_$]*\)\{let t;/g; | |
| +const DISABLED_SETTINGS_BRANCH = "if(0){let t;"; | |
| +const TRANSPARENT_BACKDROP_PROBE = "isSystemBackdropSupported():!0}"; | |
| +const OPAQUE_BACKDROP_PROBE = "isSystemBackdropSupported():!1}"; | |
| +const TRANSPARENT_TOKEN_CHAIN = "--color-token-side-bar-background:var(--vscode-sideBar-background);--color-token-bg-primary:var(--color-token-side-bar-background);--color-token-diff-surface:var(--color-token-main-surface-primary)}"; | |
| +const OPAQUE_TOKEN_CHAIN = "--color-token-side-bar-background:var(--color-background-surface-under);--color-token-bg-primary:var(--color-background-surface);--color-token-diff-surface:var(--color-token-main-surface-primary)}"; | |
| +const ROOT_BACKGROUND_MARKER = "#root{height:100vh}"; | |
| +const ROOT_OPAQUE_BACKGROUND = "html,body{background:var(--color-background-surface);min-height:100%}#root{height:100vh;background:var(--color-background-surface)}"; | |
| +const UNPACK_DIRS = "node_modules/{better-sqlite3,node-pty}"; | |
| + | |
| +/** | |
| + * 2026-06-06 苍朮 | |
| + * Purpose: count exact substring occurrences while avoiding regular-expression escaping mistakes. | |
| + * @param {string} text - Source text to inspect. | |
| + * @param {string} needle - Exact substring to count. | |
| + * @returns {number} Number of exact occurrences in the source text. | |
| + */ | |
| +function countOccurrences(text, needle) { | |
| + return text.split(needle).length - 1; | |
| +} | |
| + | |
| +/** | |
| + * 2026-06-06 苍朮 | |
| + * Purpose: recursively list files below a directory so hashed bundle names can change across Codex releases. | |
| + * @param {string} root - Directory to scan recursively. | |
| + * @returns {Promise<string[]>} Absolute file paths contained in the directory tree. | |
| + */ | |
| +async function listFiles(root) { | |
| + const entries = await readdir(root, { withFileTypes: true }); | |
| + const files = await Promise.all( | |
| + entries.map((entry) => { | |
| + const filePath = join(root, entry.name); | |
| + return entry.isDirectory() ? listFiles(filePath) : [filePath]; | |
| + }), | |
| + ); | |
| + return files.flat(); | |
| +} | |
| + | |
| +/** | |
| + * 2026-06-06 苍朮 | |
| + * Purpose: find one bundle by filename and source anchors, failing loudly when upstream layout changes. | |
| + * @param {string} root - Directory to scan recursively. | |
| + * @param {RegExp} fileNamePattern - Regular expression matched against each basename. | |
| + * @param {(source: string) => boolean} hasAnchor - Predicate that confirms the file owns the target anchor. | |
| + * @returns {Promise<string>} Absolute path to the only matching bundle. | |
| + */ | |
| +async function findBundle(root, fileNamePattern, hasAnchor) { | |
| + const candidates = (await listFiles(root)).filter((filePath) => fileNamePattern.test(basename(filePath))); | |
| + const matches = candidates.filter((filePath) => hasAnchor(readFileSync(filePath, "utf8"))); | |
| + | |
| + if (matches.length !== 1) { | |
| + throw new Error(`Expected exactly one bundle for ${fileNamePattern}, found ${matches.length}: ${matches.join(", ")}`); | |
| + } | |
| + | |
| + return matches[0]; | |
| +} | |
| + | |
| +/** | |
| + * 2026-06-06 苍朮 | |
| + * Purpose: replace a string exactly once and fail loudly if the bundle layout changed. | |
| + * @param {string} text - Source text to patch. | |
| + * @param {string} oldValue - Exact text expected before patching. | |
| + * @param {string} newValue - Exact text to write into the source. | |
| + * @returns {string} Patched text with one occurrence replaced. | |
| + */ | |
| +function replaceExactlyOnce(text, oldValue, newValue) { | |
| + const count = countOccurrences(text, oldValue); | |
| + if (count !== 1) { | |
| + throw new Error(`Expected exactly one occurrence of ${JSON.stringify(oldValue)}, found ${count}`); | |
| + } | |
| + return text.replace(oldValue, newValue); | |
| +} | |
| + | |
| +/** | |
| + * 2026-06-06 苍朮 | |
| + * Purpose: replace an exact anchor once while allowing a previously patched archive to validate cleanly. | |
| + * @param {string} text - Source text to patch or validate. | |
| + * @param {string} oldValue - Exact text expected before patching. | |
| + * @param {string} newValue - Exact patched text expected after patching. | |
| + * @returns {string} Source text with the patched value present exactly once. | |
| + */ | |
| +function replaceOnceOrValidate(text, oldValue, newValue) { | |
| + const oldCount = countOccurrences(text, oldValue); | |
| + const newCount = countOccurrences(text, newValue); | |
| + | |
| + if (oldCount === 1 && newCount === 0) { | |
| + return text.replace(oldValue, newValue); | |
| + } | |
| + if (oldCount === 0 && newCount === 1) { | |
| + return text; | |
| + } | |
| + | |
| + throw new Error(`Expected old=1/new=0 or old=0/new=1, found old=${oldCount}, new=${newCount}`); | |
| +} | |
| + | |
| +/** | |
| + * 2026-06-09 Yubo-Cao | |
| + * Purpose: report the system-backdrop probe as unsupported so the app's own opaque-window-surface | |
| + * fallback engages on Linux. Stock Electron lacks BrowserWindow.isSystemBackdropSupported and the | |
| + * upstream default of "supported" leaves main windows with a fully transparent background color, | |
| + * which flickers under Linux compositors. With the probe reporting "unsupported", every non-overlay | |
| + * window is created with an opaque theme background and the main process notifies renderers, which | |
| + * apply the built-in `electron-opaque` stylesheet path; overlay windows keep their transparency | |
| + * because the fallback already excludes overlay appearances. | |
| + * @param {string} bundlePath - Absolute path to the Electron main-process JavaScript bundle. | |
| + * @returns {{bundlePath: string, hash: string}} Patched bundle path and SHA256 hash. | |
| + */ | |
| +function patchWindowManagerBundle(bundlePath) { | |
| + const source = replaceOnceOrValidate(readFileSync(bundlePath, "utf8"), TRANSPARENT_BACKDROP_PROBE, OPAQUE_BACKDROP_PROBE); | |
| + writeFileSync(bundlePath, source); | |
| + return { | |
| + bundlePath, | |
| + hash: createHash("sha256").update(source).digest("hex"), | |
| + }; | |
| +} | |
| + | |
| +/** | |
| + * 2026-06-09 Yubo-Cao | |
| + * Purpose: make the renderer root and shared surface tokens opaque so full-page app chrome cannot show | |
| + * through itself even if the opaque-surface notification from the main process is missed at startup. | |
| + * @param {string} bundlePath - Absolute path to the shared app CSS bundle. | |
| + * @returns {{bundlePath: string, hash: string}} Patched bundle path and SHA256 hash. | |
| + */ | |
| +function patchRendererCssBundle(bundlePath) { | |
| + let source = readFileSync(bundlePath, "utf8"); | |
| + source = replaceOnceOrValidate(source, TRANSPARENT_TOKEN_CHAIN, OPAQUE_TOKEN_CHAIN); | |
| + source = replaceOnceOrValidate(source, ROOT_BACKGROUND_MARKER, ROOT_OPAQUE_BACKGROUND); | |
| + writeFileSync(bundlePath, source); | |
| + return { | |
| + bundlePath, | |
| + hash: createHash("sha256").update(source).digest("hex"), | |
| + }; | |
| +} | |
| + | |
| +/** | |
| + * 2026-06-06 苍朮 | |
| + * Purpose: keep the settings page from taking the dynamic host-navigation branch that previously exposed translucent panes. | |
| + * @param {string} bundlePath - Absolute path to the settings-page JavaScript bundle. | |
| + * @returns {{bundlePath: string, hash: string}} Patched bundle path and SHA256 hash. | |
| + */ | |
| +function patchSettingsBundle(bundlePath) { | |
| + let source = readFileSync(bundlePath, "utf8"); | |
| + const activeBranches = source.match(SETTINGS_BRANCH_PATTERN) ?? []; | |
| + const disabledBranchCount = countOccurrences(source, DISABLED_SETTINGS_BRANCH); | |
| + | |
| + if (activeBranches.length === 1 && disabledBranchCount === 0) { | |
| + source = replaceExactlyOnce(source, activeBranches[0], DISABLED_SETTINGS_BRANCH); | |
| + } else if (activeBranches.length !== 0 || disabledBranchCount !== 1) { | |
| + throw new Error( | |
| + `Expected one active settings branch or one disabled settings branch, found active=${activeBranches.length}, disabled=${disabledBranchCount}`, | |
| + ); | |
| + } | |
| + | |
| + if ((source.match(SETTINGS_BRANCH_PATTERN) ?? []).length !== 0) { | |
| + throw new Error("Settings host branch is still active after patching"); | |
| + } | |
| + if (countOccurrences(source, DISABLED_SETTINGS_BRANCH) !== 1) { | |
| + throw new Error("Disabled settings host branch was not written exactly once"); | |
| + } | |
| + | |
| + writeFileSync(bundlePath, source); | |
| + return { | |
| + bundlePath, | |
| + hash: createHash("sha256").update(source).digest("hex"), | |
| + }; | |
| +} | |
| + | |
| +/** | |
| + * 2026-06-06 苍朮 | |
| + * Purpose: run a command with inherited output so makepkg logs show the asar operation that failed. | |
| + * @param {string} command - Executable name to run. | |
| + * @param {string[]} args - Arguments passed to the executable. | |
| + * @returns {void} | |
| + */ | |
| +function run(command, args) { | |
| + execFileSync(command, args, { stdio: "inherit" }); | |
| +} | |
| + | |
| +/** | |
| + * 2026-06-06 苍朮 | |
| + * Purpose: extract, patch, and repack Codex's app.asar while preserving native module unpacking. | |
| + * @param {string} appAsar - Path to the app.asar archive inside the extracted Linux bundle. | |
| + * @returns {Promise<void>} Resolves after the archive has been replaced with the patched version. | |
| + */ | |
| +async function main(appAsar) { | |
| + if (!appAsar || !existsSync(appAsar)) { | |
| + throw new Error(`app.asar not found: ${appAsar ?? "<missing argument>"}`); | |
| + } | |
| + | |
| + const appUnpacked = `${appAsar}.unpacked`; | |
| + if (!existsSync(appUnpacked)) { | |
| + throw new Error(`app.asar.unpacked not found: ${appUnpacked}`); | |
| + } | |
| + | |
| + const tempRoot = mkdtempSync(join(dirname(appAsar), ".codex-app-unofficial-asar-")); | |
| + const extractedRoot = join(tempRoot, "app"); | |
| + const patchedAsar = join(tempRoot, "app.asar"); | |
| + const patchedUnpacked = `${patchedAsar}.unpacked`; | |
| + | |
| + try { | |
| + run("asar", ["extract", appAsar, extractedRoot]); | |
| + const mainResult = patchWindowManagerBundle( | |
| + await findBundle(join(extractedRoot, ".vite", "build"), /^main-[A-Za-z0-9_-]+\.js$/, (source) => | |
| + source.includes(TRANSPARENT_BACKDROP_PROBE) || source.includes(OPAQUE_BACKDROP_PROBE), | |
| + ), | |
| + ); | |
| + const cssResult = patchRendererCssBundle( | |
| + await findBundle(join(extractedRoot, "webview", "assets"), /^app-[A-Za-z0-9_-]*\.css$/, (source) => | |
| + source.includes(TRANSPARENT_TOKEN_CHAIN) || source.includes(OPAQUE_TOKEN_CHAIN), | |
| + ), | |
| + ); | |
| + const settingsResult = patchSettingsBundle( | |
| + await findBundle(join(extractedRoot, "webview", "assets"), /^settings-page-[A-Za-z0-9_-]+\.js$/, (source) => | |
| + (source.match(SETTINGS_BRANCH_PATTERN) ?? []).length > 0 || source.includes(DISABLED_SETTINGS_BRANCH), | |
| + ), | |
| + ); | |
| + | |
| + run("asar", ["pack", "--unpack-dir", UNPACK_DIRS, extractedRoot, patchedAsar]); | |
| + renameSync(patchedAsar, appAsar); | |
| + if (existsSync(patchedUnpacked)) { | |
| + rmSync(appUnpacked, { recursive: true, force: true }); | |
| + renameSync(patchedUnpacked, appUnpacked); | |
| + } | |
| + | |
| + console.log(`Patched ${mainResult.bundlePath}`); | |
| + console.log(`Patched main-process bundle sha256=${mainResult.hash}`); | |
| + console.log(`Patched ${cssResult.bundlePath}`); | |
| + console.log(`Patched renderer CSS bundle sha256=${cssResult.hash}`); | |
| + console.log(`Patched ${settingsResult.bundlePath}`); | |
| + console.log(`Patched settings bundle sha256=${settingsResult.hash}`); | |
| + } finally { | |
| + rmSync(tempRoot, { recursive: true, force: true }); | |
| + } | |
| +} | |
| + | |
| +main(process.argv[2]).catch((error) => { | |
| + console.error(error); | |
| + process.exit(1); | |
| +}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment