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#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct BundledBinarySpec<'a> {
18 pub bundle_root: &'a Path,
20 pub version: &'a str,
22 pub platform: Option<&'a str>,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct BundledBinary {
29 pub binary_path: PathBuf,
31 pub platform: String,
33 pub version: String,
35}
36
37#[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
96pub 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
179pub 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 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}