diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 69b4b36a2..7144904cf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,6 +61,10 @@ jobs: target: x86_64-pc-windows-msvc steps: - uses: actions/checkout@master + - name: Update Rustup (temporary workaround) + run: rustup self update + shell: bash + if: startsWith(matrix.os, 'windows') - name: Install Rust (rustup) run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} shell: bash diff --git a/src/lib.rs b/src/lib.rs index 0f330ebd9..9d133a0d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,8 @@ mod winapi; mod com; #[cfg(windows)] mod setup_config; +#[cfg(windows)] +mod vs_instances; pub mod windows_registry; diff --git a/src/vs_instances.rs b/src/vs_instances.rs new file mode 100644 index 000000000..31d3dd147 --- /dev/null +++ b/src/vs_instances.rs @@ -0,0 +1,199 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::io::BufRead; +use std::path::PathBuf; + +use crate::setup_config::{EnumSetupInstances, SetupInstance}; + +pub enum VsInstance { + Com(SetupInstance), + Vswhere(VswhereInstance), +} + +impl VsInstance { + pub fn installation_name(&self) -> Option> { + match self { + VsInstance::Com(s) => s + .installation_name() + .ok() + .and_then(|s| s.into_string().ok()) + .map(Cow::from), + VsInstance::Vswhere(v) => v.map.get("installationName").map(Cow::from), + } + } + + pub fn installation_path(&self) -> Option { + match self { + VsInstance::Com(s) => s.installation_path().ok().map(PathBuf::from), + VsInstance::Vswhere(v) => v.map.get("installationPath").map(PathBuf::from), + } + } + + pub fn installation_version(&self) -> Option> { + match self { + VsInstance::Com(s) => s + .installation_version() + .ok() + .and_then(|s| s.into_string().ok()) + .map(Cow::from), + VsInstance::Vswhere(v) => v.map.get("installationVersion").map(Cow::from), + } + } +} + +pub enum VsInstances { + ComBased(EnumSetupInstances), + VswhereBased(VswhereInstance), +} + +impl IntoIterator for VsInstances { + type Item = VsInstance; + #[allow(bare_trait_objects)] + type IntoIter = Box>; + + fn into_iter(self) -> Self::IntoIter { + match self { + VsInstances::ComBased(e) => { + Box::new(e.into_iter().filter_map(Result::ok).map(VsInstance::Com)) + } + VsInstances::VswhereBased(v) => Box::new(std::iter::once(VsInstance::Vswhere(v))), + } + } +} + +#[derive(Debug)] +pub struct VswhereInstance { + map: HashMap, +} + +impl TryFrom<&Vec> for VswhereInstance { + type Error = &'static str; + + fn try_from(output: &Vec) -> Result { + let map: HashMap<_, _> = output + .lines() + .filter_map(Result::ok) + .filter_map(|s| { + let mut splitn = s.splitn(2, ": "); + Some((splitn.next()?.to_owned(), splitn.next()?.to_owned())) + }) + .collect(); + + if !map.contains_key("installationName") + || !map.contains_key("installationPath") + || !map.contains_key("installationVersion") + { + return Err("required properties not found"); + } + + Ok(Self { map }) + } +} + +#[cfg(test)] +mod tests_ { + use std::borrow::Cow; + use std::convert::TryFrom; + use std::path::PathBuf; + + #[test] + fn it_parses_vswhere_output_correctly() { + let output = br"instanceId: 58104422 +installDate: 21/02/2021 21:50:33 +installationName: VisualStudio/16.9.2+31112.23 +installationPath: C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools +installationVersion: 16.9.31112.23 +productId: Microsoft.VisualStudio.Product.BuildTools +productPath: C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\Tools\LaunchDevCmd.bat +state: 4294967295 +isComplete: 1 +isLaunchable: 1 +isPrerelease: 0 +isRebootRequired: 0 +displayName: Visual Studio Build Tools 2019 +description: The Visual Studio Build Tools allows you to build native and managed MSBuild-based applications without requiring the Visual Studio IDE. There are options to install the Visual C++ compilers and libraries, MFC, ATL, and C++/CLI support. +channelId: VisualStudio.16.Release +channelUri: https://aka.ms/vs/16/release/channel +enginePath: C:\Program Files (x86)\Microsoft Visual Studio\Installer\resources\app\ServiceHub\Services\Microsoft.VisualStudio.Setup.Service +releaseNotes: https://docs.microsoft.com/en-us/visualstudio/releases/2019/release-notes-v16.9#16.9.2 +thirdPartyNotices: https://go.microsoft.com/fwlink/?LinkId=660909 +updateDate: 2021-03-17T21:16:46.5963702Z +catalog_buildBranch: d16.9 +catalog_buildVersion: 16.9.31112.23 +catalog_id: VisualStudio/16.9.2+31112.23 +catalog_localBuild: build-lab +catalog_manifestName: VisualStudio +catalog_manifestType: installer +catalog_productDisplayVersion: 16.9.2 +catalog_productLine: Dev16 +catalog_productLineVersion: 2019 +catalog_productMilestone: RTW +catalog_productMilestoneIsPreRelease: False +catalog_productName: Visual Studio +catalog_productPatchVersion: 2 +catalog_productPreReleaseMilestoneSuffix: 1.0 +catalog_productSemanticVersion: 16.9.2+31112.23 +catalog_requiredEngineVersion: 2.9.3365.38425 +properties_campaignId: 156063665.1613940062 +properties_channelManifestId: VisualStudio.16.Release/16.9.2+31112.23 +properties_nickname: +properties_setupEngineFilePath: C:\Program Files (x86)\Microsoft Visual Studio\Installer\vs_installershell.exe +" + .to_vec(); + + let vswhere_instance = super::VswhereInstance::try_from(&output); + assert!(vswhere_instance.is_ok()); + + let vs_instance = super::VsInstance::Vswhere(vswhere_instance.unwrap()); + assert_eq!( + vs_instance.installation_name(), + Some(Cow::from("VisualStudio/16.9.2+31112.23")) + ); + assert_eq!( + vs_instance.installation_path(), + Some(PathBuf::from( + r"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools" + )) + ); + assert_eq!( + vs_instance.installation_version(), + Some(Cow::from("16.9.31112.23")) + ); + } + + #[test] + fn it_returns_an_error_for_empty_output() { + let output = b"".to_vec(); + + let vswhere_instance = super::VswhereInstance::try_from(&output); + + assert!(vswhere_instance.is_err()); + } + + #[test] + fn it_returns_an_error_for_output_consisting_of_empty_lines() { + let output = br" + +" + .to_vec(); + + let vswhere_instance = super::VswhereInstance::try_from(&output); + + assert!(vswhere_instance.is_err()); + } + + #[test] + fn it_returns_an_error_for_output_without_required_properties() { + let output = br"instanceId: 58104422 +installDate: 21/02/2021 21:50:33 +productId: Microsoft.VisualStudio.Product.BuildTools +productPath: C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\Tools\LaunchDevCmd.bat +" + .to_vec(); + + let vswhere_instance = super::VswhereInstance::try_from(&output); + + assert!(vswhere_instance.is_err()); + } +} diff --git a/src/windows_registry.rs b/src/windows_registry.rs index 00b3a79d2..ceed7883b 100644 --- a/src/windows_registry.rs +++ b/src/windows_registry.rs @@ -170,7 +170,9 @@ pub fn find_vs_version() -> Result { mod impl_ { use crate::com; use crate::registry::{RegistryKey, LOCAL_MACHINE}; - use crate::setup_config::{EnumSetupInstances, SetupConfiguration, SetupInstance}; + use crate::setup_config::SetupConfiguration; + use crate::vs_instances::{VsInstances, VswhereInstance}; + use std::convert::TryFrom; use std::env; use std::ffi::OsString; use std::fs::File; @@ -178,6 +180,7 @@ mod impl_ { use std::iter; use std::mem; use std::path::{Path, PathBuf}; + use std::process::Command; use std::str::FromStr; use super::MSVC_FAMILY; @@ -216,22 +219,18 @@ mod impl_ { } #[allow(bare_trait_objects)] - fn vs16_instances() -> Box> { - let instances = if let Some(instances) = vs15plus_instances() { + fn vs16_instances(target: &str) -> Box> { + let instances = if let Some(instances) = vs15plus_instances(target) { instances } else { return Box::new(iter::empty()); }; - Box::new(instances.filter_map(|instance| { - let instance = instance.ok()?; - let installation_name = instance.installation_name().ok()?; - if installation_name.to_str()?.starts_with("VisualStudio/16.") { - Some(PathBuf::from(instance.installation_path().ok()?)) - } else if installation_name - .to_str()? - .starts_with("VisualStudioPreview/16.") - { - Some(PathBuf::from(instance.installation_path().ok()?)) + Box::new(instances.into_iter().filter_map(|instance| { + let installation_name = instance.installation_name()?; + if installation_name.starts_with("VisualStudio/16.") { + Some(instance.installation_path()?) + } else if installation_name.starts_with("VisualStudioPreview/16.") { + Some(instance.installation_path()?) } else { None } @@ -239,7 +238,7 @@ mod impl_ { } fn find_tool_in_vs16_path(tool: &str, target: &str) -> Option { - vs16_instances() + vs16_instances(target) .filter_map(|path| { let path = path.join(tool); if !path.is_file() { @@ -267,11 +266,62 @@ mod impl_ { // [online]: https://blogs.msdn.microsoft.com/vcblog/2017/03/06/finding-the-visual-c-compiler-tools-in-visual-studio-2017/ // // Returns MSVC 15+ instances (15, 16 right now), the order should be consider undefined. - fn vs15plus_instances() -> Option { + // + // However, on ARM64 this method doesn't work because VS Installer fails to register COM component on ARM64. + // Hence, as the last resort we try to use vswhere.exe to list available instances. + fn vs15plus_instances(target: &str) -> Option { + vs15plus_instances_using_com().or_else(|| vs15plus_instances_using_vswhere(target)) + } + + fn vs15plus_instances_using_com() -> Option { com::initialize().ok()?; let config = SetupConfiguration::new().ok()?; - config.enum_all_instances().ok() + let enum_setup_instances = config.enum_all_instances().ok()?; + + Some(VsInstances::ComBased(enum_setup_instances)) + } + + fn vs15plus_instances_using_vswhere(target: &str) -> Option { + let program_files_path: PathBuf = env::var("ProgramFiles(x86)") + .or_else(|_| env::var("ProgramFiles")) + .ok()? + .into(); + + let vswhere_path = + program_files_path.join(r"Microsoft Visual Studio\Installer\vswhere.exe"); + + if !vswhere_path.exists() { + return None; + } + + let arch = target.split('-').next().unwrap(); + let tools_arch = match arch { + "i586" | "i686" | "x86_64" => Some("x86.x64"), + "arm" | "thumbv7a" => Some("ARM"), + "aarch64" => Some("ARM64"), + _ => None, + }; + + let vswhere_output = Command::new(vswhere_path) + .args(&[ + "-latest", + "-products", + "*", + "-requires", + &format!("Microsoft.VisualStudio.Component.VC.Tools.{}", tools_arch?), + "-format", + "text", + "-nologo", + ]) + .stderr(std::process::Stdio::inherit()) + .output() + .ok()?; + + let vs_instances = + VsInstances::VswhereBased(VswhereInstance::try_from(&vswhere_output.stdout).ok()?); + + Some(vs_instances) } // Inspired from official microsoft/vswhere ParseVersionString @@ -284,15 +334,16 @@ mod impl_ { } pub fn find_msvc_15plus(tool: &str, target: &str) -> Option { - let iter = vs15plus_instances()?; - iter.filter_map(|instance| { - let instance = instance.ok()?; - let version = parse_version(instance.installation_version().ok()?.to_str()?)?; - let tool = tool_from_vs15plus_instance(tool, target, &instance)?; - Some((version, tool)) - }) - .max_by(|(a_version, _), (b_version, _)| a_version.cmp(b_version)) - .map(|(_version, tool)| tool) + let iter = vs15plus_instances(target)?; + iter.into_iter() + .filter_map(|instance| { + let version = parse_version(&instance.installation_version()?)?; + let instance_path = instance.installation_path()?; + let tool = tool_from_vs15plus_instance(tool, target, &instance_path)?; + Some((version, tool)) + }) + .max_by(|(a_version, _), (b_version, _)| a_version.cmp(b_version)) + .map(|(_version, tool)| tool) } // While the paths to Visual Studio 2017's devenv and MSBuild could @@ -303,14 +354,11 @@ mod impl_ { // // [more reliable]: https://github.com/alexcrichton/cc-rs/pull/331 fn find_tool_in_vs15_path(tool: &str, target: &str) -> Option { - let mut path = match vs15plus_instances() { + let mut path = match vs15plus_instances(target) { Some(instances) => instances - .filter_map(|instance| { - instance - .ok() - .and_then(|instance| instance.installation_path().ok()) - }) - .map(|path| PathBuf::from(path).join(tool)) + .into_iter() + .filter_map(|instance| instance.installation_path()) + .map(|path| path.join(tool)) .find(|ref path| path.is_file()), None => None, }; @@ -337,10 +385,10 @@ mod impl_ { fn tool_from_vs15plus_instance( tool: &str, target: &str, - instance: &SetupInstance, + instance_path: &PathBuf, ) -> Option { let (bin_path, host_dylib_path, lib_path, include_path) = - vs15plus_vc_paths(target, instance)?; + vs15plus_vc_paths(target, instance_path)?; let tool_path = bin_path.join(tool); if !tool_path.exists() { return None; @@ -364,9 +412,8 @@ mod impl_ { fn vs15plus_vc_paths( target: &str, - instance: &SetupInstance, + instance_path: &PathBuf, ) -> Option<(PathBuf, PathBuf, PathBuf, PathBuf)> { - let instance_path: PathBuf = instance.installation_path().ok()?.into(); let version_path = instance_path.join(r"VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt"); let mut version_file = File::open(version_path).ok()?;