Skip to main content

codex_install_context/
lib.rs

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    /// The package root that contains the metadata file and layout directories.
26    pub package_dir: AbsolutePathBuf,
27    /// Directory containing the Codex entrypoint executable.
28    pub bin_dir: AbsolutePathBuf,
29    /// Directory containing managed helper binaries and data files, when present.
30    pub resources_dir: Option<AbsolutePathBuf>,
31    /// Folder that should be prepended to the PATH, when present.
32    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        /// The managed standalone release directory. Legacy installs use paths
45        /// such as
46        /// `~/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl`.
47        /// Package-layout installs use the package root that contains `bin/`,
48        /// `codex-resources/`, and `codex-path/`.
49        release_dir: AbsolutePathBuf,
50        /// The bundled resource directory for managed dependencies.
51        resources_dir: Option<AbsolutePathBuf>,
52        /// The platform of the standalone release, either `Unix` or `Windows`.
53        platform: StandalonePlatform,
54    },
55    /// A Codex binary launched through the npm-managed `codex.js` shim.
56    Npm,
57    /// A Codex binary launched through the bun-managed `codex.js` shim.
58    Bun,
59    /// A Codex binary launched through the pnpm-managed `codex.js` shim.
60    Pnpm,
61    /// A Codex binary that appears to come from a Homebrew install prefix.
62    Brew,
63    /// Any other execution environment.
64    ///
65    /// This commonly covers `cargo run`, app-bundled Codex binaries, custom
66    /// internal launchers, and tests that execute Codex from an arbitrary path.
67    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            /*is_macos*/ false,
313            /*current_exe*/ Some(&exe_path),
314            /*method_override*/ None,
315            /*codex_home*/ 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            /*is_macos*/ false,
347            /*current_exe*/ Some(&exe_path),
348            /*method_override*/ None,
349            /*codex_home*/ 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            /*is_macos*/ false,
389            /*current_exe*/ Some(&exe_path),
390            /*method_override*/ None,
391            /*codex_home*/ 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            /*is_macos*/ false,
452            /*current_exe*/ Some(&exe_path),
453            /*method_override*/ None,
454            /*codex_home*/ 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            /*is_macos*/ false,
500            /*current_exe*/ Some(&exe_path),
501            /*method_override*/ 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            /*is_macos*/ false,
525            /*current_exe*/ Some(&exe_path),
526            /*method_override*/ None,
527            /*codex_home*/ 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            /*is_macos*/ false,
548            /*current_exe*/ Some(&exe_path),
549            /*method_override*/ None,
550            /*codex_home*/ 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            /*is_macos*/ false,
561            /*current_exe*/ Some(Path::new("/tmp/codex")),
562            /*method_override*/ 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            /*is_macos*/ false,
574            /*current_exe*/ Some(Path::new("/tmp/codex")),
575            /*method_override*/ 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            /*is_macos*/ false,
587            /*current_exe*/ Some(Path::new("/tmp/codex")),
588            /*method_override*/ 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            /*is_macos*/ true,
603            /*current_exe*/ Some(Path::new("/opt/homebrew/bin/codex")),
604            /*method_override*/ None,
605            /*codex_home*/ None,
606        );
607        assert_eq!(
608            context,
609            InstallContext {
610                method: InstallMethod::Brew,
611                package_layout: None,
612            }
613        );
614    }
615}