mirror of
https://github.com/openai/codex.git
synced 2026-04-27 16:15:09 +00:00
Extract rollout into its own crate (#15548)
This commit is contained in:
46
codex-rs/utils/path-utils/src/env.rs
Normal file
46
codex-rs/utils/path-utils/src/env.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! Functions for environment detection that need to be shared across crates.
|
||||
|
||||
fn env_var_set(key: &str) -> bool {
|
||||
std::env::var(key).is_ok_and(|v| !v.trim().is_empty())
|
||||
}
|
||||
|
||||
/// Returns true if the current process is running under Windows Subsystem for Linux.
|
||||
pub fn is_wsl() -> bool {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if std::env::var_os("WSL_DISTRO_NAME").is_some() {
|
||||
return true;
|
||||
}
|
||||
match std::fs::read_to_string("/proc/version") {
|
||||
Ok(version) => version.to_lowercase().contains("microsoft"),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when Codex is likely running in an environment without a usable GUI.
|
||||
///
|
||||
/// This is intentionally conservative and is used by frontends to avoid flows that would try to
|
||||
/// open a browser (e.g. device-code auth fallback).
|
||||
pub fn is_headless_environment() -> bool {
|
||||
if env_var_set("CI")
|
||||
|| env_var_set("SSH_CONNECTION")
|
||||
|| env_var_set("SSH_CLIENT")
|
||||
|| env_var_set("SSH_TTY")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if !env_var_set("DISPLAY") && !env_var_set("WAYLAND_DISPLAY") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
205
codex-rs/utils/path-utils/src/lib.rs
Normal file
205
codex-rs/utils/path-utils/src/lib.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
//! Path normalization, symlink resolution, and atomic writes shared across Codex crates.
|
||||
|
||||
pub mod env;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
pub fn normalize_for_path_comparison(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
|
||||
let canonical = path.as_ref().canonicalize()?;
|
||||
Ok(normalize_for_wsl(canonical))
|
||||
}
|
||||
|
||||
pub fn normalize_for_native_workdir(path: impl AsRef<Path>) -> PathBuf {
|
||||
normalize_for_native_workdir_with_flag(path.as_ref().to_path_buf(), cfg!(windows))
|
||||
}
|
||||
|
||||
pub struct SymlinkWritePaths {
|
||||
pub read_path: Option<PathBuf>,
|
||||
pub write_path: PathBuf,
|
||||
}
|
||||
|
||||
/// Resolve the final filesystem target for `path` while retaining a safe write path.
|
||||
///
|
||||
/// This follows symlink chains (including relative symlink targets) until it reaches a
|
||||
/// non-symlink path. If the chain cycles or any metadata/link resolution fails, it
|
||||
/// returns `read_path: None` and uses the original absolute path as `write_path`.
|
||||
/// There is no fixed max-resolution count; cycles are detected via a visited set.
|
||||
pub fn resolve_symlink_write_paths(path: &Path) -> io::Result<SymlinkWritePaths> {
|
||||
let root = AbsolutePathBuf::from_absolute_path(path)
|
||||
.map(AbsolutePathBuf::into_path_buf)
|
||||
.unwrap_or_else(|_| path.to_path_buf());
|
||||
let mut current = root.clone();
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
// Follow symlink chains while guarding against cycles.
|
||||
loop {
|
||||
let meta = match std::fs::symlink_metadata(¤t) {
|
||||
Ok(meta) => meta,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
return Ok(SymlinkWritePaths {
|
||||
read_path: Some(current.clone()),
|
||||
write_path: current,
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(SymlinkWritePaths {
|
||||
read_path: None,
|
||||
write_path: root,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if !meta.file_type().is_symlink() {
|
||||
return Ok(SymlinkWritePaths {
|
||||
read_path: Some(current.clone()),
|
||||
write_path: current,
|
||||
});
|
||||
}
|
||||
|
||||
// If we've already seen this path, the chain cycles.
|
||||
if !visited.insert(current.clone()) {
|
||||
return Ok(SymlinkWritePaths {
|
||||
read_path: None,
|
||||
write_path: root,
|
||||
});
|
||||
}
|
||||
|
||||
let target = match std::fs::read_link(¤t) {
|
||||
Ok(target) => target,
|
||||
Err(_) => {
|
||||
return Ok(SymlinkWritePaths {
|
||||
read_path: None,
|
||||
write_path: root,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let next = if target.is_absolute() {
|
||||
AbsolutePathBuf::from_absolute_path(&target)
|
||||
} else if let Some(parent) = current.parent() {
|
||||
AbsolutePathBuf::resolve_path_against_base(&target, parent)
|
||||
} else {
|
||||
return Ok(SymlinkWritePaths {
|
||||
read_path: None,
|
||||
write_path: root,
|
||||
});
|
||||
};
|
||||
|
||||
let next = match next {
|
||||
Ok(path) => path.into_path_buf(),
|
||||
Err(_) => {
|
||||
return Ok(SymlinkWritePaths {
|
||||
read_path: None,
|
||||
write_path: root,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_atomically(write_path: &Path, contents: &str) -> io::Result<()> {
|
||||
let parent = write_path.parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("path {} has no parent directory", write_path.display()),
|
||||
)
|
||||
})?;
|
||||
std::fs::create_dir_all(parent)?;
|
||||
let tmp = NamedTempFile::new_in(parent)?;
|
||||
std::fs::write(tmp.path(), contents)?;
|
||||
tmp.persist(write_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_for_wsl(path: PathBuf) -> PathBuf {
|
||||
normalize_for_wsl_with_flag(path, env::is_wsl())
|
||||
}
|
||||
|
||||
fn normalize_for_native_workdir_with_flag(path: PathBuf, is_windows: bool) -> PathBuf {
|
||||
if is_windows {
|
||||
dunce::simplified(&path).to_path_buf()
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_for_wsl_with_flag(path: PathBuf, is_wsl: bool) -> PathBuf {
|
||||
if !is_wsl {
|
||||
return path;
|
||||
}
|
||||
|
||||
if !is_wsl_case_insensitive_path(&path) {
|
||||
return path;
|
||||
}
|
||||
|
||||
lower_ascii_path(path)
|
||||
}
|
||||
|
||||
fn is_wsl_case_insensitive_path(path: &Path) -> bool {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Component;
|
||||
|
||||
let mut components = path.components();
|
||||
let Some(Component::RootDir) = components.next() else {
|
||||
return false;
|
||||
};
|
||||
let Some(Component::Normal(mnt)) = components.next() else {
|
||||
return false;
|
||||
};
|
||||
if !ascii_eq_ignore_case(mnt.as_bytes(), b"mnt") {
|
||||
return false;
|
||||
}
|
||||
let Some(Component::Normal(drive)) = components.next() else {
|
||||
return false;
|
||||
};
|
||||
let drive_bytes = drive.as_bytes();
|
||||
drive_bytes.len() == 1 && drive_bytes[0].is_ascii_alphabetic()
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = path;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn ascii_eq_ignore_case(left: &[u8], right: &[u8]) -> bool {
|
||||
left.len() == right.len()
|
||||
&& left
|
||||
.iter()
|
||||
.zip(right)
|
||||
.all(|(lhs, rhs)| lhs.to_ascii_lowercase() == *rhs)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn lower_ascii_path(path: PathBuf) -> PathBuf {
|
||||
use std::ffi::OsString;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
|
||||
// WSL mounts Windows drives under /mnt/<drive>, which are case-insensitive.
|
||||
let bytes = path.as_os_str().as_bytes();
|
||||
let mut lowered = Vec::with_capacity(bytes.len());
|
||||
for byte in bytes {
|
||||
lowered.push(byte.to_ascii_lowercase());
|
||||
}
|
||||
PathBuf::from(OsString::from_vec(lowered))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn lower_ascii_path(path: PathBuf) -> PathBuf {
|
||||
path
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "path_utils_tests.rs"]
|
||||
mod tests;
|
||||
78
codex-rs/utils/path-utils/src/path_utils_tests.rs
Normal file
78
codex-rs/utils/path-utils/src/path_utils_tests.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
#[cfg(unix)]
|
||||
mod symlinks {
|
||||
use super::super::resolve_symlink_write_paths;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
#[test]
|
||||
fn symlink_cycles_fall_back_to_root_write_path() -> std::io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let a = dir.path().join("a");
|
||||
let b = dir.path().join("b");
|
||||
|
||||
symlink(&b, &a)?;
|
||||
symlink(&a, &b)?;
|
||||
|
||||
let resolved = resolve_symlink_write_paths(&a)?;
|
||||
|
||||
assert_eq!(resolved.read_path, None);
|
||||
assert_eq!(resolved.write_path, a);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod wsl {
|
||||
use super::super::normalize_for_wsl_with_flag;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn wsl_mnt_drive_paths_lowercase() {
|
||||
let normalized = normalize_for_wsl_with_flag(PathBuf::from("/mnt/C/Users/Dev"), true);
|
||||
|
||||
assert_eq!(normalized, PathBuf::from("/mnt/c/users/dev"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wsl_non_drive_paths_unchanged() {
|
||||
let path = PathBuf::from("/mnt/cc/Users/Dev");
|
||||
let normalized = normalize_for_wsl_with_flag(path.clone(), true);
|
||||
|
||||
assert_eq!(normalized, path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wsl_non_mnt_paths_unchanged() {
|
||||
let path = PathBuf::from("/home/Dev");
|
||||
let normalized = normalize_for_wsl_with_flag(path.clone(), true);
|
||||
|
||||
assert_eq!(normalized, path);
|
||||
}
|
||||
}
|
||||
|
||||
mod native_workdir {
|
||||
use super::super::normalize_for_native_workdir_with_flag;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn windows_verbatim_paths_are_simplified() {
|
||||
let path = PathBuf::from(r"\\?\D:\c\x\worktrees\2508\swift-base");
|
||||
let normalized = normalize_for_native_workdir_with_flag(path, true);
|
||||
|
||||
assert_eq!(
|
||||
normalized,
|
||||
PathBuf::from(r"D:\c\x\worktrees\2508\swift-base")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_windows_paths_are_unchanged() {
|
||||
let path = PathBuf::from(r"\\?\D:\c\x\worktrees\2508\swift-base");
|
||||
let normalized = normalize_for_native_workdir_with_flag(path.clone(), false);
|
||||
|
||||
assert_eq!(normalized, path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user