mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Use AbsolutePathBuf in skill loading and codex_home (#17407)
Helps with FS migration later
This commit is contained in:
@@ -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()?;
|
||||
|
||||
@@ -8,6 +8,7 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user