Skip to main content

codex/
bundled_binary.rs

1use std::{
2    env, fs as std_fs,
3    path::{Path, PathBuf},
4};
5
6#[cfg(unix)]
7use std::os::unix::fs::PermissionsExt;
8
9use thiserror::Error;
10
11/// Specification for resolving an app-bundled Codex binary.
12///
13/// Callers supply a bundle root plus the pinned version they expect. Platform
14/// defaults to the current target triple label (e.g., `darwin-arm64` or
15/// `linux-x64`) but can be overridden when hosts manage their own layout.
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct BundledBinarySpec<'a> {
18    /// Root containing `<platform>/<version>/codex` slices managed by the host.
19    pub bundle_root: &'a Path,
20    /// Pinned Codex version directory to resolve (semantic version or channel/build id).
21    pub version: &'a str,
22    /// Optional platform label override; defaults to [`default_bundled_platform_label`].
23    pub platform: Option<&'a str>,
24}
25
26/// Resolved bundled Codex binary details.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct BundledBinary {
29    /// Canonicalized path to the bundled Codex binary (`codex` or `codex.exe`).
30    pub binary_path: PathBuf,
31    /// Platform slice resolved under the bundle root.
32    pub platform: String,
33    /// Version slice resolved under the platform directory.
34    pub version: String,
35}
36
37/// Errors that may occur while resolving a bundled Codex binary.
38#[derive(Debug, Error)]
39pub enum BundledBinaryError {
40    #[error("bundled Codex version cannot be empty")]
41    EmptyVersion,
42    #[error("bundled Codex platform label cannot be empty")]
43    EmptyPlatform,
44    #[error("bundle root `{bundle_root}` does not exist or is unreadable")]
45    BundleRootUnreadable {
46        bundle_root: PathBuf,
47        #[source]
48        source: std::io::Error,
49    },
50    #[error("bundle root `{bundle_root}` is not a directory")]
51    BundleRootNotDirectory { bundle_root: PathBuf },
52    #[error("bundle platform directory `{platform_dir}` for `{platform}` does not exist or is unreadable")]
53    PlatformUnreadable {
54        platform: String,
55        platform_dir: PathBuf,
56        #[source]
57        source: std::io::Error,
58    },
59    #[error("bundle platform directory `{platform_dir}` for `{platform}` is not a directory")]
60    PlatformNotDirectory {
61        platform: String,
62        platform_dir: PathBuf,
63    },
64    #[error(
65        "bundle version directory `{version_dir}` for `{version}` does not exist or is unreadable"
66    )]
67    VersionUnreadable {
68        version: String,
69        version_dir: PathBuf,
70        #[source]
71        source: std::io::Error,
72    },
73    #[error("bundle version directory `{version_dir}` for `{version}` is not a directory")]
74    VersionNotDirectory {
75        version: String,
76        version_dir: PathBuf,
77    },
78    #[error("bundled Codex binary `{binary}` is missing or unreadable")]
79    BinaryUnreadable {
80        binary: PathBuf,
81        #[source]
82        source: std::io::Error,
83    },
84    #[error("bundled Codex binary `{binary}` is not a file")]
85    BinaryNotFile { binary: PathBuf },
86    #[error("bundled Codex binary `{binary}` is not executable")]
87    BinaryNotExecutable { binary: PathBuf },
88    #[error("failed to canonicalize bundled Codex binary `{path}`: {source}")]
89    Canonicalize {
90        path: PathBuf,
91        #[source]
92        source: std::io::Error,
93    },
94}
95
96/// Resolves a bundled Codex binary under `<bundle_root>/<platform>/<version>/`.
97///
98/// The helper never consults `PATH` or `CODEX_BINARY`; missing slices are hard
99/// errors. The resolved path is canonicalized and should be passed to
100/// [`CodexClientBuilder::binary`] to keep behavior isolated from any global
101/// Codex install.
102pub fn resolve_bundled_binary(
103    spec: BundledBinarySpec<'_>,
104) -> Result<BundledBinary, BundledBinaryError> {
105    let platform = match spec.platform {
106        Some(label) => {
107            super::normalize_non_empty(label).ok_or(BundledBinaryError::EmptyPlatform)?
108        }
109        None => default_bundled_platform_label(),
110    };
111    let version =
112        super::normalize_non_empty(spec.version).ok_or(BundledBinaryError::EmptyVersion)?;
113
114    require_directory(
115        spec.bundle_root,
116        |source| BundledBinaryError::BundleRootUnreadable {
117            bundle_root: spec.bundle_root.to_path_buf(),
118            source,
119        },
120        || BundledBinaryError::BundleRootNotDirectory {
121            bundle_root: spec.bundle_root.to_path_buf(),
122        },
123    )?;
124
125    let platform_dir = spec.bundle_root.join(&platform);
126    require_directory(
127        &platform_dir,
128        |source| BundledBinaryError::PlatformUnreadable {
129            platform: platform.clone(),
130            platform_dir: platform_dir.clone(),
131            source,
132        },
133        || BundledBinaryError::PlatformNotDirectory {
134            platform: platform.clone(),
135            platform_dir: platform_dir.clone(),
136        },
137    )?;
138
139    let version_dir = platform_dir.join(&version);
140    require_directory(
141        &version_dir,
142        |source| BundledBinaryError::VersionUnreadable {
143            version: version.clone(),
144            version_dir: version_dir.clone(),
145            source,
146        },
147        || BundledBinaryError::VersionNotDirectory {
148            version: version.clone(),
149            version_dir: version_dir.clone(),
150        },
151    )?;
152
153    let binary_path = version_dir.join(bundled_binary_filename(&platform));
154    let metadata =
155        std_fs::metadata(&binary_path).map_err(|source| BundledBinaryError::BinaryUnreadable {
156            binary: binary_path.clone(),
157            source,
158        })?;
159    if !metadata.is_file() {
160        return Err(BundledBinaryError::BinaryNotFile {
161            binary: binary_path.clone(),
162        });
163    }
164    ensure_executable(&metadata, &binary_path)?;
165
166    let canonical =
167        std_fs::canonicalize(&binary_path).map_err(|source| BundledBinaryError::Canonicalize {
168            path: binary_path.clone(),
169            source,
170        })?;
171
172    Ok(BundledBinary {
173        binary_path: canonical,
174        platform,
175        version,
176    })
177}
178
179/// Default bundled platform label for the current target (e.g., `darwin-arm64`, `linux-x64`, `windows-x64`).
180pub fn default_bundled_platform_label() -> String {
181    let os = match env::consts::OS {
182        "macos" => "darwin",
183        other => other,
184    };
185    let arch = match env::consts::ARCH {
186        "x86_64" => "x64",
187        "aarch64" => "arm64",
188        other => other,
189    };
190    format!("{os}-{arch}")
191}
192
193fn require_directory(
194    path: &Path,
195    on_read_error: impl FnOnce(std::io::Error) -> BundledBinaryError,
196    on_wrong_type: impl FnOnce() -> BundledBinaryError,
197) -> Result<(), BundledBinaryError> {
198    let metadata = std_fs::metadata(path).map_err(on_read_error)?;
199    if !metadata.is_dir() {
200        return Err(on_wrong_type());
201    }
202    Ok(())
203}
204
205fn ensure_executable(metadata: &std_fs::Metadata, binary: &Path) -> Result<(), BundledBinaryError> {
206    if binary_is_executable(metadata) {
207        return Ok(());
208    }
209    Err(BundledBinaryError::BinaryNotExecutable {
210        binary: binary.to_path_buf(),
211    })
212}
213
214fn binary_is_executable(metadata: &std_fs::Metadata) -> bool {
215    #[cfg(unix)]
216    {
217        metadata.permissions().mode() & 0o111 != 0
218    }
219    #[cfg(not(unix))]
220    {
221        // Windows does not use executable bits; existence is sufficient.
222        true
223    }
224}
225
226pub(super) fn bundled_binary_filename(platform: &str) -> &'static str {
227    if platform.to_ascii_lowercase().contains("windows") {
228        "codex.exe"
229    } else {
230        "codex"
231    }
232}