From e8f6efcc7fcc8d331511200eacafcc5ea4b84ad7 Mon Sep 17 00:00:00 2001 From: Jesper Brynolf Date: Mon, 14 Jul 2025 23:37:51 +0200 Subject: [PATCH] Adds support for retrieving different paths from an installation. Signed-off-by: Jesper Brynolf --- src/lib.rs | 492 ++++------------------------------------- src/versions.rs | 201 +++++++++++++++++ src/vs_installation.rs | 324 +++++++++++++++++++++++++++ src/vs_llvm.rs | 52 +++++ src/vs_paths.rs | 22 ++ src/win_sdk.rs | 52 +---- 6 files changed, 644 insertions(+), 499 deletions(-) create mode 100644 src/versions.rs create mode 100644 src/vs_installation.rs create mode 100644 src/vs_llvm.rs create mode 100644 src/vs_paths.rs diff --git a/src/lib.rs b/src/lib.rs index 6c5d02d..95b9b0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,96 +2,44 @@ //! This crates provides the functionality of finding //! the msbuild binary on the system. //! +//! But it also provides functionality for finding other +//! paths that may be needed when using msbuild e.g. +//! WinSDK. +//! //! # Environment Variables //! - The `VS_WHERE_PATH` environment variable can be used in order //! overwrite the default path where the crate tries to locate //! the `vswhere.exe` binary. //! //! - The `VS_INSTALLATION_PATH` environment variable can be used in order -//! to overwrite specify a path to Visual Studio -//! Note! The path must still lead to a binary the fulfills the version +//! to overwrite specify a path to Visual Studio installation +//! Note! The path must still lead to an installation that fulfills the version //! requirements otherwise the crate will try to probe the system //! for a suitable version. //! //! - The `WIN_SDK_PATH` environment variable can be used in order to //! to overwrite in what location the library will search for //! WinSDK installations. -use lenient_semver::Version; -use serde_json::Value; use std::{ convert::TryFrom, io::{Error, ErrorKind}, path::{Path, PathBuf}, }; +mod versions; + +pub(crate) mod vs_paths; + +pub mod vs_installation; +pub mod vs_llvm; pub mod vs_where; pub mod win_sdk; +pub use versions::{VsInstallationVersion, VsProductLineVersion}; +pub use vs_installation::VsInstallation; +pub use vs_llvm::VsLlvm; pub use vs_where::VsWhere; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] -pub struct InstallationVersion<'a>(Version<'a>); - -impl<'a> InstallationVersion<'a> { - pub fn parse(value: &'a str) -> std::io::Result> { - Version::parse(value).map_or_else( - |e| { - Err(Error::new( - ErrorKind::InvalidData, - format!("Failed to parse &str as a InstallationVersion: {}", e), - )) - }, - |v| Ok(InstallationVersion(v)), - ) - } -} - -/// Enum holding the product line versions. -pub enum ProductLineVersion { - Vs2022, - Vs2019, - Vs2017, -} - -impl ProductLineVersion { - /// The non inclusive max installation version for a - /// specific product line version. - pub fn installation_version_max(&self) -> InstallationVersion { - // Constant values that are always safe to parse. - match self { - Self::Vs2022 => InstallationVersion::parse("18.0.0.0").unwrap(), - Self::Vs2019 => InstallationVersion::parse("17.0.0.0").unwrap(), - Self::Vs2017 => InstallationVersion::parse("16.0.0.0").unwrap(), - } - } - - /// The inclusive min installation version for a - /// specific product line version. - pub fn installation_version_min(&self) -> InstallationVersion { - match self { - Self::Vs2022 => InstallationVersion::parse("17.0.0.0").unwrap(), - Self::Vs2019 => InstallationVersion::parse("16.0.0.0").unwrap(), - Self::Vs2017 => InstallationVersion::parse("15.0.0.0").unwrap(), - } - } -} - -impl TryFrom<&str> for ProductLineVersion { - type Error = Error; - - fn try_from(s: &str) -> std::io::Result { - match s { - "2017" => Ok(ProductLineVersion::Vs2017), - "2019" => Ok(ProductLineVersion::Vs2019), - "2022" => Ok(ProductLineVersion::Vs2022), - _ => Err(Error::new( - ErrorKind::InvalidData, - format!("Product line version {} did not match any known values.", s), - )), - } - } -} - /// Type for finding and interactive with /// the msbuild executable. pub struct MsBuild { @@ -99,7 +47,6 @@ pub struct MsBuild { } impl MsBuild { - const ENV_KEY: &'static str = "VS_INSTALLATION_PATH"; /// Finds the msbuild executable that is associated with provided product line version /// if no version is provided then the first installation of msbuild that is found /// will be selected. @@ -107,12 +54,15 @@ impl MsBuild { /// # Examples /// /// ``` - /// let product_line_version: Optional<&str> = Some("2017"); - /// let msbuild: MsBuild = MsBuild::find_msbuild(product_line_version); + /// use msbuild::MsBuild; + /// + /// let product_line_version: Option<&str> = Some("2017"); + /// let msbuild: MsBuild = MsBuild::find_msbuild(product_line_version) + /// .expect("A 2017 VS installation should exist"); /// ``` pub fn find_msbuild(product_line_version: Option<&str>) -> std::io::Result { product_line_version - .map(ProductLineVersion::try_from) + .map(VsProductLineVersion::try_from) .transpose() .and_then(|potential_plv| { let max = potential_plv @@ -125,34 +75,26 @@ impl MsBuild { }) } - /// Finds a msbuild that with the highest installation version that is in a range + /// Finds a msbuild with the highest installation version that is in a range /// between max (exclusive) and min(inclusive). /// /// # Examples /// /// ``` /// // Find the latest supported version for msbuild - /// use msbuild::{MsBuild, ProductLineVersion}; + /// use msbuild::{MsBuild, VsProductLineVersion}; /// /// let msbuild = MsBuild::find_msbuild_in_range( - /// Some(ProductLineVersion::Vs2022.installation_version_max()), - /// Some(ProductLineVersion::Vs2017.installation_version_min()), + /// Some(VsProductLineVersion::Vs2022.installation_version_max()), + /// Some(VsProductLineVersion::Vs2017.installation_version_min()), /// ); /// ``` pub fn find_msbuild_in_range( - max: Option, - min: Option, + max: Option, + min: Option, ) -> std::io::Result { - VsWhere::find_vswhere() - .and_then(|vswhere| vswhere.run(None)) - .and_then(|output| Self::parse_from_json(&output)) - .and_then(|v: Value| { - Self::list_instances(&v) - .and_then(|instances| Self::find_match(instances, max.as_ref(), min.as_ref())) - }) - .map(|p| MsBuild { - path: p.as_path().join("MsBuild/Current/Bin/msbuild.exe"), - }) + VsInstallation::find_in_range(max, min) + .and_then(|vs_installation| Self::try_from(&vs_installation)) } /// Executes msbuild using the provided project_path and @@ -186,139 +128,22 @@ impl MsBuild { } }) } +} - // Internal function for parsing a string as json object. - fn parse_from_json(value: &str) -> std::io::Result { - serde_json::from_str(value).map_err(|e| { - Error::new( - ErrorKind::InvalidData, - format!("Failed to parse command output as json ({})", e), - ) - }) - } - - // Internal function for listing the instances inthe json value. - fn list_instances(v: &Value) -> std::io::Result<&Vec> { - v.as_array().ok_or_else(|| { - Error::new( - ErrorKind::InvalidData, - "json data did not contain any installation instances.", - ) - }) - } - - // Internal function for finding the instances that matches the - // version range and, if specified, the path in the environment - // variable. - fn find_match( - instances_json: &[Value], - max: Option<&InstallationVersion>, - min: Option<&InstallationVersion>, - ) -> std::io::Result { - let env_installation_path: Option = std::env::var(MsBuild::ENV_KEY) - .ok() - .map(|v| PathBuf::from(&v)); - - // Parse the instance json data and filter result based on version. - let validated_instances = MsBuild::validate_instances_json(instances_json, max, min); +impl TryFrom<&VsInstallation> for MsBuild { + type Error = Error; - if let Some(specified_installation_path) = env_installation_path { - // Finds the specified installation path among the parsed - // and validated instances. - validated_instances - .iter() - .filter_map(|(_, p)| { - if specified_installation_path.starts_with(p) { - Some(p.to_path_buf()) - } else { - None - } - }) - .next() - .ok_or(Error::new( - ErrorKind::NotFound, - "No instance found that matched requirements.", - )) - } else { - // Select the latest version. - validated_instances - .iter() - .max_by_key(|(v, _)| v) - .map(|(_, p)| p.to_path_buf()) - .ok_or(Error::new( - ErrorKind::NotFound, - "No instance found that matched requirements.", - )) + fn try_from(vs_installation: &VsInstallation) -> std::io::Result { + let path: PathBuf = vs_installation + .path() + .join("MsBuild/Current/Bin/msbuild.exe"); + if !path.is_file() { + return Err(Error::new( + ErrorKind::NotFound, + format!("No msbuild executable found at {}", path.display()), + )); } - } - - /// Internal function that extracts a collection of parsed - /// installation instances with a version within the given - /// interval. - fn validate_instances_json<'a>( - instances_json: &'a [Value], - max: Option<&'a InstallationVersion>, - min: Option<&'a InstallationVersion>, - ) -> Vec<(InstallationVersion<'a>, &'a Path)> { - instances_json - .iter() - .filter_map(|i| { - MsBuild::parse_installation_version(i) - .and_then(|installation_version| { - if MsBuild::has_version_in_range( - &installation_version.0, - max.map(|v| &v.0), - min.map(|v| &v.0), - ) { - MsBuild::parse_installation_path(i).map(|installation_path| { - Some((installation_version, installation_path)) - }) - } else { - // Maybe log(trace) that an instance was found that was not in the range. - Ok(None) - } - }) - .unwrap_or_else(|e| { - print!("Encounted an error during parsing of instance data: {}", e); - None - }) - }) - .collect() - } - - fn parse_installation_path(json_value: &Value) -> std::io::Result<&Path> { - json_value - .get("installationPath") - .and_then(|path_json_value: &Value| path_json_value.as_str()) - .ok_or(Error::new( - ErrorKind::InvalidData, - "Failed to retrieve `installationPath`.", - )) - .map(Path::new) - } - - fn parse_installation_version(json_value: &Value) -> std::io::Result> { - json_value - .get("installationVersion") - .and_then(|version_json_value: &Value| version_json_value.as_str()) - .and_then(|version_str: &str| Version::parse(version_str).ok()) - .map(InstallationVersion) - .ok_or(Error::new( - ErrorKind::InvalidData, - "Failed to retrieve `installationVersion`.", - )) - } - - /// Internal function to check if a version is in the range - /// if it has been specified. - fn has_version_in_range( - version: &Version, - max: Option<&Version>, - min: Option<&Version>, - ) -> bool { - let is_below_max: bool = max.map_or(true, |max_version| max_version > version); - let is_above_min: bool = min.map_or(true, |min_version| version >= min_version); - is_below_max && is_above_min + Ok(MsBuild { path }) } } @@ -326,235 +151,4 @@ impl MsBuild { // Unit tests of the private functions and methods // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_msbuild_has_version_in_range() { - let max = Some( - Version::parse("4.3.2.1") - .expect("It should be possible to create a Version object from the string 4.3.2.1"), - ); - let min = Some( - Version::parse("1.2.3.4") - .expect("It should be possible to create a Version object from the string 1.2.3.4"), - ); - // Check with no min or max - assert!( - MsBuild::has_version_in_range( - &Version::parse("0.0.0.0").expect( - "It should be possible to create a Version object from the string 0.0.0.0" - ), - None, - None - ), - "The version 0.0.0.0 should be in range when no min or max values have been specified." - ); - // Check outside of range with min value. - assert!( - !MsBuild::has_version_in_range( - &Version::parse("0.0.0.0").expect( - "It should be possible to create a Version object from the string 0.0.0.0" - ), - None, - min.as_ref() - ), - "The version 0.0.0.0 should not be in range when min is 1.2.3.4" - ); - // Check inside of range with min value - assert!( - MsBuild::has_version_in_range( - &Version::parse("1.2.3.300").expect( - "It should be possible to create a Version object from the string 1.2.3.300" - ), - None, - min.as_ref() - ), - "The version 1.2.3.300 should be in range when min is 1.2.3.4 and no max is given." - ); - // Check out of range with max value - assert!( - !MsBuild::has_version_in_range( - &Version::parse("4.3.2.11").expect( - "It should be possible to create a Version object from the string 4.3.2.11" - ), - max.as_ref(), - None, - ), - "The version 4.3.2.11 should not be in range when max is 4.3.2.1 and no min is given." - ); - // Check in range with max value - assert!( - MsBuild::has_version_in_range( - &Version::parse("4.0.2.11").expect( - "It should be possible to create a Version object from the string 4.0.2.11" - ), - max.as_ref(), - None, - ), - "The version 4.3.2.11 should not be in range when max is 4.3.2.1 and no min is given." - ); - // Check in range with min and max - assert!( - MsBuild::has_version_in_range( - &Version::parse("4.0.2.11").expect( - "It should be possible to create a Version object from the string 4.0.2.11" - ), - max.as_ref(), - min.as_ref(), - ), - "The version 4.3.2.11 should not be in range when max is 4.3.2.1 and no max is given." - ); - } - - #[test] - fn test_msbuild_parse_installation_version() { - let version_str = "2.3.1.34"; - let json_value = serde_json::json!({ - "instanceId": "VisualStudio.14.0", - "installationPath": "C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\", - "installationVersion": version_str - }); - let expected = Version::parse(version_str) - .map(InstallationVersion) - .expect("It should be possible to parse the `version_str` as Version object."); - let actual = MsBuild::parse_installation_version(&json_value).expect( - "The function should be to extract an installation version from the json_value.", - ); - assert_eq!(expected, actual); - } - - #[test] - fn test_msbuild_parse_installation_path() { - let expected = Path::new("C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\"); - let json_value = serde_json::json!({ - "instanceId": "019109ba", - "installDate": "2023-08-26T14:05:02Z", - "installationName": "VisualStudio/17.12.0+35506.116", - "installationPath": expected.to_string_lossy(), - "installationVersion": "17.12.35506.116", - "productId": "Microsoft.VisualStudio.Product.Community", - "productPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\devenv.exe", - }); - let actual = MsBuild::parse_installation_path(&json_value) - .expect("The function should be to extract an installation path from the json_value."); - assert_eq!(expected, actual); - } - - #[test] - fn test_msbuild_validate_instances_json() { - let json_value = serde_json::json!([ - { - "installationPath": "C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\", - "installationVersion": "14.0", - }, - { - "installationPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community", - "installationVersion": "17.12.35506.116", - }, - { - "installationPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise", - "installationVersion": "17.08.35506.116", - }, - ]); - - let values: &Vec = json_value - .as_array() - .expect("It should be possible to parse the json as an array of objects."); - - // Sanity check. - assert_eq!( - values.len(), - 3, - "There should be 3 instances: \n {:?}", - values - ); - - let min = Some( - Version::parse("17.9") - .map(InstallationVersion) - .expect("It should be possible to parse the 17.9 as a version."), - ); - let max = Some( - Version::parse("18.0") - .map(InstallationVersion) - .expect("It should be possible to parse the 18.0 as a version."), - ); - let validated_instances = - MsBuild::validate_instances_json(values.as_slice(), max.as_ref(), min.as_ref()); - let expected_version = Version::parse("17.12.35506.116") - .map(InstallationVersion) - .expect("It should be possible to parse avlid version."); - let expected_path = - Path::new("C:\\Program Files\\Microsoft Visual Studio\\2022\\Community"); - assert_eq!( - validated_instances.len(), - 1, - "There should only be 1 element found." - ); - let (actual_version, actual_path) = validated_instances.first().unwrap(); - assert_eq!( - expected_version, *actual_version, - "The returned version was not the expected one", - ); - assert_eq!( - expected_path, *actual_path, - "The returned path was not the expected one." - ); - } - - #[test] - fn test_msbuild_find_match() { - let json_value = serde_json::json!([ - { - "installationPath": "C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\", - "installationVersion": "14.0", - }, - { - "installationPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community", - "installationVersion": "17.12.35506.116", - }, - { - "installationPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise", - "installationVersion": "17.08.35506.116", - }, - ]); - - let values: &Vec = json_value - .as_array() - .expect("It should be possible to parse the json as an array of objects."); - - // Sanity check. - assert_eq!( - values.len(), - 3, - "There should be 3 instances: \n {:?}", - values - ); - - // The min and max are now chosen so that they will include - // two possible result. - let min = Some( - Version::parse("17.7") - .map(InstallationVersion) - .expect("It should be possible to parse the 17.9 as a version."), - ); - let max = Some( - Version::parse("18.0") - .map(InstallationVersion) - .expect("It should be possible to parse the 18.0 as a version."), - ); - - // The expected values, when no environment variable have been set, - // is the one with the latest version. - let expected = PathBuf::from("C:\\Program Files\\Microsoft Visual Studio\\2022\\Community"); - - let actual = MsBuild::find_match(values, max.as_ref(), min.as_ref()) - .expect("The function is expected to return a valid result."); - - assert_eq!( - expected, actual, - "The resulting path does not match the expected one." - ); - } -} +mod test {} diff --git a/src/versions.rs b/src/versions.rs new file mode 100644 index 0000000..1815587 --- /dev/null +++ b/src/versions.rs @@ -0,0 +1,201 @@ +//! Module containing code that handles versions. +use lenient_semver::Version; +use std::{ + convert::TryFrom, + io::{Error, ErrorKind}, +}; + +/// Type used for specifying the version of the installation. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct VsInstallationVersion<'a>(Version<'a>); + +impl<'a> VsInstallationVersion<'a> { + /// Parses the VsInstallationVersion from a string. + pub fn parse(value: &'a str) -> std::io::Result> { + Version::parse(value).map_or_else( + |e| { + Err(Error::new( + ErrorKind::InvalidData, + format!("Failed to parse &str as a VsInstallationVersion: {}", e), + )) + }, + |v| Ok(VsInstallationVersion(v)), + ) + } + + /// Crate function for checking if the version is in the specified range. + pub(crate) fn is_in_range( + &self, + max: Option<&VsInstallationVersion>, + min: Option<&VsInstallationVersion>, + ) -> bool { + has_version_in_range(&self.0, max.map(|v| &v.0), min.map(|v| &v.0)) + } +} + +/// Enum holding the VS product line versions. +pub enum VsProductLineVersion { + Vs2022, + Vs2019, + Vs2017, +} + +impl VsProductLineVersion { + /// The non inclusive max installation version for a + /// specific product line version. + pub fn installation_version_max(&self) -> VsInstallationVersion { + // Constant values that are always safe to parse. + match self { + Self::Vs2022 => VsInstallationVersion::parse("18.0.0.0").unwrap(), + Self::Vs2019 => VsInstallationVersion::parse("17.0.0.0").unwrap(), + Self::Vs2017 => VsInstallationVersion::parse("16.0.0.0").unwrap(), + } + } + + /// The inclusive min installation version for a + /// specific product line version. + pub fn installation_version_min(&self) -> VsInstallationVersion { + match self { + Self::Vs2022 => VsInstallationVersion::parse("17.0.0.0").unwrap(), + Self::Vs2019 => VsInstallationVersion::parse("16.0.0.0").unwrap(), + Self::Vs2017 => VsInstallationVersion::parse("15.0.0.0").unwrap(), + } + } +} + +impl TryFrom<&str> for VsProductLineVersion { + type Error = Error; + + fn try_from(s: &str) -> std::io::Result { + match s { + "2017" => Ok(VsProductLineVersion::Vs2017), + "2019" => Ok(VsProductLineVersion::Vs2019), + "2022" => Ok(VsProductLineVersion::Vs2022), + _ => Err(Error::new( + ErrorKind::InvalidData, + format!("Product line version {} did not match any known values.", s), + )), + } + } +} + +/// The windows SDK version. +#[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> { + 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)), + ) + } + + /// Crate function for checking if the version is in the specified range. + pub(crate) fn is_in_range( + &self, + max: Option<&WinSdkVersion>, + min: Option<&WinSdkVersion>, + ) -> bool { + has_version_in_range(&self.0, max.map(|v| &v.0), min.map(|v| &v.0)) + } +} + +/// Internal function to check if a version is in the range +/// if it has been specified. +fn has_version_in_range(version: &Version, max: Option<&Version>, min: Option<&Version>) -> bool { + let is_below_max: bool = max.map_or(true, |max_version| max_version > version); + let is_above_min: bool = min.map_or(true, |min_version| version >= min_version); + is_below_max && is_above_min +} + +// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Unit tests of the private functions and methods +// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_msbuild_has_version_in_range() { + let max = Some( + Version::parse("4.3.2.1") + .expect("It should be possible to create a Version object from the string 4.3.2.1"), + ); + let min = Some( + Version::parse("1.2.3.4") + .expect("It should be possible to create a Version object from the string 1.2.3.4"), + ); + // Check with no min or max + assert!( + has_version_in_range( + &Version::parse("0.0.0.0").expect( + "It should be possible to create a Version object from the string 0.0.0.0" + ), + None, + None + ), + "The version 0.0.0.0 should be in range when no min or max values have been specified." + ); + // Check outside of range with min value. + assert!( + !has_version_in_range( + &Version::parse("0.0.0.0").expect( + "It should be possible to create a Version object from the string 0.0.0.0" + ), + None, + min.as_ref() + ), + "The version 0.0.0.0 should not be in range when min is 1.2.3.4" + ); + // Check inside of range with min value + assert!( + has_version_in_range( + &Version::parse("1.2.3.300").expect( + "It should be possible to create a Version object from the string 1.2.3.300" + ), + None, + min.as_ref() + ), + "The version 1.2.3.300 should be in range when min is 1.2.3.4 and no max is given." + ); + // Check out of range with max value + assert!( + !has_version_in_range( + &Version::parse("4.3.2.11").expect( + "It should be possible to create a Version object from the string 4.3.2.11" + ), + max.as_ref(), + None, + ), + "The version 4.3.2.11 should not be in range when max is 4.3.2.1 and no min is given." + ); + // Check in range with max value + assert!( + has_version_in_range( + &Version::parse("4.0.2.11").expect( + "It should be possible to create a Version object from the string 4.0.2.11" + ), + max.as_ref(), + None, + ), + "The version 4.3.2.11 should not be in range when max is 4.3.2.1 and no min is given." + ); + // Check in range with min and max + assert!( + has_version_in_range( + &Version::parse("4.0.2.11").expect( + "It should be possible to create a Version object from the string 4.0.2.11" + ), + max.as_ref(), + min.as_ref(), + ), + "The version 4.3.2.11 should not be in range when max is 4.3.2.1 and no max is given." + ); + } +} diff --git a/src/vs_installation.rs b/src/vs_installation.rs new file mode 100644 index 0000000..9cbcfb2 --- /dev/null +++ b/src/vs_installation.rs @@ -0,0 +1,324 @@ +//! Module for code related to a full installation of VS or just +//! the VS build tools. +use crate::{versions::VsInstallationVersion, vs_where::VsWhere}; +use serde_json::Value; +use std::{ + io::{Error, ErrorKind}, + path::{Path, PathBuf}, +}; + +/// Type containing information about the installation. +pub struct VsInstallation { + path: PathBuf, +} + +impl VsInstallation { + const ENV_KEY: &'static str = "VS_INSTALLATION_PATH"; + + /// The path of the VS installation. + pub fn path(&self) -> &Path { + self.path.as_path() + } + + /// Finds a VS installation with the highest installation version that is in a range + /// between max (exclusive) and min(inclusive). + /// # Examples + /// + /// ``` + /// // Find the latest supported version for msbuild + /// use msbuild::installation::{VsInstallationVersion, VsInstallation}; + /// + /// let max = Some(VsInstallationVersion::parse("17.10.35013.160").unwrap()); + /// let min = Some(VsInstallationVersion::parse("17.0.0.0").unwrap()); + /// + /// let vs_installation = VsInstallation::find_in_range(max, min); + /// ``` + pub fn find_in_range( + max: Option, + min: Option, + ) -> std::io::Result { + VsWhere::find_vswhere() + .and_then(|vswhere| vswhere.run(None)) + .and_then(|output| Self::parse_from_json(&output)) + .and_then(|v: Value| { + Self::list_instances(&v) + .and_then(|instances| Self::find_match(instances, max.as_ref(), min.as_ref())) + }) + .map(|path| VsInstallation { path }) + } + + // Internal function for finding the instances that matches the + // version range and, if specified, the path in the environment + // variable. + fn find_match( + instances_json: &[Value], + max: Option<&VsInstallationVersion>, + min: Option<&VsInstallationVersion>, + ) -> std::io::Result { + let env_installation_path: Option = + std::env::var(Self::ENV_KEY).ok().map(|v| PathBuf::from(&v)); + + // Parse the instance json data and filter result based on version. + let validated_instances = Self::validate_instances_json(instances_json, max, min); + + if let Some(specified_installation_path) = env_installation_path { + // Finds the specified installation path among the parsed + // and validated instances. + validated_instances + .iter() + .filter_map(|(_, p)| { + if specified_installation_path.starts_with(p) { + Some(p.to_path_buf()) + } else { + None + } + }) + .next() + .ok_or(Error::new( + ErrorKind::NotFound, + "No instance found that matched requirements.", + )) + } else { + // Select the latest version. + validated_instances + .iter() + .max_by_key(|(v, _)| v) + .map(|(_, p)| p.to_path_buf()) + .ok_or(Error::new( + ErrorKind::NotFound, + "No instance found that matched requirements.", + )) + } + } + + /// Internal function that extracts a collection of parsed + /// installation instances with a version within the given + /// interval. + fn validate_instances_json<'a>( + instances_json: &'a [Value], + max: Option<&'a VsInstallationVersion>, + min: Option<&'a VsInstallationVersion>, + ) -> Vec<(VsInstallationVersion<'a>, &'a Path)> { + instances_json + .iter() + .filter_map(|i| { + VsInstallation::parse_installation_version(i) + .and_then(|installation_version| { + if installation_version.is_in_range(max, min) { + VsInstallation::parse_installation_path(i).map(|installation_path| { + Some((installation_version, installation_path)) + }) + } else { + // Maybe log(trace) that an instance was found that was not in the range. + Ok(None) + } + }) + .unwrap_or_else(|e| { + print!("Encounted an error during parsing of instance data: {}", e); + None + }) + }) + .collect() + } + + // Internal function for parsing a string as json object. + fn parse_from_json(value: &str) -> std::io::Result { + serde_json::from_str(value).map_err(|e| { + Error::new( + ErrorKind::InvalidData, + format!("Failed to parse command output as json ({})", e), + ) + }) + } + + // Internal function for listing the instances inthe json value. + fn list_instances(v: &Value) -> std::io::Result<&Vec> { + v.as_array().ok_or_else(|| { + Error::new( + ErrorKind::InvalidData, + "json data did not contain any installation instances.", + ) + }) + } + + /// Function for parsing the installation path from + /// the return value of `vs_where`. + fn parse_installation_path(json_value: &Value) -> std::io::Result<&Path> { + json_value + .get("installationPath") + .and_then(|path_json_value: &Value| path_json_value.as_str()) + .ok_or(Error::new( + ErrorKind::InvalidData, + "Failed to retrieve `installationPath`.", + )) + .map(Path::new) + } + + /// Function for parsing the installation version from + /// the return value of `vs_where`. + fn parse_installation_version( + json_value: &Value, + ) -> std::io::Result> { + json_value + .get("installationVersion") + .and_then(|version_json_value: &Value| version_json_value.as_str()) + .and_then(|version_str: &str| VsInstallationVersion::parse(version_str).ok()) + .ok_or(Error::new( + ErrorKind::InvalidData, + "Failed to retrieve `installationVersion`.", + )) + } +} + +// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Unit tests of the private functions and methods +// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_installation_version() { + let version_str = "2.3.1.34"; + let json_value = serde_json::json!({ + "instanceId": "VisualStudio.14.0", + "installationPath": "C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\", + "installationVersion": version_str + }); + let expected = VsInstallationVersion::parse(version_str) + .expect("It should be possible to parse the `version_str` as Version object."); + let actual = VsInstallation::parse_installation_version(&json_value).expect( + "The function should be to extract an installation version from the json_value.", + ); + assert_eq!(expected, actual); + } + + #[test] + fn test_parse_installation_path() { + let expected = Path::new("C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\"); + let json_value = serde_json::json!({ + "instanceId": "019109ba", + "installDate": "2023-08-26T14:05:02Z", + "installationName": "VisualStudio/17.12.0+35506.116", + "installationPath": expected.to_string_lossy(), + "installationVersion": "17.12.35506.116", + "productId": "Microsoft.VisualStudio.Product.Community", + "productPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\devenv.exe", + }); + let actual = VsInstallation::parse_installation_path(&json_value) + .expect("The function should be to extract an installation path from the json_value."); + assert_eq!(expected, actual); + } + + #[test] + fn test_msbuild_validate_instances_json() { + let json_value = serde_json::json!([ + { + "installationPath": "C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\", + "installationVersion": "14.0", + }, + { + "installationPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community", + "installationVersion": "17.12.35506.116", + }, + { + "installationPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise", + "installationVersion": "17.08.35506.116", + }, + ]); + + let values: &Vec = json_value + .as_array() + .expect("It should be possible to parse the json as an array of objects."); + + // Sanity check. + assert_eq!( + values.len(), + 3, + "There should be 3 instances: \n {:?}", + values + ); + + let min = Some( + VsInstallationVersion::parse("17.9") + .expect("It should be possible to parse the 17.9 as a version."), + ); + let max = Some( + VsInstallationVersion::parse("18.0") + .expect("It should be possible to parse the 18.0 as a version."), + ); + let validated_instances = + VsInstallation::validate_instances_json(values.as_slice(), max.as_ref(), min.as_ref()); + let expected_version = VsInstallationVersion::parse("17.12.35506.116") + .expect("It should be possible to parse avlid version."); + let expected_path = + Path::new("C:\\Program Files\\Microsoft Visual Studio\\2022\\Community"); + assert_eq!( + validated_instances.len(), + 1, + "There should only be 1 element found." + ); + let (actual_version, actual_path) = validated_instances.first().unwrap(); + assert_eq!( + expected_version, *actual_version, + "The returned version was not the expected one", + ); + assert_eq!( + expected_path, *actual_path, + "The returned path was not the expected one." + ); + } + + #[test] + fn test_msbuild_find_match() { + let json_value = serde_json::json!([ + { + "installationPath": "C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\", + "installationVersion": "14.0", + }, + { + "installationPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community", + "installationVersion": "17.12.35506.116", + }, + { + "installationPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise", + "installationVersion": "17.08.35506.116", + }, + ]); + + let values: &Vec = json_value + .as_array() + .expect("It should be possible to parse the json as an array of objects."); + + // Sanity check. + assert_eq!( + values.len(), + 3, + "There should be 3 instances: \n {:?}", + values + ); + + // The min and max are now chosen so that they will include + // two possible result. + let min = Some( + VsInstallationVersion::parse("17.7") + .expect("It should be possible to parse the 17.9 as a version."), + ); + let max = Some( + VsInstallationVersion::parse("18.0") + .expect("It should be possible to parse the 18.0 as a version."), + ); + + // The expected values, when no environment variable have been set, + // is the one with the latest version. + let expected = PathBuf::from("C:\\Program Files\\Microsoft Visual Studio\\2022\\Community"); + + let actual = VsInstallation::find_match(values, max.as_ref(), min.as_ref()) + .expect("The function is expected to return a valid result."); + + assert_eq!( + expected, actual, + "The resulting path does not match the expected one." + ); + } +} diff --git a/src/vs_llvm.rs b/src/vs_llvm.rs new file mode 100644 index 0000000..a72b0dd --- /dev/null +++ b/src/vs_llvm.rs @@ -0,0 +1,52 @@ +//! Module for llvm parts of a VS installation. +use crate::{vs_paths::sub_directory, VsInstallation}; +use std::{ + convert::TryFrom, + io::Error, + path::{Path, PathBuf}, +}; + +/// Type holding the paths associated with LLVM in the +/// Visual compiler tools. +pub struct VsLlvm { + bin: PathBuf, + lib: PathBuf, + bin_x64: PathBuf, + lib_x64: PathBuf, +} + +impl VsLlvm { + const BIN: &'static str = "VC/Tools/Llvm/bin"; + const LIB: &'static str = "VC/Tools/Llvm/lib"; + const BIN_X64: &'static str = "VC/Tools/Llvm/x64/bin"; + const LIB_X64: &'static str = "VC/Tools/Llvm/x64/lib"; + + pub fn bin(&self) -> &Path { + self.bin.as_ref() + } + + pub fn lib(&self) -> &Path { + self.lib.as_ref() + } + + pub fn bin_x64(&self) -> &Path { + self.bin_x64.as_ref() + } + + pub fn lib_x64(&self) -> &Path { + self.lib_x64.as_ref() + } +} + +impl TryFrom<&VsInstallation> for VsLlvm { + type Error = Error; + + fn try_from(vs_installation: &VsInstallation) -> std::io::Result { + Ok(VsLlvm { + bin: sub_directory(vs_installation.path(), Self::BIN)?, + lib: sub_directory(vs_installation.path(), Self::LIB)?, + bin_x64: sub_directory(vs_installation.path(), Self::BIN_X64)?, + lib_x64: sub_directory(vs_installation.path(), Self::LIB_X64)?, + }) + } +} diff --git a/src/vs_paths.rs b/src/vs_paths.rs new file mode 100644 index 0000000..cae6172 --- /dev/null +++ b/src/vs_paths.rs @@ -0,0 +1,22 @@ +//! Internal module for code handling paths in the +//! VS installation. +use std::{ + io::{Error, ErrorKind}, + path::{Path, PathBuf}, +}; + +/// Constructs a verified object representing the path to the sub directory. +pub(crate) fn sub_directory(parent: &Path, dir: &str) -> std::io::Result { + 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) +} diff --git a/src/win_sdk.rs b/src/win_sdk.rs index 84208cc..f6fe9d4 100644 --- a/src/win_sdk.rs +++ b/src/win_sdk.rs @@ -1,7 +1,7 @@ //! Module that contains functionality for programtically //! retrieve information about the windows SDKs available on //! the system. -use lenient_semver::Version; +use crate::{versions::WinSdkVersion, vs_paths::sub_directory}; use std::{ collections::BTreeMap, fs::DirEntry, @@ -72,24 +72,6 @@ impl WinSdkIncludes { } } -/// The windows SDK version. -#[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> { - 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)), - ) - } -} - /// Struct holding information regarding the Windows SDK. pub struct WinSdk { include: WinSdkIncludes, @@ -208,9 +190,7 @@ impl WinSdk { path.file_name() .and_then(|ver_dir| ver_dir.to_str()) .and_then(|ver_dir_str| WinSdkVersion::parse(ver_dir_str).ok()) - .map_or(false, |win_sdk_ver| { - Self::has_version_in_range(&win_sdk_ver, max, min) - }) + .map_or(false, |win_sdk_ver| win_sdk_ver.is_in_range(max, min)) } fn installation_folder() -> std::io::Result { @@ -251,34 +231,6 @@ impl WinSdk { Ok(PathBuf::from(path_string)) }) } - - /// Internal function to check if a version is in the range - /// if it has been specified. - fn has_version_in_range( - version: &WinSdkVersion, - max: Option<&WinSdkVersion>, - min: Option<&WinSdkVersion>, - ) -> bool { - let is_below_max: bool = max.map_or(true, |max_version| max_version > version); - let is_above_min: bool = min.map_or(true, |min_version| version >= min_version); - is_below_max && is_above_min - } -} - -/// Constructs a verified object representing the path to the sub directory. -fn sub_directory(parent: &Path, dir: &str) -> std::io::Result { - 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) } // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////