package: include zsh fork in Codex package

Teach the Codex package builder to fetch the prebuilt zsh fork from a checked-in DotSlash manifest and install it under codex-resources/zsh/bin/zsh when an artifact is available for the package target.

Generalize the DotSlash download/cache/verify helper previously embedded in ripgrep packaging so additional checked-in DotSlash manifests can use the same SHA-256 and size validation path. Add install-context support for locating the bundled zsh fork and thread that path through config loading as the lowest-precedence zsh_path default, preserving explicit CLI/profile/global config values.

Also avoid preserving platform-specific file metadata when copying executables into the package directory so package smoke tests can use macOS system binaries as inputs.
This commit is contained in:
Michael Bolin
2026-05-20 14:54:06 -07:00
parent 896ee672cc
commit 9ccfe3cb29
21 changed files with 468 additions and 176 deletions
+3
View File
@@ -1905,6 +1905,7 @@ dependencies = [
"codex-git-utils",
"codex-guardian",
"codex-hooks",
"codex-install-context",
"codex-login",
"codex-mcp",
"codex-memories-extension",
@@ -2710,6 +2711,7 @@ dependencies = [
"codex-core",
"codex-feedback",
"codex-git-utils",
"codex-install-context",
"codex-login",
"codex-model-provider-info",
"codex-otel",
@@ -3141,6 +3143,7 @@ dependencies = [
"codex-core",
"codex-exec-server",
"codex-extension-api",
"codex-install-context",
"codex-login",
"codex-protocol",
"codex-shell-command",
+1
View File
@@ -45,6 +45,7 @@ codex-guardian = { workspace = true }
codex-git-utils = { workspace = true }
codex-file-watcher = { workspace = true }
codex-hooks = { workspace = true }
codex-install-context = { workspace = true }
codex-otel = { workspace = true }
codex-plugin = { workspace = true }
codex-shell-command = { workspace = true }
+20
View File
@@ -9,6 +9,7 @@ use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_exec_server::LOCAL_FS;
use codex_features::feature_for_key;
use codex_install_context::InstallContext;
use codex_login::AuthManager;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -159,6 +160,7 @@ impl ConfigManager {
.await?;
self.apply_runtime_feature_enablement(&mut config);
self.apply_arg0_paths(&mut config);
apply_default_zsh_path(&mut config);
Ok(config)
}
@@ -180,6 +182,7 @@ impl ConfigManager {
}
self.apply_runtime_feature_enablement(&mut config);
self.apply_arg0_paths(&mut config);
apply_default_zsh_path(&mut config);
Ok(config)
}
@@ -219,6 +222,11 @@ impl ConfigManager {
typesafe_overrides: ConfigOverrides,
fallback_cwd: Option<PathBuf>,
) -> std::io::Result<Config> {
let mut typesafe_overrides = typesafe_overrides;
if typesafe_overrides.default_zsh_path.is_none() {
typesafe_overrides.default_zsh_path = default_zsh_path();
}
let merged_cli_overrides = cli_overrides
.iter()
.cloned()
@@ -319,6 +327,18 @@ impl ConfigManager {
}
}
fn apply_default_zsh_path(config: &mut Config) {
if config.zsh_path.is_none() {
config.zsh_path = default_zsh_path();
}
}
fn default_zsh_path() -> Option<PathBuf> {
InstallContext::current()
.bundled_zsh_path()
.map(AbsolutePathBuf::into_path_buf)
}
pub(crate) fn protected_feature_keys(config_layer_stack: &ConfigLayerStack) -> BTreeSet<String> {
let mut protected_features = config_layer_stack
.effective_config()
+1
View File
@@ -507,6 +507,7 @@ fn config_overrides_from_interactive(
codex_self_exe: arg0_paths.codex_self_exe.clone(),
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
default_zsh_path: crate::default_zsh_path(),
show_raw_agent_reasoning: interactive.oss.then_some(true),
additional_writable_roots: interactive.add_dir.clone(),
..Default::default()
+8
View File
@@ -74,6 +74,7 @@ use codex_core::config::resolve_profile_v2_config_path;
use codex_features::FEATURES;
use codex_features::Stage;
use codex_features::is_known_feature_key;
use codex_install_context::InstallContext;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::read_codex_access_token_from_env;
@@ -1581,6 +1582,12 @@ async fn load_exec_server_config(
.await?)
}
pub(crate) fn default_zsh_path() -> Option<PathBuf> {
InstallContext::current()
.bundled_zsh_path()
.map(AbsolutePathBuf::into_path_buf)
}
async fn load_exec_server_remote_auth(
config: &codex_core::config::Config,
missing_auth_error: &'static str,
@@ -1716,6 +1723,7 @@ async fn run_debug_prompt_input_command(
codex_self_exe: arg0_paths.codex_self_exe,
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe,
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe,
default_zsh_path: default_zsh_path(),
show_raw_agent_reasoning: shared.oss.then_some(true),
ephemeral: Some(true),
bypass_hook_trust: shared.bypass_hook_trust.then_some(true),
+50
View File
@@ -4382,6 +4382,56 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<
Ok(())
}
#[tokio::test]
async fn default_zsh_path_is_lowest_precedence() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let configured_zsh_path = codex_home.path().join("configured-zsh");
let default_zsh_path = codex_home.path().join("packaged-zsh");
let override_zsh_path = codex_home.path().join("override-zsh");
let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides {
default_zsh_path: Some(default_zsh_path.clone()),
..Default::default()
},
codex_home.abs(),
)
.await?;
assert_eq!(config.zsh_path, Some(default_zsh_path.clone()));
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
zsh_path: Some(configured_zsh_path.abs()),
..Default::default()
},
ConfigOverrides {
default_zsh_path: Some(default_zsh_path.clone()),
..Default::default()
},
codex_home.abs(),
)
.await?;
assert_eq!(config.zsh_path, Some(configured_zsh_path.clone()));
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
zsh_path: Some(configured_zsh_path.abs()),
..Default::default()
},
ConfigOverrides {
zsh_path: Some(override_zsh_path.clone()),
default_zsh_path: Some(default_zsh_path),
..Default::default()
},
codex_home.abs(),
)
.await?;
assert_eq!(config.zsh_path, Some(override_zsh_path));
Ok(())
}
#[tokio::test]
async fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
+5 -1
View File
@@ -1366,6 +1366,7 @@ impl Config {
cfg,
ConfigOverrides {
cwd: Some(self.cwd.to_path_buf()),
default_zsh_path: refreshed_config.zsh_path.clone(),
..Default::default()
},
refreshed_config.codex_home.clone(),
@@ -2072,6 +2073,7 @@ pub struct ConfigOverrides {
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub main_execve_wrapper_exe: Option<PathBuf>,
pub zsh_path: Option<PathBuf>,
pub default_zsh_path: Option<PathBuf>,
pub base_instructions: Option<String>,
pub developer_instructions: Option<String>,
pub personality: Option<Personality>,
@@ -2469,6 +2471,7 @@ impl Config {
codex_linux_sandbox_exe,
main_execve_wrapper_exe,
zsh_path: zsh_path_override,
default_zsh_path,
base_instructions,
developer_instructions,
personality,
@@ -3235,7 +3238,8 @@ impl Config {
let compact_prompt = compact_prompt.or(file_compact_prompt);
let zsh_path = zsh_path_override
.or(config_profile.zsh_path.map(Into::into))
.or(cfg.zsh_path.map(Into::into));
.or(cfg.zsh_path.map(Into::into))
.or(default_zsh_path);
let review_model = override_review_model.or(cfg.review_model);
+1
View File
@@ -32,6 +32,7 @@ codex-config = { workspace = true }
codex-core = { workspace = true }
codex-feedback = { workspace = true }
codex-git-utils = { workspace = true }
codex-install-context = { workspace = true }
codex-login = { workspace = true }
codex-model-provider-info = { workspace = true }
codex-otel = { workspace = true }
+4
View File
@@ -70,6 +70,7 @@ use codex_core::format_exec_policy_error_with_source;
use codex_core::path_utils;
use codex_feedback::CodexFeedback;
use codex_git_utils::get_git_repo_root;
use codex_install_context::InstallContext;
use codex_login::AuthConfig;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_login::default_client::set_default_originator;
@@ -424,6 +425,9 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
zsh_path: None,
default_zsh_path: InstallContext::current()
.bundled_zsh_path()
.map(AbsolutePathBuf::into_path_buf),
base_instructions: None,
developer_instructions: None,
personality: None,
+35
View File
@@ -11,6 +11,7 @@ const PATH_DIRNAME: &str = "codex-path";
const RELEASES_DIRNAME: &str = "releases";
const RESOURCES_DIRNAME: &str = "codex-resources";
const STANDALONE_PACKAGES_DIRNAME: &str = "standalone";
const ZSH_DIRNAME: &str = "zsh";
static INSTALL_CONTEXT: OnceLock<InstallContext> = OnceLock::new();
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -166,6 +167,18 @@ impl InstallContext {
None
}
pub fn bundled_zsh_path(&self) -> Option<AbsolutePathBuf> {
if cfg!(windows) {
None
} else {
self.bundled_resource(zsh_resource_path())
}
}
pub fn bundled_zsh_bin_dir(&self) -> Option<AbsolutePathBuf> {
self.bundled_zsh_path()?.parent()
}
}
impl CodexPackageLayout {
@@ -260,6 +273,10 @@ fn default_rg_command() -> PathBuf {
}
}
fn zsh_resource_path() -> PathBuf {
PathBuf::from(ZSH_DIRNAME).join(BIN_DIRNAME).join("zsh")
}
#[cfg(test)]
mod tests {
use super::*;
@@ -345,6 +362,11 @@ mod tests {
fs::write(&exe_path, "")?;
fs::write(resources_dir.join(TEST_RESOURCE_NAME), "")?;
fs::write(path_dir.join(default_rg_command()), "")?;
if !cfg!(windows) {
let zsh_path = resources_dir.join(zsh_resource_path());
fs::create_dir_all(zsh_path.parent().expect("zsh path should have parent"))?;
fs::write(&zsh_path, "")?;
}
let canonical_package_dir =
AbsolutePathBuf::from_absolute_path(package_dir.path().canonicalize()?)?;
let canonical_bin_dir = AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?;
@@ -382,6 +404,19 @@ mod tests {
context.bundled_resource(TEST_RESOURCE_NAME),
Some(canonical_resources_dir.join(TEST_RESOURCE_NAME))
);
if cfg!(windows) {
assert_eq!(context.bundled_zsh_path(), None);
assert_eq!(context.bundled_zsh_bin_dir(), None);
} else {
assert_eq!(
context.bundled_zsh_path(),
Some(canonical_resources_dir.join(zsh_resource_path()))
);
assert_eq!(
context.bundled_zsh_bin_dir(),
Some(canonical_resources_dir.join(ZSH_DIRNAME).join(BIN_DIRNAME))
);
}
Ok(())
}
+2 -1
View File
@@ -23,8 +23,10 @@ codex-config = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-extension-api = { workspace = true }
codex-install-context = { workspace = true }
codex-login = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-cli = { workspace = true }
codex-utils-json-to-toml = { workspace = true }
rmcp = { workspace = true }
@@ -43,7 +45,6 @@ tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
[dev-dependencies]
codex-utils-absolute-path = { workspace = true }
codex-shell-command = { workspace = true }
core_test_support = { workspace = true }
mcp_test_support = { workspace = true }
@@ -4,6 +4,7 @@ use codex_arg0::Arg0DispatchPaths;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_install_context::InstallContext;
use codex_protocol::ThreadId;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::protocol::AskForApproval;
@@ -180,6 +181,9 @@ impl CodexToolCallParam {
codex_self_exe: arg0_paths.codex_self_exe.clone(),
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
default_zsh_path: InstallContext::current()
.bundled_zsh_path()
.map(codex_utils_absolute_path::AbsolutePathBuf::into_path_buf),
base_instructions,
developer_instructions,
compact_prompt,
+7
View File
@@ -1853,6 +1853,13 @@ async fn load_config_or_exit_with_fallback_cwd(
strict_config: bool,
fallback_cwd: Option<PathBuf>,
) -> Config {
let mut overrides = overrides;
if overrides.default_zsh_path.is_none() {
overrides.default_zsh_path = codex_install_context::InstallContext::current()
.bundled_zsh_path()
.map(AbsolutePathBuf::into_path_buf);
}
#[allow(clippy::print_stderr)]
match ConfigBuilder::default()
.cli_overrides(cli_kv_overrides)