Skip to content

Commit 13e102b

Browse files
authored
fix: Ability to follow symlinks (#524)
* fix: Ability to follow symlinks * add $PWD to start network * removed inherited $PWD so we fallback on currentdir() * fix test * remove ignore warnings * fmt * typo * fix test on windows * fmt * don't canonicalize on windows either
1 parent 58ef177 commit 13e102b

5 files changed

Lines changed: 91 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* feat: `icp identity import` now takes `--seed-curve`, for seed phrases for non-k256 keys.
55
* fix: `icp canister settings show` now outputs only the canister settings, consistent with the command name
66
* fix: Fail early when attempting to create an identity with an already existing name.
7+
* fix: Find icp.yaml even from within a symlinked folder.
78

89
# v0.2.3
910

crates/icp-cli/tests/common/context.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ impl TestContext {
9595
#[cfg(unix)]
9696
cmd.env("HOME", self.home_path())
9797
.env_remove("ICP_HOME")
98+
.env_remove("PWD") // don't inherit from the tester's shell
9899
// Also set XDG directories to ensure isolation on Linux
99100
.env("XDG_CONFIG_HOME", self.home_path().join(".config"))
100101
.env("XDG_DATA_HOME", self.home_path().join(".local/share"))
@@ -239,7 +240,8 @@ impl TestContext {
239240
// Also set XDG directories to ensure isolation on Linux
240241
.env("XDG_CONFIG_HOME", self.home_path().join(".config"))
241242
.env("XDG_DATA_HOME", self.home_path().join(".local/share"))
242-
.env("XDG_CACHE_HOME", self.home_path().join(".cache"));
243+
.env("XDG_CACHE_HOME", self.home_path().join(".cache"))
244+
.env("PWD", project_dir);
243245
}
244246
// run in portable mode on Windows, the user directory cannot be mocked
245247
#[cfg(windows)]

crates/icp-cli/tests/network_tests.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,15 @@ async fn network_same_port() {
6868
ctx.ping_until_healthy(&project_dir_a, "sameport-network");
6969

7070
eprintln!("second network start attempt in another project");
71+
7172
ctx.icp()
7273
.current_dir(&project_dir_b)
7374
.args(["network", "start", "sameport-network"])
7475
.assert()
7576
.failure()
7677
.stderr(contains(format!(
7778
"Error: port 8080 is in use by the sameport-network network of the project at '{}'",
78-
dunce::canonicalize(&project_dir_a).unwrap().display()
79+
project_dir_a
7980
)));
8081
}
8182

crates/icp/src/context/init.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,27 @@ pub fn initialize(
4141
// Setup global directory structure
4242
let dirs = Arc::new(Directories::new().context(DirectoriesSnafu)?);
4343

44-
// Project Root
44+
// Project Root. On Unix, prefer $PWD (the logical path the user cd'd
45+
// through) over getcwd(3), which resolves symlinks to the physical path
46+
// and would break upward traversal when the user is inside a symlinked
47+
// directory whose manifest sits above the symlink's location.
48+
#[cfg(unix)]
49+
let cwd: PathBuf = match std::env::var("PWD")
50+
.ok()
51+
.map(PathBuf::from)
52+
.filter(|p| p.is_absolute())
53+
{
54+
Some(p) => p,
55+
None => PathBuf::try_from(current_dir().context(CwdSnafu)?).context(Utf8PathSnafu)?,
56+
};
57+
58+
#[cfg(not(unix))]
59+
let cwd: PathBuf =
60+
PathBuf::try_from(current_dir().context(CwdSnafu)?).context(Utf8PathSnafu)?;
61+
4562
let project_root_locate = Arc::new(manifest::ProjectRootLocateImpl::new(
46-
dunce::canonicalize(current_dir().context(CwdSnafu)?)
47-
.context(CwdSnafu)?
48-
.try_into()
49-
.context(Utf8PathSnafu)?, // cwd
50-
project_root_override, // dir
63+
cwd,
64+
project_root_override,
5165
));
5266

5367
// Canister ID Store

crates/icp/src/manifest/mod.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,68 @@ where
176176
})?;
177177
Ok(m)
178178
}
179+
180+
#[cfg(test)]
181+
mod tests {
182+
use super::*;
183+
use camino_tempfile::Utf8TempDir;
184+
185+
fn write_manifest(dir: &Path) {
186+
std::fs::write(dir.join(PROJECT_MANIFEST), "").unwrap();
187+
}
188+
189+
#[test]
190+
fn locate_returns_cwd_when_manifest_present() {
191+
let tmp = Utf8TempDir::new().unwrap();
192+
write_manifest(tmp.path());
193+
194+
let locator = ProjectRootLocateImpl::new(tmp.path().to_path_buf(), None);
195+
assert_eq!(locator.locate().unwrap(), tmp.path());
196+
}
197+
198+
#[test]
199+
fn locate_walks_up_to_manifest() {
200+
let tmp = Utf8TempDir::new().unwrap();
201+
write_manifest(tmp.path());
202+
203+
let nested = tmp.path().join("a/b/c");
204+
std::fs::create_dir_all(&nested).unwrap();
205+
206+
let locator = ProjectRootLocateImpl::new(nested, None);
207+
assert_eq!(locator.locate().unwrap(), tmp.path());
208+
}
209+
210+
#[test]
211+
fn locate_returns_not_found_when_no_manifest_anywhere() {
212+
let tmp = Utf8TempDir::new().unwrap();
213+
let nested = tmp.path().join("a/b");
214+
std::fs::create_dir_all(&nested).unwrap();
215+
216+
// Host filesystem contains no icp.yaml above the tempdir (assumed in CI).
217+
let locator = ProjectRootLocateImpl::new(nested, None);
218+
assert!(matches!(
219+
locator.locate(),
220+
Err(ProjectRootLocateError::NotFound { .. })
221+
));
222+
}
223+
224+
// When cwd is a symlinked directory, locate() walks up via the symlink's
225+
// lexical parents
226+
#[cfg(unix)]
227+
#[test]
228+
fn locate_walks_up_through_symlink() {
229+
// target/ has no manifest anywhere above it within the test's scope.
230+
let target = Utf8TempDir::new().unwrap();
231+
232+
// project/ contains the manifest; `project/link` is a symlink to target/.
233+
let project = Utf8TempDir::new().unwrap();
234+
write_manifest(project.path());
235+
let link = project.path().join("link");
236+
std::os::unix::fs::symlink(target.path().as_std_path(), link.as_std_path()).unwrap();
237+
238+
// cwd is the symlink path; its lexical parent is `project`,
239+
// which contains the manifest.
240+
let locator = ProjectRootLocateImpl::new(link, None);
241+
assert_eq!(locator.locate().unwrap(), project.path());
242+
}
243+
}

0 commit comments

Comments
 (0)