refactor: make shared path and cache utilities wasm-safe

Move native-only path and Tokio dependencies behind cfg gates so shared crates can compile for wasm32. Replace the absolute-path implementation with a lexical normalization fallback on wasm, use a std mutex for the cache helper on wasm, and degrade git symlink creation to an unsupported-platform error instead of a compile-time failure.
This commit is contained in:
Jeremy Lewi
2026-04-13 09:58:43 -07:00
parent 5bc4c5c997
commit 609b6168b3
6 changed files with 99 additions and 3 deletions
+2
View File
@@ -30,6 +30,8 @@ pub enum GitToolingError {
PathPrefix(#[from] std::path::StripPrefixError),
#[error(transparent)]
Walkdir(#[from] WalkdirError),
#[error("{0}")]
UnsupportedPlatform(String),
#[error(transparent)]
Io(#[from] std::io::Error),
}
+9 -1
View File
@@ -34,4 +34,12 @@ pub fn create_symlink(
}
#[cfg(not(any(unix, windows)))]
compile_error!("codex-git symlink support is only implemented for Unix and Windows");
pub fn create_symlink(
_source: &Path,
_link_target: &Path,
_destination: &Path,
) -> Result<(), GitToolingError> {
Err(GitToolingError::UnsupportedPlatform(
"symlink creation is unavailable on this target".to_string(),
))
}
+3 -1
View File
@@ -10,7 +10,6 @@ workspace = true
[dependencies]
dirs = { workspace = true }
path-absolutize = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
ts-rs = { workspace = true, features = [
@@ -22,3 +21,6 @@ ts-rs = { workspace = true, features = [
pretty_assertions = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
path-absolutize = { workspace = true }
+66
View File
@@ -1,4 +1,5 @@
use dirs::home_dir;
#[cfg(not(target_arch = "wasm32"))]
use path_absolutize::Absolutize;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -6,6 +7,10 @@ use serde::Deserializer;
use serde::Serialize;
use serde::de::Error as SerdeError;
use std::cell::RefCell;
#[cfg(target_arch = "wasm32")]
use std::ffi::OsString;
#[cfg(target_arch = "wasm32")]
use std::path::Component;
use std::path::Display;
use std::path::Path;
use std::path::PathBuf;
@@ -45,13 +50,24 @@ impl AbsolutePathBuf {
base_path: B,
) -> std::io::Result<Self> {
let expanded = Self::maybe_expand_home_directory(path.as_ref());
#[cfg(not(target_arch = "wasm32"))]
let absolute_path = expanded.absolutize_from(base_path.as_ref())?;
#[cfg(target_arch = "wasm32")]
let absolute_path =
normalize_absolute_path(join_against_base(expanded, base_path.as_ref())?);
Ok(Self(absolute_path.into_owned()))
}
pub fn from_absolute_path<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
let expanded = Self::maybe_expand_home_directory(path.as_ref());
#[cfg(not(target_arch = "wasm32"))]
let absolute_path = expanded.absolutize()?;
#[cfg(target_arch = "wasm32")]
let absolute_path = normalize_absolute_path(if expanded.is_absolute() {
expanded
} else {
join_against_base(expanded, &std::env::current_dir()?)?
});
Ok(Self(absolute_path.into_owned()))
}
@@ -101,6 +117,56 @@ impl AbsolutePathBuf {
}
}
#[cfg(target_arch = "wasm32")]
fn join_against_base(path: PathBuf, base_path: &Path) -> std::io::Result<PathBuf> {
if path.is_absolute() {
return Ok(path);
}
if !base_path.is_absolute() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"base path must be absolute",
));
}
Ok(base_path.join(path))
}
#[cfg(target_arch = "wasm32")]
fn normalize_absolute_path(path: PathBuf) -> std::borrow::Cow<'static, Path> {
let mut prefix: Option<OsString> = None;
let mut has_root = false;
let mut parts: Vec<OsString> = Vec::new();
for component in path.components() {
match component {
Component::Prefix(value) => {
prefix = Some(value.as_os_str().to_os_string());
}
Component::RootDir => {
has_root = true;
}
Component::CurDir => {}
Component::ParentDir => {
let _ = parts.pop();
}
Component::Normal(part) => parts.push(part.to_os_string()),
}
}
let mut normalized = PathBuf::new();
if let Some(prefix) = prefix {
normalized.push(prefix);
}
if has_root {
normalized.push(Path::new(std::path::MAIN_SEPARATOR_STR));
}
for part in parts {
normalized.push(part);
}
std::borrow::Cow::Owned(normalized)
}
impl AsRef<Path> for AbsolutePathBuf {
fn as_ref(&self) -> &Path {
&self.0
+3 -1
View File
@@ -10,7 +10,9 @@ workspace = true
[dependencies]
lru = { workspace = true }
sha1 = { workspace = true }
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
+16
View File
@@ -5,7 +5,13 @@ use std::num::NonZeroUsize;
use lru::LruCache;
use sha1::Digest;
use sha1::Sha1;
#[cfg(target_arch = "wasm32")]
use std::sync::Mutex;
#[cfg(target_arch = "wasm32")]
use std::sync::MutexGuard;
#[cfg(not(target_arch = "wasm32"))]
use tokio::sync::Mutex;
#[cfg(not(target_arch = "wasm32"))]
use tokio::sync::MutexGuard;
/// A minimal LRU cache protected by a Tokio mutex.
@@ -119,6 +125,7 @@ where
}
}
#[cfg(not(target_arch = "wasm32"))]
fn lock_if_runtime<K, V>(m: &Mutex<LruCache<K, V>>) -> Option<MutexGuard<'_, LruCache<K, V>>>
where
K: Eq + Hash,
@@ -127,6 +134,14 @@ where
Some(tokio::task::block_in_place(|| m.blocking_lock()))
}
#[cfg(target_arch = "wasm32")]
fn lock_if_runtime<K, V>(m: &Mutex<LruCache<K, V>>) -> Option<MutexGuard<'_, LruCache<K, V>>>
where
K: Eq + Hash,
{
m.lock().ok()
}
/// Computes the SHA-1 digest of `bytes`.
///
/// Useful for content-based cache keys when you want to avoid staleness
@@ -141,6 +156,7 @@ pub fn sha1_digest(bytes: &[u8]) -> [u8; 20] {
out
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
mod tests {
use super::BlockingLruCache;