diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 929fd9e7..d69eb5a1 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -26,8 +26,10 @@ use std::path::PathBuf; use supports_color::Stream; mod mcp_cmd; +mod wsl_paths; use crate::mcp_cmd::McpCli; +use crate::wsl_paths::normalize_for_wsl; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::features::is_known_feature_key; @@ -270,7 +272,11 @@ fn run_update_action(action: UpdateAction) -> anyhow::Result<()> { let (cmd, args) = action.command_args(); let cmd_str = action.command_str(); println!("Updating Codex via `{cmd_str}`..."); - let status = std::process::Command::new(cmd).args(args).status()?; + let command_path = normalize_for_wsl(cmd); + let normalized_args: Vec = args.iter().map(normalize_for_wsl).collect(); + let status = std::process::Command::new(&command_path) + .args(&normalized_args) + .status()?; if !status.success() { anyhow::bail!("`{cmd_str}` failed with status {status}"); } diff --git a/codex-rs/cli/src/wsl_paths.rs b/codex-rs/cli/src/wsl_paths.rs new file mode 100644 index 00000000..56ce8668 --- /dev/null +++ b/codex-rs/cli/src/wsl_paths.rs @@ -0,0 +1,76 @@ +use std::ffi::OsStr; + +/// WSL-specific path helpers used by the updater logic. +/// +/// See https://github.com/openai/codex/issues/6086. +pub fn is_wsl() -> bool { + #[cfg(target_os = "linux")] + { + if std::env::var_os("WSL_DISTRO_NAME").is_some() { + return true; + } + match std::fs::read_to_string("/proc/version") { + Ok(version) => version.to_lowercase().contains("microsoft"), + Err(_) => false, + } + } + #[cfg(not(target_os = "linux"))] + { + false + } +} + +/// Convert a Windows absolute path (`C:\foo\bar` or `C:/foo/bar`) to a WSL mount path (`/mnt/c/foo/bar`). +/// Returns `None` if the input does not look like a Windows drive path. +pub fn win_path_to_wsl(path: &str) -> Option { + let bytes = path.as_bytes(); + if bytes.len() < 3 + || bytes[1] != b':' + || !(bytes[2] == b'\\' || bytes[2] == b'/') + || !bytes[0].is_ascii_alphabetic() + { + return None; + } + let drive = (bytes[0] as char).to_ascii_lowercase(); + let tail = path[3..].replace('\\', "/"); + if tail.is_empty() { + return Some(format!("/mnt/{drive}")); + } + Some(format!("/mnt/{drive}/{tail}")) +} + +/// If under WSL and given a Windows-style path, return the equivalent `/mnt//…` path. +/// Otherwise returns the input unchanged. +pub fn normalize_for_wsl>(path: P) -> String { + let value = path.as_ref().to_string_lossy().to_string(); + if !is_wsl() { + return value; + } + if let Some(mapped) = win_path_to_wsl(&value) { + return mapped; + } + value +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn win_to_wsl_basic() { + assert_eq!( + win_path_to_wsl(r"C:\Temp\codex.zip").as_deref(), + Some("/mnt/c/Temp/codex.zip") + ); + assert_eq!( + win_path_to_wsl("D:/Work/codex.tgz").as_deref(), + Some("/mnt/d/Work/codex.tgz") + ); + assert!(win_path_to_wsl("/home/user/codex").is_none()); + } + + #[test] + fn normalize_is_noop_on_unix_paths() { + assert_eq!(normalize_for_wsl("/home/u/x"), "/home/u/x"); + } +}