From 116317021de867f12ea61bd02c118c91d3c3de42 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 19 Apr 2026 10:30:13 -0700 Subject: [PATCH] Support `codex app` on macOS (Intel) and Windows (#18500) ## Summary `codex app` should be a platform-aware entry point for opening Codex Desktop or helping users install it. Before this change, the command only existed on macOS and its default installer URL always pointed at the Apple Silicon DMG, which sent Intel Mac users to the wrong build. This updates the macOS path to choose the Apple Silicon or Intel DMG based on the detected processor, while keeping `--download-url` as an advanced override. It also enables `codex app` on Windows, where the CLI opens an installed Codex Desktop app when available and otherwise opens the Windows installer URL. --------- Co-authored-by: Felipe Coury --- codex-rs/cli/src/app_cmd.rs | 18 ++-- codex-rs/cli/src/desktop_app/mac.rs | 37 ++++++- codex-rs/cli/src/desktop_app/mod.rs | 15 ++- codex-rs/cli/src/desktop_app/windows.rs | 132 ++++++++++++++++++++++++ codex-rs/cli/src/main.rs | 10 +- 5 files changed, 197 insertions(+), 15 deletions(-) create mode 100644 codex-rs/cli/src/desktop_app/windows.rs diff --git a/codex-rs/cli/src/app_cmd.rs b/codex-rs/cli/src/app_cmd.rs index cb761c131..c28182b4c 100644 --- a/codex-rs/cli/src/app_cmd.rs +++ b/codex-rs/cli/src/app_cmd.rs @@ -1,21 +1,25 @@ use clap::Parser; use std::path::PathBuf; -const DEFAULT_CODEX_DMG_URL: &str = "https://persistent.oaistatic.com/codex-app-prod/Codex.dmg"; - #[derive(Debug, Parser)] pub struct AppCommand { /// Workspace path to open in Codex Desktop. #[arg(value_name = "PATH", default_value = ".")] pub path: PathBuf, - /// Override the macOS DMG download URL (advanced). - #[arg(long, default_value = DEFAULT_CODEX_DMG_URL)] - pub download_url: String, + /// Override the app installer download URL (advanced). + #[arg(long = "download-url")] + pub download_url_override: Option, } -#[cfg(target_os = "macos")] pub async fn run_app(cmd: AppCommand) -> anyhow::Result<()> { let workspace = std::fs::canonicalize(&cmd.path).unwrap_or(cmd.path); - crate::desktop_app::run_app_open_or_install(workspace, cmd.download_url).await + #[cfg(target_os = "macos")] + { + crate::desktop_app::run_app_open_or_install(workspace, cmd.download_url_override).await + } + #[cfg(target_os = "windows")] + { + crate::desktop_app::run_app_open_or_install(workspace, cmd.download_url_override).await + } } diff --git a/codex-rs/cli/src/desktop_app/mac.rs b/codex-rs/cli/src/desktop_app/mac.rs index d404f5b7c..10a47806b 100644 --- a/codex-rs/cli/src/desktop_app/mac.rs +++ b/codex-rs/cli/src/desktop_app/mac.rs @@ -1,12 +1,17 @@ use anyhow::Context as _; +use std::ffi::CString; use std::path::Path; use std::path::PathBuf; use tempfile::Builder; use tokio::process::Command; +const CODEX_DMG_URL_ARM64: &str = "https://persistent.oaistatic.com/codex-app-prod/Codex.dmg"; +const CODEX_DMG_URL_X64: &str = + "https://persistent.oaistatic.com/codex-app-prod/Codex-latest-x64.dmg"; + pub async fn run_mac_app_open_or_install( workspace: PathBuf, - download_url: String, + download_url_override: Option, ) -> anyhow::Result<()> { if let Some(app_path) = find_existing_codex_app_path() { eprintln!( @@ -17,6 +22,14 @@ pub async fn run_mac_app_open_or_install( return Ok(()); } eprintln!("Codex Desktop not found; downloading installer..."); + let download_url = download_url_override.unwrap_or_else(|| { + let default_url = if is_apple_silicon_mac() { + CODEX_DMG_URL_ARM64 + } else { + CODEX_DMG_URL_X64 + }; + default_url.to_string() + }); let installed_app = download_and_install_codex_to_user_applications(&download_url) .await .context("failed to download/install Codex Desktop")?; @@ -28,6 +41,28 @@ pub async fn run_mac_app_open_or_install( Ok(()) } +fn is_apple_silicon_mac() -> bool { + fn macos_sysctl_flag(name: &str) -> Option { + let name = CString::new(name).ok()?; + let mut value: libc::c_int = 0; + let mut size = std::mem::size_of_val(&value); + let result = unsafe { + libc::sysctlbyname( + name.as_ptr(), + (&mut value as *mut libc::c_int).cast::(), + &mut size, + std::ptr::null_mut(), + 0, + ) + }; + (result == 0).then_some(value != 0) + } + + std::env::consts::ARCH == "aarch64" + || macos_sysctl_flag("sysctl.proc_translated").unwrap_or(false) + || macos_sysctl_flag("hw.optional.arm64").unwrap_or(false) +} + fn find_existing_codex_app_path() -> Option { candidate_codex_app_paths() .into_iter() diff --git a/codex-rs/cli/src/desktop_app/mod.rs b/codex-rs/cli/src/desktop_app/mod.rs index 7c42315a8..5a78341c0 100644 --- a/codex-rs/cli/src/desktop_app/mod.rs +++ b/codex-rs/cli/src/desktop_app/mod.rs @@ -1,11 +1,22 @@ #[cfg(target_os = "macos")] mod mac; +#[cfg(target_os = "windows")] +mod windows; /// Run the app install/open logic for the current OS. #[cfg(target_os = "macos")] pub async fn run_app_open_or_install( workspace: std::path::PathBuf, - download_url: String, + download_url_override: Option, ) -> anyhow::Result<()> { - mac::run_mac_app_open_or_install(workspace, download_url).await + mac::run_mac_app_open_or_install(workspace, download_url_override).await +} + +/// Run the app install/open logic for the current OS. +#[cfg(target_os = "windows")] +pub async fn run_app_open_or_install( + workspace: std::path::PathBuf, + download_url_override: Option, +) -> anyhow::Result<()> { + windows::run_windows_app_open_or_install(workspace, download_url_override).await } diff --git a/codex-rs/cli/src/desktop_app/windows.rs b/codex-rs/cli/src/desktop_app/windows.rs new file mode 100644 index 000000000..932ca00cf --- /dev/null +++ b/codex-rs/cli/src/desktop_app/windows.rs @@ -0,0 +1,132 @@ +use anyhow::Context as _; +use std::path::Path; +use std::path::PathBuf; +use tokio::process::Command; + +const CODEX_WINDOWS_INSTALLER_URL: &str = + "https://get.microsoft.com/installer/download/9PLM9XGG6VKS?cid=website_cta_psi"; +const CODEX_MICROSOFT_STORE_WEB_URL: &str = "https://apps.microsoft.com/detail/9plm9xgg6vks"; + +pub async fn run_windows_app_open_or_install( + workspace: PathBuf, + download_url_override: Option, +) -> anyhow::Result<()> { + if let Some(app_id) = find_codex_app_id().await? { + eprintln!("Opening Codex Desktop..."); + open_installed_codex_app(&app_id).await?; + eprintln!( + "In Codex Desktop, open workspace {workspace}.", + workspace = display_workspace_path(&workspace) + ); + return Ok(()); + } + + eprintln!("Codex Desktop not found; opening Windows installer..."); + let download_url = download_url_override + .as_deref() + .unwrap_or(CODEX_WINDOWS_INSTALLER_URL); + if open_url(download_url).await.is_err() && download_url_override.is_none() { + open_url(CODEX_MICROSOFT_STORE_WEB_URL).await?; + } + eprintln!( + "After installing Codex Desktop, open workspace {workspace}.", + workspace = display_workspace_path(&workspace) + ); + Ok(()) +} + +async fn find_codex_app_id() -> anyhow::Result> { + let output = Command::new("powershell.exe") + .arg("-NoProfile") + .arg("-Command") + .arg("Get-StartApps -Name 'Codex' | Select-Object -First 1 -ExpandProperty AppID") + .output() + .await + .context("failed to invoke `powershell.exe`")?; + + if !output.status.success() { + return Ok(None); + } + + let app_id = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if app_id.is_empty() { + Ok(None) + } else { + Ok(Some(app_id)) + } +} + +async fn open_installed_codex_app(app_id: &str) -> anyhow::Result<()> { + let target = format!("shell:AppsFolder\\{app_id}"); + open_shell_target(&target).await +} + +async fn open_url(url: &str) -> anyhow::Result<()> { + let status = Command::new("powershell.exe") + .arg("-NoProfile") + .arg("-Command") + .arg("& { param($target) Start-Process -FilePath $target }") + .arg(url) + .status() + .await + .with_context(|| format!("failed to open {url}"))?; + + if status.success() { + Ok(()) + } else { + anyhow::bail!("failed to open {url} with {status}"); + } +} + +async fn open_shell_target(target: &str) -> anyhow::Result<()> { + // Explorer can successfully hand off shell targets and still return exit code 1. + let _status = Command::new("explorer.exe") + .arg(target) + .status() + .await + .with_context(|| format!("failed to open {target}"))?; + + Ok(()) +} + +fn display_workspace_path(workspace: &Path) -> String { + let path = workspace.display().to_string(); + if let Some(path) = path.strip_prefix(r"\\?\UNC\") { + format!(r"\\{path}") + } else if let Some(path) = path.strip_prefix(r"\\?\") { + path.to_string() + } else { + path + } +} + +#[cfg(test)] +mod tests { + use super::display_workspace_path; + use pretty_assertions::assert_eq; + use std::path::Path; + + #[test] + fn display_workspace_path_removes_windows_extended_prefix() { + assert_eq!( + display_workspace_path(Path::new(r"\\?\C:\Users\fcoury\code\codex")), + r"C:\Users\fcoury\code\codex" + ); + } + + #[test] + fn display_workspace_path_preserves_unc_prefix() { + assert_eq!( + display_workspace_path(Path::new(r"\\?\UNC\server\share\codex")), + r"\\server\share\codex" + ); + } + + #[test] + fn display_workspace_path_leaves_regular_paths_unchanged() { + assert_eq!( + display_workspace_path(Path::new(r"C:\Users\fcoury\code\codex")), + r"C:\Users\fcoury\code\codex" + ); + } +} diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 7ed8262fd..ddee32929 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -34,9 +34,9 @@ use std::io::IsTerminal; use std::path::PathBuf; use supports_color::Stream; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "windows"))] mod app_cmd; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "windows"))] mod desktop_app; mod marketplace_cmd; mod mcp_cmd; @@ -120,8 +120,8 @@ enum Subcommand { /// [experimental] Run the app server or related tooling. AppServer(AppServerCommand), - /// Launch the Codex desktop app (downloads the macOS installer if missing). - #[cfg(target_os = "macos")] + /// Launch the Codex desktop app (opens the app installer if missing). + #[cfg(any(target_os = "macos", target_os = "windows"))] App(app_cmd::AppCommand), /// Generate shell completion scripts. @@ -808,7 +808,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { } } } - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "windows"))] Some(Subcommand::App(app_cli)) => { reject_remote_mode_for_subcommand( root_remote.as_deref(),