Skip to content

Commit b421a7a

Browse files
domenkozarclaude
andcommitted
config: canonicalize existing ancestor for import-path validation
`validate_within_root` rejected valid input-relative imports on macOS, where `TMPDIR` resolves via the `/var -> /private/var` symlink and the import path's leaf does not exist yet (`Import path '<name>/...' resolves outside the git repository`). Resolve the longest existing ancestor with `canonicalize` and append the remaining components lexically, so symlinks anywhere up the chain are followed even when the leaf is absent. Split out of #2849. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f8f4828 commit b421a7a

2 files changed

Lines changed: 37 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- Fixed long lines in `devenv shell` getting a hard newline inserted at the wrap point when copying to clipboard. The shell now preserves the soft-wrap when flushing wrapped output into the terminal's scrollback, so clipboard copy keeps the original single line ([#2865](https://github.com/cachix/devenv/issues/2865)).
1818
- Fixed files declared with the `files` option not being regenerated when an auto-loaded (`devenv allow`) shell reloaded after `devenv update`. enterShell tasks (including `devenv:files`) now re-run on hot-reload, matching a fresh shell entry, instead of only updating environment variables ([#2864](https://github.com/cachix/devenv/issues/2864)).
1919
- Fixed `devenv test --no-tui` (and any other non-TUI invocation) silently discarding all output from the `enterTest` script, so the test runner's output, traces, and failure messages never reached the terminal or CI logs. Output from commands run in the shell is now printed in non-TUI mode.
20+
- Fixed import path validation rejecting valid input-relative imports on macOS, where `TMPDIR` resolves via the `/var → /private/var` symlink (`Import path '<name>/...' resolves outside the git repository`). `validate_within_root` now canonicalizes the longest existing ancestor of the path before comparing against the root.
2021

2122
### Improvements
2223

devenv-core/src/config.rs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,21 +1231,13 @@ impl Config {
12311231
&& let Some(canonical_root) = canonical_root
12321232
{
12331233
// Import path doesn't exist, but root does.
1234-
// Canonicalize the parent directory to resolve symlinks
1235-
// (e.g. /tmp -> /run/user/...), falling back to lexical
1236-
// normalization only when the parent doesn't exist either.
1237-
let abs_import = if let Some(parent) = import_path.parent() {
1238-
if let Ok(canonical_parent) = parent.canonicalize() {
1239-
canonical_parent.join(import_path.file_name().unwrap_or_default())
1240-
} else if import_path.is_absolute() {
1241-
Self::normalize_path_components(import_path)
1242-
} else if let Ok(cwd) = std::env::current_dir() {
1243-
Self::normalize_path_components(&cwd.join(import_path))
1244-
} else {
1245-
return Ok(());
1246-
}
1247-
} else if import_path.is_absolute() {
1248-
Self::normalize_path_components(import_path)
1234+
// Walk up the path looking for an existing ancestor that we can
1235+
// canonicalize (so symlinks like macOS `/var -> /private/var` get
1236+
// resolved), then append the remaining components lexically.
1237+
let abs_import = if import_path.is_absolute() {
1238+
Self::canonicalize_existing_ancestor(import_path)
1239+
} else if let Ok(cwd) = std::env::current_dir() {
1240+
Self::canonicalize_existing_ancestor(&cwd.join(import_path))
12491241
} else {
12501242
return Ok(());
12511243
};
@@ -1273,6 +1265,35 @@ impl Config {
12731265
Ok(())
12741266
}
12751267

1268+
/// Resolve an absolute path by canonicalizing the longest existing prefix
1269+
/// and appending the remaining components lexically. This handles paths
1270+
/// whose leaves don't exist yet while still resolving symlinks anywhere
1271+
/// up the chain (e.g. macOS `/var/folders/...` -> `/private/var/folders/...`).
1272+
fn canonicalize_existing_ancestor(path: &Path) -> PathBuf {
1273+
let mut probe = path.to_path_buf();
1274+
let mut tail: Vec<std::ffi::OsString> = Vec::new();
1275+
loop {
1276+
match probe.canonicalize() {
1277+
Ok(canonical) => {
1278+
let mut result = canonical;
1279+
for component in tail.iter().rev() {
1280+
result.push(component);
1281+
}
1282+
return result;
1283+
}
1284+
Err(_) => {
1285+
let file_name = probe.file_name().map(|n| n.to_os_string());
1286+
if !probe.pop() || file_name.is_none() {
1287+
// Nothing exists along the way; fall back to lexical
1288+
// normalization of the original path.
1289+
return Self::normalize_path_components(path);
1290+
}
1291+
tail.push(file_name.unwrap());
1292+
}
1293+
}
1294+
}
1295+
}
1296+
12761297
/// Normalizes a path by resolving `.` and `..` components without requiring the path to exist.
12771298
fn normalize_path_components(path: &Path) -> PathBuf {
12781299
let mut components = Vec::new();

0 commit comments

Comments
 (0)