1use std::ffi::OsStr;
2use std::path::Path;
3use std::path::PathBuf;
4use std::sync::OnceLock;
5
6use codex_utils_absolute_path::AbsolutePathBuf;
7
8const BIN_DIRNAME: &str = "bin";
9const PACKAGE_METADATA_FILENAME: &str = "codex-package.json";
10const PATH_DIRNAME: &str = "codex-path";
11const RELEASES_DIRNAME: &str = "releases";
12const RESOURCES_DIRNAME: &str = "codex-resources";
13const STANDALONE_PACKAGES_DIRNAME: &str = "standalone";
14const ZSH_DIRNAME: &str = "zsh";
15static INSTALL_CONTEXT: OnceLock<InstallContext> = OnceLock::new();
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum StandalonePlatform {
19 Unix,
20 Windows,
21}
22
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct CodexPackageLayout {
25 pub package_dir: AbsolutePathBuf,
27 pub bin_dir: AbsolutePathBuf,
29 pub resources_dir: Option<AbsolutePathBuf>,
31 pub path_dir: Option<AbsolutePathBuf>,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct InstallContext {
37 pub method: InstallMethod,
38 pub package_layout: Option<CodexPackageLayout>,
39}
40
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub enum InstallMethod {
43 Standalone {
44 release_dir: AbsolutePathBuf,
50 resources_dir: Option<AbsolutePathBuf>,
52 platform: StandalonePlatform,
54 },
55 Npm,
57 Bun,
59 Pnpm,
61 Brew,
63 Other,
68}
69
70impl InstallContext {
71 pub fn from_exe(
72 is_macos: bool,
73 current_exe: Option<&Path>,
74 method_override: Option<InstallMethod>,
75 ) -> Self {
76 let codex_home = codex_utils_home_dir::find_codex_home().ok();
77 Self::from_exe_with_codex_home(
78 is_macos,
79 current_exe,
80 method_override,
81 codex_home.as_deref(),
82 )
83 }
84
85 fn from_exe_with_codex_home(
86 is_macos: bool,
87 current_exe: Option<&Path>,
88 method_override: Option<InstallMethod>,
89 codex_home: Option<&Path>,
90 ) -> Self {
91 let package_layout = current_exe.and_then(CodexPackageLayout::from_exe);
92 let method = if let Some(method) = method_override {
93 method
94 } else if let Some(exe_path) = current_exe {
95 install_method_from_exe(exe_path, codex_home, package_layout.as_ref(), is_macos)
96 } else {
97 InstallMethod::Other
98 };
99
100 Self {
101 method,
102 package_layout,
103 }
104 }
105
106 pub fn current() -> &'static Self {
107 INSTALL_CONTEXT.get_or_init(|| {
108 let current_exe = std::env::current_exe().ok();
109 let method_override = if std::env::var_os("CODEX_MANAGED_BY_PNPM").is_some() {
110 Some(InstallMethod::Pnpm)
111 } else if std::env::var_os("CODEX_MANAGED_BY_NPM").is_some() {
112 Some(InstallMethod::Npm)
113 } else if std::env::var_os("CODEX_MANAGED_BY_BUN").is_some() {
114 Some(InstallMethod::Bun)
115 } else {
116 None
117 };
118 Self::from_exe(
119 cfg!(target_os = "macos"),
120 current_exe.as_deref(),
121 method_override,
122 )
123 })
124 }
125
126 pub fn rg_command(&self) -> PathBuf {
127 if let Some(package_layout) = &self.package_layout
128 && let Some(path_dir) = &package_layout.path_dir
129 {
130 let bundled_rg = path_dir.join(default_rg_command());
131 if bundled_rg.is_file() {
132 return bundled_rg.into_path_buf();
133 }
134 }
135
136 if let InstallMethod::Standalone {
137 resources_dir: Some(resources_dir),
138 ..
139 } = &self.method
140 {
141 let bundled_rg = resources_dir.join(default_rg_command());
142 if bundled_rg.is_file() {
143 return bundled_rg.into_path_buf();
144 }
145 }
146
147 default_rg_command()
148 }
149
150 pub fn bundled_resource(&self, file_name: impl AsRef<Path>) -> Option<AbsolutePathBuf> {
151 if let Some(package_layout) = &self.package_layout
152 && let Some(resources_dir) = &package_layout.resources_dir
153 {
154 let resource = resources_dir.join(file_name.as_ref());
155 if resource.is_file() {
156 return Some(resource);
157 }
158 }
159
160 if let InstallMethod::Standalone {
161 resources_dir: Some(resources_dir),
162 ..
163 } = &self.method
164 {
165 let resource = resources_dir.join(file_name);
166 if resource.is_file() {
167 return Some(resource);
168 }
169 }
170
171 None
172 }
173
174 pub fn bundled_zsh_path(&self) -> Option<AbsolutePathBuf> {
175 if cfg!(windows) {
176 None
177 } else {
178 self.bundled_resource(zsh_resource_path())
179 }
180 }
181
182 pub fn bundled_zsh_bin_dir(&self) -> Option<AbsolutePathBuf> {
183 self.bundled_zsh_path()?.parent()
184 }
185}
186
187impl CodexPackageLayout {
188 fn from_exe(exe_path: &Path) -> Option<Self> {
189 let canonical_exe = canonical_absolute_path(exe_path)?;
190 let exe_dir = canonical_exe.parent()?;
191 match exe_dir.file_name() {
192 Some(name) if name == OsStr::new(BIN_DIRNAME) => Self::from_package_bin_dir(exe_dir),
193 Some(_) | None => None,
194 }
195 }
196
197 fn from_package_bin_dir(bin_dir: AbsolutePathBuf) -> Option<Self> {
198 let package_dir = bin_dir.parent()?;
199 if !package_dir.join(PACKAGE_METADATA_FILENAME).is_file() {
200 return None;
201 }
202
203 Some(Self {
204 resources_dir: existing_dir(package_dir.join(RESOURCES_DIRNAME)),
205 path_dir: existing_dir(package_dir.join(PATH_DIRNAME)),
206 package_dir,
207 bin_dir,
208 })
209 }
210}
211
212fn install_method_from_exe(
213 exe_path: &Path,
214 codex_home: Option<&Path>,
215 package_layout: Option<&CodexPackageLayout>,
216 is_macos: bool,
217) -> InstallMethod {
218 if let Some(standalone_method) = standalone_install_method(exe_path, codex_home, package_layout)
219 {
220 return standalone_method;
221 }
222
223 if is_macos && (exe_path.starts_with("/opt/homebrew") || exe_path.starts_with("/usr/local")) {
224 InstallMethod::Brew
225 } else {
226 InstallMethod::Other
227 }
228}
229
230fn standalone_install_method(
231 exe_path: &Path,
232 codex_home: Option<&Path>,
233 package_layout: Option<&CodexPackageLayout>,
234) -> Option<InstallMethod> {
235 let canonical_codex_home = canonical_absolute_path(codex_home?)?;
236 let release_dir = if let Some(package_layout) = package_layout {
237 package_layout.package_dir.clone()
238 } else {
239 canonical_absolute_path(exe_path)?.parent()?
240 };
241 let releases_root = canonical_codex_home
242 .join("packages")
243 .join(STANDALONE_PACKAGES_DIRNAME)
244 .join(RELEASES_DIRNAME);
245 if !release_dir.starts_with(releases_root.as_path()) {
246 return None;
247 }
248
249 let resources_dir = release_dir.join(RESOURCES_DIRNAME);
250 Some(InstallMethod::Standalone {
251 release_dir,
252 resources_dir: resources_dir.is_dir().then_some(resources_dir),
253 platform: standalone_platform(),
254 })
255}
256
257fn canonical_absolute_path(path: &Path) -> Option<AbsolutePathBuf> {
258 let canonical_path = std::fs::canonicalize(path).ok()?;
259 AbsolutePathBuf::from_absolute_path(canonical_path).ok()
260}
261
262fn standalone_platform() -> StandalonePlatform {
263 if cfg!(windows) {
264 StandalonePlatform::Windows
265 } else {
266 StandalonePlatform::Unix
267 }
268}
269
270fn existing_dir(path: AbsolutePathBuf) -> Option<AbsolutePathBuf> {
271 path.is_dir().then_some(path)
272}
273
274fn default_rg_command() -> PathBuf {
275 if cfg!(windows) {
276 PathBuf::from("rg.exe")
277 } else {
278 PathBuf::from("rg")
279 }
280}
281
282fn zsh_resource_path() -> PathBuf {
283 PathBuf::from(ZSH_DIRNAME).join(BIN_DIRNAME).join("zsh")
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use pretty_assertions::assert_eq;
290 use std::fs;
291
292 const TEST_RESOURCE_NAME: &str = "codex-test-helper";
293
294 #[test]
295 fn detects_standalone_install_from_release_layout() -> std::io::Result<()> {
296 let codex_home = tempfile::tempdir()?;
297 let release_dir = codex_home
298 .path()
299 .join("packages/standalone/releases/1.2.3-x86_64-unknown-linux-musl");
300 let resources_dir = release_dir.join(RESOURCES_DIRNAME);
301 fs::create_dir_all(&resources_dir)?;
302 let exe_path = release_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
303 fs::write(&exe_path, "")?;
304 fs::write(resources_dir.join(default_rg_command()), "")?;
305 fs::write(resources_dir.join(TEST_RESOURCE_NAME), "")?;
306 let canonical_release_dir =
307 AbsolutePathBuf::from_absolute_path(release_dir.canonicalize()?)?;
308 let canonical_resources_dir =
309 AbsolutePathBuf::from_absolute_path(resources_dir.canonicalize()?)?;
310
311 let context = InstallContext::from_exe_with_codex_home(
312 false,
313 Some(&exe_path),
314 None,
315 Some(codex_home.path()),
316 );
317 assert_eq!(
318 context,
319 InstallContext {
320 method: InstallMethod::Standalone {
321 release_dir: canonical_release_dir,
322 resources_dir: Some(canonical_resources_dir.clone()),
323 platform: standalone_platform(),
324 },
325 package_layout: None,
326 }
327 );
328 assert_eq!(
329 context.bundled_resource(TEST_RESOURCE_NAME),
330 Some(canonical_resources_dir.join(TEST_RESOURCE_NAME))
331 );
332 Ok(())
333 }
334
335 #[test]
336 fn standalone_rg_falls_back_when_resources_are_missing() -> std::io::Result<()> {
337 let codex_home = tempfile::tempdir()?;
338 let release_dir = codex_home
339 .path()
340 .join("packages/standalone/releases/1.2.3-x86_64-unknown-linux-musl");
341 fs::create_dir_all(&release_dir)?;
342 let exe_path = release_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
343 fs::write(&exe_path, "")?;
344
345 let context = InstallContext::from_exe_with_codex_home(
346 false,
347 Some(&exe_path),
348 None,
349 Some(codex_home.path()),
350 );
351 assert_eq!(context.rg_command(), default_rg_command());
352 Ok(())
353 }
354
355 #[test]
356 fn detects_package_layout_independently_from_install_method() -> std::io::Result<()> {
357 let package_dir = tempfile::tempdir()?;
358 let bin_dir = package_dir.path().join(BIN_DIRNAME);
359 let resources_dir = package_dir.path().join(RESOURCES_DIRNAME);
360 let path_dir = package_dir.path().join(PATH_DIRNAME);
361 fs::create_dir_all(&bin_dir)?;
362 fs::create_dir_all(&resources_dir)?;
363 fs::create_dir_all(&path_dir)?;
364 fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?;
365 let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
366 fs::write(&exe_path, "")?;
367 fs::write(resources_dir.join(TEST_RESOURCE_NAME), "")?;
368 fs::write(path_dir.join(default_rg_command()), "")?;
369 if !cfg!(windows) {
370 let zsh_path = resources_dir.join(zsh_resource_path());
371 fs::create_dir_all(zsh_path.parent().expect("zsh path should have parent"))?;
372 fs::write(&zsh_path, "")?;
373 }
374 let canonical_package_dir =
375 AbsolutePathBuf::from_absolute_path(package_dir.path().canonicalize()?)?;
376 let canonical_bin_dir = AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?;
377 let canonical_resources_dir =
378 AbsolutePathBuf::from_absolute_path(resources_dir.canonicalize()?)?;
379 let canonical_path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?;
380 let package_layout = CodexPackageLayout {
381 package_dir: canonical_package_dir,
382 bin_dir: canonical_bin_dir,
383 resources_dir: Some(canonical_resources_dir.clone()),
384 path_dir: Some(canonical_path_dir.clone()),
385 };
386
387 let context = InstallContext::from_exe_with_codex_home(
388 false,
389 Some(&exe_path),
390 None,
391 None,
392 );
393 assert_eq!(
394 context,
395 InstallContext {
396 method: InstallMethod::Other,
397 package_layout: Some(package_layout),
398 }
399 );
400 assert_eq!(
401 context.rg_command(),
402 canonical_path_dir
403 .join(default_rg_command())
404 .into_path_buf()
405 );
406 assert_eq!(
407 context.bundled_resource(TEST_RESOURCE_NAME),
408 Some(canonical_resources_dir.join(TEST_RESOURCE_NAME))
409 );
410 if cfg!(windows) {
411 assert_eq!(context.bundled_zsh_path(), None);
412 assert_eq!(context.bundled_zsh_bin_dir(), None);
413 } else {
414 assert_eq!(
415 context.bundled_zsh_path(),
416 Some(canonical_resources_dir.join(zsh_resource_path()))
417 );
418 assert_eq!(
419 context.bundled_zsh_bin_dir(),
420 Some(canonical_resources_dir.join(ZSH_DIRNAME).join(BIN_DIRNAME))
421 );
422 }
423 Ok(())
424 }
425
426 #[test]
427 fn standalone_package_layout_keeps_standalone_install_method() -> std::io::Result<()> {
428 let codex_home = tempfile::tempdir()?;
429 let package_dir = codex_home
430 .path()
431 .join("packages/standalone/releases/1.2.3-x86_64-unknown-linux-musl");
432 let bin_dir = package_dir.join(BIN_DIRNAME);
433 let resources_dir = package_dir.join(RESOURCES_DIRNAME);
434 let path_dir = package_dir.join(PATH_DIRNAME);
435 fs::create_dir_all(&bin_dir)?;
436 fs::create_dir_all(&resources_dir)?;
437 fs::create_dir_all(&path_dir)?;
438 fs::write(package_dir.join(PACKAGE_METADATA_FILENAME), "{}")?;
439 let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
440 fs::write(&exe_path, "")?;
441 fs::write(resources_dir.join(TEST_RESOURCE_NAME), "")?;
442 fs::write(path_dir.join(default_rg_command()), "")?;
443 let canonical_package_dir =
444 AbsolutePathBuf::from_absolute_path(package_dir.canonicalize()?)?;
445 let canonical_bin_dir = AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?;
446 let canonical_resources_dir =
447 AbsolutePathBuf::from_absolute_path(resources_dir.canonicalize()?)?;
448 let canonical_path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?;
449
450 let context = InstallContext::from_exe_with_codex_home(
451 false,
452 Some(&exe_path),
453 None,
454 Some(codex_home.path()),
455 );
456 assert_eq!(
457 context,
458 InstallContext {
459 method: InstallMethod::Standalone {
460 release_dir: canonical_package_dir.clone(),
461 resources_dir: Some(canonical_resources_dir.clone()),
462 platform: standalone_platform(),
463 },
464 package_layout: Some(CodexPackageLayout {
465 package_dir: canonical_package_dir,
466 bin_dir: canonical_bin_dir,
467 resources_dir: Some(canonical_resources_dir.clone()),
468 path_dir: Some(canonical_path_dir.clone()),
469 }),
470 }
471 );
472 assert_eq!(
473 context.rg_command(),
474 canonical_path_dir
475 .join(default_rg_command())
476 .into_path_buf()
477 );
478 assert_eq!(
479 context.bundled_resource(TEST_RESOURCE_NAME),
480 Some(canonical_resources_dir.join(TEST_RESOURCE_NAME))
481 );
482 Ok(())
483 }
484
485 #[test]
486 fn npm_managed_package_keeps_package_layout() -> std::io::Result<()> {
487 let package_dir = tempfile::tempdir()?;
488 let bin_dir = package_dir.path().join(BIN_DIRNAME);
489 let path_dir = package_dir.path().join(PATH_DIRNAME);
490 fs::create_dir_all(&bin_dir)?;
491 fs::create_dir_all(&path_dir)?;
492 fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?;
493 let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
494 fs::write(&exe_path, "")?;
495 fs::write(path_dir.join(default_rg_command()), "")?;
496 let canonical_path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?;
497
498 let context = InstallContext::from_exe(
499 false,
500 Some(&exe_path),
501 Some(InstallMethod::Npm),
502 );
503 assert_eq!(context.method, InstallMethod::Npm);
504 assert!(context.package_layout.is_some());
505 assert_eq!(
506 context.rg_command(),
507 canonical_path_dir
508 .join(default_rg_command())
509 .into_path_buf()
510 );
511 Ok(())
512 }
513
514 #[test]
515 fn standalone_package_rg_falls_back_when_codex_path_is_missing() -> std::io::Result<()> {
516 let package_dir = tempfile::tempdir()?;
517 let bin_dir = package_dir.path().join(BIN_DIRNAME);
518 fs::create_dir_all(&bin_dir)?;
519 fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?;
520 let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
521 fs::write(&exe_path, "")?;
522
523 let context = InstallContext::from_exe_with_codex_home(
524 false,
525 Some(&exe_path),
526 None,
527 None,
528 );
529 assert_eq!(context.rg_command(), default_rg_command());
530 Ok(())
531 }
532
533 #[test]
534 fn bundled_file_lookups_ignore_directories() -> std::io::Result<()> {
535 let package_dir = tempfile::tempdir()?;
536 let bin_dir = package_dir.path().join(BIN_DIRNAME);
537 let resources_dir = package_dir.path().join(RESOURCES_DIRNAME);
538 let path_dir = package_dir.path().join(PATH_DIRNAME);
539 fs::create_dir_all(&bin_dir)?;
540 fs::create_dir_all(resources_dir.join(TEST_RESOURCE_NAME))?;
541 fs::create_dir_all(path_dir.join(default_rg_command()))?;
542 fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?;
543 let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
544 fs::write(&exe_path, "")?;
545
546 let context = InstallContext::from_exe_with_codex_home(
547 false,
548 Some(&exe_path),
549 None,
550 None,
551 );
552 assert_eq!(context.rg_command(), default_rg_command());
553 assert_eq!(context.bundled_resource(TEST_RESOURCE_NAME), None);
554 Ok(())
555 }
556
557 #[test]
558 fn package_manager_method_overrides_take_precedence() {
559 let pnpm_context = InstallContext::from_exe(
560 false,
561 Some(Path::new("/tmp/codex")),
562 Some(InstallMethod::Pnpm),
563 );
564 assert_eq!(
565 pnpm_context,
566 InstallContext {
567 method: InstallMethod::Pnpm,
568 package_layout: None,
569 }
570 );
571
572 let npm_context = InstallContext::from_exe(
573 false,
574 Some(Path::new("/tmp/codex")),
575 Some(InstallMethod::Npm),
576 );
577 assert_eq!(
578 npm_context,
579 InstallContext {
580 method: InstallMethod::Npm,
581 package_layout: None,
582 }
583 );
584
585 let bun_context = InstallContext::from_exe(
586 false,
587 Some(Path::new("/tmp/codex")),
588 Some(InstallMethod::Bun),
589 );
590 assert_eq!(
591 bun_context,
592 InstallContext {
593 method: InstallMethod::Bun,
594 package_layout: None,
595 }
596 );
597 }
598
599 #[test]
600 fn brew_is_detected_on_macos_prefixes() {
601 let context = InstallContext::from_exe_with_codex_home(
602 true,
603 Some(Path::new("/opt/homebrew/bin/codex")),
604 None,
605 None,
606 );
607 assert_eq!(
608 context,
609 InstallContext {
610 method: InstallMethod::Brew,
611 package_layout: None,
612 }
613 );
614 }
615}