|
#!/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 "$@" |