Files
punktfunk/packaging/windows/drivers/vendor/wdk-build/src/cargo_make.rs
T
enricobuehler 9fd19b90a9
windows-drivers / probe-and-proto (push) Successful in 24s
apple / swift (push) Successful in 1m8s
windows-drivers / driver-build (push) Failing after 43s
ci / rust (push) Successful in 1m31s
ci / web (push) Successful in 1m5s
ci / docs-site (push) Successful in 52s
apple / screenshots (push) Failing after 2m35s
windows-host / package (push) Successful in 5m23s
ci / bench (push) Successful in 4m48s
android / android (push) Successful in 10m1s
decky / build-publish (push) Successful in 26s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
deb / build-publish (push) Successful in 3m29s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m21s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m23s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m18s
docker / deploy-docs (push) Successful in 21s
feat(windows-drivers): vendor wdk 0.5.1 + add ApiSubset::Iddcx (M1 spike)
Vendor the published, self-contained windows-drivers-rs 0.5.1 crates
(wdk-build, wdk-sys) under vendor/ and add a first-class ApiSubset::Iddcx that
bindgens iddcx/1.10/IddCx.h in an extra pass reusing bindgen::Builder::wdk_default
(allowlist_file (?i).*iddcx.* — emits only IddCx items; WDF/DXGI types resolve to
the shared base/wdf bindings, type-identity by construction). Mirrors the existing
gpio/hid/spb subsets exactly: wdk-build gets the enum variant + iddcx_headers()
(UMDF-only), wdk-sys gets generate_iddcx + the iddcx feature + pub mod iddcx.
[patch.crates-io] redirects all wdk-sys/wdk-build (incl. wdk 0.4.1 transitive) to
the patched copies. wdk-probe enables the iddcx feature.

MAKE-OR-BREAK: does IddCx.h bindgen in wdk-sys config without a header conflict
(issue #515) + does the generated module compile (type-identity)? CI answers it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:12:43 +00:00

1274 lines
47 KiB
Rust

// Copyright (c) Microsoft Corporation
// License: MIT OR Apache-2.0
//! Utilities for `cargo-make` tasks used to package binaries dependent on the
//! `WDK`.
//!
//! This module provides functions used in the rust scripts in
//! `rust-driver-makefile.toml`. This includes argument parsing functionality
//! used by `rust-driver-makefile.toml` to validate and forward arguments common
//! to cargo commands. It uses a combination of `clap` and `clap_cargo` to
//! provide a CLI very close to cargo's own, but only exposes the arguments
//! supported by `rust-driver-makefile.toml`.
use core::{fmt, ops::RangeFrom};
use std::{
env,
panic::UnwindSafe,
path::{Path, PathBuf, absolute},
process::Command,
};
use anyhow::Context;
use cargo_metadata::{Metadata, MetadataCommand, camino::Utf8Path};
use clap::{Args, ColorChoice, CommandFactory, FromArgMatches, Parser};
use tracing::{instrument, trace};
use crate::{
ConfigError,
CpuArchitecture,
IoError,
metadata,
utils::{detect_wdk_content_root, detect_windows_sdk_version, get_wdk_version_number, set_var},
};
/// The filename of the main makefile for Rust Windows drivers.
pub const RUST_DRIVER_MAKEFILE_NAME: &str = "rust-driver-makefile.toml";
/// The filename of the samples makefile for Rust Windows drivers.
pub const RUST_DRIVER_SAMPLE_MAKEFILE_NAME: &str = "rust-driver-sample-makefile.toml";
const PATH_ENV_VAR: &str = "Path";
/// The environment variable that [`setup_wdk_version`] stores the WDK version
/// in.
pub const WDK_VERSION_ENV_VAR: &str = "WDK_BUILD_DETECTED_VERSION";
/// The first WDK version with the new `InfVerif` behavior.
const MINIMUM_SAMPLES_FLAG_WDK_VERSION: i32 = 25798;
const WDK_INF_ADDITIONAL_FLAGS_ENV_VAR: &str = "WDK_BUILD_ADDITIONAL_INFVERIF_FLAGS";
const WDK_BUILD_OUTPUT_DIRECTORY_ENV_VAR: &str = "WDK_BUILD_OUTPUT_DIRECTORY";
/// The name of the environment variable that cargo-make uses during `cargo
/// build` and `cargo test` commands
const CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR: &str = "CARGO_MAKE_CARGO_BUILD_TEST_FLAGS";
const CARGO_MAKE_DISABLE_COLOR_ENV_VAR: &str = "CARGO_MAKE_DISABLE_COLOR";
const CARGO_MAKE_PROFILE_ENV_VAR: &str = "CARGO_MAKE_PROFILE";
const CARGO_MAKE_CARGO_PROFILE_ENV_VAR: &str = "CARGO_MAKE_CARGO_PROFILE";
const CARGO_MAKE_CRATE_TARGET_TRIPLE_ENV_VAR: &str = "CARGO_MAKE_CRATE_TARGET_TRIPLE";
const CARGO_MAKE_CRATE_CUSTOM_TRIPLE_TARGET_DIRECTORY_ENV_VAR: &str =
"CARGO_MAKE_CRATE_CUSTOM_TRIPLE_TARGET_DIRECTORY";
const CARGO_MAKE_RUST_DEFAULT_TOOLCHAIN_ENV_VAR: &str = "CARGO_MAKE_RUST_DEFAULT_TOOLCHAIN";
const CARGO_MAKE_CRATE_NAME_ENV_VAR: &str = "CARGO_MAKE_CRATE_NAME";
const CARGO_MAKE_CRATE_FS_NAME_ENV_VAR: &str = "CARGO_MAKE_CRATE_FS_NAME";
const CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY_ENV_VAR: &str =
"CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY";
const CARGO_MAKE_CURRENT_TASK_NAME_ENV_VAR: &str = "CARGO_MAKE_CURRENT_TASK_NAME";
/// `clap` uses an exit code of 2 for usage errors: <https://github.com/clap-rs/clap/blob/14fd853fb9c5b94e371170bbd0ca2bf28ef3abff/clap_builder/src/util/mod.rs#L30C18-L30C28>
const CLAP_USAGE_EXIT_CODE: i32 = 2;
// This range is inclusive of 25798. FIXME: update with range end after /sample
// flag is added to InfVerif CLI
const MISSING_SAMPLE_FLAG_WDK_BUILD_NUMBER_RANGE: RangeFrom<u32> = 25798..;
trait ParseCargoArgs {
fn parse_cargo_args(&self);
}
#[derive(Parser, Debug)]
#[command(styles = clap_cargo::style::CLAP_STYLING)]
struct CommandLineInterface {
#[command(flatten)]
base: BaseOptions,
#[command(flatten)]
#[command(next_help_heading = "Package Selection")]
workspace: clap_cargo::Workspace,
#[command(flatten)]
#[command(next_help_heading = "Feature Selection")]
features: clap_cargo::Features,
#[command(flatten)]
compilation_options: CompilationOptions,
#[command(flatten)]
manifest_options: ManifestOptions,
}
#[derive(Args, Debug)]
struct BaseOptions {
#[arg(long, help = "Do not print cargo log messages")]
quiet: bool,
#[arg(short, long, action = clap::ArgAction::Count, help = "Use verbose output (-vv very verbose/build.rs output)")]
verbose: u8,
}
#[derive(Args, Debug)]
#[command(next_help_heading = "Compilation Options")]
struct CompilationOptions {
#[arg(
short,
long,
help = "Build artifacts in release mode, with optimizations"
)]
release: bool,
#[arg(
long,
value_name = "PROFILE-NAME",
help = "Build artifacts with the specified profile"
)]
profile: Option<String>,
#[arg(
short,
long,
value_name = "N",
allow_negative_numbers = true,
help = "Number of parallel jobs, defaults to # of CPUs."
)]
jobs: Option<String>,
// FIXME: support building multiple targets at once
#[arg(long, value_name = "TRIPLE", help = "Build for a target triple")]
target: Option<String>,
#[allow(clippy::option_option)] // This is how clap_derive expects "optional value for optional argument" args
#[arg(
long,
value_name = "FMTS",
require_equals = true,
help = "Timing output formats (unstable) (comma separated): html, json"
)]
timings: Option<Option<String>>,
}
#[derive(Args, Debug)]
#[command(next_help_heading = "Manifest Options")]
struct ManifestOptions {
#[arg(long, help = "Require Cargo.lock and cache are up to date")]
frozen: bool,
#[arg(long, help = "Require Cargo.lock is up to date")]
locked: bool,
#[arg(long, help = "Run without accessing the network")]
offline: bool,
}
impl ParseCargoArgs for CommandLineInterface {
fn parse_cargo_args(&self) {
let Self {
base,
workspace,
features,
compilation_options,
manifest_options,
} = self;
base.parse_cargo_args();
workspace.parse_cargo_args();
features.parse_cargo_args();
compilation_options.parse_cargo_args();
manifest_options.parse_cargo_args();
}
}
impl ParseCargoArgs for BaseOptions {
fn parse_cargo_args(&self) {
let Self { quiet, verbose } = self;
if *quiet && *verbose > 0 {
eprintln!("Cannot specify both --quiet and --verbose");
std::process::exit(CLAP_USAGE_EXIT_CODE);
}
if *quiet {
append_to_space_delimited_env_var(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR, "--quiet");
}
if *verbose > 0 {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
format!("-{}", "v".repeat((*verbose).into())).as_str(),
);
}
}
}
impl ParseCargoArgs for clap_cargo::Workspace {
fn parse_cargo_args(&self) {
let Self {
package,
workspace,
all,
exclude,
..
} = self;
if !package.is_empty() {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
package
.iter()
.fold(
String::with_capacity({
const MINIMUM_PACKAGE_SPEC_LENGTH: usize = 1;
const MINIMUM_PACKAGE_ARG_LENGTH: usize =
"--package ".len() + MINIMUM_PACKAGE_SPEC_LENGTH + " ".len();
package.len() * MINIMUM_PACKAGE_ARG_LENGTH
}),
|mut package_args, package_spec| {
package_args.push_str("--package ");
package_args.push_str(package_spec);
package_args.push(' ');
package_args
},
)
.trim_end(),
);
}
if *workspace {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
"--workspace",
);
}
if !exclude.is_empty() {
if !*workspace {
eprintln!("--exclude can only be used together with --workspace");
std::process::exit(CLAP_USAGE_EXIT_CODE);
}
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
exclude
.iter()
.fold(
String::with_capacity({
const MINIMUM_PACKAGE_SPEC_LENGTH: usize = 1;
const MINIMUM_EXCLUDE_ARG_LENGTH: usize =
"--exclude ".len() + MINIMUM_PACKAGE_SPEC_LENGTH + " ".len();
package.len() * MINIMUM_EXCLUDE_ARG_LENGTH
}),
|mut exclude_args, package_spec| {
exclude_args.push_str("--exclude ");
exclude_args.push_str(package_spec);
exclude_args.push(' ');
exclude_args
},
)
.trim_end(),
);
}
if *all {
append_to_space_delimited_env_var(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR, "--all");
}
}
}
impl ParseCargoArgs for clap_cargo::Features {
fn parse_cargo_args(&self) {
let Self {
all_features,
no_default_features,
features,
..
} = self;
if *all_features {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
"--all-features",
);
}
if *no_default_features {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
"--no-default-features",
);
}
if !features.is_empty() {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
features
.iter()
.fold(
String::with_capacity({
const MINIMUM_FEATURE_NAME_LENGTH: usize = 1;
const MINIMUM_FEATURE_ARG_LENGTH: usize =
"--features ".len() + MINIMUM_FEATURE_NAME_LENGTH + " ".len();
features.len() * MINIMUM_FEATURE_ARG_LENGTH
}),
|mut feature_args: String, feature| {
feature_args.push_str("--features ");
feature_args.push_str(feature);
feature_args.push(' ');
feature_args
},
)
.trim_end(),
);
}
}
}
impl ParseCargoArgs for CompilationOptions {
fn parse_cargo_args(&self) {
let Self {
release,
profile,
jobs,
target,
timings,
} = self;
if *release && profile.is_some() {
eprintln!("the `--release` flag should not be specified with the `--profile` flag");
std::process::exit(CLAP_USAGE_EXIT_CODE);
}
let cargo_make_cargo_profile = match env::var(CARGO_MAKE_PROFILE_ENV_VAR)
.unwrap_or_else(|_| panic!("{CARGO_MAKE_PROFILE_ENV_VAR} should be set by cargo-make"))
.as_str()
{
"release" => {
// cargo-make release profile sets the `--profile release` flag
if let Some(profile) = &profile {
if profile != "release" {
eprintln!(
"Specifying `--profile release` for cargo-make conflicts with the \
setting `--profile {profile}` to forward to tasks"
);
std::process::exit(CLAP_USAGE_EXIT_CODE);
}
}
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
"--profile release",
);
"release".to_string()
}
_ => {
// All other cargo-make profiles do not set a specific cargo profile. Cargo
// profiles set by --release, --profile <PROFILE>, or -p <PROFILE> (after
// the cargo-make task name) are forwarded to cargo
// commands
if *release {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
"--release",
);
"release".to_string()
} else if let Some(profile) = &profile {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
format!("--profile {profile}").as_str(),
);
profile.into()
} else {
env::var(CARGO_MAKE_CARGO_PROFILE_ENV_VAR).unwrap_or_else(|_| {
panic!("{CARGO_MAKE_CARGO_PROFILE_ENV_VAR} should be set by cargo-make")
})
}
}
};
set_var(CARGO_MAKE_CARGO_PROFILE_ENV_VAR, &cargo_make_cargo_profile);
if let Some(jobs) = &jobs {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
format!("--jobs {jobs}").as_str(),
);
}
if let Some(target) = &target {
set_var(CARGO_MAKE_CRATE_TARGET_TRIPLE_ENV_VAR, target);
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
format!("--target {target}").as_str(),
);
}
configure_wdf_build_output_dir(target.as_ref(), &cargo_make_cargo_profile);
if let Some(timings_option) = &timings {
timings_option.as_ref().map_or_else(
|| {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
"--timings",
);
},
|timings_value| {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
format!("--timings {timings_value}").as_str(),
);
},
);
}
}
}
impl ParseCargoArgs for ManifestOptions {
fn parse_cargo_args(&self) {
let Self {
frozen,
locked,
offline,
} = self;
if *frozen {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
"--frozen",
);
}
if *locked {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
"--locked",
);
}
if *offline {
append_to_space_delimited_env_var(
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
"--offline",
);
}
}
}
/// Parses the command line arguments, validates that they are supported by
/// `rust-driver-makefile.toml`, and then returns a list of environment variable
/// names that were updated.
///
/// These environment variable names should be passed to
/// [`forward_printed_env_vars`] to forward values to cargo-make.
///
/// # Panics
///
/// This function will panic if there's an internal error (i.e. bug) in its
/// argument processing.
#[must_use]
pub fn validate_command_line_args() -> impl IntoIterator<Item = String> {
const TOOLCHAIN_ARG_POSITION: usize = 1;
let mut env_args = env::args_os().collect::<Vec<_>>();
// +<toolchain> is a special argument that can't currently be handled by clap parsing: https://github.com/clap-rs/clap/issues/2468
let toolchain_arg = if env_args
.get(TOOLCHAIN_ARG_POSITION)
.is_some_and(|arg| arg.to_string_lossy().starts_with('+'))
{
Some(
env_args
.remove(TOOLCHAIN_ARG_POSITION)
.to_string_lossy()
.strip_prefix('+')
.expect("Toolchain arg should have a + prefix")
.to_string(),
)
} else {
None
};
if let Some(toolchain) = toolchain_arg {
set_var(CARGO_MAKE_RUST_DEFAULT_TOOLCHAIN_ENV_VAR, toolchain);
}
CommandLineInterface::from_arg_matches_mut(
&mut CommandLineInterface::command()
.color(if is_cargo_make_color_disabled() {
ColorChoice::Never
} else {
// `ColorChoice::Always` is used instead of `ColorChoice::Auto` to force color.
// This function is always executed from rust-script invoked by cargo-make,
// whose piping of stdout/stderr disables color by default.
ColorChoice::Always
})
.get_matches_from(env_args),
)
.unwrap_or_else(|err| err.exit())
.parse_cargo_args();
[
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS_ENV_VAR,
CARGO_MAKE_CARGO_PROFILE_ENV_VAR,
CARGO_MAKE_CRATE_TARGET_TRIPLE_ENV_VAR,
CARGO_MAKE_RUST_DEFAULT_TOOLCHAIN_ENV_VAR,
WDK_BUILD_OUTPUT_DIRECTORY_ENV_VAR,
]
.into_iter()
.filter(|env_var_name| env::var_os(env_var_name).is_some())
.map(ToString::to_string)
}
fn is_cargo_make_color_disabled() -> bool {
env::var(CARGO_MAKE_DISABLE_COLOR_ENV_VAR)
.map(|value| {
!matches!(
value.trim().to_ascii_lowercase().as_str(),
// when color is enabled in cargo-make, the env var is guaranteed to be set to one
// of the below values, or not be set at all
"0" | "false" | "no" | ""
)
})
.unwrap_or(false)
}
/// Prepends the path variable with the necessary paths to access WDK(+SDK)
/// tools.
///
/// # Errors
///
/// This function returns a [`ConfigError::WdkContentRootDetectionError`] if the
/// WDK content root directory could not be found.
///
/// # Panics
///
/// This function will panic if the CPU architecture cannot be determined from
/// [`env::consts::ARCH`] or if the PATH variable contains non-UTF8
/// characters.
pub fn setup_path() -> Result<impl IntoIterator<Item = String>, ConfigError> {
let wdk_content_root =
detect_wdk_content_root().ok_or(ConfigError::WdkContentRootDetectionError)?;
let sdk_version = detect_windows_sdk_version(&wdk_content_root)?;
let host_arch = CpuArchitecture::try_from_cargo_str(env::consts::ARCH)
.expect("The rust standard library should always set env::consts::ARCH");
let wdk_bin_root = get_wdk_bin_root(&wdk_content_root, &sdk_version);
let host_windows_sdk_ver_bin_path = {
let path = wdk_bin_root.join(host_arch.as_windows_str());
absolute(&path).map_err(|source| IoError::with_path(path, source))?
}
.to_str()
.expect("WDK bin path should be valid UTF-8")
.to_string();
let x86_windows_sdk_ver_bin_path = {
let path = wdk_bin_root.join("x86");
absolute(&path).map_err(|source| IoError::with_path(path, source))?
}
.to_str()
.expect("WDK x86 bin path should be valid UTF-8")
.to_string();
if let Ok(sdk_bin_path) = env::var("WindowsSdkBinPath") {
let sdk_bin_path = {
let path = PathBuf::from(sdk_bin_path)
.join(&sdk_version)
.join(host_arch.as_windows_str());
absolute(&path).map_err(|source| IoError::with_path(path, source))?
}
.to_str()
.expect("WindowsSdkBinPath should be valid UTF-8")
.to_string();
prepend_to_semicolon_delimited_env_var(PATH_ENV_VAR, sdk_bin_path);
}
prepend_to_semicolon_delimited_env_var(
PATH_ENV_VAR,
format!("{host_windows_sdk_ver_bin_path};{x86_windows_sdk_ver_bin_path}",),
);
let wdk_tool_root = get_wdk_tools_root(&wdk_content_root, &sdk_version);
let host_windows_sdk_version_tool_path = {
let path = wdk_tool_root.join(host_arch.as_windows_str());
absolute(&path).map_err(|source| IoError::with_path(path, source))?
}
.to_str()
.expect("WDK tool path should be valid UTF-8")
.to_string();
prepend_to_semicolon_delimited_env_var(PATH_ENV_VAR, host_windows_sdk_version_tool_path);
Ok([PATH_ENV_VAR].map(ToString::to_string))
}
fn get_wdk_tools_root(wdk_content_root: &Path, sdk_version: &str) -> PathBuf {
get_path_from_env("WDKToolRoot", wdk_content_root, "tools", sdk_version)
}
fn get_wdk_bin_root(wdk_content_root: &Path, sdk_version: &str) -> PathBuf {
get_path_from_env("WDKBinRoot", wdk_content_root, "bin", sdk_version)
}
/// Reads path from the given env variable or falls back to
/// constructing it from WDK content root.
///
/// The path in `env_var` is already the full path because eWDK
/// and Nuget CLI set it that way. In the fallback however we
/// have to append `sub_folder` and `sdk_version` manually.
fn get_path_from_env(
env_var: &str,
wdk_content_root: &Path,
sub_folder: &str,
sdk_version: &str,
) -> PathBuf {
env::var(env_var).map_or_else(
|e| {
trace!(
"Could not read env var '{env_var}': {e:?}. Constructing path from WDK content \
root"
);
wdk_content_root.join(sub_folder).join(sdk_version)
},
PathBuf::from,
)
}
/// Forwards the specified environment variables in this process to the parent
/// cargo-make. This is facilitated by printing to `stdout`, and having the
/// `rust-env-update` plugin parse the printed output.
///
/// # Panics
///
/// Panics if any of the `env_vars` do not exist or contain a non-UTF8 value.
pub fn forward_printed_env_vars(env_vars: impl IntoIterator<Item = impl AsRef<str>>) {
// This print signifies the start of the forwarding and signals to the
// `rust-env-update` plugin that it should forward args
println!("FORWARDING ARGS TO CARGO-MAKE:");
for env_var_name in env_vars {
let env_var_name = env_var_name.as_ref();
// Since this executes in a child process to cargo-make, we need to forward the
// values we want to change to duckscript, in order to get it to modify the
// parent process (ie. cargo-make)
println!(
"{env_var_name}={}",
env::var(env_var_name).unwrap_or_else(|_| panic!(
"{env_var_name} should be the name of an environment variable that is set and \
contains a valid UTF-8 value"
))
);
}
// This print signifies the end of the forwarding and signals to the
// `rust-env-update` plugin that it should stop forwarding args
println!("END OF FORWARDING ARGS TO CARGO-MAKE");
}
/// Adds the WDK version to the environment in the full string form of
/// 10.xxx.yyy.zzz, where x, y, and z are numerical values.
///
/// # Errors
///
/// This function returns a [`ConfigError::WdkContentRootDetectionError`] if the
/// WDK content root directory could not be found, or if the WDK version is
/// ill-formed.
pub fn setup_wdk_version() -> Result<impl IntoIterator<Item = String>, ConfigError> {
let Some(wdk_content_root) = detect_wdk_content_root() else {
return Err(ConfigError::WdkContentRootDetectionError);
};
let detected_sdk_version = detect_windows_sdk_version(&wdk_content_root)?;
if let Ok(existing_version) = std::env::var(WDK_VERSION_ENV_VAR) {
if detected_sdk_version == existing_version {
// Skip updating. This can happen in certain recursive
// cargo-make cases.
return Ok([WDK_VERSION_ENV_VAR].map(ToString::to_string));
}
// We have a bad version string set somehow. Return an error.
return Err(ConfigError::WdkContentRootDetectionError);
}
if !crate::utils::validate_wdk_version_format(&detected_sdk_version) {
return Err(ConfigError::WdkVersionStringFormatError {
version: detected_sdk_version,
});
}
set_var(WDK_VERSION_ENV_VAR, detected_sdk_version);
Ok([WDK_VERSION_ENV_VAR].map(ToString::to_string))
}
/// Sets the `WDK_INFVERIF_SAMPLE_FLAG` environment variable to contain the
/// appropriate flag for building samples.
///
/// # Errors
///
/// This function returns a [`ConfigError::WdkContentRootDetectionError`] if
/// an invalid WDK version is provided.
///
/// # Panics
///
/// This function will panic if the function for validating a WDK version string
/// is ever changed to no longer validate that each part of the version string
/// is an i32.
pub fn setup_infverif_for_samples<S: AsRef<str> + ToString + ?Sized>(
version: &S,
) -> Result<impl IntoIterator<Item = String>, ConfigError> {
let validated_version_string = crate::utils::get_wdk_version_number(version)?;
// Safe to unwrap as we called .parse::<i32>().is_ok() in our call to
// validate_wdk_version_format above.
let version = validated_version_string
.parse::<i32>()
.expect("Unable to parse the build number of the WDK version string as an int!");
let sample_flag = if version > MINIMUM_SAMPLES_FLAG_WDK_VERSION {
// Note: Not currently implemented, so in samples TOML we currently skip
// infverif
"/samples"
} else {
"/msft"
};
append_to_space_delimited_env_var(WDK_INF_ADDITIONAL_FLAGS_ENV_VAR, sample_flag);
Ok([WDK_INF_ADDITIONAL_FLAGS_ENV_VAR].map(ToString::to_string))
}
/// Returns the path to the WDK build output directory for the current
/// cargo-make flow
///
/// # Panics
///
/// This function will panic if the `WDK_BUILD_OUTPUT_DIRECTORY` environment
/// variable is not set
#[must_use]
pub fn get_wdk_build_output_directory() -> PathBuf {
PathBuf::from(
env::var("WDK_BUILD_OUTPUT_DIRECTORY")
.expect("WDK_BUILD_OUTPUT_DIRECTORY should have been set by the wdk-build-init task"),
)
}
/// Returns the name of the current cargo package cargo-make is processing
///
/// # Panics
///
/// This function will panic if the `CARGO_MAKE_CRATE_FS_NAME` environment
/// variable is not set
#[must_use]
pub fn get_current_package_name() -> String {
env::var(CARGO_MAKE_CRATE_FS_NAME_ENV_VAR).unwrap_or_else(|_| {
panic!(
"{} should be set by cargo-make",
&CARGO_MAKE_CRATE_FS_NAME_ENV_VAR
)
})
}
/// Copies the file or directory at `path_to_copy` to the Driver Package folder
///
/// # Errors
///
/// This function returns a [`ConfigError::IoError`] if the it encounters IO
/// errors while copying the file or creating the directory
///
/// # Panics
///
/// This function will panic if `path_to_copy` does end with a valid file or
/// directory name
pub fn copy_to_driver_package_folder<P: AsRef<Path>>(path_to_copy: P) -> Result<(), ConfigError> {
let path_to_copy = path_to_copy.as_ref();
let package_folder_path: PathBuf =
get_wdk_build_output_directory().join(format!("{}_package", get_current_package_name()));
if !package_folder_path.exists() {
std::fs::create_dir(&package_folder_path)
.map_err(|source| IoError::with_path(&package_folder_path, source))?;
}
let destination_path = package_folder_path.join(
path_to_copy
.file_name()
.expect("path_to_copy should always end with a valid file or directory name"),
);
std::fs::copy(path_to_copy, &destination_path)
.map_err(|source| IoError::with_src_dest_paths(path_to_copy, destination_path, source))?;
Ok(())
}
/// Symlinks `rust-driver-makefile.toml` to the `target` folder where it can be
/// extended from a `Makefile.toml`.
///
/// This is necessary so that paths in the `rust-driver-makefile.toml` can to be
/// relative to `CARGO_MAKE_CURRENT_TASK_INITIAL_MAKEFILE_DIRECTORY`
///
/// # Errors
///
/// This function returns:
/// - [`ConfigError::CargoMetadataError`] if there is an error executing or
/// parsing `cargo_metadata`
/// - [`ConfigError::MultipleWdkBuildCratesDetected`] if there are multiple
/// versions of the WDK build crate detected
/// - [`ConfigError::IoError`] if there is an error creating or updating the
/// symlink to `rust-driver-makefile.toml`
///
/// # Panics
///
/// This function will panic if the `CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY`
/// environment variable is not set
pub fn load_rust_driver_makefile() -> Result<(), ConfigError> {
load_wdk_build_makefile(RUST_DRIVER_MAKEFILE_NAME)
}
/// Symlinks `rust-driver-sample-makefile.toml` to the `target` folder where it
/// can be extended from a `Makefile.toml`.
///
/// This is necessary so that paths in the `rust-driver-sample-makefile.toml`
/// can to be relative to `CARGO_MAKE_CURRENT_TASK_INITIAL_MAKEFILE_DIRECTORY`
///
/// # Errors
///
/// This function returns:
/// - [`ConfigError::CargoMetadataError`] if there is an error executing or
/// parsing `cargo_metadata`
/// - [`ConfigError::MultipleWdkBuildCratesDetected`] if there are multiple
/// versions of the WDK build crate detected
/// - [`ConfigError::IoError`] if there is an error creating or updating the
/// symlink to `rust-driver-sample-makefile.toml`
///
/// # Panics
///
/// This function will panic if the `CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY`
/// environment variable is not set
pub fn load_rust_driver_sample_makefile() -> Result<(), ConfigError> {
load_wdk_build_makefile(RUST_DRIVER_SAMPLE_MAKEFILE_NAME)
}
/// Symlinks a [`wdk_build`] `cargo-make` makefile to the `target` folder where
/// it can be extended from a downstream `Makefile.toml`.
///
/// This is necessary so that paths in the [`wdk_build`] makefile can be
/// relative to `CARGO_MAKE_CURRENT_TASK_INITIAL_MAKEFILE_DIRECTORY`. The
/// version of `wdk-build` from which the file being symlinked to comes from is
/// determined by the working directory of the process that invokes this
/// function. For example, if this function is ultimately executing in a
/// `cargo_make` `load_script`, the files will be symlinked from the `wdk-build`
/// version that is in the `.Cargo.lock` file, and not the `wdk-build` version
/// specified in the `load_script`.
///
/// # Errors
///
/// This function returns:
/// - [`ConfigError::CargoMetadataError`] if there is an error executing or
/// parsing `cargo_metadata`
/// - [`ConfigError::MultipleWdkBuildCratesDetected`] if there are multiple
/// versions of the WDK build crate detected
/// - [`ConfigError::IoError`] if there is an error creating or updating the
/// symlink to the makefile.
///
/// # Panics
///
/// This function will panic if the `CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY`
/// environment variable is not set
#[instrument(level = "trace")]
fn load_wdk_build_makefile<S: AsRef<str> + AsRef<Utf8Path> + AsRef<Path> + fmt::Debug>(
makefile_name: S,
) -> Result<(), ConfigError> {
let cargo_metadata = MetadataCommand::new().exec()?;
trace!(cargo_metadata_output = ?cargo_metadata);
let wdk_build_package_matches = cargo_metadata
.packages
.into_iter()
.filter(|package| package.name == "wdk-build")
.collect::<Vec<_>>();
match wdk_build_package_matches.len() {
0 => {
return Err(ConfigError::NoWdkBuildCrateDetected);
}
1 => {}
_ => {
return Err(ConfigError::MultipleWdkBuildCratesDetected {
package_ids: wdk_build_package_matches
.iter()
.map(|package_info| package_info.id.clone())
.collect(),
});
}
}
let rust_driver_makefile_toml_path = wdk_build_package_matches[0]
.manifest_path
.parent()
.expect("The parsed manifest_path should have a valid parent directory")
.join(&makefile_name)
.into_std_path_buf();
let cargo_make_workspace_working_directory =
env::var(CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY_ENV_VAR).unwrap_or_else(|_| {
panic!("{CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY_ENV_VAR} should be set by cargo-make")
});
let destination_path = Path::new(&cargo_make_workspace_working_directory)
.join("target")
.join(&makefile_name);
// Only create a new symlink if the existing one is not already pointing to the
// correct file
if !destination_path.exists() {
std::os::windows::fs::symlink_file(&rust_driver_makefile_toml_path, &destination_path)
.map_err(|source| {
IoError::with_src_dest_paths(
rust_driver_makefile_toml_path,
destination_path,
source,
)
})?;
} else if !destination_path.is_symlink()
|| std::fs::read_link(&destination_path)
.map_err(|source| IoError::with_path(&destination_path, source))?
!= rust_driver_makefile_toml_path
{
std::fs::remove_file(&destination_path)
.map_err(|source| IoError::with_path(&destination_path, source))?;
std::os::windows::fs::symlink_file(&rust_driver_makefile_toml_path, &destination_path)
.map_err(|source| {
IoError::with_src_dest_paths(
rust_driver_makefile_toml_path,
destination_path,
source,
)
})?;
}
// Symlink is already up to date
Ok(())
}
/// Get [`cargo_metadata::Metadata`] based off of manifest in
/// `CARGO_MAKE_WORKING_DIRECTORY`
///
/// # Errors
///
/// This function will return a [`cargo_metadata::Error`] if `cargo_metadata`
/// fails
///
/// # Panics
///
/// This function will panic if executed outside of a `cargo-make` task
pub fn get_cargo_metadata() -> cargo_metadata::Result<Metadata> {
let manifest_path = {
let mut p: PathBuf = std::path::PathBuf::from(
std::env::var("CARGO_MAKE_WORKING_DIRECTORY")
.expect("CARGO_MAKE_WORKING_DIRECTORY should be set by cargo-make"),
);
p.push("Cargo.toml");
p
};
cargo_metadata::MetadataCommand::new()
.manifest_path(manifest_path)
.exec()
}
/// Execute a `FnOnce` closure, and handle its contents in a way compatible with
/// `cargo-make`'s `condition_script`:
/// 1. If the closure panics, the panic is caught and it returns an `Ok(())`.
/// This ensures that panics encountered in `condition_script_closure` will
/// not default to skipping the task.
/// 2. If the closure executes without panicking, forward the result to
/// `cargo-make`. `Ok` types will result in the task being run, and `Err`
/// types will print the `Err` contents and then skip the task.
///
/// If you want your task to be skipped, return an `Err` from
/// `condition_script_closure`. If you want the task to execute, return an
/// `Ok(())` from `condition_script_closure`
///
/// # Errors
///
/// This function returns an error whenever `condition_script_closure` returns
/// an error
///
/// # Panics
///
/// Panics if `CARGO_MAKE_CURRENT_TASK_NAME` is not set in the environment
pub fn condition_script<F, E>(condition_script_closure: F) -> anyhow::Result<(), E>
where
F: FnOnce() -> anyhow::Result<(), E> + UnwindSafe,
{
std::panic::catch_unwind(condition_script_closure).unwrap_or_else(|_| {
// Note: Any panic messages has already been printed by this point
let cargo_make_task_name = env::var(CARGO_MAKE_CURRENT_TASK_NAME_ENV_VAR)
.expect("CARGO_MAKE_CURRENT_TASK_NAME should be set by cargo-make");
eprintln!(
r#"`condition_script` for "{cargo_make_task_name}" task panicked while executing. \
Defaulting to running "{cargo_make_task_name}" task."#
);
Ok(())
})
}
/// `cargo-make` condition script for `package-driver-flow` task in
/// [`rust-driver-makefile.toml`](../rust-driver-makefile.toml)
///
/// # Errors
///
/// This function returns an error whenever it determines that the
/// `package-driver-flow` `cargo-make` task should be skipped (i.e. when the
/// current package isn't a cdylib depending on the WDK, or when no valid WDK
/// configurations are detected)
///
/// # Panics
///
/// Panics if `CARGO_MAKE_CURRENT_TASK_NAME` is not set in the environment
pub fn package_driver_flow_condition_script() -> anyhow::Result<()> {
condition_script(|| {
// Get the current package name via `CARGO_MAKE_CRATE_NAME_ENV_VAR` instead of
// `CARGO_MAKE_CRATE_FS_NAME_ENV_VAR`, since `cargo_metadata` output uses the
// non-preprocessed name (ie. - instead of _)
let current_package_name = env::var(CARGO_MAKE_CRATE_NAME_ENV_VAR).unwrap_or_else(|_| {
panic!(
"{} should be set by cargo-make",
&CARGO_MAKE_CRATE_NAME_ENV_VAR
)
});
let cargo_metadata = get_cargo_metadata()?;
// Skip task if the current crate is not a driver (i.e. a cdylib with a
// `package.metadata.wdk` section)
let current_package = cargo_metadata
.packages
.iter()
.find(|package| package.name == current_package_name)
.expect("The current package should be present in the cargo metadata output");
if current_package.metadata["wdk"].is_null() {
return Err::<(), anyhow::Error>(
metadata::TryFromCargoMetadataError::NoWdkConfigurationsDetected.into(),
)
.with_context(|| {
"Skipping package-driver-flow cargo-make task because the current crate does not \
have a package.metadata.wdk section"
});
}
if !current_package
.targets
.iter()
.any(|target| target.kind.contains(&cargo_metadata::TargetKind::CDyLib))
{
return Err::<(), anyhow::Error>(
metadata::TryFromCargoMetadataError::NoWdkConfigurationsDetected.into(),
)
.with_context(|| {
"Skipping package-driver-flow cargo-make task because the current crate does not \
contain a cdylib target"
});
}
match metadata::Wdk::try_from(&cargo_metadata) {
Err(e @ metadata::TryFromCargoMetadataError::NoWdkConfigurationsDetected) => {
// Skip task only if no WDK configurations are detected
Err::<(), anyhow::Error>(e.into()).with_context(|| {
"Skipping package-driver-flow cargo-make task because the current crate is not \
a driver"
})
}
Ok(_) => Ok(()),
Err(unexpected_error) => {
eprintln!("Unexpected error: {unexpected_error:#?}");
// Do not silently skip task if unexpected error in parsing WDK Metadata occurs
Ok(())
}
}
})
}
/// `cargo-make` condition script for `generate-certificate` task in
/// [`rust-driver-makefile.toml`](../rust-driver-makefile.toml)
///
/// # Errors
///
/// This functions returns an error whenever it determines that the
/// `generate-certificate` `cargo-make` task should be skipped. This only
/// occurs when `WdrLocalTestCert` already exists in `WDRTestCertStore`.
///
/// # Panics
///
/// Panics if `CARGO_MAKE_CURRENT_TASK_NAME` is not set in the environment.
pub fn generate_certificate_condition_script() -> anyhow::Result<()> {
condition_script(|| {
let mut command = Command::new("certmgr");
command.args([
"-put".as_ref(),
"-s".as_ref(),
"WDRTestCertStore".as_ref(),
"-c".as_ref(),
"-n".as_ref(),
"WdrLocalTestCert".as_ref(),
get_wdk_build_output_directory()
.join("WDRLocalTestCert.cer")
.as_os_str(),
]);
let output = command.output().unwrap_or_else(|err| {
panic!(
"Failed to run certmgr.exe {} due to error: {}",
command
.get_args()
.map(|arg| arg.to_string_lossy())
.collect::<Vec<_>>()
.join(" "),
err
)
});
match output.status.code() {
Some(0) => Err(anyhow::anyhow!(
"WDRLocalTestCert found in WDRTestCertStore. Skipping certificate generation."
)),
Some(1) => {
eprintln!(
"WDRLocalTestCert not found in WDRTestCertStore. Generating new certificate."
);
Ok(())
}
Some(_) => {
eprintln!("Unknown status code found from certmgr. Generating new certificate.");
Ok(())
}
None => {
unreachable!("Unreachable, no status code found from certmgr.");
}
}
})
}
fn configure_wdf_build_output_dir(target_arg: Option<&String>, cargo_make_cargo_profile: &str) {
let cargo_make_crate_custom_triple_target_directory =
env::var(CARGO_MAKE_CRATE_CUSTOM_TRIPLE_TARGET_DIRECTORY_ENV_VAR).unwrap_or_else(|_| {
panic!(
"{CARGO_MAKE_CRATE_CUSTOM_TRIPLE_TARGET_DIRECTORY_ENV_VAR} should be set by \
cargo-make."
)
});
let wdk_build_output_directory = {
let mut output_dir = cargo_make_crate_custom_triple_target_directory;
// Providing the "--target" flag causes the build output to go into a subdirectory: https://doc.rust-lang.org/cargo/guide/build-cache.html#build-cache
if let Some(target) = target_arg {
output_dir += "/";
output_dir += target;
}
if cargo_make_cargo_profile == "dev" {
// Cargo puts "dev" profile builds in the "debug" target folder: https://doc.rust-lang.org/cargo/guide/build-cache.html#build-cache.
// This also supports cargo-make profile of "development" since cargo-make maps
// CARGO_MAKE_PROFILE value of "development" to CARGO_MAKE_CARGO_PROFILE of
// "dev".
output_dir += "/debug";
} else {
output_dir += "/";
output_dir += cargo_make_cargo_profile;
}
output_dir
};
set_var(
WDK_BUILD_OUTPUT_DIRECTORY_ENV_VAR,
wdk_build_output_directory,
);
}
fn append_to_space_delimited_env_var<S, T>(env_var_name: S, string_to_append: T)
where
S: AsRef<str>,
T: AsRef<str>,
{
let env_var_name: &str = env_var_name.as_ref();
let string_to_append: &str = string_to_append.as_ref();
let mut env_var_value: String = env::var(env_var_name).unwrap_or_default();
env_var_value.push(' ');
env_var_value.push_str(string_to_append);
set_var(env_var_name, env_var_value.trim());
}
fn prepend_to_semicolon_delimited_env_var<S, T>(env_var_name: S, string_to_prepend: T)
where
S: AsRef<str>,
T: AsRef<str>,
{
let env_var_name = env_var_name.as_ref();
let string_to_prepend = string_to_prepend.as_ref();
let mut env_var_value = string_to_prepend.to_string();
env_var_value.push(';');
env_var_value.push_str(env::var(env_var_name).unwrap_or_default().as_str());
set_var(env_var_name, env_var_value);
}
/// `cargo-make` condition script for `infverif` task in
/// [`rust-driver-sample-makefile.toml`](../rust-driver-sample-makefile.toml)
///
/// # Errors
///
/// This function returns an error whenever it determines that the
/// `infverif` `cargo-make` task should be skipped (i.e. when the WDK Version is
/// bugged and does not contain /samples flag)
///
/// # Panics
/// Panics if `CARGO_MAKE_CURRENT_TASK_NAME` is not set in the environment
pub fn driver_sample_infverif_condition_script() -> anyhow::Result<()> {
condition_script(|| {
let wdk_version = env::var(WDK_VERSION_ENV_VAR).expect(
"WDK_BUILD_DETECTED_VERSION should always be set by wdk-build-init cargo make task",
);
let wdk_build_number = str::parse::<u32>(
&get_wdk_version_number(&wdk_version).expect("Failed to get WDK version number"),
)
.unwrap_or_else(|_| {
panic!("Couldn't parse WDK version number! Version number: {wdk_version}")
});
if MISSING_SAMPLE_FLAG_WDK_BUILD_NUMBER_RANGE.contains(&wdk_build_number) {
// cargo_make will interpret returning an error from the rust-script
// condition_script as skipping the task
return Err::<(), anyhow::Error>(anyhow::Error::msg(format!(
"Skipping InfVerif. InfVerif in WDK Build {wdk_build_number} is bugged and does \
not contain the /samples flag.",
)));
}
Ok(())
})
}
#[cfg(test)]
mod tests {
use crate::ConfigError;
const WDK_TEST_OLD_INF_VERSION: &str = "10.0.22061.0";
const WDK_TEST_NEW_INF_VERSION: &str = "10.0.26100.0";
#[test]
fn check_env_passing() -> Result<(), ConfigError> {
crate::cargo_make::setup_infverif_for_samples(WDK_TEST_OLD_INF_VERSION)?;
let env_string = std::env::var_os(crate::cargo_make::WDK_INF_ADDITIONAL_FLAGS_ENV_VAR)
.map_or_else(
|| panic!("Couldn't get OS string"),
|os_env_string| os_env_string.to_string_lossy().into_owned(),
);
assert_eq!(env_string.split(' ').next_back(), Some("/msft"));
crate::cargo_make::setup_infverif_for_samples(WDK_TEST_NEW_INF_VERSION)?;
let env_string = std::env::var_os(crate::cargo_make::WDK_INF_ADDITIONAL_FLAGS_ENV_VAR)
.map_or_else(
|| panic!("Couldn't get OS string"),
|os_env_string| os_env_string.to_string_lossy().into_owned(),
);
assert_eq!(env_string.split(' ').next_back(), Some("/samples"));
Ok(())
}
}