Use AbsolutePathBuf in skill loading and codex_home (#17407)

Helps with FS migration later
This commit is contained in:
pakrym-oai
2026-04-13 10:26:51 -07:00
committed by GitHub
parent d25a9822a7
commit ac82443d07
86 changed files with 850 additions and 625 deletions
+107 -2
View File
@@ -19,7 +19,7 @@ mod absolutize;
/// using [AbsolutePathBufGuard::new]. If no base path is set, the
/// deserialization will fail unless the path being deserialized is already
/// absolute.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, JsonSchema, TS)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, JsonSchema, TS)]
pub struct AbsolutePathBuf(PathBuf);
impl AbsolutePathBuf {
@@ -54,6 +54,18 @@ impl AbsolutePathBuf {
Ok(Self(absolutize::absolutize(&expanded)?))
}
pub fn from_absolute_path_checked<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
let expanded = Self::maybe_expand_home_directory(path.as_ref());
if !expanded.is_absolute() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("path is not absolute: {}", path.as_ref().display()),
));
}
Ok(Self(absolutize::absolutize_from(&expanded, Path::new("/"))))
}
pub fn current_dir() -> std::io::Result<Self> {
let current_dir = std::env::current_dir()?;
Ok(Self(absolutize::absolutize_from(
@@ -75,6 +87,10 @@ impl AbsolutePathBuf {
Self::resolve_path_against_base(path, &self.0)
}
pub fn canonicalize(&self) -> std::io::Result<Self> {
dunce::canonicalize(&self.0).map(Self)
}
pub fn parent(&self) -> Option<Self> {
self.0.parent().map(|p| {
debug_assert!(
@@ -85,6 +101,16 @@ impl AbsolutePathBuf {
})
}
pub fn ancestors(&self) -> impl Iterator<Item = Self> + '_ {
self.0.ancestors().map(|p| {
debug_assert!(
p.is_absolute(),
"ancestor of AbsolutePathBuf must be absolute"
);
Self(p.to_path_buf())
})
}
pub fn as_path(&self) -> &Path {
&self.0
}
@@ -174,6 +200,24 @@ pub mod test_support {
use std::path::Path;
use std::path::PathBuf;
/// Creates a platform-absolute [`PathBuf`] from a Unix-style absolute test path.
///
/// On Windows, `/tmp/example` maps to `C:\tmp\example`.
pub fn test_path_buf(unix_path: &str) -> PathBuf {
if cfg!(windows) {
let mut path = PathBuf::from(r"C:\");
path.extend(
unix_path
.trim_start_matches('/')
.split('/')
.filter(|segment| !segment.is_empty()),
);
path
} else {
PathBuf::from(unix_path)
}
}
/// Extension methods for converting paths into [`AbsolutePathBuf`] values in tests.
pub trait PathExt {
/// Converts an already absolute path into an [`AbsolutePathBuf`].
@@ -183,7 +227,8 @@ pub mod test_support {
impl PathExt for Path {
#[expect(clippy::expect_used)]
fn abs(&self) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(self).expect("path should already be absolute")
AbsolutePathBuf::from_absolute_path_checked(self)
.expect("path should already be absolute")
}
}
@@ -280,7 +325,9 @@ impl<'de> Deserialize<'de> for AbsolutePathBuf {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::test_path_buf;
use pretty_assertions::assert_eq;
use std::fs;
use tempfile::tempdir;
#[test]
@@ -294,6 +341,14 @@ mod tests {
assert_eq!(abs_path_buf.as_path(), absolute_path.as_path());
}
#[test]
fn from_absolute_path_checked_rejects_relative_path() {
let err = AbsolutePathBuf::from_absolute_path_checked("relative/path")
.expect_err("relative path should fail");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn relative_path_is_resolved_against_base_path() {
let temp_dir = tempdir().expect("base dir");
@@ -311,6 +366,56 @@ mod tests {
assert_eq!(abs_path_buf.as_path(), base_dir.join("file.txt").as_path());
}
#[test]
fn canonicalize_returns_absolute_path_buf() {
let temp_dir = tempdir().expect("base dir");
fs::create_dir(temp_dir.path().join("one")).expect("create one dir");
fs::create_dir(temp_dir.path().join("two")).expect("create two dir");
fs::write(temp_dir.path().join("two").join("file.txt"), "").expect("write file");
let abs_path_buf =
AbsolutePathBuf::from_absolute_path(temp_dir.path().join("one/../two/./file.txt"))
.expect("absolute path");
assert_eq!(
abs_path_buf
.canonicalize()
.expect("path should canonicalize")
.as_path(),
dunce::canonicalize(temp_dir.path().join("two").join("file.txt"))
.expect("expected path should canonicalize")
.as_path()
);
}
#[test]
fn canonicalize_returns_error_for_missing_path() {
let temp_dir = tempdir().expect("base dir");
let abs_path_buf = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("missing.txt"))
.expect("absolute path");
assert!(abs_path_buf.canonicalize().is_err());
}
#[test]
fn ancestors_returns_absolute_path_bufs() {
let abs_path_buf =
AbsolutePathBuf::from_absolute_path_checked(test_path_buf("/tmp/one/two"))
.expect("absolute path");
let ancestors = abs_path_buf
.ancestors()
.map(|path| path.to_path_buf())
.collect::<Vec<_>>();
let expected = vec![
test_path_buf("/tmp/one/two"),
test_path_buf("/tmp/one"),
test_path_buf("/tmp"),
test_path_buf("/"),
];
assert_eq!(ancestors, expected);
}
#[test]
fn relative_to_current_dir_resolves_relative_path() -> std::io::Result<()> {
let current_dir = std::env::current_dir()?;
+1
View File
@@ -8,6 +8,7 @@ license.workspace = true
workspace = true
[dependencies]
codex-utils-absolute-path = { workspace = true }
dirs = { workspace = true }
[dev-dependencies]
+10 -5
View File
@@ -1,3 +1,4 @@
use codex_utils_absolute_path::AbsolutePathBuf;
use dirs::home_dir;
use std::path::PathBuf;
@@ -9,14 +10,14 @@ use std::path::PathBuf;
/// value will be canonicalized and this function will Err otherwise.
/// - If `CODEX_HOME` is not set, this function does not verify that the
/// directory exists.
pub fn find_codex_home() -> std::io::Result<PathBuf> {
pub fn find_codex_home() -> std::io::Result<AbsolutePathBuf> {
let codex_home_env = std::env::var("CODEX_HOME")
.ok()
.filter(|val| !val.is_empty());
find_codex_home_from_env(codex_home_env.as_deref())
}
fn find_codex_home_from_env(codex_home_env: Option<&str>) -> std::io::Result<PathBuf> {
fn find_codex_home_from_env(codex_home_env: Option<&str>) -> std::io::Result<AbsolutePathBuf> {
// Honor the `CODEX_HOME` environment variable when it is set to allow users
// (and tests) to override the default location.
match codex_home_env {
@@ -39,12 +40,13 @@ fn find_codex_home_from_env(codex_home_env: Option<&str>) -> std::io::Result<Pat
format!("CODEX_HOME points to {val:?}, but that path is not a directory"),
))
} else {
path.canonicalize().map_err(|err| {
let canonical = path.canonicalize().map_err(|err| {
std::io::Error::new(
err.kind(),
format!("failed to canonicalize CODEX_HOME {val:?}: {err}"),
)
})
})?;
AbsolutePathBuf::from_absolute_path(canonical)
}
}
None => {
@@ -55,7 +57,7 @@ fn find_codex_home_from_env(codex_home_env: Option<&str>) -> std::io::Result<Pat
)
})?;
p.push(".codex");
Ok(p)
AbsolutePathBuf::from_absolute_path(p)
}
}
}
@@ -63,6 +65,7 @@ fn find_codex_home_from_env(codex_home_env: Option<&str>) -> std::io::Result<Pat
#[cfg(test)]
mod tests {
use super::find_codex_home_from_env;
use codex_utils_absolute_path::AbsolutePathBuf;
use dirs::home_dir;
use pretty_assertions::assert_eq;
use std::fs;
@@ -115,6 +118,7 @@ mod tests {
.path()
.canonicalize()
.expect("canonicalize temp home");
let expected = AbsolutePathBuf::from_absolute_path(expected).expect("absolute home");
assert_eq!(resolved, expected);
}
@@ -124,6 +128,7 @@ mod tests {
find_codex_home_from_env(/*codex_home_env*/ None).expect("default CODEX_HOME");
let mut expected = home_dir().expect("home dir");
expected.push(".codex");
let expected = AbsolutePathBuf::from_absolute_path(expected).expect("absolute home");
assert_eq!(resolved, expected);
}
}