diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 555521877015..600bc10c7bb4 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2280,6 +2280,7 @@ dependencies = [ "codex-utils-home-dir", "codex-windows-sandbox", "dotenvy", + "pathdiff", "pretty_assertions", "tempfile", "tokio", diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index bb45db45521a..d79719799757 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -28,6 +28,7 @@ tokio = { workspace = true, features = ["rt-multi-thread"] } [target.'cfg(windows)'.dependencies] codex-windows-sandbox = { workspace = true } +pathdiff = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index f44dc145b15d..90c580469055 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -403,7 +403,7 @@ fn prepare_path_entry_for_codex_aliases( #[cfg(windows)] { let batch_script = path.join(format!("{filename}.bat")); - let exe = exe.display(); + let exe = windows_batch_executable_path(&exe, path); std::fs::write( &batch_script, format!( @@ -447,6 +447,14 @@ fn prepare_path_entry_for_codex_aliases( )) } +#[cfg(windows)] +fn windows_batch_executable_path(executable: &Path, alias_directory: &Path) -> String { + pathdiff::diff_paths(executable, alias_directory) + .filter(|relative_path| relative_path.is_relative()) + .map(|relative_path| format!("%~dp0{}", relative_path.display())) + .unwrap_or_else(|| executable.display().to_string()) +} + fn path_env_with_package_path_dir( install_context: &InstallContext, existing_path: Option, @@ -540,6 +548,8 @@ mod tests { use pretty_assertions::assert_eq; use std::fs; use std::fs::File; + #[cfg(windows)] + use std::os::windows::process::CommandExt; use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; @@ -593,6 +603,50 @@ mod tests { }) } + #[cfg(windows)] + #[test] + fn windows_batch_alias_preserves_unicode_executable_paths() -> anyhow::Result<()> { + let root = TempDir::new()?; + let profile = root.path().join("用户"); + let alias_directory = profile.join(".codex").join("tmp").join("arg0"); + let executable_directory = profile.join("bin"); + fs::create_dir_all(&alias_directory)?; + fs::create_dir_all(&executable_directory)?; + + let system_root = std::env::var_os("SystemRoot") + .ok_or_else(|| anyhow::anyhow!("missing Windows system root"))?; + let command_shell = PathBuf::from(system_root).join("System32").join("cmd.exe"); + let executable = executable_directory.join("cmd.exe"); + fs::copy(&command_shell, &executable)?; + + let batch_path = alias_directory.join("apply_patch.bat"); + let executable_path = super::windows_batch_executable_path(&executable, &alias_directory); + fs::write( + &batch_path, + format!("@echo off\r\n\"{executable_path}\" /d /c exit 37\r\n"), + )?; + + let output = std::process::Command::new(command_shell) + .args(["/d", "/c"]) + .raw_arg(format!("chcp 437>nul & call \"{}\"", batch_path.display())) + .output()?; + + assert_eq!(output.status.code(), Some(37)); + Ok(()) + } + + #[cfg(windows)] + #[test] + fn windows_batch_alias_preserves_cross_volume_executable_paths() { + assert_eq!( + super::windows_batch_executable_path( + Path::new(r"D:\Tools\codex.exe"), + Path::new(r"C:\Users\person\.codex\tmp\arg0"), + ), + r"D:\Tools\codex.exe", + ); + } + #[test] fn linux_sandbox_exe_path_prefers_codex_linux_sandbox_alias() -> std::io::Result<()> { let temp_dir = TempDir::new()?;