Skip to content

Instantly share code, notes, and snippets.

@brentp
Last active February 19, 2026 05:10
Show Gist options
  • Select an option

  • Save brentp/b6bd8d009f8936bc0b0c12f29473e0ef to your computer and use it in GitHub Desktop.

Select an option

Save brentp/b6bd8d009f8936bc0b0c12f29473e0ef to your computer and use it in GitHub Desktop.
codex-desktop
*.dmg
*.AppImage
unpacked
*node_modules*
*.appimage*
build

Codex DMG -> Linux AppImage

Everything is handled by one script:

./build-all.sh ./Codex.dmg

What it does

  • extracts Codex.dmg
  • builds ./build with Linux wrappers and launchers
  • rebuilds native modules (node-pty, better-sqlite3)
  • installs bundled electron@40 into ./build if missing
  • builds ./Codex-x86_64.AppImage
  • installs desktop integration in your user profile:
    • ~/.local/share/applications/codex-app.desktop
    • ~/.local/share/icons/hicolor/*/apps/codex-app.png
    • ~/.local/share/pixmaps/codex-app.png

Requirements

  • node and npm
  • 7z
  • mksquashfs
  • native build tools: make and either g++ or clang++
  • Linux codex CLI in PATH (or set CODEX_LINUX_CLI_PATH)
  • Linux rg in PATH (or set CODEX_LINUX_RG_PATH)
  • an existing AppImage to borrow runtime bytes from
    • default search paths include ~/Applications/cursor.AppImage
    • or set APPIMAGE_RUNTIME_SOURCE=/path/to/Some.AppImage

Outputs

  • AppImage: ./Codex-x86_64.AppImage
  • Bundle launcher: ./build/codex
  • Direct runner: ./build/run-codex-linux.sh

Useful options

  • CODEX_INSTALL_DESKTOP=0 to skip writing desktop/icon files in ~/.local/share
  • CODEX_DESKTOP_ID=my-codex to change desktop entry/icon id
  • CODEX_NO_SANDBOX=1 (default inside AppImage launcher)
  • CODEX_NO_SANDBOX=0 to keep sandboxing on; requires either:
    • setuid sandbox helper configured on chrome-sandbox (root:root, mode 4755), or
    • user namespaces allowed by the host
  • CODEX_REDIRECT_STDIO=0 to keep logs in terminal for debugging
  • CODEX_FORCE_SOFTWARE_GL=1 to force software GL (default is hardware acceleration)

Sandbox setup (CODEX_NO_SANDBOX=0)

Use chown + chmod on Electron's chrome-sandbox helper (no chroot step is needed):

sudo chown root:root /home/brentp/Downloads/codex-app/build/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 /home/brentp/Downloads/codex-app/build/node_modules/electron/dist/chrome-sandbox

Verify:

ls -l /home/brentp/Downloads/codex-app/build/node_modules/electron/dist/chrome-sandbox

Expected mode/owner includes root root and setuid (-rwsr-xr-x).

If CODEX_NO_SANDBOX=0 still fails on your distro, user namespaces may be restricted by host policy. In that case run with:

CODEX_NO_SANDBOX=1
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DMG_PATH="${1:-$SCRIPT_DIR/Codex.dmg}"
BUNDLE_DIR="${2:-$SCRIPT_DIR/build}"
OUT_APPIMAGE="${3:-$SCRIPT_DIR/Codex-x86_64.AppImage}"
INSTALL_DESKTOP="${CODEX_INSTALL_DESKTOP:-1}"
DESKTOP_ID="${CODEX_DESKTOP_ID:-codex-app}"
log() {
echo "[*] $*"
}
die() {
echo "$*" >&2
exit 1
}
require_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || die "Missing required tool: $cmd"
}
extract_icns_largest_png() {
local icns_path="$1"
local out_path="$2"
[[ -f "$icns_path" ]] || return 1
python3 - "$icns_path" "$out_path" <<'PY' >/dev/null 2>&1 || return 1
import sys
try:
from PIL import Image
except Exception:
raise SystemExit(1)
icns_path, out_path = sys.argv[1], sys.argv[2]
img = Image.open(icns_path)
best = None
for idx in range(getattr(img, "n_frames", 1)):
try:
img.seek(idx)
except Exception:
break
frame = img.convert("RGBA")
if best is None or frame.size[0] * frame.size[1] > best.size[0] * best.size[1]:
best = frame
if best is None:
best = img.convert("RGBA")
max_size = 512
if best.size[0] > max_size or best.size[1] > max_size:
resample = Image.Resampling.LANCZOS if hasattr(Image, "Resampling") else Image.LANCZOS
best.thumbnail((max_size, max_size), resample)
best.save(out_path, format="PNG")
PY
}
find_linux_icon() {
local resources_dir="$1"
local app_dir="$2"
local scratch_icon="${3:-}"
local icon_path="$resources_dir/codex-icon.png"
if [[ -f "$icon_path" ]]; then
printf '%s\n' "$icon_path"
return 0
fi
local icns_path="$resources_dir/electron.icns"
if [[ -n "$scratch_icon" && -f "$icns_path" ]] && extract_icns_largest_png "$icns_path" "$scratch_icon"; then
printf '%s\n' "$scratch_icon"
return 0
fi
local candidates=()
shopt -s nullglob
candidates=("$app_dir"/webview/assets/app-*.png)
shopt -u nullglob
if (( ${#candidates[@]} > 0 )); then
printf '%s\n' "${candidates[0]}"
return 0
fi
icon_path="$app_dir/webview/apps/vscode.png"
if [[ -f "$icon_path" ]]; then
printf '%s\n' "$icon_path"
return 0
fi
return 1
}
extract_asar() {
local asar_path="$1"
local dest_dir="$2"
local unpacked_dir="$3"
local js_file
js_file="$(mktemp)"
cat > "$js_file" <<'NODE'
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
function fail(message) {
console.error(message);
process.exit(1);
}
function mkdirp(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
}
function parseAsar(asarPath) {
const data = fs.readFileSync(asarPath);
if (data.length < 16) {
fail(`Invalid ASAR (too small): ${asarPath}`);
}
const headerSize = data.readUInt32LE(12);
const headerStart = 16;
const headerEnd = headerStart + headerSize;
if (headerEnd > data.length) {
fail(`Invalid ASAR header size: ${headerSize}`);
}
let header;
try {
header = JSON.parse(data.slice(headerStart, headerEnd).toString('utf8'));
} catch (err) {
fail(`Failed to parse ASAR header: ${err.message}`);
}
return {
data,
header,
dataStart: headerEnd,
};
}
function writeFile(outputPath, content, executable) {
mkdirp(path.dirname(outputPath));
fs.writeFileSync(outputPath, content);
if (executable) {
fs.chmodSync(outputPath, 0o755);
}
}
function copyUnpacked(unpackedRoot, relPath, outputPath, executable) {
if (!unpackedRoot) {
fail(`ASAR entry is unpacked but no unpacked dir was provided: ${relPath}`);
}
const src = path.join(unpackedRoot, relPath);
if (!fs.existsSync(src)) {
fail(`Missing unpacked file: ${src}`);
}
mkdirp(path.dirname(outputPath));
fs.copyFileSync(src, outputPath);
if (executable) {
fs.chmodSync(outputPath, 0o755);
}
}
function extractTree(node, prefix, ctx) {
if (!node || !node.files) {
return;
}
for (const [name, child] of Object.entries(node.files)) {
const relPath = prefix ? path.join(prefix, name) : name;
const outPath = path.join(ctx.destDir, relPath);
if (child.files) {
mkdirp(outPath);
extractTree(child, relPath, ctx);
continue;
}
if (typeof child.link === 'string') {
mkdirp(path.dirname(outPath));
try {
fs.symlinkSync(child.link, outPath);
} catch {
fs.writeFileSync(outPath, child.link, 'utf8');
}
continue;
}
if (child.unpacked === true) {
copyUnpacked(ctx.unpackedDir, relPath, outPath, child.executable === true);
continue;
}
if (typeof child.offset === 'undefined' || typeof child.size !== 'number') {
fail(`Invalid file entry in ASAR for: ${relPath}`);
}
const start = ctx.dataStart + Number(child.offset);
const end = start + child.size;
if (start < 0 || end > ctx.data.length || end < start) {
fail(`Invalid file range for ${relPath}: start=${start} end=${end}`);
}
writeFile(outPath, ctx.data.slice(start, end), child.executable === true);
}
}
function main() {
const [asarPath, destDir, unpackedDir] = process.argv.slice(2);
if (!asarPath || !destDir) {
fail('Usage: extract-asar.js <app.asar> <dest_dir> [app.asar.unpacked_dir]');
}
const resolvedAsar = path.resolve(asarPath);
const resolvedDest = path.resolve(destDir);
const resolvedUnpacked = unpackedDir ? path.resolve(unpackedDir) : null;
if (!fs.existsSync(resolvedAsar)) {
fail(`ASAR not found: ${resolvedAsar}`);
}
if (resolvedUnpacked && !fs.existsSync(resolvedUnpacked)) {
fail(`Unpacked directory not found: ${resolvedUnpacked}`);
}
mkdirp(resolvedDest);
const parsed = parseAsar(resolvedAsar);
extractTree(parsed.header, '', {
...parsed,
destDir: resolvedDest,
unpackedDir: resolvedUnpacked,
});
console.log(`Extracted ${resolvedAsar} -> ${resolvedDest}`);
}
main();
NODE
node "$js_file" "$asar_path" "$dest_dir" "$unpacked_dir"
rm -f "$js_file"
}
patch_main_bundle() {
local main_bundle="$1"
[[ -f "$main_bundle" ]] || return 0
if ! grep -q 'CODEX_ICON_PATH' "$main_bundle"; then
perl -0pi -e 's/show:o,\.\.\.process\.platform==="win32"\?\{autoHideMenuBar:!0\}:\{\},/show:o,...process.platform==="linux"&&process.env.CODEX_ICON_PATH?{icon:process.env.CODEX_ICON_PATH}:{},...process.platform==="win32"?{autoHideMenuBar:!0}:{},/g' "$main_bundle"
fi
if ! grep -q 'CODEX_SKIP_SHELL_ENV' "$main_bundle"; then
perl -0pi -e 's/\}catch\(t\)\{const e=t instanceof Error\?t\.message:String\(t\);fn\(\)\.warning\("Failed to load shell env",\{safe:\{\},sensitive:\{message:e\}\}\)\}\}Mge\(\);/}catch(t){const e=t instanceof Error?t.message:String(t);fn().warning("Failed to load shell env",{safe:{},sensitive:{message:e}})}}process.env.CODEX_SKIP_SHELL_ENV!=="1"&&Mge();/g' "$main_bundle"
fi
perl -0pi -e 's/await Np\.refresh\(\{triggerProviderRefresh:!0\}\),await bu\(Ht\),await xM\.flushPendingDeepLinks\(\)/await bu(Ht),await Np.refresh({triggerProviderRefresh:!0}),await xM.flushPendingDeepLinks()/g' "$main_bundle"
if ! grep -q '__codexLinuxTray' "$main_bundle"; then
perl -0pi -e 's/j\.app\.whenReady\(\)\.then\(async\(\)=>\{if\(xM\.registerProtocolClient\(\),/j.app.whenReady().then(async()=>{if(process.platform==="linux"&&process.env.CODEX_ENABLE_TRAY!=="0")try{const t=process.env.CODEX_TRAY_ICON_PATH||process.env.CODEX_ICON_PATH||ie.join(wd,"..","codex-icon.png"),e=Ee.existsSync(t)?j.nativeImage.createFromPath(t):j.nativeImage.createFromPath(ie.join(wd,"webview","apps","vscode.png"));if(!e.isEmpty()){const n=e.resize({width:22,height:22}),r=new j.Tray(n),i=j.Menu.buildFromTemplate([{label:"Show Codex",click:()=>{P4()}},{type:"separator"},{label:"Quit",click:()=>{j.app.quit()}}]);r.setToolTip("Codex"),r.setContextMenu(i),r.on("click",()=>{P4()}),r.on("double-click",()=>{P4()}),globalThis.__codexLinuxTray=r,fn().info("Linux tray icon initialized",{safe:{iconPath:t},sensitive:{}})}else fn().warning("Linux tray icon image is empty",{safe:{iconPath:t},sensitive:{}})}catch(t){fn().warning("Failed to create Linux tray icon",{safe:{},sensitive:{error:t}})}if(xM.registerProtocolClient(),/g' "$main_bundle"
fi
}
write_resource_wrappers() {
local resources_dir="$1"
cat > "$resources_dir/codex" <<'WRAPPER'
#!/usr/bin/env bash
set -euo pipefail
SELF_PATH="$(readlink -f "$0" 2>/dev/null || printf '%s' "$0")"
if [[ -n "${CODEX_LINUX_CLI_PATH:-}" && -x "${CODEX_LINUX_CLI_PATH}" ]]; then
exec "${CODEX_LINUX_CLI_PATH}" "$@"
fi
IFS=':' read -r -a PATH_PARTS <<<"${PATH:-}"
for path_dir in "${PATH_PARTS[@]}"; do
[[ -n "$path_dir" ]] || continue
candidate="$path_dir/codex"
[[ -x "$candidate" ]] || continue
candidate_real="$(readlink -f "$candidate" 2>/dev/null || printf '%s' "$candidate")"
if [[ "$candidate_real" != "$SELF_PATH" ]]; then
exec "$candidate" "$@"
fi
done
echo "No Linux codex CLI found." >&2
echo "Install one in PATH (for example: npm i -g @openai/codex)" >&2
echo "or set CODEX_LINUX_CLI_PATH to a Linux codex binary." >&2
exit 127
WRAPPER
cat > "$resources_dir/rg" <<'WRAPPER'
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${CODEX_LINUX_RG_PATH:-}" && -x "${CODEX_LINUX_RG_PATH}" ]]; then
exec "${CODEX_LINUX_RG_PATH}" "$@"
fi
if command -v rg >/dev/null 2>&1; then
exec "$(command -v rg)" "$@"
fi
echo "No Linux ripgrep (rg) found." >&2
echo "Install rg or set CODEX_LINUX_RG_PATH." >&2
exit 127
WRAPPER
cat > "$resources_dir/codex-box" <<'WRAPPER'
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${CODEX_LINUX_CODEX_BOX_PATH:-}" && -x "${CODEX_LINUX_CODEX_BOX_PATH}" ]]; then
exec "${CODEX_LINUX_CODEX_BOX_PATH}" "$@"
fi
if command -v codex-box >/dev/null 2>&1; then
exec "$(command -v codex-box)" "$@"
fi
if command -v codex >/dev/null 2>&1; then
exec "$(command -v codex)" "$@"
fi
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -x "$self_dir/codex" ]]; then
exec "$self_dir/codex" "$@"
fi
echo "No Linux codex-box provider found." >&2
echo "Install codex/codex-box in PATH or set CODEX_LINUX_CODEX_BOX_PATH." >&2
exit 127
WRAPPER
chmod +x "$resources_dir/codex" "$resources_dir/rg" "$resources_dir/codex-box"
}
write_bundle_scripts() {
local out_dir="$1"
local abs_out_dir
cat > "$out_dir/linux-common.sh" <<'SH'
#!/usr/bin/env bash
codex_default_log_file() {
local log_dir="${CODEX_LOG_DIR:-$HOME/.local/state/codex-linux}"
printf '%s\n' "${CODEX_LOG_FILE:-$log_dir/codex.log}"
}
codex_ensure_log_parent() {
local log_file="$1"
mkdir -p "$(dirname -- "$log_file")"
}
codex_truncate_log_if_requested() {
local log_file="$1"
if [[ "${CODEX_TRUNCATE_LOG_ON_START:-1}" == "1" ]]; then
: >"$log_file"
fi
}
codex_redirect_stdio_if_requested() {
local log_file="$1"
if [[ "${CODEX_REDIRECT_STDIO:-1}" == "1" && "${CODEX_LOG_REDIRECTED:-0}" != "1" ]]; then
codex_ensure_log_parent "$log_file"
codex_truncate_log_if_requested "$log_file"
if [[ -t 2 ]]; then
echo "Codex logs are redirected to: $log_file" >&2
echo "Set CODEX_REDIRECT_STDIO=0 to keep logs in the terminal." >&2
fi
export CODEX_LOG_REDIRECTED=1
exec >>"$log_file" 2>&1
fi
}
codex_detect_icon() {
local root_dir="$1"
local app_dir="$2"
if [[ -f "$root_dir/resources/codex-icon.png" ]]; then
printf '%s\n' "$root_dir/resources/codex-icon.png"
return 0
fi
local candidates=()
shopt -s nullglob
candidates=("$app_dir"/webview/assets/app-*.png)
shopt -u nullglob
if (( ${#candidates[@]} > 0 )); then
printf '%s\n' "${candidates[0]}"
return 0
fi
if [[ -f "$app_dir/webview/apps/vscode.png" ]]; then
printf '%s\n' "$app_dir/webview/apps/vscode.png"
return 0
fi
return 1
}
SH
cat > "$out_dir/run-codex-linux.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMMON_SH="$ROOT_DIR/linux-common.sh"
APP_DIR="$ROOT_DIR/resources/app"
WEBVIEW_INDEX="$APP_DIR/webview/index.html"
RESOURCE_CODEX="$ROOT_DIR/resources/codex"
RESOURCE_EXTENSION_CODEX="$ROOT_DIR/resources/extension/bin/codex"
if [[ ! -f "$COMMON_SH" ]]; then
echo "Missing shared runtime helper: $COMMON_SH" >&2
exit 1
fi
source "$COMMON_SH"
if [[ ! -d "$APP_DIR" ]]; then
echo "Missing app directory: $APP_DIR" >&2
exit 1
fi
LOG_FILE="$(codex_default_log_file)"
codex_redirect_stdio_if_requested "$LOG_FILE"
export PATH="${PATH:+$PATH:}$ROOT_DIR/resources"
SYSTEM_CODEX_BIN="$(command -v codex 2>/dev/null || true)"
SYSTEM_CODEX_REAL="$(readlink -f "$SYSTEM_CODEX_BIN" 2>/dev/null || true)"
RESOURCE_CODEX_REAL="$(readlink -f "$RESOURCE_CODEX" 2>/dev/null || true)"
if [[ -z "${CODEX_LINUX_CLI_PATH:-}" && -n "$SYSTEM_CODEX_BIN" && "$SYSTEM_CODEX_REAL" != "$RESOURCE_CODEX_REAL" ]]; then
export CODEX_LINUX_CLI_PATH="$SYSTEM_CODEX_BIN"
fi
if [[ -z "${CODEX_CLI_PATH:-}" ]]; then
if [[ -x "$RESOURCE_EXTENSION_CODEX" ]]; then
export CODEX_CLI_PATH="$RESOURCE_EXTENSION_CODEX"
elif [[ -x "$RESOURCE_CODEX" ]]; then
export CODEX_CLI_PATH="$RESOURCE_CODEX"
fi
fi
if [[ -z "${CUSTOM_CLI_PATH:-}" && -n "${CODEX_CLI_PATH:-}" ]]; then
export CUSTOM_CLI_PATH="$CODEX_CLI_PATH"
fi
: "${BUILD_FLAVOR:=prod}"
export BUILD_FLAVOR
: "${CODEX_SKIP_SHELL_ENV:=1}"
export CODEX_SKIP_SHELL_ENV
if [[ -z "${ELECTRON_RENDERER_URL:-}" && -f "$WEBVIEW_INDEX" ]]; then
export ELECTRON_RENDERER_URL="file://$WEBVIEW_INDEX"
fi
if [[ -z "${CODEX_ICON_PATH:-}" ]]; then
DETECTED_ICON="$(codex_detect_icon "$ROOT_DIR" "$APP_DIR" || true)"
if [[ -n "$DETECTED_ICON" ]]; then
export CODEX_ICON_PATH="$DETECTED_ICON"
fi
fi
if [[ "${CODEX_ENABLE_TRAY:-1}" != "0" && "${XDG_CURRENT_DESKTOP:-}" == *GNOME* ]] && command -v gsettings >/dev/null 2>&1; then
GNOME_EXTENSIONS="$(gsettings get org.gnome.shell enabled-extensions 2>/dev/null || true)"
if [[ "$GNOME_EXTENSIONS" != *appindicatorsupport* && "$GNOME_EXTENSIONS" != *ubuntu-appindicators* ]]; then
echo "GNOME AppIndicator extension not detected; tray icons may be hidden." >&2
echo "Install: sudo apt install gnome-shell-extension-appindicator libappindicator3-1" >&2
fi
fi
ELECTRON_BIN="${CODEX_ELECTRON_BIN:-}"
if [[ -z "$ELECTRON_BIN" && -x "$ROOT_DIR/node_modules/.bin/electron" ]]; then
ELECTRON_BIN="$ROOT_DIR/node_modules/.bin/electron"
fi
if [[ -z "$ELECTRON_BIN" ]] && command -v electron >/dev/null 2>&1; then
ELECTRON_BIN="$(command -v electron)"
fi
if [[ -z "$ELECTRON_BIN" ]]; then
echo "No Electron runtime found." >&2
echo "Install Electron 40 or set CODEX_ELECTRON_BIN." >&2
echo "Tip: npm --prefix '$ROOT_DIR' install --no-save electron@40.0.0" >&2
exit 1
fi
ELECTRON_EXEC="$ELECTRON_BIN"
if [[ -x "$ROOT_DIR/node_modules/electron/dist/electron" ]]; then
ELECTRON_EXEC="$ROOT_DIR/node_modules/electron/dist/electron"
fi
ELECTRON_REAL="$(readlink -f "$ELECTRON_EXEC" 2>/dev/null || true)"
if [[ -z "$ELECTRON_REAL" ]]; then
ELECTRON_REAL="$ELECTRON_EXEC"
fi
ELECTRON_DIR="$(cd "$(dirname "$ELECTRON_REAL")" && pwd)"
SANDBOX_BIN="$ELECTRON_DIR/chrome-sandbox"
USE_SETUID_SANDBOX=0
SETUID_SANDBOX_MISCONFIGURED=0
if [[ "${CODEX_NO_SANDBOX:-0}" != "1" && -f "$SANDBOX_BIN" ]]; then
SANDBOX_OWNER_UID="$(stat -c '%u' "$SANDBOX_BIN" 2>/dev/null || true)"
if [[ "$SANDBOX_OWNER_UID" == "0" && -u "$SANDBOX_BIN" ]]; then
USE_SETUID_SANDBOX=1
else
SETUID_SANDBOX_MISCONFIGURED=1
fi
fi
USERNS_SANDBOX_OK=0
if [[ "${CODEX_NO_SANDBOX:-0}" != "1" && "$USE_SETUID_SANDBOX" != "1" ]]; then
if command -v unshare >/dev/null 2>&1 && unshare -Ur true >/dev/null 2>&1; then
USERNS_SANDBOX_OK=1
if [[ "$SETUID_SANDBOX_MISCONFIGURED" == "1" ]]; then
echo "Electron setuid sandbox helper is not configured: $SANDBOX_BIN" >&2
echo "Using user-namespace sandbox instead (--disable-setuid-sandbox)." >&2
echo "To enable setuid sandbox instead:" >&2
echo " sudo chown root:root '$SANDBOX_BIN'" >&2
echo " sudo chmod 4755 '$SANDBOX_BIN'" >&2
fi
else
echo "No usable Linux sandbox with CODEX_NO_SANDBOX=0." >&2
echo "This host blocks user-namespace sandbox and setuid sandbox is not configured." >&2
echo "To enable setuid sandbox:" >&2
echo " sudo chown root:root '$SANDBOX_BIN'" >&2
echo " sudo chmod 4755 '$SANDBOX_BIN'" >&2
echo "Or run without sandbox (less secure):" >&2
echo " CODEX_NO_SANDBOX=1 $0" >&2
exit 1
fi
fi
ARGS=("$APP_DIR")
if [[ -n "${CODEX_WM_CLASS:-Codex}" ]]; then
ARGS+=("--class=${CODEX_WM_CLASS:-Codex}")
fi
if [[ "${CODEX_NO_SANDBOX:-0}" == "1" ]]; then
ARGS+=(--no-sandbox --disable-setuid-sandbox)
elif [[ "$USE_SETUID_SANDBOX" != "1" && "$USERNS_SANDBOX_OK" == "1" ]]; then
# Keeps Chromium sandboxing enabled via user namespaces when setuid helper is unavailable.
ARGS+=(--disable-setuid-sandbox)
fi
if [[ "${CODEX_DISABLE_VULKAN:-1}" == "1" ]]; then
ARGS+=(--disable-vulkan)
fi
if [[ "${CODEX_FORCE_SOFTWARE_GL:-0}" == "1" ]]; then
ARGS+=(--use-angle=swiftshader --disable-gpu-sandbox)
fi
if [[ -n "${CODEX_OZONE_PLATFORM:-}" ]]; then
ARGS+=("--ozone-platform=${CODEX_OZONE_PLATFORM}")
elif [[ "${XDG_SESSION_TYPE:-}" == "wayland" && "${XDG_CURRENT_DESKTOP:-}" == *GNOME* ]]; then
ARGS+=(--ozone-platform=x11)
fi
if [[ -n "${CODEX_ELECTRON_FLAGS:-}" ]]; then
read -r -a CODEX_ELECTRON_FLAG_ARR <<<"${CODEX_ELECTRON_FLAGS}"
ARGS+=("${CODEX_ELECTRON_FLAG_ARR[@]}")
fi
ARGS+=("$@")
exec "$ELECTRON_EXEC" "${ARGS[@]}"
SH
cat > "$out_dir/rebuild-native-modules.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$ROOT_DIR/resources/app"
UNPACKED_DIR="$ROOT_DIR/resources/app.asar.unpacked"
CXX_BIN="${CXX:-}"
if [[ ! -d "$APP_DIR" ]]; then
echo "Missing app directory: $APP_DIR" >&2
exit 1
fi
if [[ ! -d "$UNPACKED_DIR" ]]; then
echo "Missing unpacked resources: $UNPACKED_DIR" >&2
exit 1
fi
if ! command -v npm >/dev/null 2>&1; then
echo "npm is required to rebuild native modules" >&2
exit 1
fi
if [[ -z "$CXX_BIN" ]]; then
if command -v g++ >/dev/null 2>&1; then
CXX_BIN="g++"
elif command -v clang++ >/dev/null 2>&1; then
CXX_BIN="clang++"
fi
fi
if ! command -v make >/dev/null 2>&1; then
echo "Missing native build tool: make." >&2
echo "Install it on Ubuntu with: sudo apt install -y build-essential" >&2
exit 1
fi
if [[ -z "$CXX_BIN" || ! -x "$(command -v "$CXX_BIN" 2>/dev/null || true)" ]]; then
echo "Missing native C++ compiler (g++ or clang++)." >&2
echo "Install them on Ubuntu with: sudo apt install -y build-essential" >&2
exit 1
fi
export CXX="$CXX_BIN"
echo "[*] Using CXX=$CXX"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
cat > "$WORK_DIR/package.json" <<'PKG'
{
"name": "codex-linux-native-rebuild",
"private": true,
"version": "0.0.0"
}
PKG
npm_config_runtime=electron \
npm_config_target=40.0.0 \
npm_config_disturl=https://electronjs.org/headers \
npm --prefix "$WORK_DIR" install --no-save node-pty@1.1.0 better-sqlite3@12.4.6
mkdir -p "$APP_DIR/node_modules/node-pty/build/Release"
mkdir -p "$APP_DIR/node_modules/better-sqlite3/build/Release"
mkdir -p "$UNPACKED_DIR/node_modules/node-pty/build/Release"
mkdir -p "$UNPACKED_DIR/node_modules/better-sqlite3/build/Release"
cp -f "$WORK_DIR/node_modules/node-pty/build/Release/pty.node" "$APP_DIR/node_modules/node-pty/build/Release/pty.node"
cp -f "$WORK_DIR/node_modules/node-pty/build/Release/pty.node" "$UNPACKED_DIR/node_modules/node-pty/build/Release/pty.node"
if [[ -f "$WORK_DIR/node_modules/node-pty/build/Release/spawn-helper" ]]; then
cp -f "$WORK_DIR/node_modules/node-pty/build/Release/spawn-helper" "$APP_DIR/node_modules/node-pty/build/Release/spawn-helper"
cp -f "$WORK_DIR/node_modules/node-pty/build/Release/spawn-helper" "$UNPACKED_DIR/node_modules/node-pty/build/Release/spawn-helper"
fi
cp -f "$WORK_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node" "$APP_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
cp -f "$WORK_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node" "$UNPACKED_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
echo "[*] Native module rebuild complete."
SH
abs_out_dir="$(cd "$out_dir" && pwd)"
cat > "$out_dir/codex" <<SH
#!/usr/bin/env bash
set -euo pipefail
DEFAULT_BUNDLE_DIR="$abs_out_dir"
BUNDLE_DIR="\${CODEX_BUNDLE_DIR:-}"
if [[ -z "\$BUNDLE_DIR" ]]; then
SELF_PATH="\$(readlink -f "\$0" 2>/dev/null || printf '%s' "\$0")"
SELF_DIR="\$(cd "\$(dirname "\$SELF_PATH")" && pwd)"
if [[ -x "\$SELF_DIR/run-codex-linux.sh" ]]; then
BUNDLE_DIR="\$SELF_DIR"
elif [[ -x "\$DEFAULT_BUNDLE_DIR/run-codex-linux.sh" ]]; then
BUNDLE_DIR="\$DEFAULT_BUNDLE_DIR"
fi
fi
if [[ -z "\$BUNDLE_DIR" || ! -x "\$BUNDLE_DIR/run-codex-linux.sh" ]]; then
echo "Could not find run-codex-linux.sh." >&2
echo "Set CODEX_BUNDLE_DIR to your build directory." >&2
exit 1
fi
COMMON_SH="\$BUNDLE_DIR/linux-common.sh"
if [[ ! -f "\$COMMON_SH" ]]; then
echo "Missing shared runtime helper: \$COMMON_SH" >&2
exit 1
fi
source "\$COMMON_SH"
LOG_FILE="\$(codex_default_log_file)"
codex_ensure_log_parent "\$LOG_FILE"
: >"\$LOG_FILE"
DETACH_MODE="\${CODEX_DETACH:-1}"
if [[ "\$DETACH_MODE" == "1" ]]; then
CODEX_LOG_REDIRECTED=1 CODEX_TRUNCATE_LOG_ON_START=0 nohup "\$BUNDLE_DIR/run-codex-linux.sh" "\$@" >>"\$LOG_FILE" 2>&1 < /dev/null &
if [[ "\${CODEX_LAUNCH_STATUS:-0}" == "1" ]]; then
echo "Codex started in background (pid \$!)."
echo "Logs: \$LOG_FILE"
fi
exit 0
fi
CODEX_LOG_REDIRECTED=1 CODEX_TRUNCATE_LOG_ON_START=0 exec "\$BUNDLE_DIR/run-codex-linux.sh" "\$@" >>"\$LOG_FILE" 2>&1
SH
chmod +x \
"$out_dir/linux-common.sh" \
"$out_dir/run-codex-linux.sh" \
"$out_dir/rebuild-native-modules.sh" \
"$out_dir/codex"
}
prepare_bundle() {
local dmg_path="$1"
local out_dir="$2"
local tmp_dir app_path resources_src app_asar app_unpacked app_dir icon_out found_icon main_bundle
require_cmd 7z
require_cmd node
[[ -f "$dmg_path" ]] || die "DMG not found: $dmg_path"
tmp_dir="$(mktemp -d)"
log "Extracting DMG..."
set +e
7z x -y "$dmg_path" "-o$tmp_dir" >/dev/null
local extract_status=$?
set -e
if [[ "$extract_status" -ne 0 && "$extract_status" -ne 1 && "$extract_status" -ne 2 ]]; then
rm -rf "$tmp_dir"
die "7z failed while extracting DMG (exit $extract_status)"
fi
app_path="$(find "$tmp_dir" -type d -name 'Codex.app' | head -n1 || true)"
[[ -n "$app_path" ]] || { rm -rf "$tmp_dir"; die "Could not find Codex.app inside extracted DMG"; }
resources_src="$app_path/Contents/Resources"
[[ -d "$resources_src" ]] || { rm -rf "$tmp_dir"; die "Could not find Resources directory: $resources_src"; }
mkdir -p "$out_dir"
log "Copying Resources payload..."
rm -rf "$out_dir/resources"
cp -a "$resources_src" "$out_dir/resources"
# Drop stale launchers/scripts from older layouts.
rm -f "$out_dir/codex-app" "$out_dir/prepare-linux-port.sh" "$out_dir/build-appimage.sh"
find "$out_dir/resources" -type f -name '*:com.apple.cs.*' -delete || true
app_asar="$out_dir/resources/app.asar"
app_unpacked="$out_dir/resources/app.asar.unpacked"
app_dir="$out_dir/resources/app"
[[ -f "$app_asar" ]] || { rm -rf "$tmp_dir"; die "Missing app.asar in copied resources"; }
[[ -d "$app_unpacked" ]] || { rm -rf "$tmp_dir"; die "Missing app.asar.unpacked in copied resources"; }
log "Expanding app.asar into resources/app ..."
rm -rf "$app_dir"
extract_asar "$app_asar" "$app_dir" "$app_unpacked"
icon_out="$out_dir/resources/codex-icon.png"
if [[ ! -f "$icon_out" ]]; then
found_icon="$(find_linux_icon "$out_dir/resources" "$app_dir" "$icon_out" || true)"
if [[ -n "$found_icon" && -f "$found_icon" && "$found_icon" != "$icon_out" ]]; then
cp -f "$found_icon" "$icon_out"
fi
fi
main_bundle="$(find "$out_dir/resources/app/.vite/build" -maxdepth 1 -type f -name 'main-*.js' | head -n1 || true)"
if [[ -n "$main_bundle" ]]; then
patch_main_bundle "$main_bundle"
fi
if [[ ! -f "$out_dir/resources/app.asar.macos" ]]; then
mv "$app_asar" "$out_dir/resources/app.asar.macos"
fi
if [[ -f "$out_dir/resources/codex" && ! -f "$out_dir/resources/codex.macos-arm64" ]]; then
mv "$out_dir/resources/codex" "$out_dir/resources/codex.macos-arm64"
fi
if [[ -f "$out_dir/resources/rg" && ! -f "$out_dir/resources/rg.macos-arm64" ]]; then
mv "$out_dir/resources/rg" "$out_dir/resources/rg.macos-arm64"
fi
write_resource_wrappers "$out_dir/resources"
mkdir -p "$out_dir/resources/extension/bin"
ln -sfn ../../codex "$out_dir/resources/extension/bin/codex"
write_bundle_scripts "$out_dir"
rm -rf "$tmp_dir"
log "Linux port prepared at: $out_dir"
}
rebuild_native_modules() {
local out_dir="$1"
log "Rebuilding native modules..."
"$out_dir/rebuild-native-modules.sh"
}
ensure_bundled_electron() {
local out_dir="$1"
if [[ ! -x "$out_dir/node_modules/electron/dist/electron" ]]; then
log "Installing bundled Electron 40 runtime..."
npm --prefix "$out_dir" install --no-save electron@40.0.0
fi
}
build_appimage() {
local bundle_dir="$1"
local out_appimage="$2"
local runtime_src offset work_dir appdir icon_src runtime_bin squashfs_img tmp_out
require_cmd mksquashfs
[[ -d "$bundle_dir" ]] || die "Bundle directory not found: $bundle_dir"
[[ -x "$bundle_dir/node_modules/electron/dist/electron" ]] || die "Missing bundled Electron runtime in: $bundle_dir/node_modules/electron/dist/electron"
runtime_src="${APPIMAGE_RUNTIME_SOURCE:-}"
if [[ -z "$runtime_src" ]]; then
for candidate in \
"$HOME/.joplin/Joplin.AppImage" \
"$HOME/Applications/cursor.AppImage" \
"$HOME/Applications/mux-0.15.2-x86_64.AppImage" \
"$HOME/Applications/mux-0.14.1-x86_64.AppImage"
do
if [[ -x "$candidate" ]]; then
runtime_src="$candidate"
break
fi
done
fi
[[ -x "$runtime_src" ]] || die "No suitable AppImage runtime source found. Set APPIMAGE_RUNTIME_SOURCE=/path/to/SomeApp.AppImage"
offset="$("$runtime_src" --appimage-offset)"
[[ "$offset" =~ ^[0-9]+$ ]] || die "Failed to get AppImage offset from runtime source: $runtime_src"
log "Using AppImage runtime source: $runtime_src"
log "Runtime offset: $offset"
work_dir="$SCRIPT_DIR/.appimage-work"
appdir="$work_dir/Codex.AppDir"
rm -rf "$work_dir"
mkdir -p "$appdir/opt"
cp -a "$bundle_dir" "$appdir/opt/codex-app"
mkdir -p "$appdir/usr/bin"
cat > "$appdir/usr/bin/codex-app" <<'RUNNER'
#!/usr/bin/env bash
set -euo pipefail
APPDIR_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
BUNDLE_DIR="$APPDIR_PATH/opt/codex-app"
export CODEX_BUNDLE_DIR="$BUNDLE_DIR"
export CODEX_DETACH=0
export CODEX_NO_SANDBOX="${CODEX_NO_SANDBOX:-1}"
"$BUNDLE_DIR/run-codex-linux.sh" "$@"
status=$?
if [[ $status -ne 0 ]]; then
DEFAULT_LOG_DIR="${CODEX_LOG_DIR:-$HOME/.local/state/codex-linux}"
LOG_FILE="${CODEX_LOG_FILE:-$DEFAULT_LOG_DIR/codex.log}"
echo "Codex failed to start (exit $status). See log: $LOG_FILE" >&2
fi
exit $status
RUNNER
chmod +x "$appdir/usr/bin/codex-app"
cat > "$appdir/AppRun" <<'APPRUN'
#!/usr/bin/env bash
set -euo pipefail
APPDIR_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "$APPDIR_PATH/usr/bin/codex-app" "$@"
APPRUN
chmod +x "$appdir/AppRun"
cat > "$appdir/codex-app.desktop" <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=Codex
Comment=Codex Desktop (Linux Port)
Exec=codex-app
Icon=codex-app
Categories=Development;
Terminal=false
StartupNotify=true
StartupWMClass=Codex
DESKTOP
icon_src="$(find_linux_icon "$bundle_dir/resources" "$bundle_dir/resources/app" "$work_dir/codex-icon.png" || true)"
[[ -f "$icon_src" ]] || die "Could not find icon source in bundle."
cp "$icon_src" "$appdir/codex-app.png"
cp "$icon_src" "$appdir/.DirIcon"
runtime_bin="$work_dir/runtime"
squashfs_img="$work_dir/app.squashfs"
dd if="$runtime_src" of="$runtime_bin" bs=1 count="$offset" status=none
chmod +x "$runtime_bin"
log "Building squashfs payload..."
mksquashfs "$appdir" "$squashfs_img" -root-owned -noappend -comp xz >/dev/null
log "Assembling AppImage..."
mkdir -p "$(dirname "$out_appimage")"
tmp_out="${out_appimage}.tmp.$$"
trap 'rm -f "$tmp_out"' EXIT
cat "$runtime_bin" "$squashfs_img" > "$tmp_out"
chmod +x "$tmp_out"
mv -f "$tmp_out" "$out_appimage"
trap - EXIT
"$out_appimage" --appimage-offset >/dev/null 2>&1 || die "AppImage was created but failed self-check."
rm -rf "$work_dir"
log "AppImage created: $out_appimage"
}
install_desktop_integration() {
local appimage_abs icon_src applications_dir pixmaps_dir desktop_file
local icon_dirs=(16x16 24x24 32x32 48x48 64x64 128x128 256x256 512x512)
if [[ "$INSTALL_DESKTOP" != "1" ]]; then
log "Skipping desktop integration (CODEX_INSTALL_DESKTOP=$INSTALL_DESKTOP)"
return 0
fi
appimage_abs="$(readlink -f "$OUT_APPIMAGE" 2>/dev/null || printf '%s' "$OUT_APPIMAGE")"
icon_src="$BUNDLE_DIR/resources/codex-icon.png"
if [[ ! -f "$icon_src" ]]; then
log "Skipping desktop integration: icon not found at $icon_src"
return 0
fi
applications_dir="$HOME/.local/share/applications"
pixmaps_dir="$HOME/.local/share/pixmaps"
desktop_file="$applications_dir/${DESKTOP_ID}.desktop"
mkdir -p "$applications_dir" "$pixmaps_dir"
for dir in "${icon_dirs[@]}"; do
mkdir -p "$HOME/.local/share/icons/hicolor/$dir/apps"
cp -f "$icon_src" "$HOME/.local/share/icons/hicolor/$dir/apps/${DESKTOP_ID}.png"
done
cp -f "$icon_src" "$pixmaps_dir/${DESKTOP_ID}.png"
cat > "$desktop_file" <<DESKTOP
[Desktop Entry]
Type=Application
Name=Codex
Comment=Codex Desktop (Linux Port)
Exec=${appimage_abs}
TryExec=${appimage_abs}
Icon=${DESKTOP_ID}
Categories=Development;
Terminal=false
StartupNotify=true
StartupWMClass=Codex
DESKTOP
chmod +x "$appimage_abs"
update-desktop-database "$applications_dir" >/dev/null 2>&1 || true
gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" >/dev/null 2>&1 || true
log "Installed desktop entry: $desktop_file"
}
main() {
[[ -f "$DMG_PATH" ]] || die "DMG not found: $DMG_PATH"$'\n'"Usage: $0 [/path/to/Codex.dmg] [bundle-dir] [out-appimage]"
prepare_bundle "$DMG_PATH" "$BUNDLE_DIR"
rebuild_native_modules "$BUNDLE_DIR"
ensure_bundled_electron "$BUNDLE_DIR"
build_appimage "$BUNDLE_DIR" "$OUT_APPIMAGE"
install_desktop_integration
log "Done"
echo " Bundle: $BUNDLE_DIR"
echo " AppImage: $OUT_APPIMAGE"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment