From 3000218eb913109bacf0f5cec0dd74284a969cb5 Mon Sep 17 00:00:00 2001 From: Shravan Vasista Date: Mon, 27 Jul 2026 18:59:10 +0530 Subject: [PATCH 1/2] fix(cargo-wdk): workspace resolution in `build` and `clean` actions to match `cargo` --- crates/cargo-wdk/src/actions/build/mod.rs | 26 +++ crates/cargo-wdk/src/actions/clean/error.rs | 2 + crates/cargo-wdk/src/actions/clean/mod.rs | 179 +++++++++++++++++--- crates/cargo-wdk/src/cli.rs | 3 +- 4 files changed, 189 insertions(+), 21 deletions(-) diff --git a/crates/cargo-wdk/src/actions/build/mod.rs b/crates/cargo-wdk/src/actions/build/mod.rs index 4411f9c8f..7d27b33eb 100644 --- a/crates/cargo-wdk/src/actions/build/mod.rs +++ b/crates/cargo-wdk/src/actions/build/mod.rs @@ -158,6 +158,32 @@ impl<'a> BuildAction<'a> { return self.run_from_workspace_root(&self.working_dir); } + if let Ok(cargo_metadata) = self.get_cargo_metadata(&self.working_dir) { + let owns_working_dir = cargo_metadata.workspace_packages().iter().any(|p| { + p.manifest_path + .parent() + .and_then(|path| absolute(path.as_std_path()).ok()) + .is_some_and(|package_dir| package_dir.starts_with(&self.working_dir)) + }); + + if owns_working_dir { + let workspace_root = absolute(cargo_metadata.workspace_root.as_std_path()) + .map_err(|e| { + BuildActionError::NotAbsolute( + cargo_metadata.workspace_root.clone().into(), + e, + ) + })?; + debug!( + "Working directory {} lies inside the workspace rooted at {}; running build \ + from workspace root", + self.working_dir.display(), + workspace_root.display() + ); + return self.run_from_workspace_root(&workspace_root); + } + } + // Emulated workspaces support let dirs = self.fs.read_dir_entries(&self.working_dir)?; debug!( diff --git a/crates/cargo-wdk/src/actions/clean/error.rs b/crates/cargo-wdk/src/actions/clean/error.rs index c71bf9a41..06e73743d 100644 --- a/crates/cargo-wdk/src/actions/clean/error.rs +++ b/crates/cargo-wdk/src/actions/clean/error.rs @@ -13,6 +13,8 @@ use crate::providers::error::{CommandError, FileError}; pub enum CleanActionError { #[error(transparent)] FileIo(#[from] FileError), + #[error("Provided path is not absolute: {0}")] + NotAbsolute(PathBuf, #[source] std::io::Error), #[error("No valid rust projects in the current working directory: {0}")] NoValidRustProjectsInTheDirectory(PathBuf), #[error("One or more projects failed to clean in the emulated workspace: {0}")] diff --git a/crates/cargo-wdk/src/actions/clean/mod.rs b/crates/cargo-wdk/src/actions/clean/mod.rs index 557690240..70c9855d0 100644 --- a/crates/cargo-wdk/src/actions/clean/mod.rs +++ b/crates/cargo-wdk/src/actions/clean/mod.rs @@ -7,12 +7,13 @@ mod error; use std::path::{Path, PathBuf, absolute}; use anyhow::Result; +use clap_cargo::Features; use error::CleanActionError; use mockall_double::double; use tracing::{debug, error as err, info}; #[double] -use crate::providers::{exec::CommandExec, fs::Fs}; +use crate::providers::{exec::CommandExec, fs::Fs, metadata::Metadata}; use crate::trace; /// Action that removes build artifacts produced by the `build` command for a @@ -24,6 +25,7 @@ pub struct CleanAction<'a> { // Injected deps command_exec: &'a CommandExec, fs: &'a Fs, + metadata: &'a Metadata, } impl<'a> CleanAction<'a> { @@ -34,6 +36,7 @@ impl<'a> CleanAction<'a> { /// * `verbosity_level` - The verbosity level for logging /// * `command_exec` - The command execution provider instance /// * `fs` - The file system provider instance + /// * `metadata` - The metadata provider instance /// /// # Returns /// * `Result` - A result containing either a new instance of @@ -47,6 +50,7 @@ impl<'a> CleanAction<'a> { verbosity_level: clap_verbosity_flag::Verbosity, command_exec: &'a CommandExec, fs: &'a Fs, + metadata: &'a Metadata, ) -> Result { anyhow::ensure!( !working_dir.as_os_str().is_empty(), @@ -57,6 +61,7 @@ impl<'a> CleanAction<'a> { verbosity_level, command_exec, fs, + metadata, }) } @@ -95,6 +100,36 @@ impl<'a> CleanAction<'a> { return self.run_cargo_clean(&self.working_dir); } + if let Ok(cargo_metadata) = self.metadata.get_cargo_metadata_at_path( + &self.working_dir, + Vec::new(), + &Features::default(), + ) { + let owns_working_dir = cargo_metadata.workspace_packages().iter().any(|p| { + p.manifest_path + .parent() + .and_then(|path| absolute(path.as_std_path()).ok()) + .is_some_and(|package_dir| package_dir.starts_with(&self.working_dir)) + }); + + if owns_working_dir { + let workspace_root = absolute(cargo_metadata.workspace_root.as_std_path()) + .map_err(|e| { + CleanActionError::NotAbsolute( + cargo_metadata.workspace_root.clone().into(), + e, + ) + })?; + debug!( + "Working directory {} lies inside the workspace rooted at {}; running cargo \ + clean from workspace root", + self.working_dir.display(), + workspace_root.display() + ); + return self.run_cargo_clean(&workspace_root); + } + } + // Emulated workspaces support let dirs = self.fs.read_dir_entries(&self.working_dir)?; debug!( @@ -186,7 +221,7 @@ mod tests { fs::DirEntryInfo, }; #[double] - use crate::providers::{exec::CommandExec, fs::Fs}; + use crate::providers::{exec::CommandExec, fs::Fs, metadata::Metadata}; fn ok_output() -> Output { Output { @@ -241,10 +276,76 @@ mod tests { }); } - fn run_action(cwd: &Path, fs: &Fs, exec: &CommandExec) -> Result<(), CleanActionError> { - CleanAction::new(cwd, clap_verbosity_flag::Verbosity::default(), exec, fs) - .expect("CleanAction::new should succeed") - .run() + fn metadata_not_in_workspace() -> Metadata { + let mut metadata = Metadata::default(); + metadata + .expect_get_cargo_metadata_at_path() + .returning(|_, _, _| { + Err(cargo_metadata::Error::CargoMetadata { + stderr: "not a workspace".to_string(), + }) + }); + metadata + } + + fn metadata_owning_dir(workspace_root: &Path, member_dir: &Path) -> Metadata { + let member_fwd = member_dir.to_string_lossy().replace('\\', "/"); + let member_fwd = member_fwd.trim_start_matches("//?/").to_string(); + let id = format!("path+file:///{member_fwd}#pkg@0.1.0"); + let json = serde_json::json!({ + "target_directory": workspace_root.join("target").to_string_lossy(), + "workspace_root": workspace_root.to_string_lossy(), + "packages": [{ + "name": "pkg", + "version": "0.1.0", + "id": id, + "dependencies": [], + "targets": [{ + "kind": ["lib"], + "crate_types": ["lib"], + "name": "pkg", + "src_path": member_dir.join("src").join("lib.rs").to_string_lossy(), + "edition": "2021", + "doc": true, + "doctest": false, + "test": true + }], + "features": {}, + "manifest_path": member_dir.join("Cargo.toml").to_string_lossy(), + "authors": [], + "categories": [], + "keywords": [], + "edition": "2021", + "metadata": null + }], + "workspace_members": [id], + "metadata": null, + "version": 1 + }); + let parsed: cargo_metadata::Metadata = + serde_json::from_value(json).expect("valid cargo metadata"); + let mut metadata = Metadata::default(); + metadata + .expect_get_cargo_metadata_at_path() + .returning(move |_, _, _| Ok(parsed.clone())); + metadata + } + + fn run_action( + cwd: &Path, + fs: &Fs, + exec: &CommandExec, + metadata: &Metadata, + ) -> Result<(), CleanActionError> { + CleanAction::new( + cwd, + clap_verbosity_flag::Verbosity::default(), + exec, + fs, + metadata, + ) + .expect("CleanAction::new should succeed") + .run() } #[test] @@ -252,8 +353,16 @@ mod tests { let cwd = PathBuf::from("C:\\tmp"); let fs = Fs::default(); let exec = CommandExec::default(); + let metadata = Metadata::default(); assert!( - CleanAction::new(&cwd, clap_verbosity_flag::Verbosity::default(), &exec, &fs,).is_ok() + CleanAction::new( + &cwd, + clap_verbosity_flag::Verbosity::default(), + &exec, + &fs, + &metadata, + ) + .is_ok() ); } @@ -262,9 +371,16 @@ mod tests { let cwd = PathBuf::from(""); let fs = Fs::default(); let exec = CommandExec::default(); - let err = CleanAction::new(&cwd, clap_verbosity_flag::Verbosity::default(), &exec, &fs) - .err() - .expect("CleanAction::new should fail for empty working_dir"); + let metadata = Metadata::default(); + let err = CleanAction::new( + &cwd, + clap_verbosity_flag::Verbosity::default(), + &exec, + &fs, + &metadata, + ) + .err() + .expect("CleanAction::new should fail for empty working_dir"); assert_eq!(err.to_string(), "working_dir must not be empty"); } @@ -275,9 +391,10 @@ mod tests { let cwd = PathBuf::from("C:\\tmp"); let mut fs = Fs::default(); let mut exec = CommandExec::default(); + let metadata = Metadata::default(); mock_cargo_toml(&mut fs, &cwd, true); mock_cargo_clean(&mut exec, &cwd, true); - assert!(run_action(&cwd, &fs, &exec).is_ok()); + assert!(run_action(&cwd, &fs, &exec, &metadata).is_ok()); } #[test] @@ -285,10 +402,11 @@ mod tests { let cwd = PathBuf::from("C:\\tmp"); let mut fs = Fs::default(); let mut exec = CommandExec::default(); + let metadata = Metadata::default(); mock_cargo_toml(&mut fs, &cwd, true); mock_cargo_clean(&mut exec, &cwd, false); assert!(matches!( - run_action(&cwd, &fs, &exec), + run_action(&cwd, &fs, &exec, &metadata), Err(CleanActionError::CargoClean(_)) )); } @@ -300,25 +418,40 @@ mod tests { let cwd = PathBuf::from("C:\\tmp"); let mut fs = Fs::default(); let exec = CommandExec::default(); + let metadata = metadata_not_in_workspace(); mock_cargo_toml(&mut fs, &cwd, false); mock_read_dir(&mut fs, &cwd, &[]); assert!(matches!( - run_action(&cwd, &fs, &exec), + run_action(&cwd, &fs, &exec, &metadata), Err(CleanActionError::NoValidRustProjectsInTheDirectory(_)) )); } + #[test] + fn run_cleans_workspace_root_when_invoked_from_subdirectory() { + let workspace_root = PathBuf::from("C:\\tmp\\ws"); + let group_dir = workspace_root.join("group"); + let member_dir = group_dir.join("pkg"); + let mut fs = Fs::default(); + let mut exec = CommandExec::default(); + let metadata = metadata_owning_dir(&workspace_root, &member_dir); + mock_cargo_toml(&mut fs, &group_dir, false); + mock_cargo_clean(&mut exec, &workspace_root, true); + assert!(run_action(&group_dir, &fs, &exec, &metadata).is_ok()); + } + #[test] fn run_cleans_single_rust_project_in_emulated_workspace() { let cwd = PathBuf::from("C:\\tmp"); let pkg_a = cwd.join("pkg-a"); let mut fs = Fs::default(); let mut exec = CommandExec::default(); + let metadata = metadata_not_in_workspace(); mock_cargo_toml(&mut fs, &cwd, false); mock_read_dir(&mut fs, &cwd, &[("pkg-a", true)]); mock_cargo_toml(&mut fs, &pkg_a, true); mock_cargo_clean(&mut exec, &pkg_a, true); - assert!(run_action(&cwd, &fs, &exec).is_ok()); + assert!(run_action(&cwd, &fs, &exec, &metadata).is_ok()); } #[test] @@ -328,13 +461,14 @@ mod tests { let pkg_b = cwd.join("pkg-b"); let mut fs = Fs::default(); let mut exec = CommandExec::default(); + let metadata = metadata_not_in_workspace(); mock_cargo_toml(&mut fs, &cwd, false); mock_read_dir(&mut fs, &cwd, &[("pkg-a", true), ("pkg-b", true)]); mock_cargo_toml(&mut fs, &pkg_a, true); mock_cargo_toml(&mut fs, &pkg_b, true); mock_cargo_clean(&mut exec, &pkg_a, true); mock_cargo_clean(&mut exec, &pkg_b, true); - assert!(run_action(&cwd, &fs, &exec).is_ok()); + assert!(run_action(&cwd, &fs, &exec, &metadata).is_ok()); } #[test] @@ -343,12 +477,13 @@ mod tests { let pkg_a = cwd.join("pkg-a"); let mut fs = Fs::default(); let mut exec = CommandExec::default(); + let metadata = metadata_not_in_workspace(); mock_cargo_toml(&mut fs, &cwd, false); // README.md (is_dir=false) is filtered out before any Cargo.toml probe. mock_read_dir(&mut fs, &cwd, &[("README.md", false), ("pkg-a", true)]); mock_cargo_toml(&mut fs, &pkg_a, true); mock_cargo_clean(&mut exec, &pkg_a, true); - assert!(run_action(&cwd, &fs, &exec).is_ok()); + assert!(run_action(&cwd, &fs, &exec, &metadata).is_ok()); } #[test] @@ -358,12 +493,13 @@ mod tests { let pkg_a = cwd.join("pkg-a"); let mut fs = Fs::default(); let mut exec = CommandExec::default(); + let metadata = metadata_not_in_workspace(); mock_cargo_toml(&mut fs, &cwd, false); mock_read_dir(&mut fs, &cwd, &[("docs", true), ("pkg-a", true)]); mock_cargo_toml(&mut fs, &docs, false); mock_cargo_toml(&mut fs, &pkg_a, true); mock_cargo_clean(&mut exec, &pkg_a, true); - assert!(run_action(&cwd, &fs, &exec).is_ok()); + assert!(run_action(&cwd, &fs, &exec, &metadata).is_ok()); } #[test] @@ -373,12 +509,13 @@ mod tests { let scripts = cwd.join("scripts"); let mut fs = Fs::default(); let exec = CommandExec::default(); + let metadata = metadata_not_in_workspace(); mock_cargo_toml(&mut fs, &cwd, false); mock_read_dir(&mut fs, &cwd, &[("docs", true), ("scripts", true)]); mock_cargo_toml(&mut fs, &docs, false); mock_cargo_toml(&mut fs, &scripts, false); assert!(matches!( - run_action(&cwd, &fs, &exec), + run_action(&cwd, &fs, &exec, &metadata), Err(CleanActionError::NoValidRustProjectsInTheDirectory(_)) )); } @@ -390,6 +527,7 @@ mod tests { let pkg_bad = cwd.join("pkg-bad"); let mut fs = Fs::default(); let mut exec = CommandExec::default(); + let metadata = metadata_not_in_workspace(); mock_cargo_toml(&mut fs, &cwd, false); mock_read_dir(&mut fs, &cwd, &[("pkg-ok", true), ("pkg-bad", true)]); mock_cargo_toml(&mut fs, &pkg_ok, true); @@ -397,7 +535,7 @@ mod tests { mock_cargo_clean(&mut exec, &pkg_ok, true); mock_cargo_clean(&mut exec, &pkg_bad, false); assert!(matches!( - run_action(&cwd, &fs, &exec), + run_action(&cwd, &fs, &exec, &metadata), Err(CleanActionError::OneOrMoreRustProjectsFailedToClean(_)) )); } @@ -407,6 +545,7 @@ mod tests { let cwd = PathBuf::from("C:\\tmp"); let mut fs = Fs::default(); let exec = CommandExec::default(); + let metadata = metadata_not_in_workspace(); mock_cargo_toml(&mut fs, &cwd, false); let cwd_clone = cwd.clone(); fs.expect_read_dir_entries().returning(move |_| { @@ -416,7 +555,7 @@ mod tests { )) }); assert!(matches!( - run_action(&cwd, &fs, &exec), + run_action(&cwd, &fs, &exec, &metadata), Err(CleanActionError::FileIo(_)) )); } diff --git a/crates/cargo-wdk/src/cli.rs b/crates/cargo-wdk/src/cli.rs index bbdcddcdc..6620095e0 100644 --- a/crates/cargo-wdk/src/cli.rs +++ b/crates/cargo-wdk/src/cli.rs @@ -258,7 +258,8 @@ impl Cli { Ok(()) } Subcmd::Clean => { - CleanAction::new(Path::new("."), self.verbose, &command_exec, &fs)?.run()?; + CleanAction::new(Path::new("."), self.verbose, &command_exec, &fs, &metadata)? + .run()?; Ok(()) } } From e5525cf4e506c0a0e41d22c2b526c8c40f42e03f Mon Sep 17 00:00:00 2001 From: Shravan Vasista Date: Tue, 28 Jul 2026 08:28:49 +0530 Subject: [PATCH 2/2] refactor(cargo-wdk): add `find_workspace_root` and use it in `build` and `clean` actions --- crates/cargo-wdk/src/actions/build/mod.rs | 40 ++-- crates/cargo-wdk/src/actions/clean/error.rs | 2 - crates/cargo-wdk/src/actions/clean/mod.rs | 100 ++------- crates/cargo-wdk/src/actions/mod.rs | 223 ++++++++++++++++++++ 4 files changed, 256 insertions(+), 109 deletions(-) diff --git a/crates/cargo-wdk/src/actions/build/mod.rs b/crates/cargo-wdk/src/actions/build/mod.rs index 7d27b33eb..d359f6980 100644 --- a/crates/cargo-wdk/src/actions/build/mod.rs +++ b/crates/cargo-wdk/src/actions/build/mod.rs @@ -158,34 +158,26 @@ impl<'a> BuildAction<'a> { return self.run_from_workspace_root(&self.working_dir); } - if let Ok(cargo_metadata) = self.get_cargo_metadata(&self.working_dir) { - let owns_working_dir = cargo_metadata.workspace_packages().iter().any(|p| { - p.manifest_path - .parent() - .and_then(|path| absolute(path.as_std_path()).ok()) - .is_some_and(|package_dir| package_dir.starts_with(&self.working_dir)) - }); + let dirs = self.fs.read_dir_entries(&self.working_dir)?; - if owns_working_dir { - let workspace_root = absolute(cargo_metadata.workspace_root.as_std_path()) - .map_err(|e| { - BuildActionError::NotAbsolute( - cargo_metadata.workspace_root.clone().into(), - e, - ) - })?; - debug!( - "Working directory {} lies inside the workspace rooted at {}; running build \ - from workspace root", - self.working_dir.display(), - workspace_root.display() - ); - return self.run_from_workspace_root(&workspace_root); - } + if let Some(workspace_root) = super::find_workspace_root( + self.metadata, + self.fs, + &self.working_dir, + &dirs, + self.locked, + self.features, + ) { + debug!( + "Working directory {} lies inside the workspace rooted at {}; running build from \ + workspace root", + self.working_dir.display(), + workspace_root.display() + ); + return self.run_from_workspace_root(&workspace_root); } // Emulated workspaces support - let dirs = self.fs.read_dir_entries(&self.working_dir)?; debug!( "Checking for valid Rust projects in the working directory: {}", self.working_dir.display() diff --git a/crates/cargo-wdk/src/actions/clean/error.rs b/crates/cargo-wdk/src/actions/clean/error.rs index 06e73743d..c71bf9a41 100644 --- a/crates/cargo-wdk/src/actions/clean/error.rs +++ b/crates/cargo-wdk/src/actions/clean/error.rs @@ -13,8 +13,6 @@ use crate::providers::error::{CommandError, FileError}; pub enum CleanActionError { #[error(transparent)] FileIo(#[from] FileError), - #[error("Provided path is not absolute: {0}")] - NotAbsolute(PathBuf, #[source] std::io::Error), #[error("No valid rust projects in the current working directory: {0}")] NoValidRustProjectsInTheDirectory(PathBuf), #[error("One or more projects failed to clean in the emulated workspace: {0}")] diff --git a/crates/cargo-wdk/src/actions/clean/mod.rs b/crates/cargo-wdk/src/actions/clean/mod.rs index 70c9855d0..9ebd64ba9 100644 --- a/crates/cargo-wdk/src/actions/clean/mod.rs +++ b/crates/cargo-wdk/src/actions/clean/mod.rs @@ -100,38 +100,26 @@ impl<'a> CleanAction<'a> { return self.run_cargo_clean(&self.working_dir); } - if let Ok(cargo_metadata) = self.metadata.get_cargo_metadata_at_path( + let dirs = self.fs.read_dir_entries(&self.working_dir)?; + + if let Some(workspace_root) = super::find_workspace_root( + self.metadata, + self.fs, &self.working_dir, - Vec::new(), + &dirs, + false, &Features::default(), ) { - let owns_working_dir = cargo_metadata.workspace_packages().iter().any(|p| { - p.manifest_path - .parent() - .and_then(|path| absolute(path.as_std_path()).ok()) - .is_some_and(|package_dir| package_dir.starts_with(&self.working_dir)) - }); - - if owns_working_dir { - let workspace_root = absolute(cargo_metadata.workspace_root.as_std_path()) - .map_err(|e| { - CleanActionError::NotAbsolute( - cargo_metadata.workspace_root.clone().into(), - e, - ) - })?; - debug!( - "Working directory {} lies inside the workspace rooted at {}; running cargo \ - clean from workspace root", - self.working_dir.display(), - workspace_root.display() - ); - return self.run_cargo_clean(&workspace_root); - } + debug!( + "Working directory {} lies inside the workspace rooted at {}; running cargo clean \ + from workspace root", + self.working_dir.display(), + workspace_root.display() + ); + return self.run_cargo_clean(&workspace_root); } // Emulated workspaces support - let dirs = self.fs.read_dir_entries(&self.working_dir)?; debug!( "Checking for valid Rust projects in the working directory: {}", self.working_dir.display() @@ -288,49 +276,6 @@ mod tests { metadata } - fn metadata_owning_dir(workspace_root: &Path, member_dir: &Path) -> Metadata { - let member_fwd = member_dir.to_string_lossy().replace('\\', "/"); - let member_fwd = member_fwd.trim_start_matches("//?/").to_string(); - let id = format!("path+file:///{member_fwd}#pkg@0.1.0"); - let json = serde_json::json!({ - "target_directory": workspace_root.join("target").to_string_lossy(), - "workspace_root": workspace_root.to_string_lossy(), - "packages": [{ - "name": "pkg", - "version": "0.1.0", - "id": id, - "dependencies": [], - "targets": [{ - "kind": ["lib"], - "crate_types": ["lib"], - "name": "pkg", - "src_path": member_dir.join("src").join("lib.rs").to_string_lossy(), - "edition": "2021", - "doc": true, - "doctest": false, - "test": true - }], - "features": {}, - "manifest_path": member_dir.join("Cargo.toml").to_string_lossy(), - "authors": [], - "categories": [], - "keywords": [], - "edition": "2021", - "metadata": null - }], - "workspace_members": [id], - "metadata": null, - "version": 1 - }); - let parsed: cargo_metadata::Metadata = - serde_json::from_value(json).expect("valid cargo metadata"); - let mut metadata = Metadata::default(); - metadata - .expect_get_cargo_metadata_at_path() - .returning(move |_, _, _| Ok(parsed.clone())); - metadata - } - fn run_action( cwd: &Path, fs: &Fs, @@ -427,19 +372,6 @@ mod tests { )); } - #[test] - fn run_cleans_workspace_root_when_invoked_from_subdirectory() { - let workspace_root = PathBuf::from("C:\\tmp\\ws"); - let group_dir = workspace_root.join("group"); - let member_dir = group_dir.join("pkg"); - let mut fs = Fs::default(); - let mut exec = CommandExec::default(); - let metadata = metadata_owning_dir(&workspace_root, &member_dir); - mock_cargo_toml(&mut fs, &group_dir, false); - mock_cargo_clean(&mut exec, &workspace_root, true); - assert!(run_action(&group_dir, &fs, &exec, &metadata).is_ok()); - } - #[test] fn run_cleans_single_rust_project_in_emulated_workspace() { let cwd = PathBuf::from("C:\\tmp"); @@ -545,7 +477,9 @@ mod tests { let cwd = PathBuf::from("C:\\tmp"); let mut fs = Fs::default(); let exec = CommandExec::default(); - let metadata = metadata_not_in_workspace(); + // `read_dir_entries` is attempted before the metadata probe, so no + // metadata expectation is needed here. + let metadata = Metadata::default(); mock_cargo_toml(&mut fs, &cwd, false); let cwd_clone = cwd.clone(); fs.expect_read_dir_entries().returning(move |_| { diff --git a/crates/cargo-wdk/src/actions/mod.rs b/crates/cargo-wdk/src/actions/mod.rs index e59a880d1..e8b4981e3 100644 --- a/crates/cargo-wdk/src/actions/mod.rs +++ b/crates/cargo-wdk/src/actions/mod.rs @@ -13,11 +13,18 @@ pub mod new; use std::{ fmt::{self, Display}, + path::{Path, PathBuf, absolute}, str::FromStr, }; +use clap_cargo::Features; +use mockall_double::double; use wdk_build::CpuArchitecture; +use crate::providers::fs::DirEntryInfo; +#[double] +use crate::providers::{fs::Fs, metadata::Metadata}; + pub const KMDF_STR: &str = "kmdf"; pub const UMDF_STR: &str = "umdf"; pub const WDM_STR: &str = "wdm"; @@ -92,3 +99,219 @@ impl Display for DriverType { write!(f, "{s}") } } + +/// Resolves the root of the workspace for a working directory that has no +/// `Cargo.toml` of its own. +fn find_workspace_root( + metadata: &Metadata, + fs: &Fs, + working_dir: &Path, + dirs: &[DirEntryInfo], + locked: bool, + features: &Features, +) -> Option { + let working_dir_trimmed: PathBuf = working_dir + .to_string_lossy() + .trim_start_matches(r"\\?\") + .into(); + let other_options = if locked { + vec!["--locked".to_string()] + } else { + Vec::new() + }; + let cargo_metadata = metadata + .get_cargo_metadata_at_path(&working_dir_trimmed, other_options, features) + .ok()?; + + let member_dirs: Vec = cargo_metadata + .workspace_packages() + .iter() + .filter_map(|p| { + p.manifest_path + .parent() + .and_then(|path| absolute(path.as_std_path()).ok()) + }) + .collect(); + + let is_emulated_workspace = dirs.iter().any(|entry| { + entry.is_dir + && fs.exists(&entry.path.join("Cargo.toml")) + && absolute(&entry.path).is_ok_and(|child_dir| { + !member_dirs + .iter() + .any(|member| member.starts_with(&child_dir)) + }) + }); + if is_emulated_workspace { + return None; + } + + absolute(cargo_metadata.workspace_root.as_std_path()).ok() +} + +#[cfg(test)] +mod tests { + use std::path::{Path, PathBuf}; + + use clap_cargo::Features; + use mockall::predicate::eq; + use mockall_double::double; + + use super::find_workspace_root; + use crate::providers::fs::DirEntryInfo; + #[double] + use crate::providers::{fs::Fs, metadata::Metadata}; + + fn dir_entry(path: PathBuf, is_dir: bool) -> DirEntryInfo { + DirEntryInfo { path, is_dir } + } + + /// A `Metadata` mock that reports the directory is not inside any + /// workspace. + fn metadata_not_in_workspace() -> Metadata { + let mut metadata = Metadata::default(); + metadata + .expect_get_cargo_metadata_at_path() + .returning(|_, _, _| { + Err(cargo_metadata::Error::CargoMetadata { + stderr: "not a workspace".to_string(), + }) + }); + metadata + } + + /// A `Metadata` mock that reports a single workspace member at + /// `member_dir`, rooted at `workspace_root`. + fn metadata_owning_dir(workspace_root: &Path, member_dir: &Path) -> Metadata { + let member_fwd = member_dir.to_string_lossy().replace('\\', "/"); + let member_fwd = member_fwd.trim_start_matches("//?/").to_string(); + let id = format!("path+file:///{member_fwd}#pkg@0.1.0"); + let json = serde_json::json!({ + "target_directory": workspace_root.join("target").to_string_lossy(), + "workspace_root": workspace_root.to_string_lossy(), + "packages": [{ + "name": "pkg", + "version": "0.1.0", + "id": id, + "dependencies": [], + "targets": [{ + "kind": ["lib"], + "crate_types": ["lib"], + "name": "pkg", + "src_path": member_dir.join("src").join("lib.rs").to_string_lossy(), + "edition": "2021", + "doc": true, + "doctest": false, + "test": true + }], + "features": {}, + "manifest_path": member_dir.join("Cargo.toml").to_string_lossy(), + "authors": [], + "categories": [], + "keywords": [], + "edition": "2021", + "metadata": null + }], + "workspace_members": [id], + "metadata": null, + "version": 1 + }); + let parsed: cargo_metadata::Metadata = + serde_json::from_value(json).expect("valid cargo metadata"); + let mut metadata = Metadata::default(); + metadata + .expect_get_cargo_metadata_at_path() + .returning(move |_, _, _| Ok(parsed.clone())); + metadata + } + + mod find_workspace_root { + use super::*; + + #[test] + fn returns_none_when_not_in_a_workspace() { + // A loose directory not inside any Cargo workspace. + let metadata = metadata_not_in_workspace(); + let fs = Fs::default(); + let working_dir = PathBuf::from("C:\\tmp\\loose"); + assert_eq!( + find_workspace_root( + &metadata, + &fs, + &working_dir, + &[], + false, + &Features::default() + ), + None + ); + } + + #[test] + fn returns_workspace_root_from_intermediate_subdirectory() { + // An intermediate directory that is an ancestor of a workspace member. + let workspace_root = PathBuf::from("C:\\tmp\\ws"); + let group_dir = workspace_root.join("group"); + let member_dir = group_dir.join("pkg"); + let metadata = metadata_owning_dir(&workspace_root, &member_dir); + let mut fs = Fs::default(); + fs.expect_exists() + .with(eq(member_dir.join("Cargo.toml"))) + .returning(|_| true); + let dirs = [dir_entry(member_dir, true)]; + assert_eq!( + find_workspace_root( + &metadata, + &fs, + &group_dir, + &dirs, + false, + &Features::default() + ), + Some(workspace_root) + ); + } + + #[test] + fn returns_workspace_root_from_dir_without_child_projects() { + // A directory inside the workspace with no child projects (e.g. `docs/` or a + // member's `src/`). + let workspace_root = PathBuf::from("C:\\tmp\\ws"); + let docs_dir = workspace_root.join("docs"); + let member_dir = workspace_root.join("pkg"); + let metadata = metadata_owning_dir(&workspace_root, &member_dir); + let fs = Fs::default(); + assert_eq!( + find_workspace_root(&metadata, &fs, &docs_dir, &[], false, &Features::default()), + Some(workspace_root) + ); + } + + #[test] + fn returns_none_for_emulated_workspace_with_non_member_children() { + // An emulated-workspace directory whose children are independent projects + // excluded from the workspace. + let workspace_root = PathBuf::from("C:\\tmp\\ws"); + let emulated_dir = workspace_root.join("examples"); + let member_dir = workspace_root.join("crates").join("pkg"); + let child = emulated_dir.join("proj-a"); + let metadata = metadata_owning_dir(&workspace_root, &member_dir); + let mut fs = Fs::default(); + fs.expect_exists() + .with(eq(child.join("Cargo.toml"))) + .returning(|_| true); + let dirs = [dir_entry(child, true)]; + assert_eq!( + find_workspace_root( + &metadata, + &fs, + &emulated_dir, + &dirs, + false, + &Features::default() + ), + None + ); + } + } +}