Skip to content

Add native Windows source-build support - #313

Open
hoangvu12 wants to merge 18 commits into
zeronsh:mainfrom
hoangvu12:windows-native-support
Open

Add native Windows source-build support#313
hoangvu12 wants to merge 18 commits into
zeronsh:mainfrom
hoangvu12:windows-native-support

Conversation

@hoangvu12

@hoangvu12 hoangvu12 commented Sep 10, 2026

Copy link
Copy Markdown

Adds native Windows source builds: application storage and locking, agent discovery/launching, terminal cleanup, and desktop fixes. Includes Windows tests and shared Linux/macOS regression checks.

Source builds only; installers, managed updates, and background services remain follow-ups. The renderer changes come from Zui #7.

Local validation: 321 engine/harness tests passed, plus a Unix-path cross-compilation check. CI, a fresh release build, and GUI validation remain pending for this revision.

See the Windows guide for setup and limitations.

Embed the Zeron icon as resource 1, which GPUI loads from the executable, for Windows debug and release builds.
Separate iOS, edge, and shared Rust test triggers, restrict deployment runs to relevant paths, and build the Windows GUI fixture only for explicit native GUI runs.
Contain children at creation, preserve launch and stdio behavior, and share startup-attribute handling. Keep idle terminal batching demand-driven and cover cleanup, discovery, and launch regressions.
Remove unrelated workflow reorganization, retain shared Rust coverage, and shorten the Windows setup guide.
@hoangvu12
hoangvu12 marked this pull request as ready for review September 11, 2026 01:27

@wingleeio wingleeio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed a48e1fb. Requesting changes for the three issues below: a failed Windows update can remove the launchable installation, Claude availability no longer honors its environment override, and Windows npm Codex discovery is not used by account login.

Validation on Linux: 174 engine unit tests, 130 harness unit tests, 8 auth integration tests, and 5 updater unit tests passed (317 total). A separate isolated regression probe reproduced the Claude override failure. I inspected both pinned UI dependency changes and the new updater dependency. I could not run the Windows build, ConPTY/Job Object tests, GUI probes, or a real Windows update here; this head currently has no successful Windows CI result.

WorkOS on Windows: I found no Windows-specific blocker in the implemented authentication path. The desktop uses GPUI's Windows ShellExecuteW browser opener, then receives the OAuth callback on http://127.0.0.1:<port>/callback; exchange, refresh, and organization requests go through the existing edge API. It does not depend on registering a custom URL scheme. Session persistence uses the new application data directory, shared by GUI and CLI. WorkOS documents loopback redirects for native clients (redirect requirements); the configured client must allow the callback URI/port pattern.

That is a source-level compatibility assessment plus passing stub-server auth tests on Linux, not confirmation of an authenticated Windows run. Please include cargo test --release --locked -p zeron-engine --test auth in Windows CI (the current workflow skips that integration target), and verify browser sign-in, app restart/session restore, refresh, org selection, and logout on Windows before claiming authenticated support. Windows session-file privacy currently relies on inherited directory ACLs; the Unix-only 0600 enforcement does not apply there.

The PR description also needs updating: the current diff adds portable packaging, publication, and in-app updates, although the description still lists them as follow-ups.

Comment thread crates/update/src/windows.rs Outdated
Comment thread crates/harness/src/claude/mod.rs
Comment thread crates/engine/src/agent_accounts.rs Outdated
A failed Windows update could remove the launchable installation:
self-replace renames the running executable away and schedules its
deletion before copying the replacement back. Replace it with a
hand-rolled swap that copies and re-verifies first, moves the running
image aside only then, and restores the backup on every post-move
failure; the relaunched instance deletes the renamed backup once the
old image exits, and the next update attempt heals a crash between
the two renames. The new failure-injection test holds a real sharing
violation on the incoming copy after the old image has moved.

Claude availability ignored CLAUDE_CODE_EXECUTABLE: installed() used
bare discovery while the launch resolver honored the override, so an
override-only install showed as unavailable (also on Linux/macOS).
installed() now uses the validated launch resolver, with a
cross-platform override-only availability test.

Codex account login spawned a bare `codex` command that neither
followed the npm payload layout nor honored CODEX_EXECUTABLE. The
engine now launches `codex login` through the harness's own
resolution and child-PATH composition via codex::login_command.

Windows CI additionally runs the zeron-engine auth integration tests.
Both the resolver and the spawner were hard-coded to .exe, so
npm-installed agents were undiscoverable and unlaunchable: npm leaves
only a `name.cmd` shim on PATH with the real binary buried under
node_modules. Discovery is now PATHEXT-aware — per directory,
extensions in PATHEXT order, limited to what we can launch (.com,
.exe, .bat, .cmd) — and overrides accept batch shims that exist on
disk, so availability and launches agree.

Resolved .cmd/.bat scripts spawn through `cmd.exe /d /s /c` with
cross-spawn-style per-argument escaping: the shim is the only
interpreted layer, agent arguments stay literal (spaces, backslashes,
quotes, unicode, cmd metacharacters round-trip verbatim — proven by
re-invoking a real program through a shim fixture), and Job Object
ownership covers the whole tree through cmd.exe's membership. The
codex node_modules vendor special-case is deleted: it searched the
wrong vendor subdirectory (`bin/` instead of `codex/`), and its
fixture encoded that wrong layout, so CI passed while real npm
installs failed. GUI launches additionally backfill %APPDATA%\npm,
%LOCALAPPDATA%\{pnpm,Programs\nodejs}, scoop shims, and
%USERPROFILE%\{.local\bin,.bun\bin} from PATH.

opencode and cursor resolution migrate off raw $HOME to the shared
USERPROFILE-aware home helper; cursor also probes
%LOCALAPPDATA%\cursor-agent. The engine catalog and login tests use
the real npm shim layout, batch-override harness tests assert safe
launch instead of rejection, and the shell-script fixture suites are
gated to Unix where they actually run.
@hoangvu12
hoangvu12 requested a review from wingleeio September 11, 2026 12:50

@wingleeio wingleeio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 73b245c. The three earlier requests are addressed on this head (the swap now copies and verifies before moving the running image aside, Claude installed() uses the launch resolver, and Codex login goes through codex::login_command). Requesting changes again for one behavior regression and three new tests that cannot pass on Linux.

Validation on Linux (this head): cargo check --locked --workspace --all-targets clean; engine 174 unit tests, auth 8, updater 10, app 5, claude 9, codex 20, cursor 20, acp_quiet 1, acp_stall 6 all passing. Failing: harness unit tests 129/131, tests/acp.rs 26/27, codex_login_resolution 3/4, claude_availability 2/3 — details inline. The terminals integration test (m5_repos_diffs_terminals) passed 21/22 with the one failure being a pre-existing SHA-prefix fuzzy-match flake that also reproduces independently of this branch. I rebuilt against the pinned Zui/gpui-base revisions; both match their upstream main heads.

CI: I approved the fork workflow runs on this head. Preview networking and UI tests (all five jobs) are green. Windows tests: green on this head (build, shader layout, app/updater, engine+harness unit, native harness integration, catalog, auth, UI, and the no-HOME CLI startup probe; the native GUI job is dispatch-only and was not run). CI, a fresh release build, and the GUI probes are no longer "pending" for this revision..

Nits (no change required to land):

  • release.yml: publish now requires the Windows job, so a Windows-only build failure (fxc discovery, runner image drift) blocks Linux and macOS releases. That is a reasonable "every platform" policy, just calling it out as a deliberate change from the previous comment's intent.
  • crates/update/src/lib.rs release_base: ZERON_RELEASES_URL is honored on every platform and is not HTTPS-checked, unlike the Windows zeron-update.json feed. Consider the same https:// guard.
  • process/windows/command.rs quote_batch_argument: agent arguments passed to a .cmd shim are quoted but not %-escaped, so a %NAME% inside an argv element is still expanded by cmd.exe. Today every harness argv is program-controlled (flags, model ids, settings JSON), so this is not reachable from prompts; worth a comment or ^% escaping so it stays that way.
  • terminals/windows.rs Process::wait_for: terminating the job when the shell exits also kills anything the user detached from that shell (start notepad, background servers), which differs from the Unix terminals where orphans survive. If intentional, a doc note in windows-development.md would help.

let spec_args: Vec<String> = self.spec.args.iter().map(|a| a.to_string()).collect();
if let Some(p) = &self.executable {
return Ok(Launch::Program(p.clone(), spec_args));
return crate::executable::validate_native_override(p)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Model discovery now errors instead of falling back for a missing override

models() starts with self.resolve_launch()?. On main, an executable/env override that does not exist passed through here and failed at spawn, so discover_models() returned Err and models() served (self.spec.models)(). With validate_native_override the ? fires first, so a stale GROK_EXECUTABLE/PI_ACP_EXECUTABLE/etc. now fails model discovery outright. The pre-existing regression test for this path, models_fall_back_to_the_static_catalog_when_the_probe_fails in tests/acp.rs, fails on this head with NotInstalled("/nonexistent/never-a-pi-acp does not exist").

Meanwhile AcpHarness::installed() (line ~1125) still returns true for any Some(executable) or non-empty env override, so the registry can advertise an ACP agent as installed whose models() and run() both fail. That is the exact inconsistency this PR fixed for Claude and Codex. Please pick one: keep the static-catalog fallback (return Ok(static) when resolve_launch() fails here), or make ACP consistent with the other harnesses by gating installed() on resolve_launch().is_ok() and updating the test to expect the error. Either way the existing test needs to reflect the chosen behavior.

assert_eq!(
find_on_paths_with(
"tool",
vec![bin.join("tool.CmD")],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Two new unit tests assume a case-insensitive filesystem

windows_extra_candidates_resolve_through_pathext_variants writes tool.cmd and expects the tool.CmD extra to resolve, and windows_pathext_environment_reorders_extensions (line 429) writes agent.cmd and expects the .CMD PATHEXT variant to resolve. Both go through is_file() on the real filesystem, so they pass on NTFS and default APFS but fail on Linux and case-sensitive macOS volumes:

windows_extra_candidates_resolve_through_pathext_variants: left: None, right: Some(".../bin/tool.CmD")
windows_pathext_environment_reorders_extensions: shim variant resolves

This breaks cargo test -p zeron-harness --lib on Linux, which is the suite the PR description cites as the shared regression check. Please either match the on-disk case in these two tests (keep the case-insensitivity claim to the candidate_names/extra_variants output assertions, which need no filesystem) or gate the case-dependent assertions with #[cfg(windows)]. Both were added in 6f5a40a.

let bin = dir.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
// The shim plays both roles itself.
std::fs::copy(fake_codex(&bin), bin.join("codex.cmd")).unwrap();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] codex_login_runs_the_npm_cmd_shim cannot pass on Unix

This binary is not cfg(windows)-gated, but .cmd discovery only exists through the Windows PATHEXT variants: on Unix find_on_paths("codex", ..) looks for a file literally named codex, so bin/codex.cmd is never a candidate. On a runner without codex the child fails with NotInstalled while the parent expects true. On a dev machine with /usr/local/bin/codex it is worse: the absolute fallback resolves the real CLI, start_login launches a real codex login (against the throwaway CODEX_HOME, so no credentials are touched), and the assertion fails on the URL instead:

left: "", right: "https://auth.openai.com/authorize?probe=fake"

Please gate this test (or the whole file) with #[cfg(windows)], and add the same /opt/homebrew/bin/codex / /usr/local/bin/codex guard the without_any_cli test already has to the override-only test so it never reaches a real CLI.

std::fs::write(&exe, if cfg!(windows) { b"MZ" } else { b"sh" }).unwrap();
// PATH without any claude: only the override makes Claude installed.
probe(dir.path(), Some(&exe), true);
probe(dir.path(), None, false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Second probe has no guard for a system-wide claude

The expected = false probe here removes HOME and empties PATH, but resolve_claude_executable still checks the absolute /opt/homebrew/bin/claude and /usr/local/bin/claude fallbacks, so on any machine with claude installed there the child reports installed = true and this test fails. The sibling claude_availability_reports_not_installed_without_any_cli already skips in that case; please apply the same guard (or drop this duplicate probe, since the sibling covers it).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants