Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b9db4c5
feat: implement clean action module with errors and tests
svasista-ms Apr 1, 2026
ef2d04e
feat: add clean subcommand to CLI
svasista-ms Apr 1, 2026
4075932
feat: add integration tests for `cargo wdk clean` command
svasista-ms Apr 1, 2026
9ce3a8e
refactor: run clean command within build command integration tests
svasista-ms Apr 2, 2026
4b58929
refactor: update CleanActionError types and test names based on metho…
svasista-ms Apr 2, 2026
64e7bcc
refactor(tests): run clean command again after the build command and …
svasista-ms Apr 6, 2026
fbf1c4b
refactor(tests): remove unnecessary Clippy lint suppression for mocka…
svasista-ms Apr 6, 2026
9cbdc66
refactor: simplify project validation logic in clean action
svasista-ms Apr 6, 2026
915366a
refactor: update CargoClean error
svasista-ms Apr 6, 2026
3fe851f
refactor: simplify subdirectory name extraction in run method
svasista-ms Apr 6, 2026
6bb2c93
refactor: add documentation for CleanAction struct
svasista-ms Apr 6, 2026
b7e2469
refactor: add validation for non-empty working directory in CleanAction
svasista-ms Apr 6, 2026
6e1fe56
refactor: improve test function names for clarity in CleanAction tests
svasista-ms Apr 7, 2026
ed2a887
refactor: add validation to ensure working directory is not empty in …
svasista-ms Apr 20, 2026
47285be
refactor: enhance error handling in CleanAction tests
svasista-ms Apr 20, 2026
95a2721
refactor: add assertions to verify target directories do not exist af…
svasista-ms Apr 20, 2026
3aee35d
refactor: improve error handling in new_fails_if_working_dir_is_empty…
svasista-ms Apr 27, 2026
730a953
refactor: move logging of cleaning packages to conditional check in r…
svasista-ms Apr 27, 2026
80cc4ec
refactor: move tests into CleanAction `mod.rs` file
svasista-ms Apr 28, 2026
abf8412
refactor: directory entry handling in CleanAction and BuildAction
svasista-ms May 4, 2026
8a5961a
docs: add cargo-wdk preview callout to Cargo Make section in README (…
krishnakumar4a4 Apr 14, 2026
5d37942
refactor: fix `collapsable_if` lint, rust 1.95 `assert_matches` desta…
leon-xd Apr 16, 2026
740225c
feat!: stack-based format buffer for `wdk` crate (#611)
leon-xd Apr 17, 2026
2567210
fix(cargo-wdk): use `--message-format=json-render-diagnostics` in the…
svasista-ms May 1, 2026
4cb3626
refactor(wdk-build): fix clippy lints (#655)
svasista-ms May 5, 2026
42a67e3
fix: correct typo in debug log message
svasista-ms May 5, 2026
b23b0b7
refactor: use `DirFileTypeError` variant when `file_type()` fails ins…
svasista-ms May 5, 2026
90a8726
fix: about string for the clean subcommand
svasista-ms May 5, 2026
69f895e
Merge branch 'main' into clean-command
svasista-ms May 5, 2026
3fd27e4
refactor: update variable names for clarity in build and clean actions
svasista-ms May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/cargo-wdk/src/actions/clean/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// License: MIT OR Apache-2.0
//! This module defines error types for the clean action module.

use std::path::PathBuf;

use thiserror::Error;

use crate::providers::error::{CommandError, FileError};

/// Errors for the clean action layer
#[derive(Error, Debug)]
pub enum CleanActionError {
#[error(transparent)]
FileIo(#[from] FileError),
#[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}")]
OneOrMoreRustProjectsFailedToClean(PathBuf),
#[error("Error running cargo clean command")]
Comment thread
svasista-ms marked this conversation as resolved.
Outdated
CargoClean(#[from] CommandError),
}
182 changes: 182 additions & 0 deletions crates/cargo-wdk/src/actions/clean/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright (c) Microsoft Corporation
// License: MIT OR Apache-2.0
//! This module contains the `CleanAction` struct and its associated methods
//! for cleaning build artifacts produced by the `build` command.
mod error;
#[cfg(test)]
mod tests;
Comment thread
krishnakumar4a4 marked this conversation as resolved.
Outdated

use std::path::{Path, PathBuf, absolute};

use anyhow::Result;
use error::CleanActionError;
use mockall_double::double;
Comment thread
wmmc88 marked this conversation as resolved.
use tracing::{debug, error as err, info};

#[double]
use crate::providers::{exec::CommandExec, fs::Fs};
use crate::trace;

pub struct CleanAction<'a> {
working_dir: PathBuf,
verbosity_level: clap_verbosity_flag::Verbosity,

// Injected deps
command_exec: &'a CommandExec,
fs: &'a Fs,
}
Comment thread
svasista-ms marked this conversation as resolved.

impl<'a> CleanAction<'a> {
/// Creates a new instance of `CleanAction`.
///
/// # Arguments
/// * `working_dir` - The working directory for the clean action
/// * `verbosity_level` - The verbosity level for logging
/// * `command_exec` - The command execution provider instance
/// * `fs` - The file system provider instance
///
/// # Returns
/// * `Result<Self>` - A result containing either a new instance of
/// `CleanAction` on success, or an `anyhow::Error`.
///
/// # Errors
/// * [`anyhow::Error`] - If `working_dir` is not a syntactically valid
/// path, e.g. it is empty
pub fn new(
Comment thread
svasista-ms marked this conversation as resolved.
working_dir: &Path,
verbosity_level: clap_verbosity_flag::Verbosity,
command_exec: &'a CommandExec,
fs: &'a Fs,
) -> Result<Self> {
Ok(Self {
working_dir: absolute(working_dir)?,
verbosity_level,
command_exec,
fs,
})
}

/// Entry point method to execute the clean action flow.
///
/// The detection strategy is:
/// 1. If the working directory has a `Cargo.toml`, run `cargo clean`
/// directly (standalone project or workspace root).
/// 2. Otherwise, treat the directory as an emulated workspace: scan
/// immediate subdirectories for Rust projects and clean each. NOTE: This
/// follows the same logic as the build action.
///
/// # Returns
/// `Result<(), CleanActionError>`
///
/// # Errors
/// * `CleanActionError::FileIo` - If there is an IO error.
/// * `CleanActionError::CargoClean` - If there is an error running the
/// `cargo clean` command.
/// * `CleanActionError::NoValidRustProjectsInTheDirectory` - If no valid
/// Rust projects are found in the working directory.
/// * `CleanActionError::OneOrMoreRustProjectsFailedToClean` - If one or
/// more Rust projects fail to clean in an emulated workspace.
pub fn run(&self) -> Result<(), CleanActionError> {
debug!(
"Attempting to clean project at: {}",
self.working_dir.display()
);
Comment thread
svasista-ms marked this conversation as resolved.

// Standalone driver/driver workspace support
if self.fs.exists(&self.working_dir.join("Cargo.toml")) {
debug!(
"Found Cargo.toml in {}. Running cargo clean.",
self.working_dir.display()
);
return self.run_cargo_clean(&self.working_dir);
}

// Emulated workspaces support
let dirs = self.fs.read_dir_entries(&self.working_dir)?;
Comment thread
krishnakumar4a4 marked this conversation as resolved.
debug!(
"Checking for valid Rust projects in the working directory: {}",
self.working_dir.display()
);

let mut is_valid_dir_with_rust_projects = false;
for dir in &dirs {
if self.fs.dir_file_type(dir)?.is_dir()
&& self.fs.exists(&dir.path().join("Cargo.toml"))
{
debug!(
"Found at least one valid Rust project directory: {}, continuing with the \
clean flow",
dir.path()
.file_name()
.expect(
"package sub directory name ended with \"..\" which is not expected",
)
.to_string_lossy()
);
is_valid_dir_with_rust_projects = true;
break;
}
}

if !is_valid_dir_with_rust_projects {
return Err(CleanActionError::NoValidRustProjectsInTheDirectory(
self.working_dir.clone(),
));
}

info!("Cleaning package(s) in {}", self.working_dir.display());

let mut failed_at_least_one_project = false;
for dir in dirs {
debug!("Checking dir entry: {}", dir.path().display());
if !self.fs.dir_file_type(&dir)?.is_dir()
|| !self.fs.exists(&dir.path().join("Cargo.toml"))
{
debug!("Dir entry is not a valid Rust package");
continue;
}

let working_dir_path = dir.path();
let sub_dir = working_dir_path
.file_name()
.expect("package sub directory name ended with \"..\" which is not expected")
.to_string_lossy();
Comment thread
svasista-ms marked this conversation as resolved.
Outdated

debug!("Cleaning package(s) in dir {sub_dir}");
if let Err(e) = self.run_cargo_clean(&dir.path()) {
Comment thread
svasista-ms marked this conversation as resolved.
Outdated
failed_at_least_one_project = true;
err!(
"Error cleaning project: {sub_dir}, error: {:?}",
anyhow::Error::new(e)
);
}
}

debug!("Done cleaning package(s) in {}", self.working_dir.display());
if failed_at_least_one_project {
return Err(CleanActionError::OneOrMoreRustProjectsFailedToClean(
self.working_dir.clone(),
));
}

info!(
"Clean completed successfully for package(s) in {}",
self.working_dir.display()
);
Ok(())
}

/// Runs `cargo clean` in the specified directory.
fn run_cargo_clean(&self, working_dir: &Path) -> Result<(), CleanActionError> {
info!("Running cargo clean in {}", working_dir.display());
let mut args = vec!["clean"];
Comment thread
svasista-ms marked this conversation as resolved.
Comment thread
svasista-ms marked this conversation as resolved.
if let Some(flag) = trace::get_cargo_verbose_flags(self.verbosity_level) {
args.push(flag);
}
self.command_exec
.run("cargo", &args, None, Some(working_dir))
.map_err(CleanActionError::CargoClean)?;
info!("Cleaned project at {}", working_dir.display());
Ok(())
}
}
148 changes: 148 additions & 0 deletions crates/cargo-wdk/src/actions/clean/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft Corporation
// License: MIT OR Apache-2.0

use std::{
os::windows::process::ExitStatusExt,
path::PathBuf,
process::{ExitStatus, Output},
};

Comment thread
svasista-ms marked this conversation as resolved.
Outdated
use mockall::predicate::eq;
use mockall_double::double;

use super::CleanAction;
use crate::providers::error::CommandError;
#[double]
use crate::providers::{exec::CommandExec, fs::Fs};

fn create_successful_output() -> Output {
Output {
status: ExitStatus::from_raw(0),
stdout: Vec::new(),
stderr: Vec::new(),
}
}

#[test]
fn new_succeeds_for_valid_args() {
let cwd = PathBuf::from("C:\\tmp");
let mock_fs = Fs::default();
let mock_exec = CommandExec::default();

let action = CleanAction::new(
&cwd,
clap_verbosity_flag::Verbosity::default(),
&mock_exec,
&mock_fs,
);

assert!(action.is_ok());
}

#[test]
fn new_fails_for_empty_path() {
let cwd = PathBuf::from("");
let mock_fs = Fs::default();
let mock_exec = CommandExec::default();

let action = CleanAction::new(
&cwd,
clap_verbosity_flag::Verbosity::default(),
&mock_exec,
&mock_fs,
);

assert!(action.is_err());
}
Comment thread
svasista-ms marked this conversation as resolved.
Outdated
Comment thread
svasista-ms marked this conversation as resolved.
Outdated

#[test]
fn run_invokes_cargo_clean_and_succeeds() {
let cwd = PathBuf::from("C:\\tmp");
let mut mock_fs = Fs::default();
let mut mock_exec = CommandExec::default();

mock_fs
.expect_exists()
.with(eq(cwd.join("Cargo.toml")))
.returning(|_| true);

mock_exec
.expect_run()
.withf(move |cmd, args, _env, working_dir| {
cmd == "cargo"
&& args == ["clean"]
&& *working_dir == Some(PathBuf::from("C:\\tmp").as_path())
})
.returning(|_, _, _, _| Ok(create_successful_output()));

let action = CleanAction::new(
&cwd,
clap_verbosity_flag::Verbosity::default(),
&mock_exec,
&mock_fs,
)
.expect("CleanAction::new should succeed");

assert!(action.run().is_ok());
}

#[test]
fn run_returns_error_when_cargo_clean_fails() {
let cwd = PathBuf::from("C:\\tmp");
let mut mock_fs = Fs::default();
let mut mock_exec = CommandExec::default();

mock_fs
.expect_exists()
.with(eq(cwd.join("Cargo.toml")))
.returning(|_| true);

mock_exec
.expect_run()
.withf(move |cmd, args, _env, working_dir| {
cmd == "cargo"
&& args == ["clean"]
&& *working_dir == Some(PathBuf::from("C:\\tmp").as_path())
})
.returning(|_, _, _, _| {
Err(CommandError::CommandFailed {
command: "cargo".to_string(),
args: vec!["clean".to_string()],
stdout: "error".to_string(),
})
});

let action = CleanAction::new(
&cwd,
clap_verbosity_flag::Verbosity::default(),
&mock_exec,
&mock_fs,
)
.expect("CleanAction::new should succeed");

assert!(action.run().is_err());
Comment thread
krishnakumar4a4 marked this conversation as resolved.
Outdated
}

#[test]
fn run_returns_error_when_no_cargo_toml_and_no_rust_projects() {
let cwd = PathBuf::from("C:\\tmp");
let mut mock_fs = Fs::default();
let mock_exec = CommandExec::default();

mock_fs
.expect_exists()
.with(eq(cwd.join("Cargo.toml")))
.returning(|_| false);

mock_fs.expect_read_dir_entries().returning(|_| Ok(vec![]));

let action = CleanAction::new(
&cwd,
clap_verbosity_flag::Verbosity::default(),
&mock_exec,
&mock_fs,
)
.expect("CleanAction::new should succeed");

assert!(action.run().is_err());
Comment thread
krishnakumar4a4 marked this conversation as resolved.
Outdated
}
2 changes: 2 additions & 0 deletions crates/cargo-wdk/src/actions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
//! business logic of the cargo-wdk utility are:
//! * `new` - New action module
//! * `build` - Build action module
//! * `clean` - Clean action module
pub mod build;
pub mod clean;
pub mod new;

use std::{
Expand Down
10 changes: 10 additions & 0 deletions crates/cargo-wdk/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::actions::{
UMDF_STR,
WDM_STR,
build::{BuildAction, BuildActionParams},
clean::CleanAction,
new::NewAction,
};
#[double]
Expand Down Expand Up @@ -101,6 +102,11 @@ pub enum Subcmd {
New(NewArgs),
#[clap(name = "build", about = "Build the Windows Driver Kit project")]
Build(BuildArgs),
#[clap(
name = "clean",
about = "Clean build artifacts of a Windows Driver Kit project"
Comment thread
gurry marked this conversation as resolved.
Outdated
)]
Clean,
Comment thread
svasista-ms marked this conversation as resolved.
}

/// Top level command line interface for cargo wdk
Expand Down Expand Up @@ -178,6 +184,10 @@ impl Cli {
.run()?;
Ok(())
}
Subcmd::Clean => {
CleanAction::new(Path::new("."), self.verbose, &command_exec, &fs)?.run()?;
Ok(())
}
}
}
}
Expand Down
Loading
Loading