From 595534314f7636b52da3a066aded5be87bbd9888 Mon Sep 17 00:00:00 2001 From: jif Date: Thu, 1 Oct 2026 15:41:25 +0000 Subject: [PATCH] Use descriptor-safe helpers for executable test fixtures (#50018) ## Why Concurrent Linux test spawns can inherit writable descriptors for executable fixtures, preventing those fixtures from being executed even after the writing test closes its descriptor. ## What changed Replace direct executable fixture writes and copies across the Rust tests with `codex_utils_cargo_bin::write_executable` and `copy_executable`. These existing helpers complete writes and copies in separate processes on Linux. Remove the empty temporary inode before creating fake bubblewrap executables, and stage tampered bubblewrap bytes in a separate data file before copying them into the executable fixture. Add the required test dependencies. GitOrigin-RevId: c875c83088731a4933ef1574e8d79d8cecc12f54 --- codex-rs/Cargo.lock | 11 ++++++ codex-rs/app-server-daemon/Cargo.toml | 1 + .../src/backend/pid_tests.rs | 12 ++----- codex-rs/app-server-daemon/src/lib.rs | 7 +--- .../src/managed_install_path_tests.rs | 8 ++--- .../src/prepare_install_tests.rs | 31 ++++++---------- .../src/update_loop_tests.rs | 36 +++++++++---------- .../tests/common/test_app_server.rs | 2 +- .../tests/suite/v2/curated_mcp_sync.rs | 13 ++----- .../app-server/tests/suite/v2/hooks_list.rs | 13 ++----- .../tests/suite/v2/turn_start_zsh_fork.rs | 9 ++--- codex-rs/arg0/Cargo.toml | 1 + codex-rs/arg0/src/lib.rs | 2 +- codex-rs/cli/src/daemon_update_tests.rs | 4 +-- codex-rs/cli/src/desktop_app/mac.rs | 3 +- codex-rs/cli/tests/app_server_daemon.rs | 14 +++----- codex-rs/cli/tests/daemon_startup.rs | 3 +- codex-rs/cli/tests/doctor_path_safety.rs | 7 +--- .../tests/exec_server/pid_namespace_tests.rs | 4 +-- codex-rs/cli/tests/worktree.rs | 3 +- codex-rs/code-mode-host/tests/stdio.rs | 4 +-- codex-rs/core-plugins/Cargo.toml | 1 + codex-rs/core-plugins/src/npm_source_tests.rs | 11 ++---- .../core-plugins/src/startup_sync_tests.rs | 11 +----- codex-rs/core/src/git_info_tests.rs | 11 ++---- .../src/tools/handlers/unified_exec_tests.rs | 5 +-- codex-rs/core/src/tools/runtimes/mod_tests.rs | 2 +- .../zsh_fork/unix_escalation_tests.rs | 15 ++------ codex-rs/core/tests/suite/approvals.rs | 24 +++++-------- codex-rs/core/tests/suite/client.rs | 8 +---- codex-rs/core/tests/suite/external_auth.rs | 13 +++---- codex-rs/core/tests/suite/guardian_review.rs | 4 +-- codex-rs/core/tests/suite/plugins.rs | 5 ++- codex-rs/core/tests/suite/rmcp_client.rs | 3 +- codex-rs/core/tests/suite/shell_snapshot.rs | 13 ++----- codex-rs/core/tests/suite/skill_approval.rs | 7 +--- .../core/tests/suite/user_notification.rs | 4 +-- codex-rs/core/tests/suite/windows_sandbox.rs | 8 ++--- codex-rs/exec-server/tests/exec_process.rs | 10 ++---- .../exec-server/tests/file_system/shared.rs | 4 +-- .../exec-server/tests/file_system_windows.rs | 2 +- codex-rs/exec-server/tests/initialize.rs | 2 +- codex-rs/git-utils/Cargo.toml | 1 + codex-rs/git-utils/src/baseline.rs | 11 ++---- codex-rs/git-utils/src/info.rs | 16 ++------- codex-rs/linux-sandbox/src/bundled_bwrap.rs | 4 +-- codex-rs/linux-sandbox/src/launcher.rs | 5 +-- .../tests/suite/bundled_bwrap.rs | 6 +++- .../tests/suite/daemon_sockets_tests.rs | 8 +++-- .../tests/suite/managed_proxy.rs | 12 +++---- .../tests/suite/root_metadata_tests.rs | 3 +- codex-rs/login/Cargo.toml | 1 + codex-rs/login/src/auth/auth_tests.rs | 16 ++------- .../login/src/auth/default_client_tests.rs | 8 ++--- codex-rs/model-provider/Cargo.toml | 1 + codex-rs/model-provider/src/provider.rs | 2 +- codex-rs/realtime-webrtc/Cargo.toml | 1 + codex-rs/realtime-webrtc/tests/common/mod.rs | 7 ++-- .../tests/stdio_descriptor_boundary.rs | 7 ++-- codex-rs/sandboxing/Cargo.toml | 1 + codex-rs/sandboxing/src/bwrap_tests.rs | 15 +++----- codex-rs/tui/src/external_editor.rs | 11 +++--- codex-rs/tui/src/get_git_diff.rs | 14 +++----- .../tui/tests/suite/external_editor_tests.rs | 7 ++-- codex-rs/utils/pty/Cargo.toml | 1 + codex-rs/utils/pty/src/child_tests.rs | 9 ++--- codex-rs/utils/pty/src/linux_child_tests.rs | 8 ++--- codex-rs/utils/pty/src/macos_child_tests.rs | 10 ++---- .../utils/pty/src/macos_descriptor_tests.rs | 7 ++-- .../utils/pty/tests/conpty_search_path.rs | 4 +-- codex-rs/voice-host/tests/installed_client.rs | 4 +-- codex-rs/voice-host/tests/packaged_runtime.rs | 2 +- codex-rs/windows-sandbox-rs/Cargo.toml | 1 + .../src/setup_refresh_tests.rs | 6 ++-- codex-rs/worktree/Cargo.toml | 1 + codex-rs/worktree/src/git_tests.rs | 7 ++-- 76 files changed, 196 insertions(+), 372 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ae76b2d1a7..29d3afde00 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2219,6 +2219,7 @@ dependencies = [ "codex-http-client", "codex-install-context", "codex-uds", + "codex-utils-cargo-bin", "codex-utils-home-dir", "futures", "libc", @@ -2368,6 +2369,7 @@ dependencies = [ "codex-sandboxing", "codex-shell-escalation", "codex-utils-absolute-path", + "codex-utils-cargo-bin", "codex-utils-home-dir", "codex-utils-pty", "codex-windows-sandbox", @@ -3093,6 +3095,7 @@ dependencies = [ "codex-skills", "codex-tools", "codex-utils-absolute-path", + "codex-utils-cargo-bin", "codex-utils-path", "codex-utils-path-uri", "codex-utils-plugins", @@ -3475,6 +3478,7 @@ dependencies = [ "codex-file-system", "codex-protocol", "codex-utils-absolute-path", + "codex-utils-cargo-bin", "codex-utils-path-uri", "codex-utils-process", "codex-utils-pty", @@ -3814,6 +3818,7 @@ dependencies = [ "codex-protocol", "codex-secrets", "codex-terminal-detection", + "codex-utils-cargo-bin", "codex-utils-process", "codex-utils-template", "codex-workload-identity", @@ -4027,6 +4032,7 @@ dependencies = [ "codex-protocol", "codex-response-debug-context", "codex-secrets", + "codex-utils-cargo-bin", "codex-utils-process", "codex-utils-redacted-string", "http 1.4.0", @@ -4330,6 +4336,7 @@ dependencies = [ "anyhow", "codex-build-info", "codex-install-context", + "codex-utils-cargo-bin", "codex-utils-pty", "ctor 0.6.3", "futures", @@ -4476,6 +4483,7 @@ dependencies = [ "codex-protocol", "codex-uds", "codex-utils-absolute-path", + "codex-utils-cargo-bin", "codex-utils-home-dir", "codex-utils-path-uri", "codex-utils-pty", @@ -5106,6 +5114,7 @@ name = "codex-utils-pty" version = "0.0.0" dependencies = [ "anyhow", + "codex-utils-cargo-bin", "ctor 0.6.3", "filedescriptor", "lazy_static", @@ -5291,6 +5300,7 @@ dependencies = [ "codex-otel", "codex-protocol", "codex-utils-absolute-path", + "codex-utils-cargo-bin", "codex-utils-process", "codex-utils-pty", "codex-utils-string", @@ -5355,6 +5365,7 @@ dependencies = [ "anyhow", "codex-git-utils", "codex-protocol", + "codex-utils-cargo-bin", "dunce", "pretty_assertions", "serde", diff --git a/codex-rs/app-server-daemon/Cargo.toml b/codex-rs/app-server-daemon/Cargo.toml index 48e94a0782..591432bd28 100644 --- a/codex-rs/app-server-daemon/Cargo.toml +++ b/codex-rs/app-server-daemon/Cargo.toml @@ -40,6 +40,7 @@ tokio-tungstenite = { workspace = true } tracing = { workspace = true } [dev-dependencies] +codex-utils-cargo-bin = { workspace = true } pretty_assertions = { workspace = true } [target.'cfg(windows)'.dependencies] diff --git a/codex-rs/app-server-daemon/src/backend/pid_tests.rs b/codex-rs/app-server-daemon/src/backend/pid_tests.rs index 75a393ad15..72004bbc9c 100644 --- a/codex-rs/app-server-daemon/src/backend/pid_tests.rs +++ b/codex-rs/app-server-daemon/src/backend/pid_tests.rs @@ -225,20 +225,14 @@ async fn stale_record_cleanup_preserves_replacement_record() { #[cfg(unix)] #[tokio::test] async fn pid_record_captures_the_resolved_launch_binary() { - use std::os::unix::fs::PermissionsExt; - let temp = TempDir::new().expect("temp dir"); let original = temp.path().join("original-codex"); let replacement = temp.path().join("replacement-codex"); for (path, bytes) in [ - (&original, b"#!/bin/sh\nexec sleep 30\n".as_slice()), - ( - &replacement, - b"#!/bin/sh\n# replacement\nexec sleep 30\n".as_slice(), - ), + (&original, "#!/bin/sh\nexec sleep 30\n"), + (&replacement, "#!/bin/sh\n# replacement\nexec sleep 30\n"), ] { - std::fs::write(path, bytes).expect("binary"); - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).expect("executable"); + codex_utils_cargo_bin::write_executable(path, bytes).expect("binary"); } let selected = temp.path().join("current-codex"); std::os::unix::fs::symlink(&original, &selected).expect("selected binary"); diff --git a/codex-rs/app-server-daemon/src/lib.rs b/codex-rs/app-server-daemon/src/lib.rs index cadc695b23..0fd3ed9c27 100644 --- a/codex-rs/app-server-daemon/src/lib.rs +++ b/codex-rs/app-server-daemon/src/lib.rs @@ -1390,19 +1390,14 @@ mod tests { #[cfg(unix)] #[tokio::test] async fn managed_local_backend_counts_as_bootstrapped_without_updater() { - use std::os::unix::fs::PermissionsExt; - let home = TempDir::new().expect("home"); let standalone = home.path().join("packages/standalone"); let local_bin = standalone.join("local-main/bin/codex"); tokio::fs::create_dir_all(local_bin.parent().expect("bin parent")) .await .expect("local bin directory"); - tokio::fs::write(&local_bin, b"#!/bin/sh\nexec sleep 30\n") - .await + codex_utils_cargo_bin::write_executable(&local_bin, "#!/bin/sh\nexec sleep 30\n") .expect("local bin"); - std::fs::set_permissions(&local_bin, std::fs::Permissions::from_mode(0o755)) - .expect("executable local bin"); std::os::unix::fs::symlink("local-main", standalone.join("current")) .expect("current local build"); let state = home.path().join("app-server-daemon"); diff --git a/codex-rs/app-server-daemon/src/managed_install_path_tests.rs b/codex-rs/app-server-daemon/src/managed_install_path_tests.rs index b227c7ebf2..e076e7f601 100644 --- a/codex-rs/app-server-daemon/src/managed_install_path_tests.rs +++ b/codex-rs/app-server-daemon/src/managed_install_path_tests.rs @@ -114,14 +114,10 @@ fn updater_only_runs_for_stable_installer_owned_releases() { #[cfg(unix)] #[tokio::test] async fn older_managed_binary_does_not_claim_updater_support() { - use std::os::unix::fs::PermissionsExt; - let temp = tempfile::TempDir::new().expect("home"); let binary = temp.path().join("codex"); - std::fs::write(&binary, b"#!/bin/sh\nexit 2\n").expect("older binary"); - std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o755)) - .expect("executable binary"); + codex_utils_cargo_bin::write_executable(&binary, "#!/bin/sh\nexit 2\n").expect("older binary"); assert!(!super::supports_daemon_update_loop(&binary).await); - std::fs::write(&binary, b"#!/bin/sh\nexit 0\n").expect("newer binary"); + codex_utils_cargo_bin::write_executable(&binary, "#!/bin/sh\nexit 0\n").expect("newer binary"); assert!(super::supports_daemon_update_loop(&binary).await); } diff --git a/codex-rs/app-server-daemon/src/prepare_install_tests.rs b/codex-rs/app-server-daemon/src/prepare_install_tests.rs index 810a45b433..eaa89527c7 100644 --- a/codex-rs/app-server-daemon/src/prepare_install_tests.rs +++ b/codex-rs/app-server-daemon/src/prepare_install_tests.rs @@ -5,7 +5,6 @@ use super::prepare_from_package; use super::validate_package; use crate::settings::DaemonSettings; use pretty_assertions::assert_eq; -use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::path::PathBuf; @@ -28,28 +27,17 @@ fn package(root: &Path, version: &str) -> PathBuf { std::fs::create_dir_all(root.join(dir)).expect("package directory"); } let bin = root.join("bin/codex"); - std::fs::write(&bin, format!("#!/bin/sh\necho 'codex {version}'\n")).expect("codex executable"); - for file in [ - "bin/codex-code-mode-host", - "codex-path/rg", - "codex-resources/nested/runtime", - ] { - std::fs::write(root.join(file), b"runtime").expect("package file"); - if file != "codex-resources/nested/runtime" { - std::fs::set_permissions(root.join(file), std::fs::Permissions::from_mode(0o755)) - .expect("executable helper"); - } + codex_utils_cargo_bin::write_executable(&bin, &format!("#!/bin/sh\necho 'codex {version}'\n")) + .expect("codex executable"); + for file in ["bin/codex-code-mode-host", "codex-path/rg"] { + codex_utils_cargo_bin::write_executable(&root.join(file), "runtime") + .expect("executable helper"); } + std::fs::write(root.join("codex-resources/nested/runtime"), b"runtime").expect("package file"); if cfg!(target_os = "linux") { - std::fs::write(root.join("codex-resources/bwrap"), b"runtime").expect("bwrap"); - std::fs::set_permissions( - root.join("codex-resources/bwrap"), - std::fs::Permissions::from_mode(0o755), - ) - .expect("executable bwrap"); + codex_utils_cargo_bin::write_executable(&root.join("codex-resources/bwrap"), "runtime") + .expect("executable bwrap"); } - std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)) - .expect("executable permission"); std::fs::write( root.join("codex-package.json"), serde_json::json!({ @@ -120,7 +108,8 @@ async fn provisioned_macos_bundle_seeds_from_its_running_executable() { let temp = tempfile::TempDir::new().expect("temp"); let source = temp.path().join("package"); let launcher = package(&source, "0.1.0-internal-test.202609091200.1"); - std::fs::write(&launcher, b"#!/bin/sh\necho codex 0.0.0\n").expect("launcher"); + codex_utils_cargo_bin::write_executable(&launcher, "#!/bin/sh\necho codex 0.0.0\n") + .expect("launcher"); let bundle = source.join("CodexCLI.app/Contents/MacOS/codex"); std::fs::create_dir_all(bundle.parent().expect("bundle parent")).expect("bundle dir"); std::fs::write(&bundle, b"provisioned executable").expect("bundle executable"); diff --git a/codex-rs/app-server-daemon/src/update_loop_tests.rs b/codex-rs/app-server-daemon/src/update_loop_tests.rs index 598222a73e..af89c36be3 100644 --- a/codex-rs/app-server-daemon/src/update_loop_tests.rs +++ b/codex-rs/app-server-daemon/src/update_loop_tests.rs @@ -202,7 +202,11 @@ async fn explicit_update_migrates_running_and_stopped_installations() { if local { let package = root.join("releases/local-development"); std::fs::create_dir(&package).unwrap(); - std::fs::copy(&legacy.managed_codex_bin, package.join("codex")).unwrap(); + codex_utils_cargo_bin::copy_executable( + &legacy.managed_codex_bin, + &package.join("codex"), + ) + .unwrap(); std::fs::remove_file(root.join("current")).unwrap(); std::os::unix::fs::symlink(&package, root.join("current")).unwrap(); std::fs::remove_file(root.join("auto-update-version")).unwrap(); @@ -413,8 +417,6 @@ async fn cancelling_installer_stops_children_and_releases_fallback_lock() { #[cfg(unix)] fn manual_update_daemon(home: &TempDir) -> (Daemon, String) { - use std::os::unix::fs::PermissionsExt; - let target = if cfg!(target_os = "macos") { format!("{}-apple-darwin", std::env::consts::ARCH) } else { @@ -424,13 +426,11 @@ fn manual_update_daemon(home: &TempDir) -> (Daemon, String) { let standalone = home.path().join("packages/standalone"); let bin = standalone.join("releases").join(&release).join("codex"); std::fs::create_dir_all(bin.parent().expect("binary parent")).expect("release directory"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &bin, - b"#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo codex 1.0.0; else exec sleep 30; fi\n", + "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo codex 1.0.0; else exec sleep 30; fi\n", ) .expect("managed binary"); - std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)) - .expect("executable binary"); std::os::unix::fs::symlink(format!("releases/{release}"), standalone.join("current")) .expect("current release"); std::fs::write(standalone.join("auto-update-version"), &release).expect("latest marker"); @@ -698,7 +698,7 @@ async fn daemon_start_and_restart_preserve_launch_features() { r#"{"featureOverrides":{"auth_elicitation":true},"updater":{"autoUpdateEnabled":false}}"#, ) .unwrap(); - std::fs::write(&daemon.managed_codex_bin, format!( + codex_utils_cargo_bin::write_executable(&daemon.managed_codex_bin, &format!( "#!/bin/sh\nif [ \"$1\" = --version ]; then echo codex 1.0.0; exit; fi\nif [ \"$3\" = --help ]; then exit; fi\nprintf '%s\\n' \"$@\" > '{}'\nexec sleep 30\n", args_path.display(), )).unwrap(); @@ -974,17 +974,15 @@ async fn check_manual_update_restart(package_directory: &str) { let no_op = FakeInstallerHttp::new(InstallerResponse::Success( b"#!/bin/sh\n# CODEX_INSTALL_IF_LATEST CODEX_INSTALL_DAEMON_ONLY\nexit 0\n".to_vec(), )); - use std::io::Write; - std::fs::OpenOptions::new() - .append(true) - .open( - daemon - .current_managed_codex_bin() - .expect("current executable"), - ) - .expect("managed executable") - .write_all(b"\n# same-version replacement\n") - .expect("replace binary bytes"); + let bin = daemon + .current_managed_codex_bin() + .expect("current executable"); + let contents = std::fs::read_to_string(&bin).expect("managed executable"); + codex_utils_cargo_bin::write_executable( + &bin, + &format!("{contents}\n# same-version replacement\n"), + ) + .expect("replace binary bytes"); let output = manual_update_once( &no_op, &daemon, diff --git a/codex-rs/app-server/tests/common/test_app_server.rs b/codex-rs/app-server/tests/common/test_app_server.rs index 39fbef236c..86044c7c71 100644 --- a/codex-rs/app-server/tests/common/test_app_server.rs +++ b/codex-rs/app-server/tests/common/test_app_server.rs @@ -2121,7 +2121,7 @@ impl TestAppServerBuilder { (&code_mode_host_program, &staged_host), ] { std::fs::hard_link(source, destination) - .or_else(|_| std::fs::copy(source, destination).map(|_| ())) + .or_else(|_| codex_utils_cargo_bin::copy_executable(source, destination)) .with_context(|| format!("stage executable {}", source.display()))?; } program = staged_program; diff --git a/codex-rs/app-server/tests/suite/v2/curated_mcp_sync.rs b/codex-rs/app-server/tests/suite/v2/curated_mcp_sync.rs index 06c464ee34..cecf285620 100644 --- a/codex-rs/app-server/tests/suite/v2/curated_mcp_sync.rs +++ b/codex-rs/app-server/tests/suite/v2/curated_mcp_sync.rs @@ -1,4 +1,3 @@ -use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::path::PathBuf; use std::process::Command; @@ -111,16 +110,13 @@ impl CuratedMcpSyncFixture { )?; let malicious_git_helper = fixture_root.path().join("malicious-git-helper.sh"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &malicious_git_helper, - format!( + &format!( "#!/bin/sh\nprintf ran > '{}'\nexit 73\n", malicious_git_helper_marker.display() ), )?; - let mut helper_permissions = std::fs::metadata(&malicious_git_helper)?.permissions(); - helper_permissions.set_mode(0o755); - std::fs::set_permissions(&malicious_git_helper, helper_permissions)?; run_git(&real_git, &codex_home, &["init", "-b", "main"])?; run_git( &real_git, @@ -135,7 +131,7 @@ impl CuratedMcpSyncFixture { &["config", &malicious_rewrite_key, GITHUB_PLUGINS_GIT_URL], )?; let git_wrapper = git_wrapper_dir.join("git"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &git_wrapper, r#"#!/bin/sh if [ "$1" = "ls-remote" ]; then @@ -146,9 +142,6 @@ fi exec "$REAL_GIT" "$@" "#, )?; - let mut wrapper_permissions = std::fs::metadata(&git_wrapper)?.permissions(); - wrapper_permissions.set_mode(0o755); - std::fs::set_permissions(&git_wrapper, wrapper_permissions)?; MockResponsesConfig::new(&responses_server.uri()) .enable_feature(Feature::Plugins) diff --git a/codex-rs/app-server/tests/suite/v2/hooks_list.rs b/codex-rs/app-server/tests/suite/v2/hooks_list.rs index 60ee539ad3..beebe306a1 100644 --- a/codex-rs/app-server/tests/suite/v2/hooks_list.rs +++ b/codex-rs/app-server/tests/suite/v2/hooks_list.rs @@ -648,6 +648,7 @@ async fn loaded_session_refreshes_externally_updated_plugin_hooks() -> Result<() Ok(()) } +#[cfg(unix)] #[tokio::test] async fn automatic_marketplace_upgrade_refreshes_hook_runtime_for_loaded_session() -> Result<()> { skip_if_host_windows!(Ok(())); @@ -705,9 +706,9 @@ async fn automatic_marketplace_upgrade_refreshes_hook_runtime_for_loaded_session .find(|path| path.is_file()) .ok_or_else(|| anyhow::anyhow!("git was not found on PATH"))?; let wrapper_path = git_wrapper.path().join("git"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &wrapper_path, - format!( + &format!( r#"#!/bin/sh if [ "$3" = "ls-remote" ] && [ "$4" = "{marketplace}" ]; then while [ ! -e "{upgrade_gate}" ]; do @@ -721,14 +722,6 @@ exec "{real_git}" "$@" real_git = real_git.display(), ), )?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - - let mut permissions = std::fs::metadata(&wrapper_path)?.permissions(); - permissions.set_mode(0o755); - std::fs::set_permissions(&wrapper_path, permissions)?; - } let path_with_wrapper = std::env::join_paths( std::iter::once(git_wrapper.path().to_path_buf()) .chain(std::env::split_paths(&original_path)), diff --git a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs index d97e66fb67..9384541311 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs @@ -779,11 +779,11 @@ fn create_test_package_app_server(codex_home: &Path, zsh_path: &Path) -> Result< std::fs::write(package_dir.join("codex-package.json"), "{}")?; let app_server = bin_dir.join("codex-app-server"); - copy_with_permissions( + codex_utils_cargo_bin::copy_executable( &codex_utils_cargo_bin::cargo_bin("codex-app-server")?, &app_server, )?; - copy_with_permissions(zsh_path, &package_zsh_path)?; + codex_utils_cargo_bin::copy_executable(zsh_path, &package_zsh_path)?; Ok(app_server) } @@ -801,11 +801,6 @@ fn command_packaged_zsh_path(codex_home: &Path) -> PathBuf { std::fs::canonicalize(&path).unwrap_or(path) } -fn copy_with_permissions(source: &Path, destination: &Path) -> std::io::Result<()> { - std::fs::copy(source, destination)?; - std::fs::set_permissions(destination, std::fs::metadata(source)?.permissions()) -} - fn create_config_toml( codex_home: &Path, server_uri: &str, diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index b6d4c2d404..1f1bc3866e 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -33,4 +33,5 @@ codex-windows-sandbox = { workspace = true } pathdiff = { workspace = true } [dev-dependencies] +codex-utils-cargo-bin = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 5bed3b93ef..d653f6295f 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -622,7 +622,7 @@ mod tests { .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)?; + codex_utils_cargo_bin::copy_executable(&command_shell, &executable)?; let batch_path = alias_directory.join("apply_patch.bat"); let executable_path = super::windows_batch_executable_path(&executable, &alias_directory); diff --git a/codex-rs/cli/src/daemon_update_tests.rs b/codex-rs/cli/src/daemon_update_tests.rs index d93069f0da..be62c96496 100644 --- a/codex-rs/cli/src/daemon_update_tests.rs +++ b/codex-rs/cli/src/daemon_update_tests.rs @@ -8,7 +8,7 @@ fn daemon_handoff_uses_selected_executable_and_propagates_failure() -> anyhow::R let dir = tempfile::tempdir()?; let executable = dir.path().join("launching CLI"); let receipt = dir.path().join("launching CLI.args"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &executable, "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$0.args\"\n", )?; @@ -26,7 +26,7 @@ fn daemon_handoff_uses_selected_executable_and_propagates_failure() -> anyhow::R run_update_action(UpdateAction::Daemon(source), Some(&executable))?; assert_eq!(std::fs::read_to_string(&receipt)?, expected); } - std::fs::write(&executable, "#!/bin/sh\nexit 7\n")?; + codex_utils_cargo_bin::write_executable(&executable, "#!/bin/sh\nexit 7\n")?; let error = run_update_action( UpdateAction::Daemon(DaemonUpdateSource::ThisCli), Some(&executable), diff --git a/codex-rs/cli/src/desktop_app/mac.rs b/codex-rs/cli/src/desktop_app/mac.rs index 12abe08aa8..fb5a442b4a 100644 --- a/codex-rs/cli/src/desktop_app/mac.rs +++ b/codex-rs/cli/src/desktop_app/mac.rs @@ -422,7 +422,8 @@ mod tests { let executable_path = contents_path.join("MacOS/Codex"); fs::create_dir_all(executable_path.parent().expect("executable parent")) .expect("create executable directory"); - fs::copy("/usr/bin/true", &executable_path).expect("copy executable into app bundle"); + codex_utils_cargo_bin::copy_executable(Path::new("/usr/bin/true"), &executable_path) + .expect("copy executable into app bundle"); fs::write( contents_path.join("Info.plist"), r#"CFBundleIdentifiercom.openai.codexCFBundleExecutableCodex"#, diff --git a/codex-rs/cli/tests/app_server_daemon.rs b/codex-rs/cli/tests/app_server_daemon.rs index 75807f6c9b..f36f03ad24 100644 --- a/codex-rs/cli/tests/app_server_daemon.rs +++ b/codex-rs/cli/tests/app_server_daemon.rs @@ -547,7 +547,6 @@ enum InitialDaemon { } fn packaged_daemon_launch(action: &str, initial: InitialDaemon) -> Result<()> { - use std::os::unix::fs::PermissionsExt; let mut daemon = TestDaemon::new()?; let standalone = daemon.home.path().join("packages/standalone"); let package = if action == "start" && initial == InitialDaemon::Missing { @@ -565,8 +564,7 @@ fn packaged_daemon_launch(action: &str, initial: InitialDaemon) -> Result<()> { "codex-path/rg", "codex-resources/bwrap", ] { - std::fs::write(package.join(helper), b"runtime fixture")?; - std::fs::set_permissions(package.join(helper), std::fs::Permissions::from_mode(0o755))?; + codex_utils_cargo_bin::write_executable(&package.join(helper), "runtime fixture")?; } let target = format!( "{}-{}", @@ -670,13 +668,9 @@ fn packaged_daemon_launch(action: &str, initial: InitialDaemon) -> Result<()> { if initial == InitialDaemon::Legacy { assert!(!current.exists()); } - std::fs::write( - package.join("bin/codex-code-mode-host"), - b"replacement helper", - )?; - std::fs::set_permissions( - package.join("bin/codex-code-mode-host"), - std::fs::Permissions::from_mode(0o755), + codex_utils_cargo_bin::write_executable( + &package.join("bin/codex-code-mode-host"), + "replacement helper", )?; let replaced = daemon .command() diff --git a/codex-rs/cli/tests/daemon_startup.rs b/codex-rs/cli/tests/daemon_startup.rs index 8f6cce3d55..e55abb9164 100644 --- a/codex-rs/cli/tests/daemon_startup.rs +++ b/codex-rs/cli/tests/daemon_startup.rs @@ -207,7 +207,8 @@ async fn daemon_startup(command: &str) -> Result<()> { #[cfg(unix)] std::os::unix::fs::symlink(&codex, &managed)?; #[cfg(not(unix))] - fs::hard_link(&codex, &managed).or_else(|_| fs::copy(&codex, &managed).map(|_| ()))?; + fs::hard_link(&codex, &managed) + .or_else(|_| codex_utils_cargo_bin::copy_executable(&codex, &managed))?; if command != "restrictive-job" { fs::create_dir(home.path().join("app-server-daemon"))?; fs::write( diff --git a/codex-rs/cli/tests/doctor_path_safety.rs b/codex-rs/cli/tests/doctor_path_safety.rs index 0b53ebb9b4..b560486bfc 100644 --- a/codex-rs/cli/tests/doctor_path_safety.rs +++ b/codex-rs/cli/tests/doctor_path_safety.rs @@ -86,16 +86,11 @@ wire_api = "responses" ] { #[cfg(unix)] { - use std::os::unix::fs::PermissionsExt; let executable = bin.join(name); - std::fs::write( + codex_utils_cargo_bin::write_executable( &executable, "#!/bin/sh\nprintf 'helper ran\\n' >> \"$CODEX_TEST_HELPER_MARKER\"\nexit 0\n", )?; - std::fs::set_permissions( - executable, - std::fs::Permissions::from_mode(/*mode*/ 0o755), - )?; } #[cfg(windows)] std::fs::write( diff --git a/codex-rs/cli/tests/exec_server/pid_namespace_tests.rs b/codex-rs/cli/tests/exec_server/pid_namespace_tests.rs index 6129e7f0e2..57ce4074b4 100644 --- a/codex-rs/cli/tests/exec_server/pid_namespace_tests.rs +++ b/codex-rs/cli/tests/exec_server/pid_namespace_tests.rs @@ -11,7 +11,6 @@ use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; -use std::os::unix::fs::PermissionsExt; #[tokio::test] async fn pid_inheritance_is_startup_only_for_process_and_filesystem_helpers() -> Result<()> { @@ -34,7 +33,7 @@ async fn pid_inheritance_is_startup_only_for_process_and_filesystem_helpers() -> let fixture = TempDir::new()?; let wrapper = fixture.path().join("bwrap"); std::os::unix::fs::symlink(bwrap, fixture.path().join("real-bwrap"))?; - std::fs::write(&wrapper, r#"#!/bin/sh + codex_utils_cargo_bin::write_executable(&wrapper, r#"#!/bin/sh for arg in "$@"; do [ "$arg" = "--" ] && break if [ "$arg" = "--proc" ]; then @@ -44,7 +43,6 @@ for arg in "$@"; do done exec "${0%/*}/real-bwrap" "$@" "#)?; - std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o755))?; let path = format!("{}:{}", fixture.path().display(), std::env::var("PATH")?); // System bubblewrap discovery intentionally excludes executables under the command cwd. let workspace = TempDir::new()?; diff --git a/codex-rs/cli/tests/worktree.rs b/codex-rs/cli/tests/worktree.rs index 32a65e0562..928a57e649 100644 --- a/codex-rs/cli/tests/worktree.rs +++ b/codex-rs/cli/tests/worktree.rs @@ -281,7 +281,8 @@ trust_level = "trusted" #[cfg(unix)] std::os::unix::fs::symlink(program.canonicalize()?, &managed)?; #[cfg(not(unix))] - fs::hard_link(&program, &managed).or_else(|_| fs::copy(&program, &managed).map(|_| ()))?; + fs::hard_link(&program, &managed) + .or_else(|_| codex_utils_cargo_bin::copy_executable(&program, &managed))?; fs::create_dir(home.join("app-server-daemon"))?; fs::write( home.join("app-server-daemon/settings.json"), diff --git a/codex-rs/code-mode-host/tests/stdio.rs b/codex-rs/code-mode-host/tests/stdio.rs index 3636f6b419..e07582e0d4 100644 --- a/codex-rs/code-mode-host/tests/stdio.rs +++ b/codex-rs/code-mode-host/tests/stdio.rs @@ -898,9 +898,9 @@ async fn child_process_loss_cleans_up_and_rebuilds_the_shared_host() { let proxy_dir = tempfile::tempdir().expect("create host proxy directory"); let proxy_program = proxy_dir.path().join("host-proxy.sh"); let pid_path = proxy_dir.path().join("host.pid"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &proxy_program, - format!( + &format!( "#!/bin/sh\nprintf '%s\\n' \"$$\" > '{}'\nexec '{}'\n", pid_path.display(), host_program.display() diff --git a/codex-rs/core-plugins/Cargo.toml b/codex-rs/core-plugins/Cargo.toml index df9ec2752a..668da25d6c 100644 --- a/codex-rs/core-plugins/Cargo.toml +++ b/codex-rs/core-plugins/Cargo.toml @@ -60,6 +60,7 @@ zip = { workspace = true } [dev-dependencies] codex-exec-server-test-support = { workspace = true } +codex-utils-cargo-bin = { workspace = true } libc = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/core-plugins/src/npm_source_tests.rs b/codex-rs/core-plugins/src/npm_source_tests.rs index 3c2dcd94ac..eccb03c1d4 100644 --- a/codex-rs/core-plugins/src/npm_source_tests.rs +++ b/codex-rs/core-plugins/src/npm_source_tests.rs @@ -8,8 +8,6 @@ use std::io::Write; #[cfg(unix)] #[test] fn materialize_npm_plugin_source_uses_packed_package_root() { - use std::os::unix::fs::PermissionsExt; - let codex_home = tempfile::tempdir().expect("create codex home"); let fake_npm_dir = tempfile::tempdir().expect("create fake npm directory"); let archive_bytes = @@ -17,9 +15,9 @@ fn materialize_npm_plugin_source_uses_packed_package_root() { let archive_path = fake_npm_dir.path().join("fixture.tgz"); fs::write(&archive_path, &archive_bytes).expect("write fixture archive"); let fake_npm = fake_npm_dir.path().join("npm"); - fs::write( + codex_utils_cargo_bin::write_executable( &fake_npm, - format!( + &format!( r#"#!/bin/sh destination="" previous="" @@ -37,11 +35,6 @@ pwd > "$destination/pwd.txt" ), ) .expect("write fake npm"); - let mut permissions = fs::metadata(&fake_npm) - .expect("read fake npm metadata") - .permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&fake_npm, permissions).expect("make fake npm executable"); let (plugin_root, tempdir) = materialize_npm_plugin_source_with_command( codex_home.path(), diff --git a/codex-rs/core-plugins/src/startup_sync_tests.rs b/codex-rs/core-plugins/src/startup_sync_tests.rs index 8695c7a439..0fd236197e 100644 --- a/codex-rs/core-plugins/src/startup_sync_tests.rs +++ b/codex-rs/core-plugins/src/startup_sync_tests.rs @@ -499,16 +499,7 @@ fn has_plugins_clone_dirs(codex_home: &Path) -> bool { #[cfg(unix)] fn write_executable_script(path: &Path, contents: &str) { - #[cfg(unix)] - use std::os::unix::fs::PermissionsExt; - - std::fs::write(path, contents).expect("write script"); - #[cfg(unix)] - { - let mut permissions = std::fs::metadata(path).expect("metadata").permissions(); - permissions.set_mode(0o755); - std::fs::set_permissions(path, permissions).expect("chmod"); - } + codex_utils_cargo_bin::write_executable(path, contents).expect("write script"); } #[cfg(unix)] diff --git a/codex-rs/core/src/git_info_tests.rs b/codex-rs/core/src/git_info_tests.rs index 4e3f099540..d09cfb6a7c 100644 --- a/codex-rs/core/src/git_info_tests.rs +++ b/codex-rs/core/src/git_info_tests.rs @@ -35,8 +35,6 @@ use pretty_assertions::assert_eq; use std::collections::HashMap; use std::fs; use std::io; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; @@ -589,19 +587,14 @@ async fn test_get_has_changes_ignores_configured_hooks_path() { let marker_path = repo_path.join("hook-ran"); fs::create_dir_all(&hooks_dir).expect("create hook dir"); - fs::write( + codex_utils_cargo_bin::write_executable( &hook_path, - format!( + &format!( "#!/bin/sh\nprintf ran > \"{}\"\n", marker_path.to_string_lossy() ), ) .expect("write post-index-change hook"); - let mut permissions = fs::metadata(&hook_path) - .expect("read hook metadata") - .permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&hook_path, permissions).expect("mark hook executable"); Command::new("git") .args([ diff --git a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs index cf75c3a611..d834c24982 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs @@ -100,16 +100,13 @@ fn test_get_command_respects_explicit_bash_shell() -> anyhow::Result<()> { #[cfg(unix)] #[test] fn test_get_command_keeps_path_prepends_after_login_startup() -> anyhow::Result<()> { - use std::os::unix::fs::PermissionsExt; - let temp_dir = tempfile::tempdir()?; let path_dir = temp_dir.path().join("codex '[*] path"); let extra_path_dir = temp_dir.path().join("extra tools"); for directory in [&path_dir, &extra_path_dir] { std::fs::create_dir(directory)?; let rg = directory.join("rg"); - std::fs::write(&rg, "#!/bin/sh\n")?; - std::fs::set_permissions(&rg, std::fs::Permissions::from_mode(0o755))?; + codex_utils_cargo_bin::write_executable(&rg, "#!/bin/sh\n")?; } let rg = path_dir.join("rg"); let startup = temp_dir.path().join("startup"); diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index 3e6665f473..e6f63ff8ca 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -1299,7 +1299,7 @@ async fn snapshot_wrapper_replays_dummy_and_preserves_unbrokered_credentials() - let system_startup_dir = dir.path().join("system-startup"); std::fs::create_dir(&system_startup_dir)?; let system_startup_zsh = system_startup_dir.join("zsh"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &system_startup_zsh, "#!/bin/sh\nexport OPENAI_API_KEY='sk-system-startup-secret'\nexport AUTH_HEADER='Bearer sk-system-startup-secret'\nexec /bin/zsh -ax \"$@\"\n", )?; diff --git a/codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation_tests.rs index 9d3c5e8ade..ca282cd696 100644 --- a/codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation_tests.rs @@ -492,26 +492,15 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul .config .codex_home .join("permission_request_hook_log.jsonl"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &script_path, - format!( + &format!( "#!/bin/sh\ncat > {log_path}\nprintf '%s\\n' '{response}'\n", log_path = shlex::try_quote(log_path.to_string_lossy().as_ref())?, response = "{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":{\"behavior\":\"allow\"}}}", ), ) .with_context(|| format!("write hook script to {}", script_path.display()))?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - - let mut permissions = std::fs::metadata(&script_path) - .with_context(|| format!("read hook script metadata from {}", script_path.display()))? - .permissions(); - permissions.set_mode(0o755); - std::fs::set_permissions(&script_path, permissions) - .with_context(|| format!("set hook script permissions on {}", script_path.display()))?; - } std::fs::write( turn_context.config.codex_home.join("hooks.json"), serde_json::json!({ diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 8d3668f7da..5c0afeaaf5 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -2832,9 +2832,9 @@ async fn shell_startup_credentials_are_brokered( }; let zsh_path = zsh.derive_exec_args("", /*use_login_shell*/ false)[0].clone(); let path = startup_dir.join("zsh"); - fs::write( + codex_utils_cargo_bin::write_executable( &path, - format!( + &format!( "#!/bin/sh\nexport GH_ENTERPRISE_TOKEN='{REAL_GITHUB_TOKEN}'\nexport CUSTOM_API_KEY='{REAL_CUSTOM_TOKEN}'\nexport AUTH_HEADER=\"Bearer $GH_ENTERPRISE_TOKEN\"\nexec {} \"$@\"\n", shlex::try_quote(&zsh_path)? ), @@ -3476,15 +3476,12 @@ async fn env_zsh_script_spawned_by_python_can_request_escalation_under_zsh_fork( .await?; let script_path = test.cwd.path().join("runs-under-env-zsh"); - fs::write( + codex_utils_cargo_bin::write_executable( &script_path, - format!( + &format!( "#!/usr/bin/env zsh\ntouch {outside_path_arg}\nprint -r -- nested-env-zsh-complete\n" ), )?; - let mut script_permissions = fs::metadata(&script_path)?.permissions(); - script_permissions.set_mode(0o755); - fs::set_permissions(&script_path, script_permissions)?; let script_literal = serde_json::to_string(script_path.to_string_lossy().as_ref())?; let python_script = format!( @@ -3920,9 +3917,9 @@ async fn zsh_fork_inner_allowed_script_inherits_active_permission_profile() -> R let remote_bash_path = script_dir.path().join("remote_bash.py"); let outside_path = script_dir.path().join("remote-bash-unsandboxed-marker"); let outside_path_literal = serde_json::to_string(&outside_path.to_string_lossy())?; - fs::write( + codex_utils_cargo_bin::write_executable( &remote_bash_path, - format!( + &format!( r#"#!/usr/bin/env python3 import argparse import os @@ -3987,19 +3984,14 @@ if __name__ == "__main__": ), )?; let remote_bash_exec = shlex::try_join([remote_bash_path.to_string_lossy().as_ref()])?; - fs::write( + codex_utils_cargo_bin::write_executable( &wrapper_path, - format!( + &format!( r#"#!/usr/bin/env zsh exec {remote_bash_exec} "$@" "# ), )?; - for path in [&wrapper_path, &remote_bash_path] { - let mut permissions = fs::metadata(path)?.permissions(); - permissions.set_mode(0o755); - fs::set_permissions(path, permissions)?; - } let remote_bash_pattern = serde_json::to_string(&remote_bash_path.to_string_lossy())?; let rules = format!(r#"prefix_rule(pattern=[{remote_bash_pattern}], decision="allow")"#); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index e37725f07e..02b8b8acfe 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -832,7 +832,7 @@ impl ProviderAuthCommandFixture { #[cfg(unix)] let (command, args) = { let script_path = tempdir.path().join("print-token.sh"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &script_path, r#"#!/bin/sh if [ -f fail-until-401 ]; then @@ -844,12 +844,6 @@ tail -n +2 tokens.txt > tokens.next mv tokens.next tokens.txt "#, )?; - let mut permissions = std::fs::metadata(&script_path)?.permissions(); - { - use std::os::unix::fs::PermissionsExt; - permissions.set_mode(0o755); - } - std::fs::set_permissions(&script_path, permissions)?; ("./print-token.sh".to_string(), Vec::new()) }; diff --git a/codex-rs/core/tests/suite/external_auth.rs b/codex-rs/core/tests/suite/external_auth.rs index 5969992b54..5a77f6e992 100644 --- a/codex-rs/core/tests/suite/external_auth.rs +++ b/codex-rs/core/tests/suite/external_auth.rs @@ -245,16 +245,10 @@ fn bedrock_credential_export_config( #[cfg(unix)] let (command, args) = { let script_path = credentials_path.with_extension("sh"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &script_path, "#!/bin/sh\nprintf 'invoked\\n' >> \"$1\"\ncat \"$2\"\n", )?; - let mut permissions = std::fs::metadata(&script_path)?.permissions(); - { - use std::os::unix::fs::PermissionsExt; - permissions.set_mode(0o755); - } - std::fs::set_permissions(&script_path, permissions)?; ( script_path.to_string_lossy().into_owned(), vec![ @@ -439,8 +433,9 @@ async fn amazon_bedrock_aws_auth_refresh_resigns() -> anyhow::Result<()> { let aws_executable = fixture .path() .join(format!("aws{}", std::env::consts::EXE_SUFFIX)); - std::fs::hard_link(&test_executable, &aws_executable) - .or_else(|_| std::fs::copy(&test_executable, &aws_executable).map(|_| ()))?; + std::fs::hard_link(&test_executable, &aws_executable).or_else(|_| { + codex_utils_cargo_bin::copy_executable(&test_executable, &aws_executable) + })?; let inherited_path = std::env::var_os("PATH").unwrap_or_default(); let mut paths = std::env::split_paths(&inherited_path).collect::>(); paths.insert(/*index*/ 0, fixture.path().to_path_buf()); diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index bbfce0cc82..43bc0e8e8e 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -82,7 +82,6 @@ use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; use std::fs; -use std::os::unix::fs::PermissionsExt; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; @@ -2538,14 +2537,13 @@ async fn guardian_review_session_does_not_inherit_legacy_notify() -> Result<()> let notify_dir = TempDir::new()?; let notify_script = notify_dir.path().join("notify.sh"); - fs::write( + codex_utils_cargo_bin::write_executable( ¬ify_script, r#"#!/bin/bash set -e payload_path="$(dirname "${0}")/notify.jsonl" printf '%s\n' "${@: -1}" >> "${payload_path}""#, )?; - fs::set_permissions(¬ify_script, fs::Permissions::from_mode(0o755))?; let notify_file = notify_dir.path().join("notify.jsonl"); let notify_script_str = notify_script.to_str().unwrap().to_string(); let sandbox_policy_for_config = sandbox_policy.clone(); diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 2f13c1803a..178ba3e6ab 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -1415,7 +1415,10 @@ async fn agent_plugin_root_mcp_stdio_tool_round_trip_expands_reserved_paths_and_ } }; let stdio_server_name = format!("test_stdio_server{}", std::env::consts::EXE_SUFFIX); - std::fs::copy(stdio_server, plugin_root.join(&stdio_server_name))?; + codex_utils_cargo_bin::copy_executable( + std::path::Path::new(&stdio_server), + &plugin_root.join(&stdio_server_name), + )?; let mcp_config = serde_json::json!({ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", "mcpServers": { diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index c705a6a1ba..686a781f00 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -2081,7 +2081,8 @@ async fn local_stdio_server_uses_runtime_fallback_cwd_when_config_omits_cwd() -> .parent() .expect("relative test server path should include a parent"); fs::create_dir_all(target_dir).expect("create relative MCP bin directory"); - fs::copy(&rmcp_test_server_bin, &target_bin).expect("copy test stdio server"); + codex_utils_cargo_bin::copy_executable(&rmcp_test_server_bin, &target_bin) + .expect("copy test stdio server"); insert_mcp_server( config, diff --git a/codex-rs/core/tests/suite/shell_snapshot.rs b/codex-rs/core/tests/suite/shell_snapshot.rs index f9795efb17..af6881b332 100644 --- a/codex-rs/core/tests/suite/shell_snapshot.rs +++ b/codex-rs/core/tests/suite/shell_snapshot.rs @@ -63,8 +63,6 @@ use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; use serde_json::json; use std::collections::HashMap; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::path::PathBuf; use tokio::fs; @@ -763,7 +761,7 @@ async fn legacy_snapshot_omits_filtered_and_overridden_exports() -> Result<()> { let home = tempfile::tempdir()?; let shell_path = home.path().join("bash"); // Seed the capture shell without changing the test process environment. Replay uses -c. - fs::write( + codex_utils_cargo_bin::write_executable( &shell_path, r#"#!/bin/sh if [ "$1" = -lc ]; then @@ -777,9 +775,7 @@ second' fi exec /bin/bash --noprofile --norc "$@" "#, - ) - .await?; - fs::set_permissions(&shell_path, std::fs::Permissions::from_mode(/*mode*/ 0o755)).await?; + )?; let builder = test_codex() .with_user_shell( codex_shell_command::shell_detect::DetectedShell { @@ -1206,10 +1202,7 @@ async fn macos_unified_exec_resolves_command_from_tied_path_snapshot( .join("bin"); fs::create_dir_all(&command_dir).await?; let command_path = command_dir.join("snapshot-only-command"); - fs::write(&command_path, "#!/bin/sh\nprintf tied-path-command").await?; - let mut permissions = fs::metadata(&command_path).await?.permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&command_path, permissions).await?; + codex_utils_cargo_bin::write_executable(&command_path, "#!/bin/sh\nprintf tied-path-command")?; run_tool_turn_on_harness( &harness, diff --git a/codex-rs/core/tests/suite/skill_approval.rs b/codex-rs/core/tests/suite/skill_approval.rs index b62848b6bd..7f64c2a89a 100644 --- a/codex-rs/core/tests/suite/skill_approval.rs +++ b/codex-rs/core/tests/suite/skill_approval.rs @@ -83,8 +83,6 @@ fn write_skill_with_shell_script_contents( script_name: &str, script_contents: &str, ) -> Result { - use std::os::unix::fs::PermissionsExt; - let skill_dir = home.join("skills").join(name); let scripts_dir = skill_dir.join("scripts"); fs::create_dir_all(&scripts_dir)?; @@ -100,10 +98,7 @@ description: {name} skill )?; let script_path = scripts_dir.join(script_name); - fs::write(&script_path, script_contents)?; - let mut permissions = fs::metadata(&script_path)?.permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&script_path, permissions)?; + codex_utils_cargo_bin::write_executable(&script_path, script_contents)?; Ok(script_path) } diff --git a/codex-rs/core/tests/suite/user_notification.rs b/codex-rs/core/tests/suite/user_notification.rs index bc2c5314a2..5a3dc6279d 100644 --- a/codex-rs/core/tests/suite/user_notification.rs +++ b/codex-rs/core/tests/suite/user_notification.rs @@ -1,7 +1,6 @@ #![cfg(not(target_os = "windows"))] use codex_core::TurnInputRequest; -use std::os::unix::fs::PermissionsExt; use codex_protocol::protocol::EventMsg; use codex_protocol::user_input::UserInput; @@ -35,7 +34,7 @@ async fn summarize_context_three_requests_and_instructions() -> anyhow::Result<( let notify_dir = TempDir::new()?; // write a script to the notify that touches a file next to it let notify_script = notify_dir.path().join("notify.sh"); - std::fs::write( + codex_utils_cargo_bin::write_executable( ¬ify_script, r#"#!/bin/bash set -e @@ -44,7 +43,6 @@ tmp_path="${payload_path}.tmp" echo -n "${@: -1}" > "${tmp_path}" mv "${tmp_path}" "${payload_path}""#, )?; - std::fs::set_permissions(¬ify_script, std::fs::Permissions::from_mode(0o755))?; let notify_file = notify_dir.path().join("notify.txt"); let notify_script_str = notify_script.to_str().unwrap().to_string(); diff --git a/codex-rs/core/tests/suite/windows_sandbox.rs b/codex-rs/core/tests/suite/windows_sandbox.rs index 8369249ed7..28703bbe45 100644 --- a/codex-rs/core/tests/suite/windows_sandbox.rs +++ b/codex-rs/core/tests/suite/windows_sandbox.rs @@ -122,7 +122,7 @@ fn stage_windows_sandbox_helpers() -> anyhow::Result<()> { let helper = codex_utils_cargo_bin::cargo_bin(helper_name)?; let file_name = Path::new(helper_name).with_extension("exe"); let destination = resources_dir.join(file_name); - if let Err(err) = std::fs::copy(&helper, &destination) { + if let Err(err) = codex_utils_cargo_bin::copy_executable(&helper, &destination) { // A sandbox helper can briefly remain alive after the sandboxed // command exits. Bazel may retry the test while that process still // has the staged executable open, so keep the already-staged copy. @@ -152,18 +152,18 @@ fn stage_windows_sandbox_cli(fixture_bin: &Path) -> anyhow::Result<(PathBuf, Pat let codex_source = codex_utils_cargo_bin::cargo_bin("codex")?; let codex = fixture_bin.join("codex.exe"); - std::fs::copy(&codex_source, &codex) + codex_utils_cargo_bin::copy_executable(&codex_source, &codex) .with_context(|| format!("copy {} to {}", codex_source.display(), codex.display()))?; for helper_name in ["codex-windows-sandbox-setup", "codex-command-runner"] { let helper = codex_utils_cargo_bin::cargo_bin(helper_name)?; let destination = resources_dir.join(Path::new(helper_name).with_extension("exe")); - std::fs::copy(&helper, &destination) + codex_utils_cargo_bin::copy_executable(&helper, &destination) .with_context(|| format!("copy {} to {}", helper.display(), destination.display()))?; } let probe_source = codex_utils_cargo_bin::cargo_bin("codex-windows-managed-deny-probe")?; let probe = fixture_bin.join("managed-deny-probe.exe"); - std::fs::copy(&probe_source, &probe) + codex_utils_cargo_bin::copy_executable(&probe_source, &probe) .with_context(|| format!("copy {} to {}", probe_source.display(), probe.display()))?; Ok((codex, probe)) } diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index e9caa90953..4c942b01ac 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -6,8 +6,6 @@ mod shell_snapshot; mod windows_sandbox; use std::collections::HashMap; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; use std::sync::Arc; use anyhow::Context; @@ -236,11 +234,10 @@ async fn shell_snapshot_v2_filters_profile_exports_and_stays_in_memory( let runtime_path_entry = home.path().join("runtime-bin"); std::fs::create_dir(&profile_path_entry)?; let wc = profile_path_entry.join("wc"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &wc, "#!/bin/sh\nprintf x >> \"$HOME/tool-captures\"\nexec /usr/bin/wc \"$@\"\n", )?; - std::fs::set_permissions(&wc, std::fs::Permissions::from_mode(0o755))?; let posix_shell = matches!(shell_name, "sh" | "bash-sh"); let padding = if !use_remote && !tty && shell_name == "bash" { format!( @@ -815,10 +812,7 @@ async fn remote_tty_process_uses_configured_sandbox_helper_with_hostile_path() - let file = workspace.path().join("allowed.txt"); std::fs::write(&file, b"allowed")?; let hostile_helper = workspace.path().join("codex-linux-sandbox"); - std::fs::write(&hostile_helper, b"#!/bin/sh\nprintf hostile")?; - let mut permissions = std::fs::metadata(&hostile_helper)?.permissions(); - permissions.set_mode(0o755); - std::fs::set_permissions(&hostile_helper, permissions)?; + codex_utils_cargo_bin::write_executable(&hostile_helper, "#!/bin/sh\nprintf hostile")?; let path = std::env::var_os("PATH").context("PATH is not set")?; let hostile_path = std::env::join_paths( std::iter::once(workspace.path().to_path_buf()).chain(std::env::split_paths(&path)), diff --git a/codex-rs/exec-server/tests/file_system/shared.rs b/codex-rs/exec-server/tests/file_system/shared.rs index 46531b2fb9..53c3f67b5e 100644 --- a/codex-rs/exec-server/tests/file_system/shared.rs +++ b/codex-rs/exec-server/tests/file_system/shared.rs @@ -1145,11 +1145,11 @@ async fn sandboxed_file_operations_cannot_read_helper_siblings() -> Result<()> { let helper = runtime_dir.join("codex-test-helper"); std::fs::hard_link(&helper_paths.codex_exe, &helper) - .or_else(|_| std::fs::copy(&helper_paths.codex_exe, &helper).map(|_| ()))?; + .or_else(|_| codex_utils_cargo_bin::copy_executable(&helper_paths.codex_exe, &helper))?; let linux_sandbox = if helper_paths.codex_linux_sandbox_exe.is_some() { let alias = runtime_dir.join("codex-linux-sandbox"); std::fs::hard_link(&helper, &alias) - .or_else(|_| std::fs::copy(&helper, &alias).map(|_| ()))?; + .or_else(|_| codex_utils_cargo_bin::copy_executable(&helper, &alias))?; Some(alias) } else { None diff --git a/codex-rs/exec-server/tests/file_system_windows.rs b/codex-rs/exec-server/tests/file_system_windows.rs index 8193b570ea..a7ec5e7090 100644 --- a/codex-rs/exec-server/tests/file_system_windows.rs +++ b/codex-rs/exec-server/tests/file_system_windows.rs @@ -473,7 +473,7 @@ async fn file_system_elevated_relative_read_denial_uses_policy_cwd( for name in ["codex-windows-sandbox-setup", "codex-command-runner"] { let source = codex_utils_cargo_bin::cargo_bin(name)?; let destination = resources.join(Path::new(name).with_extension("exe")); - if let Err(error) = std::fs::copy(&source, &destination) + if let Err(error) = codex_utils_cargo_bin::copy_executable(&source, &destination) && !(error.kind() == std::io::ErrorKind::PermissionDenied && destination.is_file()) { return Err(error).with_context(|| format!("stage Windows sandbox helper {name}")); diff --git a/codex-rs/exec-server/tests/initialize.rs b/codex-rs/exec-server/tests/initialize.rs index 435ebcc8a0..53bb19d886 100644 --- a/codex-rs/exec-server/tests/initialize.rs +++ b/codex-rs/exec-server/tests/initialize.rs @@ -26,7 +26,7 @@ async fn exec_server_accepts_initialize(version: Option<&str>) -> anyhow::Result let bin_dir = package.path().join("bin"); std::fs::create_dir(&bin_dir)?; let executable = bin_dir.join(format!("codex{}", std::env::consts::EXE_SUFFIX)); - std::fs::copy(std::env::current_exe()?, &executable)?; + codex_utils_cargo_bin::copy_executable(&std::env::current_exe()?, &executable)?; let manifest = package.path().join("codex-package.json"); if let Some(version) = version { std::fs::write( diff --git a/codex-rs/git-utils/Cargo.toml b/codex-rs/git-utils/Cargo.toml index 41f3147685..c9409d41e0 100644 --- a/codex-rs/git-utils/Cargo.toml +++ b/codex-rs/git-utils/Cargo.toml @@ -35,6 +35,7 @@ ts-rs = { workspace = true, features = [ walkdir = { workspace = true } [dev-dependencies] +codex-utils-cargo-bin = { workspace = true } pretty_assertions = { workspace = true } [lib] diff --git a/codex-rs/git-utils/src/baseline.rs b/codex-rs/git-utils/src/baseline.rs index 083dd6010f..3dc36d7978 100644 --- a/codex-rs/git-utils/src/baseline.rs +++ b/codex-rs/git-utils/src/baseline.rs @@ -587,8 +587,6 @@ mod tests { #[cfg(unix)] #[tokio::test] async fn write_index_ignores_configured_hooks_path() { - use std::os::unix::fs::PermissionsExt; - let home = TempDir::new().expect("tempdir"); let root = home.path().join("repo"); let hooks_dir = root.join(".git/hooks-path-test"); @@ -599,19 +597,14 @@ mod tests { fs::write(root.join("MEMORY.md"), "baseline").expect("write memory"); reset_git_repository(&root).await.expect("reset repo"); fs::create_dir_all(&hooks_dir).expect("create hook dir"); - fs::write( + codex_utils_cargo_bin::write_executable( &hook_path, - format!( + &format!( "#!/bin/sh\nprintf ran > \"{}\"\n", marker_path.to_string_lossy() ), ) .expect("write post-index-change hook"); - let mut permissions = fs::metadata(&hook_path) - .expect("read hook metadata") - .permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&hook_path, permissions).expect("mark hook executable"); git_stdout( &root, &[ diff --git a/codex-rs/git-utils/src/info.rs b/codex-rs/git-utils/src/info.rs index 7f038304db..aec8a01dad 100644 --- a/codex-rs/git-utils/src/info.rs +++ b/codex-rs/git-utils/src/info.rs @@ -836,8 +836,6 @@ pub async fn current_branch_name(cwd: &Path) -> Option { mod tests { use super::*; use pretty_assertions::assert_eq; - #[cfg(unix)] - use std::os::unix::fs::PermissionsExt; use std::process::Stdio; #[tokio::test] @@ -1071,7 +1069,7 @@ mod tests { let temp_dir = tempfile::tempdir().expect("create temp dir"); let git = temp_dir.path().join("git"); let log = temp_dir.path().join("git.log"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &git, "#!/bin/sh\n\ if [ \"$1\" = \"-c\" ] && [ \"$2\" = \"safe.bareRepository=explicit\" ]; then shift 2; fi\n\ @@ -1082,11 +1080,6 @@ mod tests { esac\n", ) .expect("write fake Git"); - let mut permissions = std::fs::metadata(&git) - .expect("read fake Git metadata") - .permissions(); - permissions.set_mode(0o755); - std::fs::set_permissions(&git, permissions).expect("mark fake Git executable"); // The config response mirrors: // git -c core.fsmonitor=/tmp/fsmonitor-helper \ @@ -1137,7 +1130,7 @@ mod tests { let git = temp_dir.path().join("git"); let global_config = temp_dir.path().join("git.global"); let log = temp_dir.path().join("git.log"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &git, "#!/bin/sh\n\ if [ \"$1\" = \"-c\" ] && [ \"$2\" = \"safe.bareRepository=explicit\" ]; then shift 2; fi\n\ @@ -1151,11 +1144,6 @@ mod tests { esac\n", ) .expect("write layered-config Git"); - let mut permissions = std::fs::metadata(&git) - .expect("read layered-config Git metadata") - .permissions(); - permissions.set_mode(0o755); - std::fs::set_permissions(&git, permissions).expect("mark layered-config Git executable"); let global_status = std::process::Command::new("git") .args([ diff --git a/codex-rs/linux-sandbox/src/bundled_bwrap.rs b/codex-rs/linux-sandbox/src/bundled_bwrap.rs index 61d10547d3..b267b2b277 100644 --- a/codex-rs/linux-sandbox/src/bundled_bwrap.rs +++ b/codex-rs/linux-sandbox/src/bundled_bwrap.rs @@ -311,8 +311,6 @@ mod tests { if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create parent dir"); } - fs::write(path, b"").expect("write executable"); - fs::set_permissions(path, fs::Permissions::from_mode(0o755)) - .expect("set executable permissions"); + codex_utils_cargo_bin::write_executable(path, "").expect("write executable"); } } diff --git a/codex-rs/linux-sandbox/src/launcher.rs b/codex-rs/linux-sandbox/src/launcher.rs index 1bcbb6bf31..b72cfede17 100644 --- a/codex-rs/linux-sandbox/src/launcher.rs +++ b/codex-rs/linux-sandbox/src/launcher.rs @@ -242,7 +242,6 @@ fn exec_system_bwrap( mod tests { use super::*; use pretty_assertions::assert_eq; - use std::os::unix::fs::PermissionsExt; use tempfile::NamedTempFile; #[test] @@ -308,13 +307,11 @@ mod tests { fn detects_fd_backed_read_only_mount_support_in_system_bwrap_help() { let temp_dir = tempfile::tempdir().expect("temp directory"); let fake_bwrap_path = temp_dir.path().join("bwrap"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &fake_bwrap_path, "#!/bin/sh\nprintf '%s\\n' '--as-pid-1' '--perms' '--argv0' '--ro-bind-fd'\n", ) .expect("write fake bubblewrap"); - std::fs::set_permissions(&fake_bwrap_path, std::fs::Permissions::from_mode(0o755)) - .expect("make fake bubblewrap executable"); assert_eq!( system_bwrap_capabilities(&fake_bwrap_path), diff --git a/codex-rs/linux-sandbox/tests/suite/bundled_bwrap.rs b/codex-rs/linux-sandbox/tests/suite/bundled_bwrap.rs index 07bd3ee35b..d4691f5154 100644 --- a/codex-rs/linux-sandbox/tests/suite/bundled_bwrap.rs +++ b/codex-rs/linux-sandbox/tests/suite/bundled_bwrap.rs @@ -66,7 +66,11 @@ fn bazel_build_rejects_tampered_bundled_bwrap() { std::fs::remove_file(&bundled_bwrap).expect("read-only bundled bwrap should be replaceable"); let mut tampered_bwrap_bytes = original_bwrap_bytes; tampered_bwrap_bytes.push(0); - std::fs::write(&bundled_bwrap, &tampered_bwrap_bytes) + // Write data to a separate inode, then publish it through the descriptor-safe helper. + let tampered_data = package.path().join("tampered-bwrap-data"); + std::fs::write(&tampered_data, &tampered_bwrap_bytes) + .expect("modified bwrap data should be written"); + copy_executable(&tampered_data, &bundled_bwrap) .expect("modified bwrap should be copied into the package"); let bwrap_permissions = std::fs::metadata(&bwrap_binary) .expect("built bwrap metadata should be readable") diff --git a/codex-rs/linux-sandbox/tests/suite/daemon_sockets_tests.rs b/codex-rs/linux-sandbox/tests/suite/daemon_sockets_tests.rs index e6bd251113..0aaf460f87 100644 --- a/codex-rs/linux-sandbox/tests/suite/daemon_sockets_tests.rs +++ b/codex-rs/linux-sandbox/tests/suite/daemon_sockets_tests.rs @@ -14,8 +14,12 @@ async fn private_tmp_mount_preserves_daemon_socket_isolation() { let private = tempfile::tempdir_in("/tmp").unwrap(); let test_executable = std::env::current_exe().unwrap(); // Keep both executables available after the original /tmp is hidden. - std::fs::copy(&test_executable, private.path().join("test")).unwrap(); - std::fs::copy(codex_linux_sandbox_exe(), private.path().join("sandbox")).unwrap(); + codex_utils_cargo_bin::copy_executable(&test_executable, &private.path().join("test")).unwrap(); + codex_utils_cargo_bin::copy_executable( + &codex_linux_sandbox_exe(), + &private.path().join("sandbox"), + ) + .unwrap(); let (_, test_module) = module_path!().split_once("::").unwrap(); let fixture_test = format!("{test_module}::private_tmp_fixture"); // Mount setup needs capabilities, which bubblewrap only accepts as namespace root. diff --git a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs index 7fd68518c6..02b5d0cad8 100644 --- a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs +++ b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs @@ -27,7 +27,6 @@ use std::net::Shutdown; use std::net::TcpListener; use std::net::TcpStream; use std::os::unix::fs::MetadataExt; -use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::process::Output; use std::process::Stdio; @@ -322,7 +321,7 @@ async fn proc_mount_denial_preserves_legacy_fallback_and_explicit_pid_inheritanc }; let tempdir = tempfile::tempdir().expect("create PID namespace fixture"); let wrapper = tempdir.path().join("bwrap"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &wrapper, r#"#!/bin/sh for arg in "$@"; do @@ -336,8 +335,6 @@ exec "$CODEX_TEST_REAL_BWRAP" "$@" "#, ) .expect("write proc-denying bubblewrap wrapper"); - std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o755)) - .expect("make bubblewrap wrapper executable"); let protected_file = tempdir.path().join("protected"); std::fs::write(&protected_file, "original").expect("write protected file"); let pid_namespace = std::fs::read_link("/proc/self/ns/pid").expect("read caller PID namespace"); @@ -473,7 +470,8 @@ async fn unsupported_system_bwrap_falls_back_to_bundled_bwrap() { let sandbox_executable = tempdir.path().join("codex-linux-sandbox"); let original_executable = env!("CARGO_BIN_EXE_codex-linux-sandbox"); if std::fs::hard_link(original_executable, &sandbox_executable).is_err() { - std::fs::copy(original_executable, &sandbox_executable).expect("copy sandbox executable"); + codex_utils_cargo_bin::copy_executable(Path::new(original_executable), &sandbox_executable) + .expect("copy sandbox executable"); } let resources_dir = tempdir.path().join("codex-resources"); @@ -484,13 +482,11 @@ async fn unsupported_system_bwrap_falls_back_to_bundled_bwrap() { let system_dir = tempdir.path().join("system"); std::fs::create_dir(&system_dir).expect("create fake system binary directory"); let unsupported_bwrap = system_dir.join("bwrap"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &unsupported_bwrap, "#!/bin/sh\nif [ \"$1\" = \"--help\" ]; then printf '%s\\n' '--perms'; exit 0; fi\nexit 91\n", ) .expect("write unsupported system bubblewrap"); - std::fs::set_permissions(&unsupported_bwrap, std::fs::Permissions::from_mode(0o755)) - .expect("make unsupported system bubblewrap executable"); let mut env = create_env_from_core_vars(); strip_proxy_env(&mut env); diff --git a/codex-rs/linux-sandbox/tests/suite/root_metadata_tests.rs b/codex-rs/linux-sandbox/tests/suite/root_metadata_tests.rs index 5b61d42a1c..8e190470a4 100644 --- a/codex-rs/linux-sandbox/tests/suite/root_metadata_tests.rs +++ b/codex-rs/linux-sandbox/tests/suite/root_metadata_tests.rs @@ -30,7 +30,8 @@ async fn root_metadata_symlinks_cannot_reopen_approved_denials() { fs::write(work.join("public/secret"), "outside").unwrap(); fs::write(private.join("reopened/secret"), "private").unwrap(); fs::write(private.join("sibling"), "sibling").unwrap(); - fs::copy(codex_linux_sandbox_exe(), work.join("sandbox")).unwrap(); + codex_utils_cargo_bin::copy_executable(&codex_linux_sandbox_exe(), &work.join("sandbox")) + .unwrap(); let path_entry = |path: &str, access| { FileSystemSandboxEntry::new( diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml index 7762770be8..50de4dc83a 100644 --- a/codex-rs/login/Cargo.toml +++ b/codex-rs/login/Cargo.toml @@ -45,6 +45,7 @@ urlencoding = { workspace = true } webbrowser = { workspace = true } [dev-dependencies] +codex-utils-cargo-bin = { workspace = true } opentelemetry_sdk = { workspace = true, features = ["testing"] } core_test_support = { workspace = true } jsonwebtoken = { workspace = true } diff --git a/codex-rs/login/src/auth/auth_tests.rs b/codex-rs/login/src/auth/auth_tests.rs index 9927a604a1..ab15a33871 100644 --- a/codex-rs/login/src/auth/auth_tests.rs +++ b/codex-rs/login/src/auth/auth_tests.rs @@ -1654,7 +1654,7 @@ impl ProviderAuthScript { #[cfg(unix)] let (command, args) = { let script_path = tempdir.path().join("print-token.sh"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &script_path, r#"#!/bin/sh if [ -f fail-once ]; then @@ -1667,12 +1667,6 @@ tail -n +2 tokens.txt > tokens.next mv tokens.next tokens.txt "#, )?; - let mut permissions = std::fs::metadata(&script_path)?.permissions(); - { - use std::os::unix::fs::PermissionsExt; - permissions.set_mode(0o755); - } - std::fs::set_permissions(&script_path, permissions)?; ("./print-token.sh".to_string(), Vec::new()) }; @@ -1721,18 +1715,12 @@ move /y tokens.next tokens.txt >nul #[cfg(unix)] let (command, args) = { let script_path = tempdir.path().join("fail.sh"); - std::fs::write( + codex_utils_cargo_bin::write_executable( &script_path, r#"#!/bin/sh exit 1 "#, )?; - let mut permissions = std::fs::metadata(&script_path)?.permissions(); - { - use std::os::unix::fs::PermissionsExt; - permissions.set_mode(0o755); - } - std::fs::set_permissions(&script_path, permissions)?; ("./fail.sh".to_string(), Vec::new()) }; diff --git a/codex-rs/login/src/auth/default_client_tests.rs b/codex-rs/login/src/auth/default_client_tests.rs index 92d9bf2195..4a5d9410d8 100644 --- a/codex-rs/login/src/auth/default_client_tests.rs +++ b/codex-rs/login/src/auth/default_client_tests.rs @@ -49,8 +49,6 @@ fn test_get_codex_user_agent() { #[test] #[cfg(target_os = "linux")] fn os_discovery_is_cached_without_freezing_user_agent_overrides() { - use std::os::unix::fs::PermissionsExt; - const PROBE_DIR: &str = "CODEX_TEST_USER_AGENT_PROBE_DIR"; if std::env::var_os(PROBE_DIR).is_some() { // Race the first lookup as well as exercising repeated requests. @@ -79,15 +77,13 @@ fn os_discovery_is_cached_without_freezing_user_agent_overrides() { ("getconf", "64\n"), ] { let script = temp.path().join(program); - std::fs::write( + codex_utils_cargo_bin::write_executable( &script, - format!( + &format!( "#!/bin/sh\nprintf '{program}\\n' >> \"${PROBE_DIR}/calls\"\nprintf '{output}'\n" ), ) .expect("write probe"); - std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)) - .expect("make probe executable"); } let output = std::process::Command::new(std::env::current_exe().expect("test executable")) .args([ diff --git a/codex-rs/model-provider/Cargo.toml b/codex-rs/model-provider/Cargo.toml index 26c67921cb..ffda92383e 100644 --- a/codex-rs/model-provider/Cargo.toml +++ b/codex-rs/model-provider/Cargo.toml @@ -38,6 +38,7 @@ url = { workspace = true } [dev-dependencies] base64 = { workspace = true } +codex-utils-cargo-bin = { workspace = true } codex-utils-redacted-string = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 7d99c556dd..8b340a1e9b 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -914,7 +914,7 @@ mod tests { let executable = std::env::current_exe().expect("test executable should be available"); let aws = counter.join(format!("aws{}", std::env::consts::EXE_SUFFIX)); std::fs::hard_link(&executable, &aws) - .or_else(|_| std::fs::copy(&executable, &aws).map(|_| ())) + .or_else(|_| codex_utils_cargo_bin::copy_executable(&executable, &aws)) .expect("test executable should be installed as aws"); let existing_path = std::env::var_os("PATH").unwrap_or_default(); let path = std::env::join_paths( diff --git a/codex-rs/realtime-webrtc/Cargo.toml b/codex-rs/realtime-webrtc/Cargo.toml index 2d1b3b422f..bd1e058f37 100644 --- a/codex-rs/realtime-webrtc/Cargo.toml +++ b/codex-rs/realtime-webrtc/Cargo.toml @@ -22,6 +22,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "ti tracing = { workspace = true } [dev-dependencies] +codex-utils-cargo-bin = { workspace = true } ctor = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/realtime-webrtc/tests/common/mod.rs b/codex-rs/realtime-webrtc/tests/common/mod.rs index 137012d28c..681b6461c3 100644 --- a/codex-rs/realtime-webrtc/tests/common/mod.rs +++ b/codex-rs/realtime-webrtc/tests/common/mod.rs @@ -77,8 +77,11 @@ pub fn package(test: &str) -> Result> { fs::create_dir_all(root.join("bin"))?; fs::create_dir_all(root.join("codex-resources/voice/bin"))?; fs::write(root.join("codex-package.json"), "{}")?; - fs::copy(&source, root.join("bin").join(APP))?; - fs::copy(&source, root.join("codex-resources/voice/bin").join(HELPER))?; + codex_utils_cargo_bin::copy_executable(&source, &root.join("bin").join(APP))?; + codex_utils_cargo_bin::copy_executable( + &source, + &root.join("codex-resources/voice/bin").join(HELPER), + )?; let mut child = Command::new(root.join("bin").join(APP)) .args(["--exact", test, "--nocapture"]) .current_dir(root) diff --git a/codex-rs/rmcp-client/tests/stdio_descriptor_boundary.rs b/codex-rs/rmcp-client/tests/stdio_descriptor_boundary.rs index a910c4bc8b..988b75fe50 100644 --- a/codex-rs/rmcp-client/tests/stdio_descriptor_boundary.rs +++ b/codex-rs/rmcp-client/tests/stdio_descriptor_boundary.rs @@ -8,7 +8,6 @@ use std::fs; use std::os::fd::AsRawFd; use std::os::fd::FromRawFd; use std::os::fd::OwnedFd; -use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::symlink; use std::process::Command; use std::sync::Arc; @@ -93,8 +92,7 @@ for line in sys.stdin: let wrapper = temporary.path().join("executable-text"); // No shebang: exercise Command's shell fallback after native macOS spawn // returns ENOEXEC, including the fallback descriptor cleanup. - fs::write(&wrapper, "exec \"$@\"\n")?; - fs::set_permissions(&wrapper, fs::Permissions::from_mode(/*mode*/ 0o755))?; + codex_utils_cargo_bin::write_executable(&wrapper, "exec \"$@\"\n")?; for (launch, program, mut args) in [ ("absolute", python.clone().into_os_string(), Vec::new()), ("relative", OsString::from("./python-fixture"), Vec::new()), @@ -170,8 +168,7 @@ async fn local_stdio_preserves_exec_failure_reporting() -> Result<()> { let temporary = tempfile::tempdir()?; let script = temporary.path().join("missing-interpreter"); let interpreter = temporary.path().join("does-not-exist"); - fs::write(&script, format!("#!{}\n", interpreter.display()))?; - fs::set_permissions(&script, fs::Permissions::from_mode(/*mode*/ 0o755))?; + codex_utils_cargo_bin::write_executable(&script, &format!("#!{}\n", interpreter.display()))?; // Program resolution succeeds, but exec fails. Descriptor cleanup must // preserve the internal error pipe so launch reports the failure directly. diff --git a/codex-rs/sandboxing/Cargo.toml b/codex-rs/sandboxing/Cargo.toml index 14549e19af..64ebac488a 100644 --- a/codex-rs/sandboxing/Cargo.toml +++ b/codex-rs/sandboxing/Cargo.toml @@ -37,6 +37,7 @@ codex-utils-home-dir = { workspace = true } [dev-dependencies] anyhow = { workspace = true } +codex-utils-cargo-bin = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["macros", "rt", "time"] } diff --git a/codex-rs/sandboxing/src/bwrap_tests.rs b/codex-rs/sandboxing/src/bwrap_tests.rs index d5fd9da431..b64f9ff0cf 100644 --- a/codex-rs/sandboxing/src/bwrap_tests.rs +++ b/codex-rs/sandboxing/src/bwrap_tests.rs @@ -171,8 +171,6 @@ fn write_fake_bwrap(contents: &str) -> tempfile::TempPath { } fn write_fake_bwrap_in(dir: &Path, contents: &str) -> tempfile::TempPath { - use std::fs; - use std::os::unix::fs::PermissionsExt; use tempfile::NamedTempFile; // Bazel can mount the OS temp directory `noexec`, so prefer the current @@ -181,21 +179,18 @@ fn write_fake_bwrap_in(dir: &Path, contents: &str) -> tempfile::TempPath { let temp_file = NamedTempFile::new_in(dir) .ok() .unwrap_or_else(|| NamedTempFile::new().expect("temp file")); - // Linux rejects exec-ing a file that is still open for writing. let path = temp_file.into_temp_path(); - fs::write(&path, contents).expect("write fake bwrap"); - let permissions = fs::Permissions::from_mode(0o755); - fs::set_permissions(&path, permissions).expect("chmod fake bwrap"); + // A sibling spawn may retain the temporary file's writable descriptor. + // Remove that inode before the helper creates the executable in its child. + std::fs::remove_file(&path).expect("remove empty temporary file"); + codex_utils_cargo_bin::write_executable(&path, contents).expect("write fake bwrap"); path } fn write_named_fake_bwrap_in(dir: &Path) -> PathBuf { use std::fs; - use std::os::unix::fs::PermissionsExt; let path = dir.join("bwrap"); - fs::write(&path, "#!/bin/sh\n").expect("write fake bwrap"); - let permissions = fs::Permissions::from_mode(0o755); - fs::set_permissions(&path, permissions).expect("chmod fake bwrap"); + codex_utils_cargo_bin::write_executable(&path, "#!/bin/sh\n").expect("write fake bwrap"); fs::canonicalize(path).expect("canonicalize fake bwrap") } diff --git a/codex-rs/tui/src/external_editor.rs b/codex-rs/tui/src/external_editor.rs index 3c4de6cb74..583beb3129 100644 --- a/codex-rs/tui/src/external_editor.rs +++ b/codex-rs/tui/src/external_editor.rs @@ -294,14 +294,13 @@ mod tests { #[tokio::test] #[cfg(unix)] async fn run_editor_returns_updated_content() { - use std::os::unix::fs::PermissionsExt; - let dir = tempdir().unwrap(); let script_path = dir.path().join("edit.sh"); - fs::write(&script_path, "#!/bin/sh\nprintf \"edited\" > \"$1\"\n").unwrap(); - let mut perms = fs::metadata(&script_path).unwrap().permissions(); - perms.set_mode(0o755); - fs::set_permissions(&script_path, perms).unwrap(); + codex_utils_cargo_bin::write_executable( + &script_path, + "#!/bin/sh\nprintf \"edited\" > \"$1\"\n", + ) + .unwrap(); let cmd = vec![script_path.to_string_lossy().to_string()]; let policy = FileSystemSandboxPolicy::read_only(); diff --git a/codex-rs/tui/src/get_git_diff.rs b/codex-rs/tui/src/get_git_diff.rs index 21085db72f..c23dee689b 100644 --- a/codex-rs/tui/src/get_git_diff.rs +++ b/codex-rs/tui/src/get_git_diff.rs @@ -267,8 +267,6 @@ mod tests { #[cfg(unix)] use std::fs; use std::future::Future; - #[cfg(unix)] - use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use std::pin::Pin; #[cfg(unix)] @@ -823,13 +821,11 @@ mod tests { #[cfg(unix)] fn write_marker_helper(path: &Path) { - fs::write(path, "#!/bin/sh\nprintf ran >> \"$0.ran\"\nexit 1\n") - .expect("write helper script"); - let mut permissions = fs::metadata(path) - .expect("read helper metadata") - .permissions(); - permissions.set_mode(/*mode*/ 0o755); - fs::set_permissions(path, permissions).expect("make helper executable"); + codex_utils_cargo_bin::write_executable( + path, + "#!/bin/sh\nprintf ran >> \"$0.ran\"\nexit 1\n", + ) + .expect("write helper script"); } fn assert_command_metadata(commands: &[WorkspaceCommand], cwd: &Path) { diff --git a/codex-rs/tui/tests/suite/external_editor_tests.rs b/codex-rs/tui/tests/suite/external_editor_tests.rs index 8b58e629da..fe5f019f84 100644 --- a/codex-rs/tui/tests/suite/external_editor_tests.rs +++ b/codex-rs/tui/tests/suite/external_editor_tests.rs @@ -4,8 +4,6 @@ use super::PtyCodex; use super::write_test_config; use anyhow::Result; use anyhow::ensure; -use std::fs; -use std::os::unix::fs::PermissionsExt; use std::time::Duration; use std::time::Instant; @@ -22,9 +20,9 @@ fn external_editors_keep_the_screen_and_return_the_draft() -> Result<()> { let home = tempfile::tempdir()?; write_test_config(home.path(), &repo_root)?; let editor = home.path().join("editor.sh"); - fs::write( + codex_utils_cargo_bin::write_executable( &editor, - format!( + &format!( "#!/bin/sh\n{editor_output}\n: > \"$0.ready\"\nIFS= read -r ignored\n\ {exit_screen}\nprintf 'edited draft' > \"$1\"\n", exit_screen = if editor_output.contains("1049h") { @@ -34,7 +32,6 @@ fn external_editors_keep_the_screen_and_return_the_draft() -> Result<()> { }, ), )?; - fs::set_permissions(&editor, fs::Permissions::from_mode(0o755))?; let args = if fullscreen { vec!["-c", "sandbox_mode=\"read-only\""] } else { diff --git a/codex-rs/utils/pty/Cargo.toml b/codex-rs/utils/pty/Cargo.toml index d235d6995f..f7cabb6975 100644 --- a/codex-rs/utils/pty/Cargo.toml +++ b/codex-rs/utils/pty/Cargo.toml @@ -13,6 +13,7 @@ portable-pty = { workspace = true } tokio = { workspace = true, features = ["io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] } [dev-dependencies] +codex-utils-cargo-bin = { workspace = true } ctor = { workspace = true } tempfile = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/utils/pty/src/child_tests.rs b/codex-rs/utils/pty/src/child_tests.rs index 452e34374c..d6fa63ed9a 100644 --- a/codex-rs/utils/pty/src/child_tests.rs +++ b/codex-rs/utils/pty/src/child_tests.rs @@ -215,12 +215,10 @@ async fn preserved_descriptor_can_use_the_last_slot_below_the_limit() -> anyhow: #[tokio::test] async fn process_mode_is_preserved_by_both_backends() -> anyhow::Result<()> { use crate::ProcessMode; - use std::os::unix::fs::PermissionsExt; let root = tempfile::tempdir()?; std::os::unix::fs::symlink("/bin/cat", root.path().join("server"))?; let script = root.path().join("executable-text"); - std::fs::write(&script, "exec /bin/cat\n")?; - std::fs::set_permissions(&script, std::fs::Permissions::from_mode(/*mode*/ 0o755))?; + codex_utils_cargo_bin::write_executable(&script, "exec /bin/cat\n")?; for program in ["./server", "/bin/cat", "./executable-text"] { for mode in [ ProcessMode::Inherit, @@ -368,8 +366,6 @@ async fn detached_spawn_preserves_child_path_cwd_environment_and_arg0() -> anyho #[tokio::test] async fn detached_spawn_preserves_exec_errors_and_executable_text_handling() -> anyhow::Result<()> { - use std::os::unix::fs::PermissionsExt; - let root = tempfile::tempdir()?; let program = root.path().join("script"); let mut missing = Command::new(&program); @@ -377,8 +373,7 @@ async fn detached_spawn_preserves_exec_errors_and_executable_text_handling() -> let error = missing.spawn().err().expect("missing executable must fail"); assert_eq!(error.kind(), std::io::ErrorKind::NotFound); - std::fs::write(&program, "printf '%s' \"$1\"")?; - std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755))?; + codex_utils_cargo_bin::write_executable(&program, "printf '%s' \"$1\"")?; // Match the existing libc behavior: glibc/macOS retry executable text through // a shell, whereas musl returns ENOEXEC. let mut baseline = tokio::process::Command::new(&program); diff --git a/codex-rs/utils/pty/src/linux_child_tests.rs b/codex-rs/utils/pty/src/linux_child_tests.rs index dd36e1772d..419dd130bb 100644 --- a/codex-rs/utils/pty/src/linux_child_tests.rs +++ b/codex-rs/utils/pty/src/linux_child_tests.rs @@ -1,7 +1,5 @@ //! Linux executable lookup must match the compatibility launcher's PATH semantics. -use std::os::unix::fs::PermissionsExt; - use pretty_assertions::assert_eq; use crate::Command; @@ -13,8 +11,7 @@ async fn path_search_stops_at_invalid_candidates() -> anyhow::Result<()> { std::fs::create_dir(root.path().join("bin"))?; std::os::unix::fs::symlink("loop", root.path().join("loop"))?; let script = root.path().join("bin/server"); - std::fs::write(&script, "#!/bin/sh\nprintf later\n")?; - std::fs::set_permissions(script, std::fs::Permissions::from_mode(/*mode*/ 0o755))?; + codex_utils_cargo_bin::write_executable(&script, "#!/bin/sh\nprintf later\n")?; for candidate in ["loop".to_owned(), "x".repeat(256)] { let mut command = Command::new("server"); command @@ -49,8 +46,7 @@ async fn empty_path_entries_preserve_script_argv0() -> anyhow::Result<()> { } let root = tempfile::tempdir()?; let script = root.path().join("server"); - std::fs::write(&script, "#!/bin/sh\nprintf '%s' \"$0\"\n")?; - std::fs::set_permissions(script, std::fs::Permissions::from_mode(/*mode*/ 0o755))?; + codex_utils_cargo_bin::write_executable(&script, "#!/bin/sh\nprintf '%s' \"$0\"\n")?; for path in ["", ":missing", "missing:", ".", "./"] { let mut command = Command::new("server"); command.current_dir(root.path()).env("PATH", path); diff --git a/codex-rs/utils/pty/src/macos_child_tests.rs b/codex-rs/utils/pty/src/macos_child_tests.rs index ad41af3880..5eac63e00b 100644 --- a/codex-rs/utils/pty/src/macos_child_tests.rs +++ b/codex-rs/utils/pty/src/macos_child_tests.rs @@ -10,7 +10,6 @@ use std::os::fd::FromRawFd; use std::os::fd::OwnedFd; use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStringExt; -use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::symlink; use std::os::unix::process::ExitStatusExt; use std::ptr; @@ -35,11 +34,10 @@ async fn bare_script_search_matches_child_path_and_preserves_script_spelling() - fs::create_dir(&blocked)?; fs::write(blocked.join(&program), "not executable")?; let script = bin.join(&program); - fs::write( + codex_utils_cargo_bin::write_executable( &script, "#!/bin/sh\nprintf '%s\\n' \"$0\" \"$1\" \"$MCP_TEST\"; /bin/pwd; printf diagnostic >&2; exit 23\n", )?; - fs::set_permissions(&script, fs::Permissions::from_mode(/*mode*/ 0o755))?; symlink(&script, root.path().join(&program))?; for path in [ bin.into_os_string(), @@ -79,11 +77,10 @@ async fn relative_script_preserves_paths_stdio_environment_and_process_group() - fs::create_dir_all(root.path().join("actual/bin"))?; symlink("actual/bin", root.path().join("link"))?; let script = root.path().join("actual/server"); - fs::write( + codex_utils_cargo_bin::write_executable( &script, "#!/bin/sh\nread -r input\nprintf '%s\\n' \"$0\" \"$1\" \"$2\" \"$MCP_TEST\" \"$input\"\nprintf diagnostic >&2\nexit 23\n", )?; - fs::set_permissions(script, fs::Permissions::from_mode(0o755))?; let mut command = Command::new("./link/../server"); command .current_dir(root.path()) @@ -232,8 +229,7 @@ async fn launch_failures_preserve_os_errors() -> anyhow::Result<()> { async fn executable_text_without_shebang_retains_command_fallback() -> anyhow::Result<()> { let root = tempfile::tempdir()?; let script = root.path().join("server"); - fs::write(&script, "printf '%s' \"$0\"\n")?; - fs::set_permissions(script, fs::Permissions::from_mode(0o755))?; + codex_utils_cargo_bin::write_executable(&script, "printf '%s' \"$0\"\n")?; for program in ["./server", "server"] { let mut command = Command::new(program); command diff --git a/codex-rs/utils/pty/src/macos_descriptor_tests.rs b/codex-rs/utils/pty/src/macos_descriptor_tests.rs index 7b4da64645..ad230dafa9 100644 --- a/codex-rs/utils/pty/src/macos_descriptor_tests.rs +++ b/codex-rs/utils/pty/src/macos_descriptor_tests.rs @@ -7,7 +7,6 @@ use std::io::Seek; use std::os::fd::AsRawFd; use std::os::fd::FromRawFd; use std::os::fd::OwnedFd; -use std::os::unix::fs::PermissionsExt; use std::os::unix::net::UnixStream; use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; @@ -85,8 +84,7 @@ async fn socket_stdin_preserves_bidirectional_io_and_custom_argv0() -> anyhow::R async fn native_launch_can_reject_executable_text_without_shell_fallback() -> anyhow::Result<()> { let directory = tempfile::tempdir()?; let script = directory.path().join("no-shebang"); - fs::write(&script, "printf unexpected-fallback\n")?; - fs::set_permissions(&script, fs::Permissions::from_mode(/*mode*/ 0o755))?; + codex_utils_cargo_bin::write_executable(&script, "printf unexpected-fallback\n")?; let mut command = Command::new(script); command.fallback(SpawnFallback::ReturnError); let error = command.spawn().err().expect("executable text must fail"); @@ -114,8 +112,7 @@ async fn inherited_descriptors_survive_native_and_executable_text_spawns() -> an command } else { let script = directory.path().join("executable-text"); - fs::write(&script, format!("/bin/cat {descriptor}\n"))?; - fs::set_permissions(&script, fs::Permissions::from_mode(/*mode*/ 0o755))?; + codex_utils_cargo_bin::write_executable(&script, &format!("/bin/cat {descriptor}\n"))?; Command::new(script) }; command diff --git a/codex-rs/utils/pty/tests/conpty_search_path.rs b/codex-rs/utils/pty/tests/conpty_search_path.rs index 1bb5caed35..0a1c550e14 100644 --- a/codex-rs/utils/pty/tests/conpty_search_path.rs +++ b/codex-rs/utils/pty/tests/conpty_search_path.rs @@ -42,8 +42,8 @@ fn conpty_ignores_dll_in_current_directory() -> anyhow::Result<()> { let candidate = temp_dir.join("conpty.dll"); // A kernel32 copy exports the ConPTY functions, so the old bare-name probe // would successfully load and retain this repository-controlled file. - fs::copy( - PathBuf::from(system_root) + codex_utils_cargo_bin::copy_executable( + &PathBuf::from(system_root) .join("System32") .join("kernel32.dll"), &candidate, diff --git a/codex-rs/voice-host/tests/installed_client.rs b/codex-rs/voice-host/tests/installed_client.rs index 13531ab009..d865255890 100644 --- a/codex-rs/voice-host/tests/installed_client.rs +++ b/codex-rs/voice-host/tests/installed_client.rs @@ -91,7 +91,7 @@ fn install_helper(root: &Path) -> Result { fs::write(&app, [])?; let source = cargo_bin("codex-voice-host")?; let helper = helper_dir.join(source.file_name().context("helper binary file name")?); - fs::copy(&source, &helper)?; + codex_utils_cargo_bin::copy_executable(&source, &helper)?; install_startup_libraries(helper_dir.parent().context("helper runtime directory")?)?; InstallContext::from_exe( /*is_macos*/ cfg!(target_os = "macos"), @@ -160,7 +160,7 @@ async fn installed_client_accepts_non_utf8_package_path() -> Result<()> { let app = bin.join("codex"); fs::write(&app, [])?; let source = cargo_bin("codex-voice-host")?; - fs::copy(&source, helper_dir.join(source.file_name().unwrap()))?; + codex_utils_cargo_bin::copy_executable(&source, &helper_dir.join(source.file_name().unwrap()))?; install_startup_libraries(helper_dir.parent().unwrap())?; let package = InstallContext::from_exe( /*is_macos*/ cfg!(target_os = "macos"), diff --git a/codex-rs/voice-host/tests/packaged_runtime.rs b/codex-rs/voice-host/tests/packaged_runtime.rs index 91c9daa526..2a53063ba7 100644 --- a/codex-rs/voice-host/tests/packaged_runtime.rs +++ b/codex-rs/voice-host/tests/packaged_runtime.rs @@ -48,7 +48,7 @@ async fn relocated_runtime_initializes_closes_and_rejects_duplicate_initializati let helper_source = cargo_bin("codex-voice-host")?; let name = helper_source.file_name().context("helper filename")?; fs::create_dir_all(runtime.join("bin"))?; - fs::copy(&helper_source, runtime.join("bin").join(name))?; + codex_utils_cargo_bin::copy_executable(&helper_source, &runtime.join("bin").join(name))?; fs::create_dir(root.join("bin"))?; let app_name = if cfg!(windows) { "codex.exe" } else { "codex" }; fs::write(root.join("bin").join(app_name), [])?; diff --git a/codex-rs/windows-sandbox-rs/Cargo.toml b/codex-rs/windows-sandbox-rs/Cargo.toml index 356991b1ca..2f8d9d9258 100644 --- a/codex-rs/windows-sandbox-rs/Cargo.toml +++ b/codex-rs/windows-sandbox-rs/Cargo.toml @@ -102,5 +102,6 @@ features = [ version = "0.52" [dev-dependencies] +codex-utils-cargo-bin = { workspace = true } codex-windows-sandbox-test-support = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/windows-sandbox-rs/src/setup_refresh_tests.rs b/codex-rs/windows-sandbox-rs/src/setup_refresh_tests.rs index 0a9f5d5315..51cd8cb39a 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_refresh_tests.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_refresh_tests.rs @@ -30,10 +30,10 @@ fn refresh_helper_has_no_console() -> Result<()> { let temporary = tempfile::tempdir()?; let executable = std::env::current_exe()?; let detached = temporary.path().join("detached.exe"); - fs::copy(&executable, &detached)?; - fs::copy( + codex_utils_cargo_bin::copy_executable(&executable, &detached)?; + codex_utils_cargo_bin::copy_executable( &executable, - temporary.path().join("codex-windows-sandbox-setup.exe"), + &temporary.path().join("codex-windows-sandbox-setup.exe"), )?; let output = Command::new(detached) .args(["--exact", DETACHED_TEST, "--nocapture"]) diff --git a/codex-rs/worktree/Cargo.toml b/codex-rs/worktree/Cargo.toml index 648c5d1079..05a4bdd825 100644 --- a/codex-rs/worktree/Cargo.toml +++ b/codex-rs/worktree/Cargo.toml @@ -21,4 +21,5 @@ tempfile = { workspace = true } uuid = { workspace = true, features = ["v4"] } [dev-dependencies] +codex-utils-cargo-bin = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/worktree/src/git_tests.rs b/codex-rs/worktree/src/git_tests.rs index ac565f3dfc..c1f6d33bce 100644 --- a/codex-rs/worktree/src/git_tests.rs +++ b/codex-rs/worktree/src/git_tests.rs @@ -156,14 +156,11 @@ fn checkout_does_not_execute_configured_hooks_fsmonitor_or_filters() -> Result<( #[cfg(unix)] fn write_marker_script(path: &Path, marker: &Path) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - let marker = marker.to_str().context("marker path is not valid UTF-8")?; - fs::write( + codex_utils_cargo_bin::write_executable( path, - format!("#!/bin/sh\nprintf executed > '{marker}'\ncat\n"), + &format!("#!/bin/sh\nprintf executed > '{marker}'\ncat\n"), )?; - fs::set_permissions(path, fs::Permissions::from_mode(0o755))?; Ok(()) }