use lenient_semver::Version;
use std::{
collections::BTreeMap,
fs::DirEntry,
io::{Error, ErrorKind},
path::{Path, PathBuf},
};
#[derive(Debug)]
pub struct WinSdkIncludes {
cppwinrt: PathBuf,
shared: PathBuf,
ucrt: PathBuf,
um: PathBuf,
winrt: PathBuf,
}
impl WinSdkIncludes {
const CPPWINRT_DIR: &'static str = "cppwinrt";
const SHARED_DIR: &'static str = "shared";
const UCRT_DIR: &'static str = "ucrt";
const UM_DIR: &'static str = "um";
const WINRT_DIR: &'static str = "winrt";
const EXPECTED_DIRS: [&'static str; 5] = [
Self::CPPWINRT_DIR,
Self::SHARED_DIR,
Self::UCRT_DIR,
Self::UM_DIR,
Self::WINRT_DIR,
];
pub fn create(include_path: &Path) -> std::io::Result<Self> {
Ok(Self {
cppwinrt: sub_directory(include_path, Self::CPPWINRT_DIR)?,
shared: sub_directory(include_path, Self::SHARED_DIR)?,
ucrt: sub_directory(include_path, Self::UCRT_DIR)?,
um: sub_directory(include_path, Self::UM_DIR)?,
winrt: sub_directory(include_path, Self::WINRT_DIR)?,
})
}
pub fn cppwinrt_dir(&self) -> &Path {
self.cppwinrt.as_path()
}
pub fn shared_dir(&self) -> &Path {
self.shared.as_path()
}
pub fn ucrt_dir(&self) -> &Path {
self.ucrt.as_path()
}
pub fn um_dir(&self) -> &Path {
self.um.as_path()
}
pub fn winrt_dir(&self) -> &Path {
self.winrt.as_path()
}
pub fn is_valid(path: &Path) -> bool {
path.is_dir() && !Self::EXPECTED_DIRS.iter().any(|s| !path.join(s).is_dir())
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct WinSdkVersion<'a>(Version<'a>);
impl<'a> WinSdkVersion<'a> {
pub fn parse(value: &'a str) -> std::io::Result<WinSdkVersion<'a>> {
Version::parse(value).map_or_else(
|e| {
Err(Error::new(
ErrorKind::InvalidData,
format!("Failed to parse &str as a WinSdkVersion: {}", e),
))
},
|v| Ok(WinSdkVersion(v)),
)
}
}
pub struct WinSdk {
include: WinSdkIncludes,
}
impl WinSdk {
const ENV_KEY: &'static str = "WIN_SDK_PATH";
const REG_PATH: &'static str =
"SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0";
const HKLM: winreg::RegKey = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE);
pub const fn include_dirs(&self) -> &WinSdkIncludes {
&self.include
}
pub fn find() -> std::io::Result<Self> {
Self::find_in_range(None, None)
}
pub fn find_in_range(
max: Option<WinSdkVersion>,
min: Option<WinSdkVersion>,
) -> std::io::Result<Self> {
let installation_folder = Self::installation_folder()?;
let include_versioned_dirs = Self::include_versioned_subdirs(
installation_folder.as_path(),
max.as_ref(),
min.as_ref(),
)?;
Self::select_sdk(include_versioned_dirs)
}
fn select_sdk(versioned_include_dirs: Vec<PathBuf>) -> std::io::Result<Self> {
let versioned_include_dirs_map =
Self::versioned_directory_map(versioned_include_dirs.as_slice());
let (_, d) = versioned_include_dirs_map.last_key_value().unwrap();
Ok(Self {
include: WinSdkIncludes::create(d.as_path())?,
})
}
fn versioned_directory_map(version_dirs: &[PathBuf]) -> BTreeMap<WinSdkVersion<'_>, &PathBuf> {
version_dirs
.iter()
.map(|d| {
let v = d
.file_name()
.and_then(|o| o.to_str())
.and_then(|s| WinSdkVersion::parse(s).ok())
.unwrap();
(v, d)
})
.collect::<BTreeMap<WinSdkVersion, &PathBuf>>()
}
fn include_versioned_subdirs(
parent: &Path,
max: Option<&WinSdkVersion>,
min: Option<&WinSdkVersion>,
) -> std::io::Result<Vec<PathBuf>> {
let search_dir = sub_directory(parent, "Include")?;
let found = search_dir
.read_dir()?
.filter_map(|r| r.ok())
.filter_map(Self::as_valid_path)
.filter(|path| Self::is_valid_versioned_subdir(path, max, min))
.filter(|path| WinSdkIncludes::is_valid(path))
.collect::<Vec<PathBuf>>();
if found.is_empty() {
return Err(Error::new(
ErrorKind::NotFound,
format!("No versioned `Include` directories in the specified version range were found inside `{}` dir.", search_dir.to_string_lossy()),
));
}
Ok(found)
}
fn as_valid_path(de: DirEntry) -> Option<PathBuf> {
let path = de.path();
if !path.is_dir() {
return None;
}
Some(path)
}
fn is_valid_versioned_subdir(
path: &Path,
max: Option<&WinSdkVersion>,
min: Option<&WinSdkVersion>,
) -> bool {
path.file_name()
.and_then(|ver_dir| ver_dir.to_str())
.and_then(|ver_dir_str| WinSdkVersion::parse(ver_dir_str).ok())
.is_some_and(|win_sdk_ver| Self::has_version_in_range(&win_sdk_ver, max, min))
}
fn installation_folder() -> std::io::Result<PathBuf> {
Self::installation_folder_environment_variable()
.unwrap_or_else(Self::installation_folder_from_registry)
}
fn installation_folder_environment_variable() -> Option<std::io::Result<PathBuf>> {
std::env::var(WinSdk::ENV_KEY).ok().map(|s| {
let path = PathBuf::from(s);
if !path.is_dir() {
return Err(Error::new(
ErrorKind::InvalidData,
"`WIN_SDK_PATH` environment variable contained invalid data.",
));
}
Ok(path)
})
}
fn installation_folder_from_registry() -> std::io::Result<PathBuf> {
Self::HKLM
.open_subkey(Self::REG_PATH)
.and_then(|sdk_entry| sdk_entry.get_value("InstallationFolder"))
.and_then(|path_string: String| {
let path = Path::new(path_string.as_str());
if !path.is_dir() {
return Err(Error::new(
ErrorKind::NotFound,
format!(
"The InstallationFolder `{}` does not exist.",
path_string.as_str()
),
));
}
Ok(PathBuf::from(path_string))
})
}
fn has_version_in_range(
version: &WinSdkVersion,
max: Option<&WinSdkVersion>,
min: Option<&WinSdkVersion>,
) -> bool {
let is_below_max: bool = max.is_none_or(|max_version| max_version > version);
let is_above_min: bool = min.is_none_or(|min_version| version >= min_version);
is_below_max && is_above_min
}
}
fn sub_directory(parent: &Path, dir: &str) -> std::io::Result<PathBuf> {
let sub_dir = parent.join(dir);
if !sub_dir.is_dir() {
return Err(Error::new(
ErrorKind::NotFound,
format!(
"{} does not contain the {} directory.",
parent.to_string_lossy(),
dir
),
));
}
Ok(sub_dir)
}
#[cfg(test)]
mod test {
use super::*;
use std::collections::BTreeSet;
use tempfile::tempdir;
#[test]
fn test_win_sdk_includes_create_error() {
let invalid_path = PathBuf::from(".");
let error = WinSdkIncludes::create(&invalid_path).expect_err(
"Creating a WinSdkIncludes object with an invalid path should result in an error.",
);
assert_eq!(error.kind(), ErrorKind::NotFound);
}
#[test]
fn test_win_sdk_includes() {
let temp_dir = tempdir().expect("It should be possible to create a temporary directory.");
let is_not_valid = !WinSdkIncludes::is_valid(temp_dir.path());
assert!(is_not_valid);
WinSdkIncludes::EXPECTED_DIRS.iter().for_each(|s| {
std::fs::create_dir(temp_dir.path().join(s))
.unwrap_or_else(|_| panic!("It should be possible to create the dir {}", s))
});
assert!(WinSdkIncludes::is_valid(temp_dir.path()));
let actual = WinSdkIncludes::create(temp_dir.path())
.expect("It should be possible to create a WinSdkIncludes object when all sub directories are present.");
assert_eq!(
actual.cppwinrt_dir(),
temp_dir.path().join(WinSdkIncludes::CPPWINRT_DIR).as_path()
);
assert_eq!(
actual.shared_dir(),
temp_dir.path().join(WinSdkIncludes::SHARED_DIR).as_path()
);
assert_eq!(
actual.ucrt_dir(),
temp_dir.path().join(WinSdkIncludes::UCRT_DIR).as_path()
);
assert_eq!(
actual.um_dir(),
temp_dir.path().join(WinSdkIncludes::UM_DIR).as_path()
);
assert_eq!(
actual.winrt_dir(),
temp_dir.path().join(WinSdkIncludes::WINRT_DIR).as_path()
);
}
#[test]
fn test_include_versioned_subdirs() {
let temp_dir = tempdir().expect("It should be possible to create a temporary directory.");
let parent = temp_dir.path();
let include_dir = temp_dir.path().join("Include");
std::fs::create_dir(include_dir.as_path()).expect(
"It should be possible to create a `Include` folder inside the temproary directory",
);
let expected = ["10.0.2.0", "10.0.1.0", "10.0.0.0"]
.iter()
.map(|v| {
let versioned_subdir = include_dir.as_path().join(v);
std::fs::create_dir(versioned_subdir.as_path()).expect("It should be possible to create a versioned sub dir folder inside the `Include` directory");
WinSdkIncludes::EXPECTED_DIRS.iter().for_each(|s| {
std::fs::create_dir(versioned_subdir.as_path().join(s))
.unwrap_or_else(|_| panic!("It should be possible to create the dir {}", s))
});
versioned_subdir
})
.collect::<BTreeSet<PathBuf>>();
let actual = WinSdk::include_versioned_subdirs(parent, None, None)
.expect(
"It should be possible to find a valid include sub directory in the parent folder.",
)
.into_iter()
.collect::<BTreeSet<PathBuf>>();
assert_eq!(expected, actual);
}
}