Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,9 @@ CODEX_MULTI_LAUNCH=1 CODEX_MULTI_LAUNCH_PORT_RANGE=5175-5199 ./codex-app/start.s

By default, the native package installs a companion `systemd --user` service named `codex-update-manager`.

- It checks upstream `Codex.dmg` on daemon startup, every 6 hours, and in the background on app launch when stale.
- When a new DMG is available, it rebuilds a local native package with `/opt/codex-desktop/update-builder`.
- It checks the upstream Codex Sparkle appcast on daemon startup, every 6 hours, and in the background whenever the app launches.
- When a newer upstream appcast item is available, it downloads that archive and rebuilds a local native package with `/opt/codex-desktop/update-builder`.
- The app's existing update UI is enabled on Linux. Choosing the update action asks which optional Linux features to include before the ready package is installed.
- If Codex Desktop is open, the final install waits until Electron exits.
- The updater runs unprivileged and uses `pkexec` only for the final package install.
- Codex CLI checks are best-effort and launcher-scoped. Set `CODEX_SYNC_CLI_PREFLIGHT=1` when debugging launch-time CLI preflight.
Expand All @@ -291,6 +292,7 @@ Inspect the live service and runtime files with:
```bash
systemctl --user status codex-update-manager.service
codex-update-manager status --json
codex-update-manager features --json
sed -n '1,160p' ~/.local/state/codex-update-manager/state.json
sed -n '1,160p' ~/.local/state/codex-update-manager/service.log
```
Expand All @@ -307,13 +309,18 @@ Runtime files live in standard XDG locations:

```text
~/.config/codex-update-manager/config.toml
~/.config/codex-update-manager/features.json
~/.local/state/codex-update-manager/state.json
~/.local/state/codex-update-manager/service.log
~/.cache/codex-update-manager/
~/.cache/codex-desktop/launcher.log
~/.local/state/codex-desktop/app.pid
```

`features.json` stores the opt-in Linux feature selection used for future update
rebuilds. If it is missing, the updater keeps the feature list that was bundled
with the installed package.

### Manual-update packages

For installs that must not include a resident updater, build the native package with:
Expand Down
2 changes: 2 additions & 0 deletions linux-features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Descriptor patches use the same shape as `scripts/patches/core/**/patch.js`.
They can target `main-bundle`, `webview-asset`, or `extracted-app` phases.
Feature descriptor ids are namespaced as `feature:<feature-id>:<descriptor-id>`
in patch reports and are optional by default.
Set `"hidden": true` in `feature.json` for developer-only fixtures that should
not appear in native setup or update feature-selection prompts.

Feature self-tests live inside each feature directory. Run them with:

Expand Down
1 change: 1 addition & 0 deletions linux-features/example-feature/feature.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"title": "Example Linux Feature",
"description": "Disabled example showing the linux-features ASAR patch and stage hook contract.",
"defaultEnabled": false,
"hidden": true,
"entrypoints": {
"mainBundlePatch": "./patch.js",
"stageHook": "./stage.sh"
Expand Down
2 changes: 2 additions & 0 deletions scripts/bootstrap-wizard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ def discover_features(root):
if not isinstance(feature_id, str) or not id_re.match(feature_id):
warn(f"Skipping feature with invalid id in {manifest_path}")
continue
if data.get("hidden") is True:
continue
if feature_id in features:
warn(f"Skipping duplicate Linux feature id: {feature_id}")
continue
Expand Down
69 changes: 63 additions & 6 deletions scripts/lib/linux-update-bridge-patch.js

Large diffs are not rendered by default.

42 changes: 35 additions & 7 deletions scripts/patch-linux-window-ui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1485,13 +1485,18 @@ test("adds Linux package updater behind the existing app updater manager", () =>
assert.match(patched, /codexLinuxRunUpdateManager\(\[`--help`\]\)/);
assert.match(patched, /async function codexLinuxRefreshUpdateState\(\)/);
assert.match(patched, /async function codexLinuxRefreshUpdateState\(\)\{return codexLinuxReadUpdateState\(\)\}/);
assert.match(patched, /async function codexLinuxCheckForUpdatesOnOpen\(\)/);
assert.match(patched, /codexLinuxRunUpdateManager\(\[`check-now`\]\)/);
assert.match(patched, /async function codexLinuxPromptUpdateFeatures\(\)/);
assert.match(patched, /codexLinuxRunUpdateManager\(\[`prompt-features`,`--json`\]\)/);
assert.doesNotMatch(patched, /codexLinuxRunUpdateManager\(\[`status`,`--json`\]\)/);
assert.match(patched, /await codexLinuxProbeUpdateManager\(\),e\(\)/);
assert.match(patched, /await codexLinuxProbeUpdateManager\(\),e\(\),codexLinuxCheckForUpdatesOnOpen\(\)/);
assert.match(patched, /if\(!this\.options\.enableUpdater&&process\.platform!==`linux`\)/);
assert.match(patched, /process\.platform===`linux`\?await this\.initializeLinuxPackageUpdater\(\)/);
assert.match(patched, /async initializeLinuxPackageUpdater\(\)/);
assert.match(patched, /codexLinuxRunUpdateManager\(\[`check-now`\]\)/);
assert.match(patched, /codexLinuxRunUpdateManager\(\[`install-ready`\]\)/);
assert.match(patched, /if\(!await codexLinuxPromptUpdateFeatures\(\)\)/);
assert.match(patched, /this\.setInstallProgressPercent\(0\),this\.setUpdateLifecycleState\(`installing`\)/);
assert.match(patched, /this\.setInstallProgressPercent\(null\),codexLinuxQuitForUpdate\(\)/);
assert.doesNotMatch(patched, /this\.options\.onInstallUpdatesRequested\?\.\(\)/);
Expand All @@ -1504,7 +1509,7 @@ test("does not run bootstrap probe-state migration on class-style updater bundle
const patched = applyPatchTwice(applyLinuxAppUpdaterBridgePatch, source);

assert.match(patched, /function unrelated\(\)\{i\(\);let o=1;return o\}/);
assert.match(patched, /await codexLinuxProbeUpdateManager\(\),e\(\)/);
assert.match(patched, /await codexLinuxProbeUpdateManager\(\),e\(\),codexLinuxCheckForUpdatesOnOpen\(\)/);
assert.doesNotMatch(patched, /let s=!1,c=codexLinuxProbeUpdateManager/);
assert.doesNotMatch(patched, /getIsUpdateReady:\(\)=>s&&t/);
});
Expand All @@ -1521,10 +1526,11 @@ test("adds Linux package updater to current bootstrap updater wiring", () => {
assert.match(patched, /async function codexLinuxProbeUpdateManager\(\)/);
assert.match(patched, /codexLinuxRunUpdateManager\(\[`--help`\]\)/);
assert.match(patched, /async function codexLinuxRefreshUpdateState\(\)\{return codexLinuxReadUpdateState\(\)\}/);
assert.match(patched, /codexLinuxProbeUpdateManager\(\)\.then\(\(\)=>\{s=!0,i\(\),a\(\);return!0\}\)/);
assert.match(patched, /codexLinuxProbeUpdateManager\(\)\.then\(\(\)=>\{s=!0,i\(\),a\(\),codexLinuxCheckForUpdatesOnOpen\(\)/);
assert.match(patched, /getIsUpdateReady:\(\)=>s&&t/);
assert.match(patched, /checkForUpdates:async\(\)=>\{if\(!await c\)return;n=`checking`/);
assert.match(patched, /installUpdatesIfAvailable:async\(\)=>\{if\(!await c\)\{a\(\);return\}i\(\);if\(!t\)\{a\(\);return\}/);
assert.match(patched, /if\(!await codexLinuxPromptUpdateFeatures\(\)\)\{r=null,n=t\?`ready`:`idle`,a\(\);return\}/);
assert.match(patched, /refresh:async\(\)=>\{if\(await c\)\{try\{await codexLinuxRefreshUpdateState\(\)\}/);
assert.doesNotMatch(patched, /codexLinuxRunUpdateManager\(\[`status`,`--json`\]\)/);
});
Expand All @@ -1533,7 +1539,7 @@ test("migrates already-patched bootstrap updater bridge to probe before enabling
const patched = applyLinuxAppUpdaterBridgePatch(currentBootstrapUpdaterBundleFixture());
const oldPatched = patched
.replace(
"let s=!1,c=codexLinuxProbeUpdateManager().then(()=>{s=!0,i(),a();return!0}).catch(()=>{s=!1,t=!1,n=`idle`,a();return!1});let o=",
"let s=!1,c=codexLinuxProbeUpdateManager().then(()=>{s=!0,i(),a(),codexLinuxCheckForUpdatesOnOpen().then(()=>{i(),a()}).catch(()=>{});return!0}).catch(()=>{s=!1,t=!1,n=`idle`,a();return!1});let o=",
"i(),codexLinuxRefreshUpdateState().then(()=>{i(),a()}).catch(()=>{});let o=",
)
.replace(
Expand All @@ -1548,16 +1554,21 @@ test("migrates already-patched bootstrap updater bridge to probe before enabling
"installUpdatesIfAvailable:async()=>{if(!await c){a();return}i();if(!t){a();return}",
"installUpdatesIfAvailable:async()=>{i();if(!t)return;",
)
.replace(
"try{if(!await codexLinuxPromptUpdateFeatures()){r=null,n=t?`ready`:`idle`,a();return}let e=await codexLinuxRunUpdateManager([`install-ready`]),s=i();",
"try{let e=await codexLinuxRunUpdateManager([`install-ready`]),s=i();",
)
.replace(
"refresh:async()=>{if(await c){try{await codexLinuxRefreshUpdateState()}catch{}i()}else t=!1,n=`idle`;a()}",
"refresh:async()=>{try{await codexLinuxRefreshUpdateState()}catch{}i(),a()}",
);

const migrated = applyPatchTwice(applyLinuxAppUpdaterBridgePatch, oldPatched);

assert.match(migrated, /codexLinuxProbeUpdateManager\(\)\.then\(\(\)=>\{s=!0,i\(\),a\(\);return!0\}\)/);
assert.match(migrated, /codexLinuxProbeUpdateManager\(\)\.then\(\(\)=>\{s=!0,i\(\),a\(\),codexLinuxCheckForUpdatesOnOpen\(\)/);
assert.match(migrated, /getIsUpdateReady:\(\)=>s&&t/);
assert.match(migrated, /installUpdatesIfAvailable:async\(\)=>\{if\(!await c\)\{a\(\);return\}i\(\);if\(!t\)\{a\(\);return\}/);
assert.match(migrated, /if\(!await codexLinuxPromptUpdateFeatures\(\)\)\{r=null,n=t\?`ready`:`idle`,a\(\);return\}/);
});

test("migrates previous bootstrap updater bridge without leaving undefined probe state", () => {
Expand All @@ -1572,7 +1583,15 @@ test("migrates previous bootstrap updater bridge without leaving undefined probe
"",
)
.replace(
",s=!1,c=codexLinuxProbeUpdateManager().then(()=>{s=!0,i(),a();return!0}).catch(()=>{s=!1,t=!1,n=`idle`,a();return!1});let o=",
"async function codexLinuxCheckForUpdatesOnOpen(){try{await codexLinuxRunUpdateManager([`check-now`])}catch{}}",
"",
)
.replace(
"async function codexLinuxPromptUpdateFeatures(){try{let e=await codexLinuxRunUpdateManager([`prompt-features`,`--json`]),t=JSON.parse(e.stdout||`{}`);return t?.cancelled!==!0}catch{return!0}}",
"",
)
.replace(
",s=!1,c=codexLinuxProbeUpdateManager().then(()=>{s=!0,i(),a(),codexLinuxCheckForUpdatesOnOpen().then(()=>{i(),a()}).catch(()=>{});return!0}).catch(()=>{s=!1,t=!1,n=`idle`,a();return!1});let o=",
";i();let o=",
)
.replace(
Expand All @@ -1587,23 +1606,32 @@ test("migrates previous bootstrap updater bridge without leaving undefined probe
"installUpdatesIfAvailable:async()=>{if(!await c){a();return}i();if(!t){a();return}",
"installUpdatesIfAvailable:async()=>{i();if(!t)return;",
)
.replace(
"try{if(!await codexLinuxPromptUpdateFeatures()){r=null,n=t?`ready`:`idle`,a();return}let e=await codexLinuxRunUpdateManager([`install-ready`]),s=i();",
"try{let e=await codexLinuxRunUpdateManager([`install-ready`]),s=i();",
)
.replace(
"refresh:async()=>{if(await c){try{await codexLinuxRefreshUpdateState()}catch{}i()}else t=!1,n=`idle`;a()}",
"refresh:()=>{i(),a()}",
);

assert.doesNotMatch(oldPatched, /codexLinuxProbeUpdateManager/);
assert.doesNotMatch(oldPatched, /codexLinuxRefreshUpdateState/);
assert.doesNotMatch(oldPatched, /codexLinuxCheckForUpdatesOnOpen/);
assert.doesNotMatch(oldPatched, /codexLinuxPromptUpdateFeatures/);
assert.match(oldPatched, /i\(\);let o=/);

const migrated = applyPatchTwice(applyLinuxAppUpdaterBridgePatch, oldPatched);

assert.match(migrated, /async function codexLinuxProbeUpdateManager\(\)\{await codexLinuxRunUpdateManager\(\[`--help`\]\)\}/);
assert.match(migrated, /async function codexLinuxRefreshUpdateState\(\)\{return codexLinuxReadUpdateState\(\)\}/);
assert.match(migrated, /let s=!1,c=codexLinuxProbeUpdateManager\(\)\.then/);
assert.match(migrated, /async function codexLinuxCheckForUpdatesOnOpen\(\)\{try\{await codexLinuxRunUpdateManager\(\[`check-now`\]\)\}catch\{\}\}/);
assert.match(migrated, /async function codexLinuxPromptUpdateFeatures\(\)/);
assert.match(migrated, /let s=!1,c=codexLinuxProbeUpdateManager\(\)\.then\(\(\)=>\{s=!0,i\(\),a\(\),codexLinuxCheckForUpdatesOnOpen\(\)/);
assert.match(migrated, /getIsUpdateReady:\(\)=>s&&t/);
assert.match(migrated, /checkForUpdates:async\(\)=>\{if\(!await c\)return;n=`checking`/);
assert.match(migrated, /installUpdatesIfAvailable:async\(\)=>\{if\(!await c\)\{a\(\);return\}i\(\);if\(!t\)\{a\(\);return\}/);
assert.match(migrated, /if\(!await codexLinuxPromptUpdateFeatures\(\)\)\{r=null,n=t\?`ready`:`idle`,a\(\);return\}/);
assert.match(migrated, /refresh:async\(\)=>\{if\(await c\)\{try\{await codexLinuxRefreshUpdateState\(\)\}/);
});

Expand Down
1 change: 1 addition & 0 deletions updater/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ directories = "6.0.0"
futures-util = "0.3.32"
fs4 = "0.13.1"
notify-rust = "4.14.0"
quick-xml = "0.38.4"
reqwest = { version = "0.13.2", default-features = false, features = ["http2", "rustls", "stream"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
Expand Down
Loading
Loading