diff --git a/crates/cargo-wdk/tests/new_command_test.rs b/crates/cargo-wdk/tests/new_command_test.rs index b6705a3d8..6821ce392 100644 --- a/crates/cargo-wdk/tests/new_command_test.rs +++ b/crates/cargo-wdk/tests/new_command_test.rs @@ -139,7 +139,7 @@ fn create_and_build_new_driver_project(driver_type: &str) -> (String, String) { tmp_dir.path().join(&driver_name).display() ))); - // asert paths + // assert paths assert!(tmp_dir.join(&driver_name).is_dir()); assert!(tmp_dir.join(&driver_name).join("build.rs").is_file()); assert!(tmp_dir.join(&driver_name).join("Cargo.toml").is_file()); diff --git a/crates/wdk-build/src/cargo_make.rs b/crates/wdk-build/src/cargo_make.rs index 239c0e625..630f5fe80 100644 --- a/crates/wdk-build/src/cargo_make.rs +++ b/crates/wdk-build/src/cargo_make.rs @@ -29,6 +29,7 @@ use crate::{ utils::{detect_wdk_content_root, detect_windows_sdk_version, get_wdk_version_number}, ConfigError, CpuArchitecture, + IoError, }; /// The filename of the main makefile for Rust Windows drivers. @@ -520,22 +521,29 @@ pub fn setup_path() -> Result, ConfigError> { let wdk_bin_root = get_wdk_bin_root(&wdk_content_root, &sdk_version); - let host_windows_sdk_ver_bin_path = absolute(wdk_bin_root.join(host_arch.as_windows_str()))? - .to_str() - .expect("WDK bin path should be valid UTF-8") - .to_string(); + 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 = absolute(wdk_bin_root.join("x86"))? - .to_str() - .expect("WDK x86 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 = absolute( - PathBuf::from(sdk_bin_path) + let sdk_bin_path = { + let path = PathBuf::from(sdk_bin_path) .join(&sdk_version) - .join(host_arch.as_windows_str()), - )? + .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(); @@ -548,11 +556,13 @@ pub fn setup_path() -> Result, ConfigError> { ); let wdk_tool_root = get_wdk_tools_root(&wdk_content_root, sdk_version); - let host_windows_sdk_version_tool_path = - absolute(wdk_tool_root.join(host_arch.as_windows_str()))? - .to_str() - .expect("WDK tool path should be valid UTF-8") - .to_string(); + 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)) @@ -719,7 +729,8 @@ pub fn copy_to_driver_package_folder>(path_to_copy: P) -> Result< 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)?; + std::fs::create_dir(&package_folder_path) + .map_err(|source| IoError::with_path(&package_folder_path, source))?; } let destination_path = package_folder_path.join( @@ -727,7 +738,8 @@ pub fn copy_to_driver_package_folder>(path_to_copy: P) -> Result< .file_name() .expect("path_to_copy should always end with a valid file or directory name"), ); - std::fs::copy(path_to_copy, destination_path)?; + std::fs::copy(path_to_copy, &destination_path) + .map_err(|source| IoError::with_src_dest_paths(path_to_copy, destination_path, source))?; Ok(()) } @@ -838,7 +850,8 @@ fn load_wdk_build_makefile + AsRef + AsRef + fmt:: .manifest_path .parent() .expect("The parsed manifest_path should have a valid parent directory") - .join(&makefile_name); + .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(|_| { @@ -852,18 +865,29 @@ fn load_wdk_build_makefile + AsRef + AsRef + fmt:: // Only create a new symlink if the existing one is not already pointing to the // correct file if !destination_path.exists() { - return Ok(std::os::windows::fs::symlink_file( - rust_driver_makefile_toml_path, - destination_path, - )?); + 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)? != rust_driver_makefile_toml_path + || 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)?; - return Ok(std::os::windows::fs::symlink_file( - rust_driver_makefile_toml_path, - destination_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 diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 19b4b3e40..60c21b4a1 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -14,7 +14,7 @@ use std::{ env, fmt, - path::{absolute, PathBuf}, + path::{absolute, Path, PathBuf}, str::FromStr, sync::LazyLock, }; @@ -145,12 +145,81 @@ pub struct UmdfConfig { pub minimum_umdf_version_minor: Option, } +/// Metadata providing additional context for [`std::io::Error`] failures +/// +/// This enum provides structured information about the file system paths +/// or operations that led to an I/O error. It can represent either single +/// path operations or operations involving both source and destination paths. +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum IoErrorMetadata { + /// Path related to [`std::io::Error`] failure + #[error(r#"failed to perform an IO operation on "{path}""#)] + Path { + /// The file system path where the I/O error occurred + path: PathBuf, + }, + /// Source and destination paths related to [`std::io::Error`] failure. + /// + /// This can be provided for APIs like [`std::fs::copy`] which have both a + /// `from` and `to` path. + #[error(r#"failed to perform an IO operation from "{from_path}" to "{to_path}""#)] + SrcDestPaths { + /// The source path in a copy or move operation that failed + from_path: PathBuf, + /// The destination path in a copy or move operation that failed + to_path: PathBuf, + }, +} + +/// Dedicated error type for I/O operations with extra metadata context +/// +/// This error type wraps [`std::io::Error`] with additional [`IoErrorMetadata`] +/// to provide better context about which file system paths or operations +/// failed. It can be used directly by functions that only perform I/O +/// operations, and automatically converts to [`ConfigError`] when needed. +#[derive(Debug, Error)] +#[error("{metadata}")] +pub struct IoError { + /// Extra metadata related to the error + metadata: IoErrorMetadata, + /// [`std::io::Error`] that caused the operation to fail + #[source] + source: std::io::Error, +} + +impl IoError { + /// Creates a new `IoError` with a single path and source error. + pub fn with_path(path: impl Into, source: std::io::Error) -> Self { + Self { + metadata: IoErrorMetadata::Path { path: path.into() }, + source, + } + } + + /// Creates a new `IoError` for operations involving a source and + /// destination path. + pub fn with_src_dest_paths( + from_path: impl Into, + to_path: impl Into, + source: std::io::Error, + ) -> Self { + Self { + metadata: IoErrorMetadata::SrcDestPaths { + from_path: from_path.into(), + to_path: to_path.into(), + }, + source, + } + } +} + /// Errors that could result from configuring a build via [`wdk_build`][crate] #[derive(Debug, Error)] pub enum ConfigError { /// Error returned when an [`std::io`] operation fails #[error(transparent)] - IoError(#[from] std::io::Error), + IoError(#[from] IoError), /// Error returned when an expected directory does not exist #[error("cannot find directory: {directory}")] @@ -245,10 +314,6 @@ rustflags = [\"-C\", \"target-feature=+crt-static\"] /// [`metadata::Wdk`] #[error(transparent)] SerdeError(#[from] metadata::Error), - - /// Error returned when the UCX header file is not found - #[error("failed to find {0} header file.")] - HeaderNotFound(String, #[source] std::io::Error), } /// Subset of APIs in the Windows Driver Kit @@ -487,31 +552,16 @@ impl Config { let windows_sdk_include_path = include_directory.join(sdk_version); let crt_include_path = windows_sdk_include_path.join("km/crt"); - if !crt_include_path.is_dir() { - return Err(ConfigError::DirectoryNotFound { - directory: crt_include_path.to_string_lossy().into(), - }); - } - include_paths.push(absolute(&crt_include_path)?); + Self::validate_and_add_folder_path(&mut include_paths, &crt_include_path)?; let km_or_um_include_path = windows_sdk_include_path.join(match self.driver_config { DriverConfig::Wdm | DriverConfig::Kmdf(_) => "km", DriverConfig::Umdf(_) => "um", }); - if !km_or_um_include_path.is_dir() { - return Err(ConfigError::DirectoryNotFound { - directory: km_or_um_include_path.to_string_lossy().into(), - }); - } - include_paths.push(absolute(&km_or_um_include_path)?); + Self::validate_and_add_folder_path(&mut include_paths, &km_or_um_include_path)?; let kit_shared_include_path = windows_sdk_include_path.join("shared"); - if !kit_shared_include_path.is_dir() { - return Err(ConfigError::DirectoryNotFound { - directory: kit_shared_include_path.to_string_lossy().into(), - }); - } - include_paths.push(absolute(&kit_shared_include_path)?); + Self::validate_and_add_folder_path(&mut include_paths, &kit_shared_include_path)?; // Add other driver type-specific include paths match &self.driver_config { @@ -521,41 +571,48 @@ impl Config { "wdf/kmdf/{}.{}", kmdf_config.kmdf_version_major, kmdf_config.target_kmdf_version_minor )); - if !kmdf_include_path.is_dir() { - return Err(ConfigError::DirectoryNotFound { - directory: kmdf_include_path.to_string_lossy().into(), - }); - } - include_paths.push(absolute(&kmdf_include_path)?); + Self::validate_and_add_folder_path(&mut include_paths, &kmdf_include_path)?; // `ufxclient.h` relies on `ufxbase.h` being on the headers search path. The WDK // normally does not automatically include this search path, but it is required // here so that the headers can be processed successfully. let ufx_include_path = km_or_um_include_path.join("ufx/1.1"); - if !ufx_include_path.is_dir() { - return Err(ConfigError::DirectoryNotFound { - directory: ufx_include_path.to_string_lossy().into(), - }); - } - include_paths.push(absolute(&ufx_include_path)?); + Self::validate_and_add_folder_path(&mut include_paths, &ufx_include_path)?; } DriverConfig::Umdf(umdf_config) => { let umdf_include_path = include_directory.join(format!( "wdf/umdf/{}.{}", umdf_config.umdf_version_major, umdf_config.target_umdf_version_minor )); - if !umdf_include_path.is_dir() { - return Err(ConfigError::DirectoryNotFound { - directory: umdf_include_path.to_string_lossy().into(), - }); - } - include_paths.push(absolute(&umdf_include_path)?); + Self::validate_and_add_folder_path(&mut include_paths, &umdf_include_path)?; } } Ok(include_paths.into_iter()) } + /// Validate that a path refers to an existing directory and push its + /// canonical absolute form into the provided collection. + /// + /// This helper is used for both header include directories and library + /// directories. It normalizes paths before insertion. + fn validate_and_add_folder_path( + include_paths: &mut Vec, + path: &Path, + ) -> Result<(), ConfigError> { + // Include paths should be directories + if !path.is_dir() { + return Err(ConfigError::DirectoryNotFound { + directory: path.to_string_lossy().into(), + }); + } + + let absolute_path = absolute(path).map_err(|source| IoError::with_path(path, source))?; + + include_paths.push(absolute_path); + Ok(()) + } + /// Return library include paths required to build and link based off of /// the configuration of [`Config`]. /// @@ -573,7 +630,7 @@ impl Config { // Based off of logic from WindowsDriver.KernelMode.props & // WindowsDriver.UserMode.props in NI(22H2) WDK let windows_sdk_library_path = self.sdk_library_path(sdk_version)?; - library_paths.push(absolute(&windows_sdk_library_path)?); + Self::validate_and_add_folder_path(&mut library_paths, &windows_sdk_library_path)?; // Add other driver type-specific library paths let library_directory = self.wdk_content_root.join("Lib"); @@ -586,12 +643,7 @@ impl Config { kmdf_config.kmdf_version_major, kmdf_config.target_kmdf_version_minor )); - if !kmdf_library_path.is_dir() { - return Err(ConfigError::DirectoryNotFound { - directory: kmdf_library_path.to_string_lossy().into(), - }); - } - library_paths.push(absolute(&kmdf_library_path)?); + Self::validate_and_add_folder_path(&mut library_paths, &kmdf_library_path)?; } DriverConfig::Umdf(umdf_config) => { let umdf_library_path = library_directory.join(format!( @@ -600,12 +652,7 @@ impl Config { umdf_config.umdf_version_major, umdf_config.target_umdf_version_minor, )); - if !umdf_library_path.is_dir() { - return Err(ConfigError::DirectoryNotFound { - directory: umdf_library_path.to_string_lossy().into(), - }); - } - library_paths.push(absolute(&umdf_library_path)?); + Self::validate_and_add_folder_path(&mut library_paths, &umdf_library_path)?; } } @@ -1214,8 +1261,7 @@ impl Config { fn ucx_header(&self) -> Result { let sdk_version = utils::detect_windows_sdk_version(&self.wdk_content_root)?; let ucx_header_root_dir = self.sdk_library_path(sdk_version)?.join("ucx"); - let max_version = utils::find_max_version_in_directory(&ucx_header_root_dir) - .map_err(|e| ConfigError::HeaderNotFound("ucxclass.h".into(), e))?; + let max_version = utils::find_max_version_in_directory(&ucx_header_root_dir)?; let path = format!("ucx/{}.{}/ucxclass.h", max_version.0, max_version.1); Ok(path) } @@ -1968,4 +2014,175 @@ mod tests { assert_eq!(result, None); } } + + mod validate_and_add_folder_path { + use assert_fs::prelude::*; + + use super::*; + + #[test] + fn valid_directory_is_added_successfully() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let mut include_paths = Vec::new(); + + let result = Config::validate_and_add_folder_path(&mut include_paths, temp_dir.path()); + + assert!(result.is_ok()); + assert_eq!(include_paths.len(), 1); + assert!(include_paths[0].exists()); + assert!(include_paths[0].is_dir()); + + // Verify the exact canonicalized path was added + let expected_path = absolute(temp_dir.path()).unwrap(); + assert_eq!(include_paths[0], expected_path); + } + + #[test] + fn non_existent_path_returns_directory_not_found_error() { + let non_existent_path = std::path::Path::new("/this/path/does/not/exist"); + let mut include_paths = Vec::new(); + + let result = + Config::validate_and_add_folder_path(&mut include_paths, non_existent_path); + + assert!(result.is_err()); + #[cfg(nightly_toolchain)] + assert_matches!( + result.unwrap_err(), + ConfigError::DirectoryNotFound { directory } if directory == non_existent_path.to_string_lossy() + ); + assert_eq!(include_paths.len(), 0); + } + + #[test] + fn file_path_returns_directory_not_found_error() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let file = temp_dir.child("test_file.txt"); + file.write_str("test content").unwrap(); + let mut include_paths = Vec::new(); + + let result = Config::validate_and_add_folder_path(&mut include_paths, file.path()); + + assert!(result.is_err()); + #[cfg(nightly_toolchain)] + assert_matches!( + result.unwrap_err(), + ConfigError::DirectoryNotFound { directory } if directory == file.path().to_string_lossy() + ); + assert_eq!(include_paths.len(), 0); + } + + #[test] + fn path_is_canonicalized_before_adding() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let sub_dir = temp_dir.child("subdir"); + sub_dir.create_dir_all().unwrap(); + + // Create a path with ".." to test canonicalization + let complex_path = sub_dir.path().join("..").join("subdir"); + let mut include_paths = Vec::new(); + + let result = Config::validate_and_add_folder_path(&mut include_paths, &complex_path); + + assert!(result.is_ok()); + assert_eq!(include_paths.len(), 1); + + // The canonicalized path should not contain ".." + assert!(!include_paths[0].to_string_lossy().contains("..")); + assert!(include_paths[0].is_absolute()); + + // Verify the path resolves to the actual subdir path + let expected_path = absolute(sub_dir.path()).unwrap(); + assert_eq!(include_paths[0], expected_path); + } + + #[test] + fn multiple_paths_are_added_correctly() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let dir1 = temp_dir.child("dir1"); + let dir2 = temp_dir.child("dir2"); + dir1.create_dir_all().unwrap(); + dir2.create_dir_all().unwrap(); + + let mut include_paths = Vec::new(); + + let result1 = Config::validate_and_add_folder_path(&mut include_paths, dir1.path()); + let result2 = Config::validate_and_add_folder_path(&mut include_paths, dir2.path()); + + assert!(result1.is_ok()); + assert!(result2.is_ok()); + assert_eq!(include_paths.len(), 2); + + // Both paths should be present and different + assert_ne!(include_paths[0], include_paths[1]); + assert!(include_paths[0].exists()); + assert!(include_paths[1].exists()); + + // Verify both paths match their expected canonicalized values + let expected_path1 = absolute(dir1.path()).unwrap(); + let expected_path2 = absolute(dir2.path()).unwrap(); + assert_eq!(include_paths[0], expected_path1); + assert_eq!(include_paths[1], expected_path2); + } + + #[test] + fn nested_directory_is_handled_correctly() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let nested_dir = temp_dir.child("level1").child("level2").child("level3"); + nested_dir.create_dir_all().unwrap(); + let mut include_paths = Vec::new(); + + let result = + Config::validate_and_add_folder_path(&mut include_paths, nested_dir.path()); + + assert!(result.is_ok()); + assert_eq!(include_paths.len(), 1); + assert!(include_paths[0].exists()); + assert!(include_paths[0].is_dir()); + + // Verify the nested path matches the expected canonicalized value + let expected_path = absolute(nested_dir.path()).unwrap(); + assert_eq!(include_paths[0], expected_path); + } + + #[test] + fn same_directory_can_be_added_multiple_times() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let mut include_paths = Vec::new(); + + let result1 = Config::validate_and_add_folder_path(&mut include_paths, temp_dir.path()); + let result2 = Config::validate_and_add_folder_path(&mut include_paths, temp_dir.path()); + + assert!(result1.is_ok()); + assert!(result2.is_ok()); + assert_eq!(include_paths.len(), 2); + assert_eq!(include_paths[0], include_paths[1]); + + // Verify both entries match the expected canonicalized path + let expected_path = absolute(temp_dir.path()).unwrap(); + assert_eq!(include_paths[0], expected_path); + assert_eq!(include_paths[1], expected_path); + } + + #[cfg(windows)] + #[test] + fn windows_extended_length_paths_are_stripped() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let mut include_paths = Vec::new(); + + let result = Config::validate_and_add_folder_path(&mut include_paths, temp_dir.path()); + + assert!(result.is_ok()); + assert_eq!(include_paths.len(), 1); + + // `validate_and_add_folder_path` should always ensure that the path should not + // start with \\?\ on Windows + let path_str = include_paths[0].to_string_lossy(); + assert!(!path_str.starts_with(r"\\?\")); + + // Verify the path matches expected value + let expected_path = absolute(temp_dir.path()).unwrap(); + assert_eq!(include_paths[0], expected_path); + } + } } diff --git a/crates/wdk-build/src/utils.rs b/crates/wdk-build/src/utils.rs index 44ed26acd..3f8424221 100644 --- a/crates/wdk-build/src/utils.rs +++ b/crates/wdk-build/src/utils.rs @@ -24,7 +24,7 @@ use windows::{ }, }; -use crate::{ConfigError, CpuArchitecture, TwoPartVersion}; +use crate::{ConfigError, CpuArchitecture, IoError, TwoPartVersion}; /// Detect `WDKContentRoot` Directory. Logic is based off of Toolset.props in /// NI(22H2) WDK @@ -108,7 +108,8 @@ pub fn detect_wdk_content_root() -> Option { /// Panics if the path provided is not valid Unicode. pub fn get_latest_windows_sdk_version(path_to_search: &Path) -> Result { Ok(path_to_search - .read_dir()? + .read_dir() + .map_err(|source| IoError::with_path(path_to_search, source))? .filter_map(std::result::Result::ok) .map(|valid_directory_entry| valid_directory_entry.path()) .filter(|path| { @@ -328,29 +329,22 @@ pub fn detect_windows_sdk_version(wdk_content_root: &Path) -> Result>( directory_path: P, -) -> Result { - std::fs::read_dir(directory_path.as_ref())? +) -> Result { + let directory_path = directory_path.as_ref(); + std::fs::read_dir(directory_path) + .map_err(|source| IoError::with_path(directory_path, source))? .flatten() .filter(|entry| entry.file_type().is_ok_and(|ft| ft.is_dir())) .filter_map(|entry| entry.file_name().to_str()?.parse().ok()) .max() .ok_or_else(|| { - io::Error::new( - io::ErrorKind::NotFound, - format!( - "Maximum version in {} not found", - directory_path.as_ref().display() + IoError::with_path( + directory_path, + io::Error::new( + io::ErrorKind::NotFound, + format!("Maximum version in {} not found", directory_path.display()), ), ) }) @@ -477,7 +471,10 @@ mod tests { let temp_dir = assert_fs::TempDir::new().unwrap(); let result = find_max_version_in_directory(temp_dir.path()); assert!(result.is_err()); - assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound); + assert_eq!( + result.unwrap_err().source.kind(), + std::io::ErrorKind::NotFound + ); } #[test] @@ -516,7 +513,10 @@ mod tests { temp_dir.child("folder1").create_dir_all().unwrap(); let result = find_max_version_in_directory(temp_dir.path()); assert!(result.is_err()); - assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound); + assert_eq!( + result.unwrap_err().source.kind(), + std::io::ErrorKind::NotFound + ); // Multiple invalid directories let temp_dir = assert_fs::TempDir::new().unwrap(); @@ -528,7 +528,10 @@ mod tests { temp_dir.child(".5").create_dir_all().unwrap(); // Missing major let result = find_max_version_in_directory(temp_dir.path()); assert!(result.is_err()); - assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound); + assert_eq!( + result.unwrap_err().source.kind(), + std::io::ErrorKind::NotFound + ); } #[test] diff --git a/crates/wdk-sys/build.rs b/crates/wdk-sys/build.rs index 547b6483e..e3172eee1 100644 --- a/crates/wdk-sys/build.rs +++ b/crates/wdk-sys/build.rs @@ -30,6 +30,7 @@ use wdk_build::{ Config, ConfigError, DriverConfig, + IoError, KmdfConfig, UmdfConfig, }; @@ -125,18 +126,42 @@ pub static mut {WDFFUNCTIONS_SYMBOL_NAME_PLACEHOLDER}: *const WDFFUNC = core::pt ", ) }); -type GenerateFn = fn(&Path, &Config) -> Result<(), ConfigError>; +/// Enabled API subsets based off of cargo-features +const ENABLED_API_SUBSETS: &[ApiSubset] = &[ + ApiSubset::Base, + ApiSubset::Wdf, + #[cfg(feature = "gpio")] + ApiSubset::Gpio, + #[cfg(feature = "hid")] + ApiSubset::Hid, + #[cfg(feature = "parallel-ports")] + ApiSubset::ParallelPorts, + #[cfg(feature = "spb")] + ApiSubset::Spb, + #[cfg(feature = "storage")] + ApiSubset::Storage, + #[cfg(feature = "usb")] + ApiSubset::Usb, +]; + +type GenerateFn = fn(&Path, &Config) -> Result<(), ConfigError>; const BINDGEN_FILE_GENERATORS_TUPLES: &[(&str, GenerateFn)] = &[ ("constants.rs", generate_constants), ("types.rs", generate_types), ("base.rs", generate_base), ("wdf.rs", generate_wdf), + #[cfg(feature = "gpio")] ("gpio.rs", generate_gpio), + #[cfg(feature = "hid")] ("hid.rs", generate_hid), + #[cfg(feature = "parallel-ports")] ("parallel_ports.rs", generate_parallel_ports), + #[cfg(feature = "spb")] ("spb.rs", generate_spb), + #[cfg(feature = "storage")] ("storage.rs", generate_storage), + #[cfg(feature = "usb")] ("usb.rs", generate_usb), ]; @@ -197,22 +222,7 @@ fn initialize_tracing() -> Result<(), ParseError> { fn generate_constants(out_path: &Path, config: &Config) -> Result<(), ConfigError> { info!("Generating bindings to WDK: constants.rs"); - let header_contents = config.bindgen_header_contents([ - ApiSubset::Base, - ApiSubset::Wdf, - #[cfg(feature = "gpio")] - ApiSubset::Gpio, - #[cfg(feature = "hid")] - ApiSubset::Hid, - #[cfg(feature = "parallel-ports")] - ApiSubset::ParallelPorts, - #[cfg(feature = "spb")] - ApiSubset::Spb, - #[cfg(feature = "storage")] - ApiSubset::Storage, - #[cfg(feature = "usb")] - ApiSubset::Usb, - ])?; + let header_contents = config.bindgen_header_contents(ENABLED_API_SUBSETS.iter().copied())?; trace!(header_contents = ?header_contents); let bindgen_builder = bindgen::Builder::wdk_default(config)? @@ -220,31 +230,18 @@ fn generate_constants(out_path: &Path, config: &Config) -> Result<(), ConfigErro .header_contents("constants-input.h", &header_contents); trace!(bindgen_builder = ?bindgen_builder); + let output_file_path = out_path.join("constants.rs"); Ok(bindgen_builder .generate() .expect("Bindings should succeed to generate") - .write_to_file(out_path.join("constants.rs"))?) + .write_to_file(&output_file_path) + .map_err(|source| IoError::with_path(output_file_path, source))?) } fn generate_types(out_path: &Path, config: &Config) -> Result<(), ConfigError> { info!("Generating bindings to WDK: types.rs"); - let header_contents = config.bindgen_header_contents([ - ApiSubset::Base, - ApiSubset::Wdf, - #[cfg(feature = "gpio")] - ApiSubset::Gpio, - #[cfg(feature = "hid")] - ApiSubset::Hid, - #[cfg(feature = "parallel-ports")] - ApiSubset::ParallelPorts, - #[cfg(feature = "spb")] - ApiSubset::Spb, - #[cfg(feature = "storage")] - ApiSubset::Storage, - #[cfg(feature = "usb")] - ApiSubset::Usb, - ])?; + let header_contents = config.bindgen_header_contents(ENABLED_API_SUBSETS.iter().copied())?; trace!(header_contents = ?header_contents); let bindgen_builder = bindgen::Builder::wdk_default(config)? @@ -252,10 +249,12 @@ fn generate_types(out_path: &Path, config: &Config) -> Result<(), ConfigError> { .header_contents("types-input.h", &header_contents); trace!(bindgen_builder = ?bindgen_builder); + let output_file_path = out_path.join("types.rs"); Ok(bindgen_builder .generate() .expect("Bindings should succeed to generate") - .write_to_file(out_path.join("types.rs"))?) + .write_to_file(&output_file_path) + .map_err(|source| IoError::with_path(output_file_path, source))?) } fn generate_base(out_path: &Path, config: &Config) -> Result<(), ConfigError> { @@ -273,10 +272,12 @@ fn generate_base(out_path: &Path, config: &Config) -> Result<(), ConfigError> { .header_contents(&format!("{outfile_name}-input.h"), &header_contents); trace!(bindgen_builder = ?bindgen_builder); + let output_file_path = out_path.join(format!("{outfile_name}.rs")); Ok(bindgen_builder .generate() .expect("Bindings should succeed to generate") - .write_to_file(out_path.join(format!("{outfile_name}.rs")))?) + .write_to_file(&output_file_path) + .map_err(|source| IoError::with_path(output_file_path, source))?) } fn generate_wdf(out_path: &Path, config: &Config) -> Result<(), ConfigError> { @@ -294,10 +295,12 @@ fn generate_wdf(out_path: &Path, config: &Config) -> Result<(), ConfigError> { .allowlist_file("(?i).*wdf.*"); trace!(bindgen_builder = ?bindgen_builder); + let output_file_path = out_path.join("wdf.rs"); Ok(bindgen_builder .generate() .expect("Bindings should succeed to generate") - .write_to_file(out_path.join("wdf.rs"))?) + .write_to_file(&output_file_path) + .map_err(|source| IoError::with_path(output_file_path, source))?) } else { info!( "Skipping wdf.rs generation since driver_config is {:#?}", @@ -307,228 +310,187 @@ fn generate_wdf(out_path: &Path, config: &Config) -> Result<(), ConfigError> { } } +#[cfg(feature = "gpio")] fn generate_gpio(out_path: &Path, config: &Config) -> Result<(), ConfigError> { - cfg_if::cfg_if! { - if #[cfg(feature = "gpio")] { - info!("Generating bindings to WDK: gpio.rs"); - - let header_contents = - config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Gpio])?; - trace!(header_contents = ?header_contents); - - let bindgen_builder = { - let mut builder = bindgen::Builder::wdk_default(config)? - .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) - .header_contents("gpio-input.h", &header_contents); - - // Only allowlist files in the gpio-specific files to avoid - // duplicate definitions - for header_file in config.headers(ApiSubset::Gpio)? { - builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); - } - builder - }; - trace!(bindgen_builder = ?bindgen_builder); - - Ok(bindgen_builder - .generate() - .expect("Bindings should succeed to generate") - .write_to_file(out_path.join("gpio.rs"))?) - } else { - let _ = (out_path, config); // Silence unused variable warnings when gpio feature is not enabled + info!("Generating bindings to WDK: gpio.rs"); + + let header_contents = + config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Gpio])?; + trace!(header_contents = ?header_contents); - info!("Skipping gpio.rs generation since gpio feature is not enabled"); - Ok(()) + let bindgen_builder = { + let mut builder = bindgen::Builder::wdk_default(config)? + .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) + .header_contents("gpio-input.h", &header_contents); + + // Only allowlist files in the gpio-specific files to avoid + // duplicate definitions + for header_file in config.headers(ApiSubset::Gpio)? { + builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } - } + builder + }; + trace!(bindgen_builder = ?bindgen_builder); + + let output_file_path = out_path.join("gpio.rs"); + Ok(bindgen_builder + .generate() + .expect("Bindings should succeed to generate") + .write_to_file(&output_file_path) + .map_err(|source| IoError::with_path(output_file_path, source))?) } +#[cfg(feature = "hid")] fn generate_hid(out_path: &Path, config: &Config) -> Result<(), ConfigError> { - cfg_if::cfg_if! { - if #[cfg(feature = "hid")] { - info!("Generating bindings to WDK: hid.rs"); - - let header_contents = - config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Hid])?; - trace!(header_contents = ?header_contents); - - let bindgen_builder = { - let mut builder = bindgen::Builder::wdk_default(config)? - .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) - .header_contents("hid-input.h", &header_contents); - - // Only allowlist files in the hid-specific files to avoid - // duplicate definitions - for header_file in config.headers(ApiSubset::Hid)? { - builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); - } - builder - }; - trace!(bindgen_builder = ?bindgen_builder); - - Ok(bindgen_builder - .generate() - .expect("Bindings should succeed to generate") - .write_to_file(out_path.join("hid.rs"))?) - } else { - let _ = (out_path, config); // Silence unused variable warnings when hid feature is not enabled + info!("Generating bindings to WDK: hid.rs"); + + let header_contents = + config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Hid])?; + trace!(header_contents = ?header_contents); + + let bindgen_builder = { + let mut builder = bindgen::Builder::wdk_default(config)? + .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) + .header_contents("hid-input.h", &header_contents); - info!("Skipping hid.rs generation since hid feature is not enabled"); - Ok(()) + // Only allowlist files in the hid-specific files to avoid + // duplicate definitions + for header_file in config.headers(ApiSubset::Hid)? { + builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } - } + builder + }; + trace!(bindgen_builder = ?bindgen_builder); + + let output_file_path = out_path.join("hid.rs"); + Ok(bindgen_builder + .generate() + .expect("Bindings should succeed to generate") + .write_to_file(&output_file_path) + .map_err(|source| IoError::with_path(output_file_path, source))?) } +#[cfg(feature = "parallel-ports")] fn generate_parallel_ports(out_path: &Path, config: &Config) -> Result<(), ConfigError> { - cfg_if::cfg_if! { - if #[cfg(feature = "parallel-ports")] { - info!("Generating bindings to WDK: parallel_ports.rs"); - - let header_contents = config.bindgen_header_contents([ - ApiSubset::Base, - ApiSubset::Wdf, - ApiSubset::ParallelPorts, - ])?; - trace!(header_contents = ?header_contents); - - let bindgen_builder = { - let mut builder = bindgen::Builder::wdk_default(config)? - .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) - .header_contents("parallel-ports-input.h", &header_contents); - - // Only allowlist files in the parallel-ports-specific files to - // avoid duplicate definitions - for header_file in config.headers(ApiSubset::ParallelPorts)? { - builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); - } - builder - }; - trace!(bindgen_builder = ?bindgen_builder); - - Ok(bindgen_builder - .generate() - .expect("Bindings should succeed to generate") - .write_to_file(out_path.join("parallel_ports.rs"))?) - } else { - let _ = (out_path, config); // Silence unused variable warnings when parallel-ports feature is not enabled + info!("Generating bindings to WDK: parallel_ports.rs"); - info!( - "Skipping parallel_ports.rs generation since parallel-ports feature is not enabled" - ); - Ok(()) + let header_contents = config.bindgen_header_contents([ + ApiSubset::Base, + ApiSubset::Wdf, + ApiSubset::ParallelPorts, + ])?; + trace!(header_contents = ?header_contents); + + let bindgen_builder = { + let mut builder = bindgen::Builder::wdk_default(config)? + .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) + .header_contents("parallel-ports-input.h", &header_contents); + + // Only allowlist files in the parallel-ports-specific files to + // avoid duplicate definitions + for header_file in config.headers(ApiSubset::ParallelPorts)? { + builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } - } + builder + }; + trace!(bindgen_builder = ?bindgen_builder); + + let output_file_path = out_path.join("parallel_ports.rs"); + Ok(bindgen_builder + .generate() + .expect("Bindings should succeed to generate") + .write_to_file(&output_file_path) + .map_err(|source| IoError::with_path(output_file_path, source))?) } +#[cfg(feature = "spb")] fn generate_spb(out_path: &Path, config: &Config) -> Result<(), ConfigError> { - cfg_if::cfg_if! { - if #[cfg(feature = "spb")] { - info!("Generating bindings to WDK: spb.rs"); - - let header_contents = - config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Spb])?; - trace!(header_contents = ?header_contents); - - let bindgen_builder = { - let mut builder = bindgen::Builder::wdk_default(config)? - .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) - .header_contents("spb-input.h", &header_contents); - - // Only allowlist files in the spb-specific files to avoid - // duplicate definitions - for header_file in config.headers(ApiSubset::Spb)? { - builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); - } - builder - }; - trace!(bindgen_builder = ?bindgen_builder); - - Ok(bindgen_builder - .generate() - .expect("Bindings should succeed to generate") - .write_to_file(out_path.join("spb.rs"))?) - } else { - let _ = (out_path, config); // Silence unused variable warnings when spb feature is not enabled + info!("Generating bindings to WDK: spb.rs"); + + let header_contents = + config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Spb])?; + trace!(header_contents = ?header_contents); - info!("Skipping spb.rs generation since spb feature is not enabled"); - Ok(()) + let bindgen_builder = { + let mut builder = bindgen::Builder::wdk_default(config)? + .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) + .header_contents("spb-input.h", &header_contents); + + // Only allowlist files in the spb-specific files to avoid + // duplicate definitions + for header_file in config.headers(ApiSubset::Spb)? { + builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } - } + builder + }; + trace!(bindgen_builder = ?bindgen_builder); + + let output_file_path = out_path.join("spb.rs"); + Ok(bindgen_builder + .generate() + .expect("Bindings should succeed to generate") + .write_to_file(&output_file_path) + .map_err(|source| IoError::with_path(output_file_path, source))?) } +#[cfg(feature = "storage")] fn generate_storage(out_path: &Path, config: &Config) -> Result<(), ConfigError> { - cfg_if::cfg_if! { - if #[cfg(feature = "storage")] { - info!("Generating bindings to WDK: storage.rs"); - - let header_contents = config.bindgen_header_contents([ - ApiSubset::Base, - ApiSubset::Wdf, - ApiSubset::Storage, - ])?; - trace!(header_contents = ?header_contents); - - let bindgen_builder = { - let mut builder = bindgen::Builder::wdk_default(config)? - .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) - .header_contents("storage-input.h", &header_contents); - - // Only allowlist files in the storage-specific files to avoid - // duplicate definitions - for header_file in config.headers(ApiSubset::Storage)? { - builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); - } - builder - }; - trace!(bindgen_builder = ?bindgen_builder); - - Ok(bindgen_builder - .generate() - .expect("Bindings should succeed to generate") - .write_to_file(out_path.join("storage.rs"))?) - } else { - let _ = (out_path, config); // Silence unused variable warnings when storage feature is not enabled + info!("Generating bindings to WDK: storage.rs"); + + let header_contents = + config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Storage])?; + trace!(header_contents = ?header_contents); + + let bindgen_builder = { + let mut builder = bindgen::Builder::wdk_default(config)? + .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) + .header_contents("storage-input.h", &header_contents); - info!("Skipping storage.rs generation since storage feature is not enabled"); - Ok(()) + // Only allowlist files in the storage-specific files to avoid + // duplicate definitions + for header_file in config.headers(ApiSubset::Storage)? { + builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } - } + builder + }; + trace!(bindgen_builder = ?bindgen_builder); + + let output_file_path = out_path.join("storage.rs"); + Ok(bindgen_builder + .generate() + .expect("Bindings should succeed to generate") + .write_to_file(&output_file_path) + .map_err(|source| IoError::with_path(output_file_path, source))?) } +#[cfg(feature = "usb")] fn generate_usb(out_path: &Path, config: &Config) -> Result<(), ConfigError> { - cfg_if::cfg_if! { - if #[cfg(feature = "usb")] { - info!("Generating bindings to WDK: usb.rs"); - - let header_contents = - config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Usb])?; - trace!(header_contents = ?header_contents); - - let bindgen_builder = { - let mut builder = bindgen::Builder::wdk_default(config)? - .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) - .header_contents("usb-input.h", &header_contents); - - // Only allowlist files in the usb-specific files to avoid - // duplicate definitions - for header_file in config.headers(ApiSubset::Usb)? { - builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); - } - builder - }; - trace!(bindgen_builder = ?bindgen_builder); - - Ok(bindgen_builder - .generate() - .expect("Bindings should succeed to generate") - .write_to_file(out_path.join("usb.rs"))?) - } else { - let _ = (out_path, config); // Silence unused variable warnings when usb feature is not enabled + info!("Generating bindings to WDK: usb.rs"); + + let header_contents = + config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf, ApiSubset::Usb])?; + trace!(header_contents = ?header_contents); + + let bindgen_builder = { + let mut builder = bindgen::Builder::wdk_default(config)? + .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) + .header_contents("usb-input.h", &header_contents); - info!("Skipping usb.rs generation since usb feature is not enabled"); - Ok(()) + // Only allowlist files in the usb-specific files to avoid + // duplicate definitions + for header_file in config.headers(ApiSubset::Usb)? { + builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } - } + builder + }; + trace!(bindgen_builder = ?bindgen_builder); + + let output_file_path = out_path.join("usb.rs"); + Ok(bindgen_builder + .generate() + .expect("Bindings should succeed to generate") + .write_to_file(&output_file_path) + .map_err(|source| IoError::with_path(output_file_path, source))?) } /// Generates a `wdf_function_count.rs` file in `OUT_DIR` which contains the @@ -536,11 +498,12 @@ fn generate_usb(out_path: &Path, config: &Config) -> Result<(), ConfigError> { /// be generated here since the size of the table is derived from either a /// global symbol that newer WDF versions expose, or an enum that older versions /// use. -fn generate_wdf_function_count(out_path: &Path, config: &Config) -> std::io::Result<()> { +fn generate_wdf_function_count(out_path: &Path, config: &Config) -> Result<(), IoError> { const MINIMUM_MINOR_VERSION_TO_GENERATE_WDF_FUNCTION_COUNT: u8 = 25; let generated_file_path = out_path.join("wdf_function_count.rs"); - let mut generated_file = std::fs::File::create(generated_file_path)?; + let mut generated_file = File::create(&generated_file_path) + .map_err(|source| IoError::with_path(&generated_file_path, source))?; let is_wdf_function_count_generated = match *config { Config { @@ -585,7 +548,9 @@ fn generate_wdf_function_count(out_path: &Path, config: &Config) -> std::io::Res }, ); - generated_file.write_all(wdf_function_table_count_snippet.as_bytes())?; + generated_file + .write_all(wdf_function_table_count_snippet.as_bytes()) + .map_err(|source| IoError::with_path(generated_file_path, source))?; Ok(()) } @@ -595,20 +560,23 @@ fn generate_wdf_function_count(out_path: &Path, config: &Config) -> std::io::Res /// required in order to add an additional argument with the path to the file /// containing generated types. There is currently no other way to pass /// `OUT_DIR` of `wdk-sys` to the `proc_macro`. -fn generate_call_unsafe_wdf_function_binding_macro(out_path: &Path) -> std::io::Result<()> { +fn generate_call_unsafe_wdf_function_binding_macro(out_path: &Path) -> Result<(), IoError> { let generated_file_path = out_path.join("call_unsafe_wdf_function_binding.rs"); - let mut generated_file = std::fs::File::create(generated_file_path)?; - generated_file.write_all( - CALL_UNSAFE_WDF_BINDING_TEMPLATE - .replace( - OUT_DIR_PLACEHOLDER, - out_path.join("types.rs").to_str().expect( - "path to file with generated type information should successfully convert to \ - a str", - ), - ) - .as_bytes(), - )?; + let mut generated_file = File::create(&generated_file_path) + .map_err(|source| IoError::with_path(&generated_file_path, source))?; + generated_file + .write_all( + CALL_UNSAFE_WDF_BINDING_TEMPLATE + .replace( + OUT_DIR_PLACEHOLDER, + out_path.join("types.rs").to_str().expect( + "path to file with generated type information should successfully convert \ + to a str", + ), + ) + .as_bytes(), + ) + .map_err(|source| IoError::with_path(generated_file_path, source))?; Ok(()) } @@ -616,20 +584,161 @@ fn generate_call_unsafe_wdf_function_binding_macro(out_path: &Path) -> std::io:: /// for tests to compile. This should only generate the stubs whose names are /// dependent on the WDK configuration, and would otherwise be impossible to /// just include in `src/test_stubs.rs` directly. -fn generate_test_stubs(out_path: &Path, config: &Config) -> std::io::Result<()> { +fn generate_test_stubs(out_path: &Path, config: &Config) -> Result<(), IoError> { let stubs_file_path = out_path.join("test_stubs.rs"); - let mut stubs_file = std::fs::File::create(stubs_file_path)?; - stubs_file.write_all( - TEST_STUBS_TEMPLATE - .replace( - WDFFUNCTIONS_SYMBOL_NAME_PLACEHOLDER, - &config.compute_wdffunctions_symbol_name().expect( - "KMDF and UMDF configs should always have a computable WdfFunctions symbol \ - name", - ), - ) - .as_bytes(), - )?; + let mut stubs_file = File::create(&stubs_file_path) + .map_err(|source| IoError::with_path(&stubs_file_path, source))?; + stubs_file + .write_all( + TEST_STUBS_TEMPLATE + .replace( + WDFFUNCTIONS_SYMBOL_NAME_PLACEHOLDER, + &config.compute_wdffunctions_symbol_name().expect( + "KMDF and UMDF configs should always have a computable WdfFunctions \ + symbol name", + ), + ) + .as_bytes(), + ) + .map_err(|source| IoError::with_path(stubs_file_path, source))?; + Ok(()) +} + +/// Starts parallel bindgen tasks for generating binding files. +fn start_bindgen_tasks<'scope>( + thread_scope: &'scope thread::Scope<'scope, '_>, + out_path: &'scope Path, + config: &'scope Config, + thread_join_handles: &mut Vec>>, +) { + info_span!("bindgen generation").in_scope(|| { + for (file_name, generate_function) in BINDGEN_FILE_GENERATORS_TUPLES { + let current_span = Span::current(); + + thread_join_handles.push( + thread::Builder::new() + .name(format!("bindgen {file_name} generator")) + .spawn_scoped(thread_scope, move || { + // Parent span must be manually set since spans do not persist across thread boundaries: https://github.com/tokio-rs/tracing/issues/1391 + info_span!(parent: ¤t_span, "worker thread", generated_file_name = file_name).in_scope(|| generate_function(out_path, config)) + }) + .expect("Scoped Thread should spawn successfully"), + ); + } + }); +} + +/// Starts a task that compiles a C shim to expose WDF symbols hidden by +/// `__declspec(selectany)`. +fn start_wdf_symbol_export_tasks<'scope>( + thread_scope: &'scope thread::Scope<'scope, '_>, + out_path: &'scope Path, + config: &'scope Config, + thread_join_handles: &mut Vec>>, +) { + let current_span = Span::current(); + + // Compile a c library to expose symbols that are not exposed because of + // __declspec(selectany) + thread_join_handles.push( + thread::Builder::new() + .name("wdf.c cc compilation".to_string()) + .spawn_scoped(thread_scope, move || { + // Parent span must be manually set since spans do not persist across thread boundaries: https://github.com/tokio-rs/tracing/issues/1391 + info_span!(parent: current_span, "cc").in_scope(|| { + info!("Compiling wdf.c"); + + // Write all included headers into wdf.c (existing file, if present + // (i.e. incremental rebuild), is truncated) + let wdf_c_file_path = out_path.join("wdf.c"); + { + let mut wdf_c_file = File::create(&wdf_c_file_path) + .map_err(|source| IoError::with_path(&wdf_c_file_path, source))?; + wdf_c_file + .write_all( + config + // This should include the entirety of the `ENABLED_API_SUBSETS`, but this is currently blocked by issues with mutually exclusive headers: https://github.com/microsoft/windows-drivers-rs/issues/515 + .bindgen_header_contents([ + ApiSubset::Base, + ApiSubset::Wdf, + #[cfg(feature = "hid")] + ApiSubset::Hid, + #[cfg(feature = "spb")] + ApiSubset::Spb, + ])? + .as_bytes(), + ) + .map_err(|source| IoError::with_path(&wdf_c_file_path, source))?; + + // Explicitly sync_all to surface any IO errors (File::drop + // silently ignores close errors) + wdf_c_file + .sync_all() + .map_err(|source| IoError::with_path(&wdf_c_file_path, source))?; + } + + let mut cc_builder = cc::Build::new(); + for (key, value) in config.preprocessor_definitions() { + cc_builder.define(&key, value.as_deref()); + } + + cc_builder + .includes(config.include_paths()?) + .file(wdf_c_file_path) + .compile("wdf"); + Ok::<(), ConfigError>(()) + }) + }) + .expect("Scoped Thread should spawn successfully"), + ); +} + +/// Starts generation/compilation tasks for WDF-specific artifacts for driver +/// configurations. +/// +/// Uses the `start_*_tasks` naming convention: dispatches work to scoped +/// threads and returns after scheduling. +fn start_wdf_artifact_tasks<'scope>( + thread_scope: &'scope thread::Scope<'scope, '_>, + out_path: &'scope Path, + config: &'scope Config, + thread_join_handles: &mut Vec>>, +) -> anyhow::Result<()> { + if let DriverConfig::Kmdf(_) | DriverConfig::Umdf(_) = config.driver_config { + start_wdf_symbol_export_tasks(thread_scope, out_path, config, thread_join_handles); + + info_span!("wdf_function_count.rs generation") + .in_scope(|| generate_wdf_function_count(out_path, config))?; + + info_span!("call_unsafe_wdf_function_binding.rs generation") + .in_scope(|| generate_call_unsafe_wdf_function_binding_macro(out_path))?; + + info_span!("test_stubs.rs generation") + .in_scope(|| generate_test_stubs(out_path, config))?; + } + Ok(()) +} + +/// Joins all worker threads and collects their results +fn join_worker_threads( + thread_join_handles: Vec>>, +) -> anyhow::Result<()> { + for join_handle in thread_join_handles { + let thread_name = join_handle.thread().name().unwrap_or("UNNAMED").to_string(); + + match join_handle.join() { + // Forward panics to the main thread + Err(panic_payload) => { + panic::resume_unwind(panic_payload); + } + + Ok(thread_result) => { + thread_result.with_context(|| { + format!(r#""{thread_name}" thread failed to exit successfully"#) + })?; + } + } + } Ok(()) } @@ -644,111 +753,10 @@ fn main() -> anyhow::Result<()> { thread::scope(|thread_scope| { let mut thread_join_handles = Vec::new(); - info_span!("bindgen generation").in_scope(|| { - let out_path = &out_path; - let config = &config; - - for (file_name, generate_function) in BINDGEN_FILE_GENERATORS_TUPLES { - let current_span = Span::current(); - - thread_join_handles.push( - thread::Builder::new() - .name(format!("bindgen {file_name} generator")) - .spawn_scoped(thread_scope, move || { - // Parent span must be manually set since spans do not persist across thread boundaries: https://github.com/tokio-rs/tracing/issues/1391 - info_span!(parent: ¤t_span, "worker thread", generated_file_name = file_name).in_scope(|| generate_function(out_path, config)) - }) - .expect("Scoped Thread should spawn successfully"), - ); - } - }); - - if let DriverConfig::Kmdf(_) | DriverConfig::Umdf(_) = config.driver_config { - let current_span = Span::current(); - let config = &config; - let out_path = &out_path; - - // Compile a c library to expose symbols that are not exposed because of - // __declspec(selectany) - thread_join_handles.push( - thread::Builder::new() - .name("wdf.c cc compilation".to_string()) - .spawn_scoped(thread_scope, move || { - // Parent span must be manually set since spans do not persist across thread boundaries: https://github.com/tokio-rs/tracing/issues/1391 - info_span!(parent: current_span, "cc").in_scope(|| { - info!("Compiling wdf.c"); - - // Write all included headers into wdf.c (existing file, if present - // (i.e. incremental rebuild), is truncated) - let wdf_c_file_path = out_path.join("wdf.c"); - { - let mut wdf_c_file = File::create(&wdf_c_file_path)?; - wdf_c_file.write_all( - config - .bindgen_header_contents([ - ApiSubset::Base, - ApiSubset::Wdf, - #[cfg(feature = "hid")] - ApiSubset::Hid, - #[cfg(feature = "spb")] - ApiSubset::Spb, - ])? - .as_bytes(), - )?; - - // Explicitly sync_all to surface any IO errors (File::drop - // silently ignores close errors) - wdf_c_file.sync_all()?; - } - - let mut cc_builder = cc::Build::new(); - for (key, value) in config.preprocessor_definitions() { - cc_builder.define(&key, value.as_deref()); - } - - cc_builder - .includes(config.include_paths()?) - .file(wdf_c_file_path) - .compile("wdf"); - Ok::<(), ConfigError>(()) - }) - }) - .expect("Scoped Thread should spawn successfully"), - ); - - info_span!("wdf_function_count.rs generation").in_scope(|| { - generate_wdf_function_count(out_path, config)?; - Ok::<(), std::io::Error>(()) - })?; - - info_span!("call_unsafe_wdf_function_binding.rs generation").in_scope(|| { - generate_call_unsafe_wdf_function_binding_macro(out_path)?; - Ok::<(), std::io::Error>(()) - })?; + start_bindgen_tasks(thread_scope, &out_path, &config, &mut thread_join_handles); + start_wdf_artifact_tasks(thread_scope, &out_path, &config, &mut thread_join_handles)?; - info_span!("test_stubs.rs generation").in_scope(|| { - generate_test_stubs(out_path, config)?; - Ok::<(), std::io::Error>(()) - })?; - } - - for join_handle in thread_join_handles { - let thread_name = join_handle.thread().name().unwrap_or("UNNAMED").to_string(); - - match join_handle.join() { - // Forward panics to the main thread - Err(panic_payload) => { - panic::resume_unwind(panic_payload); - } - - Ok(thread_result) => { - thread_result.with_context(|| { - format!(r#""{thread_name}" thread failed to exit successfully"#) - })?; - } - } - } - Ok::<(), anyhow::Error>(()) + join_worker_threads(thread_join_handles) })?; Ok::<(), anyhow::Error>(()) diff --git a/crates/wdk-sys/src/test_stubs.rs b/crates/wdk-sys/src/test_stubs.rs index b9bff9880..03a6f9007 100644 --- a/crates/wdk-sys/src/test_stubs.rs +++ b/crates/wdk-sys/src/test_stubs.rs @@ -2,7 +2,7 @@ // License: MIT OR Apache-2.0 //! Any library dependency that depends on `wdk-sys` requires these stubs to -//! provide symobols to successfully compile and run tests. +//! provide symbols to successfully compile and run tests. //! //! These stubs can be brought into scope by introducing `wdk-sys` with the //! `test-stubs` feature in the `dev-dependencies` of the crate's `Cargo.toml`