diff --git a/CHANGELOG.md b/CHANGELOG.md index cc3e107c1..75e071fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * feat: `icp identity import` now takes `--seed-curve`, for seed phrases for non-k256 keys. * fix: `icp canister settings show` now outputs only the canister settings, consistent with the command name * fix: Fail early when attempting to create an identity with an already existing name. +* fix: Find icp.yaml even from within a symlinked folder. # v0.2.3 diff --git a/crates/icp-cli/tests/common/context.rs b/crates/icp-cli/tests/common/context.rs index 1b641f8ce..65cca2f52 100644 --- a/crates/icp-cli/tests/common/context.rs +++ b/crates/icp-cli/tests/common/context.rs @@ -95,6 +95,7 @@ impl TestContext { #[cfg(unix)] cmd.env("HOME", self.home_path()) .env_remove("ICP_HOME") + .env_remove("PWD") // don't inherit from the tester's shell // Also set XDG directories to ensure isolation on Linux .env("XDG_CONFIG_HOME", self.home_path().join(".config")) .env("XDG_DATA_HOME", self.home_path().join(".local/share")) @@ -239,7 +240,8 @@ impl TestContext { // Also set XDG directories to ensure isolation on Linux .env("XDG_CONFIG_HOME", self.home_path().join(".config")) .env("XDG_DATA_HOME", self.home_path().join(".local/share")) - .env("XDG_CACHE_HOME", self.home_path().join(".cache")); + .env("XDG_CACHE_HOME", self.home_path().join(".cache")) + .env("PWD", project_dir); } // run in portable mode on Windows, the user directory cannot be mocked #[cfg(windows)] diff --git a/crates/icp-cli/tests/network_tests.rs b/crates/icp-cli/tests/network_tests.rs index ebf0d38e0..18dbc2aed 100644 --- a/crates/icp-cli/tests/network_tests.rs +++ b/crates/icp-cli/tests/network_tests.rs @@ -68,6 +68,7 @@ async fn network_same_port() { ctx.ping_until_healthy(&project_dir_a, "sameport-network"); eprintln!("second network start attempt in another project"); + ctx.icp() .current_dir(&project_dir_b) .args(["network", "start", "sameport-network"]) @@ -75,7 +76,7 @@ async fn network_same_port() { .failure() .stderr(contains(format!( "Error: port 8080 is in use by the sameport-network network of the project at '{}'", - dunce::canonicalize(&project_dir_a).unwrap().display() + project_dir_a ))); } diff --git a/crates/icp/src/context/init.rs b/crates/icp/src/context/init.rs index 636fb2336..3b6d0d218 100644 --- a/crates/icp/src/context/init.rs +++ b/crates/icp/src/context/init.rs @@ -41,13 +41,27 @@ pub fn initialize( // Setup global directory structure let dirs = Arc::new(Directories::new().context(DirectoriesSnafu)?); - // Project Root + // Project Root. On Unix, prefer $PWD (the logical path the user cd'd + // through) over getcwd(3), which resolves symlinks to the physical path + // and would break upward traversal when the user is inside a symlinked + // directory whose manifest sits above the symlink's location. + #[cfg(unix)] + let cwd: PathBuf = match std::env::var("PWD") + .ok() + .map(PathBuf::from) + .filter(|p| p.is_absolute()) + { + Some(p) => p, + None => PathBuf::try_from(current_dir().context(CwdSnafu)?).context(Utf8PathSnafu)?, + }; + + #[cfg(not(unix))] + let cwd: PathBuf = + PathBuf::try_from(current_dir().context(CwdSnafu)?).context(Utf8PathSnafu)?; + let project_root_locate = Arc::new(manifest::ProjectRootLocateImpl::new( - dunce::canonicalize(current_dir().context(CwdSnafu)?) - .context(CwdSnafu)? - .try_into() - .context(Utf8PathSnafu)?, // cwd - project_root_override, // dir + cwd, + project_root_override, )); // Canister ID Store diff --git a/crates/icp/src/manifest/mod.rs b/crates/icp/src/manifest/mod.rs index d21a867d2..fe11aac7e 100644 --- a/crates/icp/src/manifest/mod.rs +++ b/crates/icp/src/manifest/mod.rs @@ -176,3 +176,68 @@ where })?; Ok(m) } + +#[cfg(test)] +mod tests { + use super::*; + use camino_tempfile::Utf8TempDir; + + fn write_manifest(dir: &Path) { + std::fs::write(dir.join(PROJECT_MANIFEST), "").unwrap(); + } + + #[test] + fn locate_returns_cwd_when_manifest_present() { + let tmp = Utf8TempDir::new().unwrap(); + write_manifest(tmp.path()); + + let locator = ProjectRootLocateImpl::new(tmp.path().to_path_buf(), None); + assert_eq!(locator.locate().unwrap(), tmp.path()); + } + + #[test] + fn locate_walks_up_to_manifest() { + let tmp = Utf8TempDir::new().unwrap(); + write_manifest(tmp.path()); + + let nested = tmp.path().join("a/b/c"); + std::fs::create_dir_all(&nested).unwrap(); + + let locator = ProjectRootLocateImpl::new(nested, None); + assert_eq!(locator.locate().unwrap(), tmp.path()); + } + + #[test] + fn locate_returns_not_found_when_no_manifest_anywhere() { + let tmp = Utf8TempDir::new().unwrap(); + let nested = tmp.path().join("a/b"); + std::fs::create_dir_all(&nested).unwrap(); + + // Host filesystem contains no icp.yaml above the tempdir (assumed in CI). + let locator = ProjectRootLocateImpl::new(nested, None); + assert!(matches!( + locator.locate(), + Err(ProjectRootLocateError::NotFound { .. }) + )); + } + + // When cwd is a symlinked directory, locate() walks up via the symlink's + // lexical parents + #[cfg(unix)] + #[test] + fn locate_walks_up_through_symlink() { + // target/ has no manifest anywhere above it within the test's scope. + let target = Utf8TempDir::new().unwrap(); + + // project/ contains the manifest; `project/link` is a symlink to target/. + let project = Utf8TempDir::new().unwrap(); + write_manifest(project.path()); + let link = project.path().join("link"); + std::os::unix::fs::symlink(target.path().as_std_path(), link.as_std_path()).unwrap(); + + // cwd is the symlink path; its lexical parent is `project`, + // which contains the manifest. + let locator = ProjectRootLocateImpl::new(link, None); + assert_eq!(locator.locate().unwrap(), project.path()); + } +}