diff --git a/Cargo.lock b/Cargo.lock index de9c6fae401..d5785fcd2fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1525,6 +1525,24 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +[[package]] +name = "dataview" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a802a2cad0ff4dfc4f3110da174b7a6928c315cae523e88638cfb72941b4d5" +dependencies = [ + "derive_pod", +] + +[[package]] +name = "dataview" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50eb3a329e19d78c3a3dfa4ec5a51ecb84fa3a20c06edad04be25356018218f9" +dependencies = [ + "derive_pod", +] + [[package]] name = "deadpool" version = "0.9.5" @@ -1656,6 +1674,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "dependency_runner" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37adbef02511efe61ff835c8e30d8d5c6a00e4688184512d831a371f2615466" +dependencies = [ + "anyhow", + "clap 3.2.23", + "dataview 1.0.1", + "fs-err", + "msvc-demangler", + "ntapi", + "pelite", + "regex", + "roxmltree", + "serde", + "serde_json", + "thiserror", + "winapi 0.3.9", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1680,6 +1719,12 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" + [[package]] name = "difference" version = "2.0.0" @@ -3340,6 +3385,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85dcb89d2b10c5f6133de2efd8c11959ce9dbb46a2f7a4cab208c4eeda6ce1ab" +[[package]] +name = "fs-err" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0845fa252299212f0389d64ba26f34fa32cfe41588355f21ed507c59a0f64541" + [[package]] name = "fs_extra" version = "1.2.0" @@ -3968,6 +4019,7 @@ dependencies = [ "convert_case 0.6.0", "cron", "data-encoding", + "dependency_runner", "derivative", "derive_more", "dirs", @@ -4749,6 +4801,15 @@ dependencies = [ "syn", ] +[[package]] +name = "msvc-demangler" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb67c6dd0fa9b00619c41c5700b6f92d5f418be49b45ddb9970fbd4569df3c8" +dependencies = [ + "bitflags", +] + [[package]] name = "multi-map" version = "1.3.0" @@ -4860,6 +4921,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.1" @@ -5323,6 +5390,25 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pelite" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a821dd5a5c4744099b50dc94a6a381c8b4b007f4d80da5334428e220945319b" +dependencies = [ + "dataview 0.1.2", + "libc", + "no-std-compat", + "pelite-macros", + "winapi 0.3.9", +] + +[[package]] +name = "pelite-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" + [[package]] name = "pem" version = "1.1.0" @@ -5998,6 +6084,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "roxmltree" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112908c3ac4711a1554b3948432ecaf5f061a951aa326977b63f7f72a86a4c0e" +dependencies = [ + "xmlparser", +] + [[package]] name = "rustc-demangle" version = "0.1.21" diff --git a/build/build/paths.yaml b/build/build/paths.yaml index f3bfb2002cd..3f43c847c58 100644 --- a/build/build/paths.yaml +++ b/build/build/paths.yaml @@ -1,4 +1,4 @@ -# This file is used to generate `target/debug/build/enso-build-/out/paths.rs`. +# This file is used to generate `target//build/enso-build-/out/paths.rs`. # Generation logic is in `ci_utils/src/paths.rs`. /: @@ -22,11 +22,26 @@ build/: prettier/: built-distribution/: - ? path: "enso-engine-" - type: engine_package - "enso-bundle-": + enso-engine-/: + ? path: enso-/ + type: engine_package + var: engine_package + enso-project-manager-/: + ? path: enso/ + type: project_manager_package + var: project_manager_package + enso-launcher-/: + ? path: enso/ + type: launcher_package + var: launcher_package + enso-bundle-/: + ? path: enso/ + type: launcher_bundle + var: launcher_bundle "project-manager-bundle-": - enso: + ? path: enso/ + type: project_manager_bundle + var: project_manager_bundle dist/: gui/: assets/: @@ -46,6 +61,8 @@ distribution/: editions/: .yaml: + enso.bundle.template: + launcher-manifest.yaml: engine/: runner-native/: src/: @@ -62,6 +79,9 @@ java/: target/: generated-java/: + parser-upload/: + test-results/: + scala-parser.js: test/: Benchmarks/: tools/: @@ -75,24 +95,39 @@ runner: # The runner native image (Linux only). CHANGELOG.md: -project-manager/: +# Launcher Package +{ path: enso/, var: launcher_package }: + bin/: + enso: + +# Project Manager Package +{ path: enso/, var: project_manager_package }: + bin/: + project-manager: + +# Project Manager Bundle +# https://enso.org/docs/developer/enso/distribution/bundles.html#project-manager-bundle +{ path: project-manager/, var: project_manager_bundle }: bin/: project-manager: dist/: + /: runtime/: + .enso.bundle: -# Engine Package +# Engine Package aka Enso Version Package # https://enso.org/docs/developer/enso/distribution/distribution.html#layout-of-an-enso-version-package { path: enso-/, var: engine_package }: bin/: components/: editions/: lib/: + Standard/: manifest.yaml: -# Engine Bundle +# Launcher Bundle aka Portable Enso Distribution # https://enso.org/docs/developer/enso/distribution/distribution.html#portable-enso-distribution-layout -{ path: enso/, var: engine_bundle }: +{ path: enso/, var: launcher_bundle }: dist/: /: edition/: diff --git a/build/build/src/engine.rs b/build/build/src/engine.rs index a0aebe4aa40..15c7d19d2e8 100644 --- a/build/build/src/engine.rs +++ b/build/build/src/engine.rs @@ -7,11 +7,12 @@ use crate::prelude::*; use crate::get_graal_version; use crate::get_java_major_version; use crate::paths::generated; -use crate::paths::ComponentPaths; -use crate::paths::Paths; +use artifact::IsArtifact; +use bundle::IsBundle; use ide_ci::future::AsyncPolicy; use ide_ci::github::Repo; +use package::IsPackage; use std::collections::BTreeSet; @@ -19,9 +20,11 @@ use std::collections::BTreeSet; // === Export === // ============== +pub mod artifact; pub mod bundle; pub mod context; pub mod env; +pub mod package; pub mod sbt; pub use context::RunContext; @@ -246,100 +249,51 @@ pub enum Operation { #[derive(Clone, PartialEq, Eq, Debug, Default)] pub struct BuiltArtifacts { - pub packages: BuiltPackageArtifacts, - pub bundles: BuiltBundleArtifacts, + pub engine_package: Option, + pub launcher_package: Option, + pub project_manager_package: Option, + pub launcher_bundle: Option, + pub project_manager_bundle: Option, } -#[derive(Clone, PartialEq, Eq, Debug, Default)] -pub struct BuiltPackageArtifacts { - pub engine: Option, - pub launcher: Option, - pub project_manager: Option, -} - -impl BuiltPackageArtifacts { - pub fn iter(&self) -> impl IntoIterator { - [&self.engine, &self.launcher, &self.project_manager].into_iter().flat_map(|b| b.iter()) - } -} - -impl IntoIterator for BuiltPackageArtifacts { - type Item = ComponentPaths; - type IntoIter = std::iter::Flatten, 3_usize>>; - - fn into_iter(self) -> Self::IntoIter { - [self.engine, self.launcher, self.project_manager].into_iter().flatten() - } -} - -#[derive(Clone, PartialEq, Eq, Debug, Default)] -pub struct BuiltBundleArtifacts { - pub launcher: Option, - pub project_manager: Option, -} - -impl BuiltBundleArtifacts { - pub fn iter(&self) -> impl IntoIterator { - [&self.project_manager, &self.launcher].into_iter().flat_map(|b| b.iter()) - } -} - -impl IntoIterator for BuiltBundleArtifacts { - type Item = ComponentPaths; - type IntoIter = std::iter::Flatten, 2_usize>>; - - fn into_iter(self) -> Self::IntoIter { - [self.launcher, self.project_manager].into_iter().flatten() - } -} - -pub async fn create_packages(paths: &Paths) -> Result> { - let mut ret = Vec::new(); - if paths.launcher.root.exists() { - debug!("Packaging launcher."); - ret.push(package_component(&paths.launcher).await?); - } - Ok(ret) -} - -#[async_trait] -trait ComponentPathExt { - async fn pack(&self) -> Result; - fn clear(&self) -> Result; -} - -#[async_trait] -impl ComponentPathExt for ComponentPaths { - async fn pack(&self) -> Result { - ide_ci::archive::create(&self.artifact_archive, [&self.dir]).await - } - fn clear(&self) -> Result { - ide_ci::fs::remove_dir_if_exists(&self.root)?; - ide_ci::fs::remove_file_if_exists(&self.artifact_archive) - } -} - -pub async fn package_component(paths: &ComponentPaths) -> Result { - #[cfg(not(target_os = "windows"))] - { - let pattern = paths - .dir - .join_iter(["bin", "*"]) - .with_extension(std::env::consts::EXE_EXTENSION) - .display() - .to_string(); - for binary in glob::glob(&pattern)? { - ide_ci::fs::allow_owner_execute(binary?)?; +impl BuiltArtifacts { + pub fn packages(&self) -> Vec<&dyn IsPackage> { + let mut packages = Vec::<&dyn IsPackage>::new(); + if let Some(engine) = &self.engine_package { + packages.push(engine); } + if let Some(launcher) = &self.launcher_package { + packages.push(launcher); + } + if let Some(project_manager) = &self.project_manager_package { + packages.push(project_manager); + } + packages } - ide_ci::archive::create(&paths.artifact_archive, [&paths.root]).await?; - Ok(paths.artifact_archive.clone()) + pub fn bundles(&self) -> Vec<&dyn IsBundle> { + let mut bundles = Vec::<&dyn IsBundle>::new(); + if let Some(launcher) = &self.launcher_bundle { + bundles.push(launcher); + } + if let Some(project_manager) = &self.project_manager_bundle { + bundles.push(project_manager); + } + bundles + } + + pub fn artifacts(&self) -> Vec<&dyn IsArtifact> { + let mut artifacts = Vec::<&dyn IsArtifact>::new(); + for package in self.packages() { + artifacts.push(package); + } + for bundle in self.bundles() { + artifacts.push(bundle); + } + artifacts + } } -////////////////////////////////// - - pub async fn deduce_graal( client: Octocrab, build_sbt: &generated::RepoRootBuildSbt, diff --git a/build/build/src/engine/artifact.rs b/build/build/src/engine/artifact.rs new file mode 100644 index 00000000000..5d1483ccf83 --- /dev/null +++ b/build/build/src/engine/artifact.rs @@ -0,0 +1,72 @@ +use crate::prelude::*; + +use ide_ci::github::release::ReleaseHandle; +use octocrab::models::repos::Asset; + + + +#[derive(Clone, Copy, Debug)] +pub enum ArtifactKind { + EnginePackage, + ProjectManagerPackage, + LauncherPackage, + ProjectManagerBundle, + LauncherBundle, +} + +/// A standalone SBT-generated artifact. +/// +/// Either a package or a bundle with one of our backend components. +pub trait IsArtifact: AsRef + Send + Sync { + /// Get the kind of this artifact. + fn kind(&self) -> ArtifactKind; + + /// Remove the artifact from the disk. + fn clear(&self) -> Result { + ide_ci::fs::remove_dir_if_exists(self) + } + + /// Get a filename stem for the compressed artifact. + /// + /// It will be used for naming release assets, so this should include the target triple. + fn asset_file_stem(&self) -> Result { + // By the convention, the parent directory to the artifact bears its asset name. + Ok(self.as_ref().try_parent()?.try_file_name()?.to_os_string()) + } + + fn upload_as_asset(&self, release: ReleaseHandle) -> BoxFuture<'static, Result> { + let path = self.as_ref().to_path_buf(); + let name = self.asset_file_stem(); + async move { release.upload_compressed_dir_as(path, name?).await }.boxed() + } +} + +impl IsArtifact for crate::paths::generated::EnginePackage { + fn kind(&self) -> ArtifactKind { + ArtifactKind::EnginePackage + } +} + +impl IsArtifact for crate::paths::generated::ProjectManagerPackage { + fn kind(&self) -> ArtifactKind { + ArtifactKind::ProjectManagerPackage + } +} + +impl IsArtifact for crate::paths::generated::ProjectManagerBundle { + fn kind(&self) -> ArtifactKind { + ArtifactKind::ProjectManagerBundle + } +} + +impl IsArtifact for crate::paths::generated::LauncherPackage { + fn kind(&self) -> ArtifactKind { + ArtifactKind::LauncherPackage + } +} + +impl IsArtifact for crate::paths::generated::LauncherBundle { + fn kind(&self) -> ArtifactKind { + ArtifactKind::LauncherBundle + } +} diff --git a/build/build/src/engine/bundle.rs b/build/build/src/engine/bundle.rs index 73be9392ddc..9ef50c881e7 100644 --- a/build/build/src/engine/bundle.rs +++ b/build/build/src/engine/bundle.rs @@ -1,94 +1,118 @@ use crate::prelude::*; -use crate::engine::ComponentPathExt; -use crate::paths::ComponentPaths; -use crate::paths::Paths; +use crate::engine::artifact::IsArtifact; +use crate::paths::generated::RepoRoot; -use anyhow::Context; -use ide_ci::programs::java::JAVA_HOME; +use ide_ci::cache::goodie::graalvm::locate_graal; +/// Bundle is like a [package][crate::paths::IsPackage] but with additional components bundled to +/// make it redistributable. +/// +/// See the [official docs](https://enso.org/docs/developer/enso/distribution/bundles.html). #[async_trait] -pub trait Bundle { - const PREFIX: &'static str; - const DIRNAME: &'static str; - - fn base_distribution(paths: &Paths) -> &ComponentPaths; - - fn suggest_paths(paths: &Paths) -> ComponentPaths { - ComponentPaths::new(&paths.build_dist_root, Self::PREFIX, Self::DIRNAME, &paths.triple) +pub trait IsBundle: AsRef + IsArtifact { + fn clear(&self) -> Result { + ide_ci::fs::remove_dir_if_exists(self.as_ref()) } - async fn create(paths: &Paths) -> Result { - let bundle = Self::suggest_paths(paths); + /// Path to the directory where GraalVM is placed. + fn graalvm_dir(&self) -> PathBuf; - bundle.clear()?; + /// Path to the directory where Engine package is placed. + fn engine_dir(&self) -> PathBuf; - let base_component = Self::base_distribution(paths); - ide_ci::fs::copy(&base_component.root, &bundle.root)?; + /// Path to the component that will be used as a bundle base. + fn base_component(&self, repo_root: &RepoRoot) -> PathBuf; - // Add engine. - let bundled_engine_dir = bundle.dir.join("dist").join(paths.version().to_string()); - place_component_at(&paths.engine, &bundled_engine_dir).await?; + /// Path to the bundle marker file. + /// + /// It is a file used by bundled executable to discern whether it is running from a bundle or + /// from a regular installation. + fn distribution_marker(&self) -> PathBuf; - // Add GraalVM runtime. - place_graal_under(bundle.dir.join("runtime")).await?; + /// Creates a bundle for a given component. This requires already built: + /// * the base component package (e.g. launcher package for launcher bundle); + /// * the engine package; + /// * the GraalVM package. + /// * + /// + /// `bundle_dir` is like: + /// ```text + /// H:\NBO\enso\built-distribution\enso-engine-0.0.0-SNAPSHOT.2022-01-19-windows-amd64\enso-0.0.0-SNAPSHOT.2022-01-19 + /// ``` + fn create(&self, repo_root: &RepoRoot) -> BoxFuture<'static, Result> { + let bundle_dir = self.as_ref().to_path_buf(); + let base_component = self.base_component(repo_root); + let engine_src_path = + repo_root.built_distribution.enso_engine_triple.engine_package.clone(); + let engine_target_dir = self.engine_dir(); + let graalvm_dir = self.graalvm_dir(); + let distribution_marker = self.distribution_marker(); - // Add portable distribution marker. - ide_ci::fs::copy( - paths.repo_root.join_iter(["distribution", "enso.bundle.template"]), - bundle.dir.join(".enso.bundle"), - )?; - Ok(bundle) - } -} - -#[derive(Clone, Copy, Debug)] -pub struct Launcher; -impl Bundle for Launcher { - const PREFIX: &'static str = "enso-bundle"; - const DIRNAME: &'static str = "enso"; - fn base_distribution(paths: &Paths) -> &ComponentPaths { - &paths.launcher - } -} - -#[derive(Clone, Copy, Debug)] -pub struct ProjectManager; -impl Bundle for ProjectManager { - const PREFIX: &'static str = "project-manager-bundle"; - const DIRNAME: &'static str = "enso"; - fn base_distribution(paths: &Paths) -> &ComponentPaths { - &paths.project_manager - } -} - -#[context("Placing a GraalVM package under {}", target_directory.as_ref().display())] -pub async fn place_graal_under(target_directory: impl AsRef) -> Result { - let graal_path = { - let java_home = JAVA_HOME.get()?; - if TARGET_OS == OS::MacOS { - // On macOS we need to drop trailing `/Contents/Home` from the path. - java_home - .parent() - .and_then(|p| p.parent()) - .context(format!("Invalid Java home for macOS: {}", java_home.display()))? - .to_path_buf() - } else { - java_home + async move { + ide_ci::fs::tokio::remove_dir_if_exists(&bundle_dir).await?; + // Start with bundled component. + ide_ci::fs::copy(&base_component, bundle_dir)?; + // Add engine. + ide_ci::fs::mirror_directory(&engine_src_path, &engine_target_dir).await?; + // Add GraalVM runtime. + place_graal_under(graalvm_dir).await?; + // Add portable distribution marker. + ide_ci::fs::create(distribution_marker)?; + Ok(()) } - }; - let graal_dirname = graal_path - .file_name() - .context(anyhow!("Invalid Graal Path deduced from JAVA_HOME: {}", graal_path.display()))?; + .boxed() + } +} + +impl IsBundle for crate::paths::generated::ProjectManagerBundle { + fn graalvm_dir(&self) -> PathBuf { + self.runtime.path.clone() + } + + fn engine_dir(&self) -> PathBuf { + self.dist.version.to_path_buf() + } + + fn base_component(&self, repo_root: &RepoRoot) -> PathBuf { + repo_root + .built_distribution + .enso_project_manager_triple + .project_manager_package + .to_path_buf() + } + + fn distribution_marker(&self) -> PathBuf { + self.enso_bundle.to_path_buf() + } +} + +impl IsBundle for crate::paths::generated::LauncherBundle { + fn graalvm_dir(&self) -> PathBuf { + self.runtime.path.clone() + } + + fn engine_dir(&self) -> PathBuf { + self.dist.version.to_path_buf() + } + + fn base_component(&self, repo_root: &RepoRoot) -> PathBuf { + repo_root.built_distribution.enso_launcher_triple.launcher_package.to_path_buf() + } + + fn distribution_marker(&self) -> PathBuf { + self.enso_portable.to_path_buf() + } +} + +/// Places a copy of the GraalVM's installation directory in the target directory. +/// +/// The GraalVM installation will be located using [`locate_graal`] function. +#[context("Failed to place a GraalVM package under {}.", target_directory.as_ref().display())] +pub async fn place_graal_under(target_directory: impl AsRef) -> Result { + let graal_path = locate_graal()?; + let graal_dirname = graal_path.try_file_name()?; ide_ci::fs::mirror_directory(&graal_path, target_directory.as_ref().join(graal_dirname)).await } - -#[context("Placing a Enso Engine package in {}", target_engine_dir.as_ref().display())] -pub async fn place_component_at( - engine_paths: &ComponentPaths, - target_engine_dir: impl AsRef, -) -> Result { - ide_ci::fs::mirror_directory(&engine_paths.dir, &target_engine_dir).await -} diff --git a/build/build/src/engine/context.rs b/build/build/src/engine/context.rs index 8ca20754d2f..3187ef8338b 100644 --- a/build/build/src/engine/context.rs +++ b/build/build/src/engine/context.rs @@ -1,14 +1,12 @@ use crate::prelude::*; use crate::engine; -use crate::engine::bundle::Bundle; use crate::engine::download_project_templates; use crate::engine::env; use crate::engine::sbt::SbtCommandProvider; use crate::engine::Benchmarks; use crate::engine::BuildConfigurationResolved; use crate::engine::BuiltArtifacts; -use crate::engine::ComponentPathExt; use crate::engine::Operation; use crate::engine::ReleaseCommand; use crate::engine::ReleaseOperation; @@ -20,6 +18,7 @@ use crate::enso::IrCaches; use crate::paths::cache_directory; use crate::paths::Paths; use crate::paths::TargetTriple; +use crate::paths::ENSO_TEST_JUNIT_DIR; use crate::project::ProcessWrapper; use ide_ci::actions::workflow::is_in_env; @@ -30,6 +29,8 @@ use ide_ci::programs::graal; use ide_ci::programs::sbt; use ide_ci::programs::Flatc; use ide_ci::programs::Sbt; +use std::env::consts::DLL_EXTENSION; +use std::env::consts::EXE_EXTENSION; use sysinfo::SystemExt; @@ -75,6 +76,34 @@ impl RunContext { Ok(context) } + pub fn expected_artifacts(&self) -> BuiltArtifacts { + BuiltArtifacts { + engine_package: self.config.build_engine_package.then(|| { + self.repo_root.built_distribution.enso_engine_triple.engine_package.clone() + }), + launcher_package: self.config.build_launcher_package.then(|| { + self.repo_root.built_distribution.enso_launcher_triple.launcher_package.clone() + }), + project_manager_package: self.config.build_project_manager_package.then(|| { + self.repo_root + .built_distribution + .enso_project_manager_triple + .project_manager_package + .clone() + }), + launcher_bundle: self.config.build_launcher_bundle.then(|| { + self.repo_root.built_distribution.enso_bundle_triple.launcher_bundle.clone() + }), + project_manager_bundle: self.config.build_project_manager_bundle.then(|| { + self.repo_root + .built_distribution + .project_manager_bundle_triple + .project_manager_bundle + .clone() + }), + } + } + /// Check that required programs are present (if not, installs them, if supported). Set /// environment variables for the build to follow. pub async fn prepare_build_env(&self) -> Result { @@ -179,8 +208,6 @@ impl RunContext { } pub async fn build(&self) -> Result { - let mut ret = BuiltArtifacts::default(); - self.prepare_build_env().await?; if ide_ci::ci::run_in_ci() { // On CI we remove IR caches. They might contain invalid or outdated data, as are using @@ -196,7 +223,10 @@ impl RunContext { if self.config.test_standard_library { // If we run tests, make sure that old and new results won't end up mixed together. - ide_ci::fs::reset_dir(&self.paths.test_results)?; + let test_results_dir = ENSO_TEST_JUNIT_DIR + .get() + .unwrap_or_else(|_| self.paths.repo_root.target.test_results.path.clone()); + ide_ci::fs::reset_dir(test_results_dir)?; } // Workaround for incremental compilation issue, as suggested by kustosz. @@ -255,13 +285,13 @@ impl RunContext { let build_native_runner = self.config.build_engine_package() && big_memory_machine && TARGET_OS != OS::Windows; + if big_memory_machine { let mut tasks = vec![]; if self.config.build_engine_package() { tasks.push("buildEngineDistribution"); tasks.push("engine-runner/assembly"); - ret.packages.engine = Some(self.paths.engine.clone()); } if build_native_runner { tasks.push("engine-runner/buildNativeImage"); @@ -275,12 +305,10 @@ impl RunContext { if self.config.build_project_manager_package() { tasks.push("buildProjectManagerDistribution"); - ret.packages.project_manager = Some(self.paths.project_manager.clone()); } if self.config.build_launcher_package() { tasks.push("buildLauncherDistribution"); - ret.packages.launcher = Some(self.paths.launcher.clone()); } // This just compiles benchmarks, not run them. At least we'll know that they can be @@ -343,6 +371,34 @@ impl RunContext { sbt.call_arg(task).await?; } } + } // End of Sbt run. + + let ret = self.expected_artifacts(); + + // Native images built by GraalVM on Windows use MSVC build tools. Thus, the generated + // binaries have MSVC CRT (C++ standard library) linked. This means that we need to ship + // the MSVC CRT DLLs along with the binaries. + if TARGET_OS == OS::Windows { + debug!("Copying MSVC CRT DLLs to the distribution."); + for package in ret.packages() { + let package_dir = package.dir(); + let binary_extensions = [EXE_EXTENSION, DLL_EXTENSION]; + let binaries = binary_extensions + .into_iter() + .map(|extension| { + let pattern = package_dir.join_iter(["**", "*"]).with_extension(extension); + glob::glob(pattern.as_str())?.try_collect_vec() + }) + .try_collect_vec()? + .into_iter() + .flatten() + .collect_vec(); + + debug!(?binaries, "Found executables in the package."); + for binary in binaries { + ide_ci::packaging::add_msvc_redist_dependencies(&binary).await?; + } + } } let enso = BuiltEnso { paths: self.paths.clone() }; @@ -388,8 +444,8 @@ impl RunContext { // Build the Parser JS Bundle sbt.call_arg("syntaxJS/fullOptJS").await?; ide_ci::fs::copy_to( - self.paths.target.join("scala-parser.js"), - self.paths.target.join("parser-upload"), + &self.paths.repo_root.target.scala_parser_js, + &self.paths.repo_root.target.parser_upload, )?; } @@ -399,10 +455,12 @@ impl RunContext { } if self.config.build_engine_package() { - let std_libs = self.paths.engine.dir.join("lib").join("Standard"); + let std_libs = + &self.repo_root.built_distribution.enso_engine_triple.engine_package.lib.standard; + // let std_libs = self.paths.engine.dir.join("lib").join("Standard"); // Compile the Standard Libraries (Unix) debug!("Compiling standard libraries under {}", std_libs.display()); - for entry in ide_ci::fs::read_dir(&std_libs)? { + for entry in ide_ci::fs::read_dir(std_libs)? { let entry = entry?; let target = entry.path().join(self.paths.version().to_string()); enso.compile_lib(target)?.run_ok().await?; @@ -432,6 +490,7 @@ impl RunContext { // ); // } + // Verify License Packages in Distributions // FIXME apparently this does not work on Windows due to some CRLF issues? if self.config.verify_packages && TARGET_OS != OS::Windows { @@ -442,23 +501,18 @@ impl RunContext { test $engineversion = $refversion || (echo "Tag version $refversion and the engine version $engineversion do not match" && false) */ - if self.config.build_engine_package() { - sbt.verify_generated_package("engine", &self.paths.engine.dir).await?; - } - if self.config.build_launcher_package() { - sbt.verify_generated_package("launcher", &self.paths.launcher.dir).await?; - } - if self.config.build_project_manager_package() { - sbt.verify_generated_package("project-manager", &self.paths.project_manager.dir) - .await?; + for package in ret.packages() { + package.verify_package_sbt(&sbt).await?; } if self.config.build_engine_package { for libname in ["Base", "Table", "Image", "Database"] { let lib_path = self - .paths - .engine - .dir - .join_iter(["lib", "Standard", libname]) + .repo_root + .built_distribution + .enso_engine_triple + .engine_package + .lib + .join_iter(["Standard", libname]) .join(self.paths.version().to_string()); sbt.verify_generated_package(libname, lib_path).await?; } @@ -483,14 +537,8 @@ impl RunContext { } } - if self.config.build_launcher_bundle { - ret.bundles.launcher = - Some(crate::engine::bundle::Launcher::create(&self.paths).await?); - } - - if self.config.build_project_manager_bundle { - ret.bundles.project_manager = - Some(crate::engine::bundle::ProjectManager::create(&self.paths).await?); + for bundle in ret.bundles() { + bundle.create(&self.repo_root).await?; } Ok(ret) @@ -507,13 +555,11 @@ impl RunContext { repo, release_id, ); - for package in artifacts.packages.into_iter() { - package.pack().await?; - release.upload_asset_file(package.artifact_archive).await?; + for package in artifacts.packages() { + package.upload_as_asset(release.clone()).await?; } - for bundle in artifacts.bundles.into_iter() { - bundle.pack().await?; - release.upload_asset_file(bundle.artifact_archive).await?; + for bundle in artifacts.bundles() { + bundle.upload_as_asset(release.clone()).await?; } if TARGET_OS == OS::Linux { release.upload_asset_file(self.paths.manifest_file()).await?; diff --git a/build/build/src/engine/package.rs b/build/build/src/engine/package.rs new file mode 100644 index 00000000000..0a1a8149909 --- /dev/null +++ b/build/build/src/engine/package.rs @@ -0,0 +1,48 @@ +use crate::prelude::*; + +use crate::engine::artifact::IsArtifact; +use crate::engine::sbt::SbtCommandProvider; + + + +/// Package is a minimal artifact with some backend component. +pub trait IsPackage: IsArtifact { + /// Get the package name that is recognized by the SBT build scripts. + /// + /// It can be used e.g. to verify the package by invoking `enso/verifyGeneratedPackage` task. + fn sbt_package_name(&self) -> &str; + + /// Primary directory of the package. + /// + /// E.g. for the Engine package it is like + /// `H:\NBO\enso\built-distribution\enso-engine-0.0.0-SNAPSHOT.2022-01-19-windows-amd64\enso-0. + /// 0.0-SNAPSHOT.2022-01-19`. + fn dir(&self) -> &Path { + self.as_ref() + } + + /// Invokes `enso/verifyGeneratedPackage` task on this package. + fn verify_package_sbt(&self, sbt: &crate::engine::sbt::Context) -> BoxFuture<'static, Result> { + let package_name = self.sbt_package_name(); + let dir = self.dir(); + sbt.verify_generated_package(package_name, dir) + } +} + +impl IsPackage for crate::paths::generated::EnginePackage { + fn sbt_package_name(&self) -> &str { + "engine" + } +} + +impl IsPackage for crate::paths::generated::ProjectManagerPackage { + fn sbt_package_name(&self) -> &str { + "project-manager" + } +} + +impl IsPackage for crate::paths::generated::LauncherPackage { + fn sbt_package_name(&self) -> &str { + "launcher" + } +} diff --git a/build/build/src/enso.rs b/build/build/src/enso.rs index 801b90e5cbc..b1258c19c1d 100644 --- a/build/build/src/enso.rs +++ b/build/build/src/enso.rs @@ -67,7 +67,7 @@ pub struct BuiltEnso { impl BuiltEnso { pub fn wrapper_script_path(&self) -> PathBuf { let filename = format!("enso{}", if TARGET_OS == OS::Windows { ".bat" } else { "" }); - self.paths.engine.dir.join_iter(["bin", &filename]) + self.paths.repo_root.built_distribution.enso_engine_triple.engine_package.bin.join(filename) } pub async fn run_benchmarks(&self, opt: BenchmarkOptions) -> Result { diff --git a/build/build/src/lib.rs b/build/build/src/lib.rs index 085ffdfb006..dda8d5f0828 100644 --- a/build/build/src/lib.rs +++ b/build/build/src/lib.rs @@ -1,4 +1,6 @@ // === Features === +#![feature(trait_upcasting)] +#![feature(try_blocks)] #![feature(hash_set_entry)] #![feature(type_alias_impl_trait)] #![feature(trait_alias)] diff --git a/build/build/src/paths.rs b/build/build/src/paths.rs index 1a8c315efd0..42d4fedb058 100644 --- a/build/build/src/paths.rs +++ b/build/build/src/paths.rs @@ -3,6 +3,7 @@ use crate::prelude::*; use crate::version::Versions; use std::env::consts::EXE_EXTENSION; +use std::env::consts::EXE_SUFFIX; use std::fmt::Formatter; @@ -43,58 +44,14 @@ pub const LIBRARIES_TO_TEST: [&str; 6] = [ "Visualization_Tests", ]; -pub const ARCHIVE_EXTENSION: &str = match TARGET_OS { - OS::Windows => "zip", - _ => "tar.gz", -}; - pub fn new_repo_root(repo_root: impl Into, triple: &TargetTriple) -> generated::RepoRoot { - generated::RepoRoot::new_root(repo_root, triple.to_string(), triple.versions.edition_name()) -} - -#[derive(Clone, PartialEq, Eq, Debug, Default)] -pub struct ComponentPaths { - // e.g. `enso-engine-0.0.0-SNAPSHOT.2022-01-19-windows-amd64` - pub name: PathBuf, - // e.g. H:\NBO\enso\built-distribution\enso-engine-0.0.0-SNAPSHOT.2022-01-19-windows-amd64 - pub root: PathBuf, - // e.g. H:\NBO\enso\built-distribution\enso-engine-0.0.0-SNAPSHOT.2022-01-19-windows-amd64\ - // enso-0.0.0-SNAPSHOT.2022-01-19 - pub dir: PathBuf, - // e.g. H:\NBO\enso\built-distribution\enso-engine-0.0.0-SNAPSHOT.2022-01-19-windows-amd64.zip - pub artifact_archive: PathBuf, -} - -impl ComponentPaths { - pub fn new( - build_root: &Path, // e.g. H:\NBO\enso\built-distribution - name_prefix: &str, - dirname: &str, - triple: &TargetTriple, - ) -> Self { - let name = PathBuf::from(iformat!("{name_prefix}-{triple.engine()}")); - let root = build_root.join(&name); - let dir = root.join(dirname); - let artifact_archive = root.with_appended_extension(ARCHIVE_EXTENSION); - Self { name, root, dir, artifact_archive } - } - - pub async fn emit_to_actions(&self, prefix: &str) -> Result { - let paths = [ - ("NAME", &self.name), - ("ROOT", &self.root), - ("DIR", &self.dir), - ("ARCHIVE", &self.artifact_archive), - ]; - for (what, path) in paths { - ide_ci::actions::workflow::set_env( - &iformat!("{prefix}_DIST_{what}"), - &path.to_string_lossy(), - ) - .await?; - } - Ok(()) - } + generated::RepoRoot::new_root( + repo_root, + triple.versions.edition_name(), + EXE_SUFFIX, + triple.to_string(), + triple.versions.version.to_string(), + ) } pub fn pretty_print_arch(arch: Arch) -> &'static str { @@ -146,14 +103,11 @@ impl Display for TargetTriple { #[derive(Clone, Debug)] pub struct Paths { - pub repo_root: generated::RepoRoot, - pub build_dist_root: PathBuf, - pub target: PathBuf, - pub launcher: ComponentPaths, - pub engine: ComponentPaths, - pub project_manager: ComponentPaths, - pub triple: TargetTriple, - pub test_results: PathBuf, + pub repo_root: generated::RepoRoot, + // pub launcher: ComponentPaths, + // pub engine: ComponentPaths, + // pub project_manager: ComponentPaths, + pub triple: TargetTriple, } impl Paths { @@ -165,28 +119,7 @@ impl Paths { pub fn new_triple(repo_root: impl Into, triple: TargetTriple) -> Result { let repo_root: PathBuf = repo_root.into().absolutize()?.into(); let repo_root = new_repo_root(repo_root, &triple); - let build_dist_root = repo_root.join("built-distribution"); - let target = repo_root.join("target"); - let launcher = ComponentPaths::new(&build_dist_root, "enso-launcher", "enso", &triple); - let engine = ComponentPaths::new( - &build_dist_root, - "enso-engine", - &format!("enso-{}", &triple.versions.version), - &triple, - ); - let project_manager = - ComponentPaths::new(&build_dist_root, "enso-project-manager", "enso", &triple); - let test_results = target.join("test-results"); - Ok(Paths { - repo_root, - build_dist_root, - target, - launcher, - engine, - project_manager, - triple, - test_results, - }) + Ok(Paths { repo_root, triple }) } /// Create a new set of paths for building the Enso with a given version number. @@ -204,18 +137,9 @@ impl Paths { /// Sets the environment variables in the current process and in GitHub Actions Runner (if being /// run in its environment), so future steps of the job also have access to them. pub async fn emit_env_to_actions(&self) -> Result { - let components = [ - ("ENGINE", &self.engine), - ("LAUNCHER", &self.launcher), - ("PROJECTMANAGER", &self.project_manager), - ]; - - for (prefix, paths) in components { - paths.emit_to_actions(prefix).await?; - } - - ide_ci::actions::workflow::set_env("TARGET_DIR", &self.target.to_string_lossy()).await?; - ENSO_TEST_JUNIT_DIR.set_workflow_env(self.test_results.as_path()).await?; + // TODO [mwu]: Check if TARGET_DIR is needed. If so, create a strongly typed wrapper. + // ide_ci::actions::workflow::set_env("TARGET_DIR", &self.repo_root.target).await?; + ENSO_TEST_JUNIT_DIR.set_workflow_env(self.repo_root.target.test_results.as_ref()).await?; Ok(()) } @@ -236,18 +160,21 @@ impl Paths { } pub fn manifest_file(&self) -> PathBuf { - self.engine.dir.join("manifest.yaml") + self.repo_root + .built_distribution + .enso_engine_triple + .engine_package + .manifest_yaml + .to_path_buf() } pub fn launcher_manifest_file(&self) -> PathBuf { - self.distribution().join("launcher-manifest.yaml") + self.repo_root.distribution.launcher_manifest_yaml.to_path_buf() } // e.g. enso2\distribution\editions\2021.20-SNAPSHOT.yaml pub fn edition_file(&self) -> PathBuf { - self.distribution() - .join_iter(["editions", &self.edition_name()]) - .with_appended_extension("yaml") + self.repo_root.distribution.editions.edition_yaml.to_path_buf() } pub async fn upload_edition_file_artifact(&self) -> Result { diff --git a/build/build/src/programs.rs b/build/build/src/programs.rs index 3d9ea72c87b..756c727eafa 100644 --- a/build/build/src/programs.rs +++ b/build/build/src/programs.rs @@ -23,7 +23,7 @@ pub mod project_manager { } } - pub fn spawn_from(bundle: &crate::paths::generated::ProjectManager) -> Command { + pub fn spawn_from(bundle: &crate::paths::generated::ProjectManagerBundle) -> Command { let binary_path = bundle.bin.project_managerexe.as_path(); let mut command = ::Command::new(binary_path); // We do this, because Project Manager runs until newline is input. We need to create a pipe diff --git a/build/build/src/project/backend.rs b/build/build/src/project/backend.rs index 8da5b538a0d..e7b730b0128 100644 --- a/build/build/src/project/backend.rs +++ b/build/build/src/project/backend.rs @@ -46,7 +46,7 @@ impl BuildInput { pub struct Artifact { /// Location of the Project Manager distribution. #[derivative(Debug(format_with = "std::fmt::Display::fmt"))] - pub path: crate::paths::generated::ProjectManager, + pub path: crate::paths::generated::ProjectManagerBundle, /// Versions of Engine that are bundled in this Project Manager distribution. /// /// Technically a Project Manager bundle can be shipped with arbitrary number of Enso Engine @@ -71,7 +71,7 @@ impl IsArtifact for Artifact {} /// distribution. #[context("Failed to list bundled engine versions: {}", project_manager_bundle)] pub async fn bundled_engine_versions( - project_manager_bundle: &crate::paths::generated::ProjectManager, + project_manager_bundle: &crate::paths::generated::ProjectManagerBundle, ) -> Result> { let mut ret = vec![]; @@ -111,12 +111,22 @@ impl IsTarget for Backend { } fn adapt_artifact(self, path: impl AsRef) -> BoxFuture<'static, Result> { - let path = crate::paths::generated::ProjectManager::new_root( - path.as_ref(), - self.target_os.exe_suffix(), + let exe_suffix = self.target_os.exe_suffix().to_owned(); + let path = path.as_ref().to_owned(); + let provisional_path = crate::paths::generated::ProjectManagerBundle::new_root( + &path, + &exe_suffix, + "", ); async move { - let engine_versions = bundled_engine_versions(&path).await?; + let engine_versions = bundled_engine_versions(&provisional_path).await?; + let path = crate::paths::generated::ProjectManagerBundle::new_root( + &path, + &exe_suffix, + engine_versions + .last() + .map_or_else(|| "".to_string(), |v| v.to_string()), + ); Ok(Artifact { path, engine_versions }) } .boxed() @@ -143,8 +153,8 @@ impl IsTarget for Backend { let context = inner.prepare_context(context, config)?; let artifacts = context.build().await?; let project_manager = - artifacts.bundles.project_manager.context("Missing project manager bundle!")?; - ide_ci::fs::mirror_directory(&project_manager.dir, &destination).await?; + artifacts.project_manager_bundle.context("Missing project manager bundle!")?; + ide_ci::fs::mirror_directory(&project_manager, &destination).await?; this.adapt_artifact(destination).await } .boxed() diff --git a/build/build/src/project/runtime.rs b/build/build/src/project/runtime.rs index 57738f94951..1148b936159 100644 --- a/build/build/src/project/runtime.rs +++ b/build/build/src/project/runtime.rs @@ -2,6 +2,7 @@ use crate::prelude::*; +use crate::engine::package::IsPackage; use crate::engine::BuildConfigurationFlags; use crate::paths::generated::EnginePackage; use crate::paths::TargetTriple; @@ -55,12 +56,10 @@ impl IsTarget for Runtime { context .and_then_async(|context| async move { let artifacts = context.build().await?; - let engine_package = artifacts - .packages - .engine - .context("Failed to find engine package artifacts.")?; - ide_ci::fs::mirror_directory(&engine_package.dir, &destination).await?; - this.adapt_artifact(engine_package.dir).await + let engine_package = + artifacts.engine_package.context("Failed to find engine package artifacts.")?; + ide_ci::fs::mirror_directory(engine_package.dir(), &destination).await?; + this.adapt_artifact(engine_package.dir()).await }) .boxed() } diff --git a/build/build/src/project_manager.rs b/build/build/src/project_manager.rs index 9ed0d244a83..998b1013a06 100644 --- a/build/build/src/project_manager.rs +++ b/build/build/src/project_manager.rs @@ -1,7 +1,8 @@ use crate::prelude::*; use crate::paths::TargetTriple; -use crate::paths::ARCHIVE_EXTENSION; + +use ide_ci::github::release::ARCHIVE_EXTENSION; diff --git a/build/build/src/release.rs b/build/build/src/release.rs index 9fcf0008c71..844a01f5210 100644 --- a/build/build/src/release.rs +++ b/build/build/src/release.rs @@ -25,6 +25,7 @@ use octocrab::models::repos::Release; use octocrab::params::repos::Reference; use reqwest::Response; use serde_json::json; +use std::env::consts::EXE_SUFFIX; use tempfile::tempdir; @@ -187,12 +188,18 @@ pub async fn get_engine_package( triple: &TargetTriple, ) -> Result { let release_id = crate::env::ENSO_RELEASE_ID.get()?; - let package_name = generated::RepoRootBuiltDistribution::new_root(".", triple.to_string()) - .enso_engine_triple - .file_name() - .context("Failed to get Engine Package name.")? - .as_str() - .to_string(); + let package_name = generated::RepoRootBuiltDistribution::new_root( + ".", + triple.versions.edition_name(), + EXE_SUFFIX, + triple.to_string(), + triple.versions.version.to_string(), + ) + .enso_engine_triple + .file_name() + .context("Failed to get Engine Package name.")? + .as_str() + .to_string(); let release = repo.find_release_by_id(release_id).await?; let asset = github::find_asset_by_text(&release, &package_name)?; diff --git a/build/ci_utils/Cargo.toml b/build/ci_utils/Cargo.toml index d71d670bda5..8f48544e0bf 100644 --- a/build/ci_utils/Cargo.toml +++ b/build/ci_utils/Cargo.toml @@ -18,6 +18,7 @@ chrono = { version = "0.4.19", features = ["serde"] } clap = { version = "3.1.5", features = ["derive", "env"] } cron = "0.12.0" data-encoding = "2.3.2" +dependency_runner = "1.0.0" derivative = "2.2.0" derive_more = "0.99" dirs = "4.0.0" diff --git a/build/ci_utils/src/actions/artifacts.rs b/build/ci_utils/src/actions/artifacts.rs index aa49e8aff05..b0de8b2d464 100644 --- a/build/ci_utils/src/actions/artifacts.rs +++ b/build/ci_utils/src/actions/artifacts.rs @@ -151,7 +151,7 @@ pub async fn upload_compressed_directory( let archive_path = tempdir.path().join(format!("{artifact_name}.tar.gz")); info!("Packing {} to {}", path_to_upload.as_ref().display(), archive_path.display()); - crate::archive::compress_directory(&archive_path, path_to_upload).await?; + crate::archive::compress_directory_contents(&archive_path, path_to_upload).await?; info!("Starting upload of {artifact_name}."); upload_single_file(&archive_path, artifact_name).await?; diff --git a/build/ci_utils/src/actions/env.rs b/build/ci_utils/src/actions/env.rs index 8a120cbed71..7127865a106 100644 --- a/build/ci_utils/src/actions/env.rs +++ b/build/ci_utils/src/actions/env.rs @@ -34,7 +34,7 @@ define_env_var! { /// The name of the person or app that initiated the workflow. For example, `octocat`. GITHUB_ACTOR, String; - /// Returns the API URL. For example: https://api.github.com. + /// Returns the API URL. For example: . GITHUB_API_URL, Url; /// The name of the base ref or target branch of the pull request in a workflow run. This is @@ -58,7 +58,7 @@ define_env_var! { /// For example, `/github/workflow/event.json`. GITHUB_EVENT_PATH, PathBuf; - /// Returns the GraphQL API URL. For example: https://api.github.com/graphql. + /// Returns the GraphQL API URL. For example: . GITHUB_GRAPHQL_URL, Url; /// The head ref or source branch of the pull request in a workflow run. This property is only @@ -117,7 +117,7 @@ define_env_var! { /// change if you re-run the workflow run. For example, 3. GITHUB_RUN_NUMBER, usize; - /// The URL of the GitHub server. For example: https://github.com. + /// The URL of the GitHub server. For example: . GITHUB_SERVER_URL, Url; /// The commit SHA that triggered the workflow. The value of this commit SHA depends on the diff --git a/build/ci_utils/src/actions/workflow/definition.rs b/build/ci_utils/src/actions/workflow/definition.rs index 505537a8890..19d91e8393d 100644 --- a/build/ci_utils/src/actions/workflow/definition.rs +++ b/build/ci_utils/src/actions/workflow/definition.rs @@ -230,7 +230,7 @@ pub struct Push { /// Common branch-related fields between some event triggers. /// -/// See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpull_requestpull_request_targetbranchesbranches-ignore +/// See: #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Branches { @@ -246,7 +246,7 @@ impl Branches { } } -/// See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request +/// See: #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PullRequestActivityType { diff --git a/build/ci_utils/src/archive.rs b/build/ci_utils/src/archive.rs index f50567a7dbf..adc1cfb9739 100644 --- a/build/ci_utils/src/archive.rs +++ b/build/ci_utils/src/archive.rs @@ -110,12 +110,15 @@ pub fn is_archive_name(path: impl AsRef) -> bool { Format::from_filename(path).is_ok() } +/// Create an archive with directory contents. +/// +/// Note that the archive will contain directory's children, not the directory itself. #[tracing::instrument( name="Packing directory.", skip_all, fields(src=%root_directory.as_ref().display(), dest=%output_archive.as_ref().display()), err)] -pub async fn compress_directory( +pub async fn compress_directory_contents( output_archive: impl AsRef, root_directory: impl AsRef, ) -> Result { diff --git a/build/ci_utils/src/cache/goodie/graalvm.rs b/build/ci_utils/src/cache/goodie/graalvm.rs index 5193a4b15d5..3658d783d99 100644 --- a/build/ci_utils/src/cache/goodie/graalvm.rs +++ b/build/ci_utils/src/cache/goodie/graalvm.rs @@ -114,6 +114,20 @@ impl GraalVM { } } +/// Locate the directory with GraalVM installation. +/// +/// It is deduced based on [`JAVA_HOME`] environment variable. Exact logic is os-specific. +#[context("Failed to locate GraalVM installation.")] +pub fn locate_graal() -> Result { + let java_home = JAVA_HOME.get()?; + Ok(if TARGET_OS == OS::MacOS { + // On macOS we need to drop trailing `/Contents/Home` from the path. + java_home.try_parent()?.try_parent()?.to_path_buf() + } else { + java_home + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/build/ci_utils/src/github/release.rs b/build/ci_utils/src/github/release.rs index bf77b0ba43b..75d74de6929 100644 --- a/build/ci_utils/src/github/release.rs +++ b/build/ci_utils/src/github/release.rs @@ -1,3 +1,5 @@ +//! Code supporting dealing with GitHub releases and their assets. + use crate::prelude::*; use crate::github::Repo; @@ -11,6 +13,15 @@ use tracing::instrument; +/// The extensions that will be used for the archives in the GitHub release assets. +/// +/// On Windows we use `.zip`, because it has out-of-the-box support in the Explorer. +/// On other platforms we use `.tar.gz`, because it is a good default. +pub const ARCHIVE_EXTENSION: &str = match TARGET_OS { + OS::Windows => "zip", + _ => "tar.gz", +}; + /// Types that uniquely identify a release and can be used to fetch it from GitHub. pub trait IsRelease: Debug { /// The release ID. @@ -23,6 +34,7 @@ pub trait IsRelease: Debug { fn octocrab(&self) -> &Octocrab; } +/// Information about release that we can provide from the [`IsRelease`] trait. #[async_trait] pub trait IsReleaseExt: IsRelease + Sync { /// Upload a new asset to the release. @@ -61,12 +73,10 @@ pub trait IsReleaseExt: IsRelease + Sync { #[instrument(skip_all, fields(source = %path.as_ref().display()))] async fn upload_asset_file(&self, path: impl AsRef + Send) -> Result { let error_msg = - format!("Failed to upload an asset from the file under {}", path.as_ref().display()); + format!("Failed to upload an asset from the file under {}.", path.as_ref().display()); async move { let path = path.as_ref().to_path_buf(); - let asset_name = path.file_name().with_context(|| { - format!("The given path {} does not contain a filename.", path.display()) - })?; + let asset_name = path.try_file_name()?; let content_type = new_mime_guess::from_path(&path).first_or_octet_stream(); let file_size = crate::fs::tokio::metadata(&path).await?.len(); let file = crate::fs::tokio::open_stream(&path).await?; @@ -77,15 +87,32 @@ pub trait IsReleaseExt: IsRelease + Sync { .context(error_msg) } - async fn upload_compressed_dir(&self, path: impl AsRef + Send) -> Result { - let dir_to_upload = path.as_ref(); + /// Upload given directory as a release asset. + /// + /// The given filename will be used, with appended [platform-specific + /// extension](ARCHIVE_EXTENSION). + async fn upload_compressed_dir_as( + &self, + dir_to_upload: impl AsRef + Send, + custom_name: impl AsRef + Send, + ) -> Result { + let dir_to_upload = dir_to_upload.as_ref(); let temp_dir = tempfile::tempdir()?; let archive_path = - dir_to_upload.with_parent(temp_dir.path()).with_appended_extension("tar.gz"); - crate::archive::compress_directory(&archive_path, &dir_to_upload).await?; + custom_name.with_parent(temp_dir.path()).with_appended_extension(ARCHIVE_EXTENSION); + crate::archive::create(&archive_path, [&dir_to_upload]).await?; self.upload_asset_file(archive_path).await } + /// Upload given directory as a release asset. + /// + /// The archive name will be deduced from the directory name. + async fn upload_compressed_dir(&self, path: impl AsRef + Send) -> Result { + let output_filename_stem = path.try_file_name()?.to_owned(); + self.upload_compressed_dir_as(path, output_filename_stem).await + } + + /// Get the information about the release. async fn get(&self) -> Result { self.octocrab() .repos(self.repo().owner(), self.repo().name()) diff --git a/build/ci_utils/src/lib.rs b/build/ci_utils/src/lib.rs index 20792045856..a10eac30ae2 100644 --- a/build/ci_utils/src/lib.rs +++ b/build/ci_utils/src/lib.rs @@ -59,6 +59,7 @@ pub mod io; pub mod log; pub mod models; pub mod os; +pub mod packaging; pub mod path; pub mod paths; pub mod platform; diff --git a/build/ci_utils/src/packaging.rs b/build/ci_utils/src/packaging.rs new file mode 100644 index 00000000000..c4dabf6d743 --- /dev/null +++ b/build/ci_utils/src/packaging.rs @@ -0,0 +1,72 @@ +use crate::prelude::*; + +use crate::fs::copy_to; +use crate::programs::vs; + +use dependency_runner::executable::Executables; +use unicase::UniCase; + + + +/// List of dynamically loaded dependencies. +/// +/// While the code in itself is portable, it supports only Windows binaries (PE files). +pub async fn pe_dependencies(binary: impl AsRef) -> Result { + let binary = binary.as_ref().to_path_buf(); + tokio::task::spawn_blocking(move || { + let mut query = + dependency_runner::query::LookupQuery::deduce_from_executable_location(&binary) + .with_context(|| { + format!("Failed to prepare dependency query for binary {}.", binary.display()) + })?; + query.parameters.extract_symbols = true; + let lookup_path = dependency_runner::path::LookupPath::deduce(&query); + dependency_runner::runner::run(&query, &lookup_path).with_context(|| { + format!("Failed to run dependency query for binary {}.", binary.display()) + }) + }) + .await? +} + +/// Place all the required MSVC CRT dependencies next to the binary. +#[context("Failed to copy MSVC CRT dependencies for binary `{}`.", binary.display())] +pub async fn add_msvc_redist_dependencies(binary: &Path) -> Result { + let binary = binary.absolutize()?; + let binary_dir = binary.try_parent()?; + let dependencies = pe_dependencies(&binary).await?; + let msvc_redist_dlls = vs::redist::list_crt_dlls(vs::Platforms::local()?).await?; + // map filename -> full path of redist dll + // Be careful about casing! Windows is case-insensitive. + let msvc_redist_dlls: HashMap<_, _> = msvc_redist_dlls + .iter() + .flat_map(|path| path.file_name().map(|filename| (UniCase::new(filename.as_str()), path))) + .collect(); + for dependency in dependencies.sorted_by_first_appearance() { + let filename = dependency.dllname.as_str(); + if let Some(redist_path) = msvc_redist_dlls.get(&UniCase::new(filename)) { + trace!("{} is a redist dll: {}.", filename, redist_path.display()); + copy_to(redist_path, binary_dir)?; + } else { + trace!("{} is not a redist dll.", filename); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::programs::vs; + + #[tokio::test] + #[ignore] + async fn lookup_dependencies() -> Result { + setup_logging()?; + vs::apply_dev_environment().await?; + let binary = Path::new( + r"H:\NBO\enso5\built-distribution\enso-project-manager-2022.1.1-dev-windows-amd64\enso\bin\project-manager.exe", + ); + add_msvc_redist_dependencies(binary).await?; + Ok(()) + } +} diff --git a/build/ci_utils/src/paths.rs b/build/ci_utils/src/paths.rs index 341540818a8..88afeb24f52 100644 --- a/build/ci_utils/src/paths.rs +++ b/build/ci_utils/src/paths.rs @@ -8,6 +8,7 @@ use proc_macro2::Span; use proc_macro2::TokenStream; use quote::quote; use regex::Regex; +use std::cell::OnceCell; use std::collections::BTreeSet; use std::iter::zip; @@ -110,11 +111,12 @@ pub fn get_string( pub struct Generator<'a> { all_nodes: &'a [&'a Node], stack: Vec<&'a Node>, + empty_set: BTreeSet, } impl<'a> Generator<'a> { pub fn new(all_nodes: &'a [&'a Node]) -> Self { - Self { all_nodes, stack: default() } + Self { all_nodes, stack: default(), empty_set: default() } } pub fn resolve(&self, r#type: &str) -> Result<&Node> { @@ -164,7 +166,7 @@ impl<'a> Generator<'a> { let children_struct = children.iter().map(|child| child.field_type(full_path)).collect_vec(); - let parameter_vars = last_node.all_parameters_vars(); + let parameter_vars = last_node.all_parameters_vars(self)?; let own_parameter_vars = last_node.own_parameter_vars(); let parent_parameter_vars: BTreeSet<_> = full_path.iter().flat_map(|n| n.own_parameter_vars()).collect(); @@ -173,8 +175,11 @@ impl<'a> Generator<'a> { let child_parameter_vars: BTreeSet<_> = last_node .children() .iter() - .flat_map(|node| node.parameters.iter()) - .map(to_ident) + .map(|node| node.all_parameters_vars(self)) + .try_collect_vec()? + .into_iter() + .flatten() + .cloned() .collect(); let all_parameters = { let mut v = parent_parameter_vars; @@ -195,21 +200,23 @@ impl<'a> Generator<'a> { let children_init = zip(last_node.children(), &children_struct) .map(|(child, children_struct)| { - if let Some(r#_type) = child.r#type.as_ref() { - // FIXME this should resolve target type and use its parameters + Result::Ok(if let Some(r#_type) = child.r#type.as_ref() { + let resolved_type = self.resolve(r#_type)?; let child_formatter = child.path_formatter(); - let child_child_parameters = child.children_parameters(); + println!("Resolved type: {}", resolved_type.path_formatter()); + info!("Resolved type: {}", resolved_type.path_formatter()); + let child_child_parameters = resolved_type.children_parameters(self)?; quote! { #children_struct::new_root(path.join(#child_formatter), #(&#child_child_parameters),*) } } else { - let child_parameters = child.all_parameters_vars(); + let child_parameters = child.all_parameters_vars(self)?; quote! { #children_struct::new_under(&path, #(&#child_parameters),*) } - } + }) }) - .collect_vec(); + .try_collect_vec()?; let opt_conversions = if parameter_vars.is_empty() { quote! { @@ -306,7 +313,7 @@ pub struct Node { #[shrinkwrap(main_field)] value: String, /// All parameters needed for this node (directly and for the children). - parameters: BTreeSet, // Wasteful but paths won't be that huge. + parameters: OnceCell>, // Wasteful but paths won't be that huge. /// The name that replaces value in variable-like contexts. /// Basically, we might not want use filepath name as name in the code. var_name: Option, @@ -351,8 +358,32 @@ impl Node { } } - pub fn all_parameters_vars(&self) -> BTreeSet { - self.parameters.iter().sorted().map(to_ident).collect() + pub fn type_dependent_parameters_vars<'a>( + &'a self, + g: &'a Generator, + ) -> Result<&'a BTreeSet> { + if let Some(r#type) = &self.r#type { + let resolved_type = g.resolve(r#type)?; + resolved_type.all_parameters_vars(g) + } else { + Ok(&g.empty_set) + } + } + + pub fn all_parameters_vars(&self, g: &Generator) -> Result<&BTreeSet> { + self.parameters.get_or_try_init(|| { + let mut ret = BTreeSet::new(); + for child in self.children() { + ret.extend(child.all_parameters_vars(g)?.clone()); + } + ret.extend(self.own_parameter_vars()); + ret.extend(self.type_dependent_parameters_vars(g)?.clone()); + Ok(ret) + }) + // let mut ret = BTreeSet::new(); + // ret.extend(self.parameters.iter().sorted().map(to_ident)); + // ret.extend(self.type_dependent_parameters_vars(g)?); + // Ok(ret) } pub fn own_parameters(&self) -> impl IntoIterator { @@ -363,8 +394,25 @@ impl Node { self.own_parameters().into_iter().map(to_ident).collect() } - pub fn children_parameters(&self) -> BTreeSet { - self.children().iter().flat_map(|child| child.parameters.iter()).map(to_ident).collect() + pub fn children_parameters(&self, g: &Generator) -> Result> { + let mut ret = BTreeSet::new(); + for child in self.children() { + ret.extend(child.all_parameters_vars(g)?.clone()); + } + Ok(ret) + // let resolved_type_params = if let Some(r#type) = &self.r#type { + // if let Ok(r#type) = g.resolve(r#type) { + // // TODO: This might not work for parameters that are type-introduced in the + // subtree // of the resolved type. + // r#type.all_parameters_vars(g).iter().map(to_ident).collect_vec() + // } else { + // warn!(%r#type, "Failed to resolve type."); + // default() + // } + // } else { + // default() + // }; + // direct_child_params.chain(resolved_type_params).collect() } pub fn children(&self) -> &[Node] { @@ -440,18 +488,18 @@ pub fn generate(forest: Vec) -> Result { generator.generate() } -pub fn collect_parameters(value: &mut Node) { - let mut child_parameters = BTreeSet::new(); - for child in value.children_mut() { - collect_parameters(child); - child_parameters.extend(child.parameters.clone()); - } - - let own_parameters = PARAMETER.find_all(&value.value).into_iter().map(ToString::to_string); - value.parameters.extend(own_parameters); - value.parameters.extend(child_parameters); - debug!("{} has {} parameters", value.value, value.parameters.len()); -} +// pub fn collect_parameters(value: &mut Node) { +// let mut child_parameters = BTreeSet::new(); +// for child in value.children_mut() { +// collect_parameters(child); +// child_parameters.extend(child.parameters.clone()); +// } +// +// let own_parameters = PARAMETER.find_all(&value.value).into_iter().map(ToString::to_string); +// value.parameters.extend(own_parameters); +// value.parameters.extend(child_parameters); +// debug!("{} has {} parameters", value.value, value.parameters.len()); +// } pub fn convert(value: &serde_yaml::Value) -> Result> { match value { @@ -464,7 +512,7 @@ pub fn convert(value: &serde_yaml::Value) -> Result> { node.add_child(child); } } - collect_parameters(&mut node); + // collect_parameters(&mut node); ret.push(node) } Ok(ret) diff --git a/build/ci_utils/src/programs/cargo.rs b/build/ci_utils/src/programs/cargo.rs index 7f761b5bc81..6c7f5e05936 100644 --- a/build/ci_utils/src/programs/cargo.rs +++ b/build/ci_utils/src/programs/cargo.rs @@ -16,7 +16,7 @@ pub mod fmt; /// Extra flags that Cargo invokes rustc with. /// -/// See: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-reads +/// See: pub const CARGO_ENCODED_RUSTFLAGS: Separated = Separated { separator: "\x1F", name: "CARGO_ENCODED_RUSTFLAGS" }; diff --git a/build/ci_utils/src/programs/npx.rs b/build/ci_utils/src/programs/npx.rs index c5e1c1c708b..1bc5c25ea35 100644 --- a/build/ci_utils/src/programs/npx.rs +++ b/build/ci_utils/src/programs/npx.rs @@ -10,3 +10,16 @@ impl Program for Npx { "npx" } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + #[ignore] + async fn call_npx() -> Result { + setup_logging()?; + Npx.cmd()?.run_ok().await?; + Ok(()) + } +} diff --git a/build/ci_utils/src/programs/tar.rs b/build/ci_utils/src/programs/tar.rs index f57d65c79f7..2e1be94ed48 100644 --- a/build/ci_utils/src/programs/tar.rs +++ b/build/ci_utils/src/programs/tar.rs @@ -266,7 +266,7 @@ impl Tar { #[cfg(test)] pub mod tests { use super::*; - use crate::archive::compress_directory; + use crate::archive::compress_directory_contents; use crate::archive::extract_to; use crate::log::setup_logging; @@ -296,7 +296,7 @@ pub mod tests { let linked_temp = archive_temp.path().join("linked"); symlink::symlink_dir(temp.path(), &linked_temp)?; - compress_directory(&archive_path, &linked_temp).await?; + compress_directory_contents(&archive_path, &linked_temp).await?; assert!(archive_path.exists()); assert!(archive_path.metadata()?.len() > 0); diff --git a/build/ci_utils/src/programs/vs.rs b/build/ci_utils/src/programs/vs.rs index f86189288cf..e689489453d 100644 --- a/build/ci_utils/src/programs/vs.rs +++ b/build/ci_utils/src/programs/vs.rs @@ -1,9 +1,66 @@ use crate::prelude::*; +use crate::define_env_var; +use crate::env; use crate::programs::cmd; use crate::programs::vswhere::VsWhere; +// ============== +// === Export === +// ============== + +pub mod redist; + + + +/// Path components from [VS installation +/// root](crate::programs::vswhere::InstanceInfo::installation_path) to the developer command file. +/// See: +pub const VC_VARS_ALL_PATH: [&str; 4] = ["VC", "Auxiliary", "Build", "vcvarsall.bat"]; + +/// Platforms, as being host/target of MSVC toolchain. +/// +/// Can be used as an argument to `vcvarsall.bat`. +#[derive(Clone, Copy, Debug, strum::Display, strum::EnumString)] +pub enum Platforms { + X64, + X86, + // In theory there's also ARM64 platform but we don't support it for Windows at the moment. +} + +impl Platforms { + /// Get the native platform for the local system (host). + pub fn local() -> Result { + Ok(match TARGET_ARCH { + Arch::X86_64 => Platforms::X64, + Arch::X86 => Platforms::X86, + _ => bail!("Unsupported target architecture: {}.", TARGET_ARCH), + }) + } +} + +impl From for Arch { + fn from(platform: Platforms) -> Self { + match platform { + Platforms::X64 => Arch::X86_64, + Platforms::X86 => Arch::X86, + } + } +} + +define_env_var! { + /// Target platform architecture. Expected to be the same as `VSCMD_ARG_TGT_ARCH`. + Platform, Platforms; + + /// Location with MSVC CRT redistributables. + /// + /// E.g. `%VCINSTALLDIR%Redist\MSVC\x.y.z`. + VCToolsRedistDir, PathBuf; + + /// The telemetry introduces undesired dependency on Power Shell. + VSCMD_SKIP_SENDTELEMETRY, bool; +} /// Microsoft C/C++ Optimizing compiler. /// @@ -18,21 +75,49 @@ impl Program for Cl { } } -pub async fn apply_dev_environment() -> Result { +/// Get the path to the `vcvarsall.bat` for the local VS installation. +/// +/// Relies on `vswhere` to find installed VS instances. +pub async fn vc_vars_all_path() -> Result { let msvc = VsWhere::msvc().await?; - let path = msvc.installation_path.join_iter(["VC", "Auxiliary", "Build", "vcvarsall.bat"]); - let changes = cmd::compare_env(|command| { - // The telemetry introduces undesired dependency on Power Shell. We should not need it to - // just set a few environment variables. - command.arg(path).arg("x64").env("VSCMD_SKIP_SENDTELEMETRY", "true") + // See the official documentation: + // https://learn.microsoft.com/en-us/cpp/build/building-on-the-command-line#developer_command_file_locations + Ok(msvc.installation_path.join_iter(VC_VARS_ALL_PATH)) +} + +/// Capture changes in environment variables that are introduced by `vcvarsall.bat`. +/// +/// The `vcvarsall.bat` script is part of Microsoft Visual C++ Build Tools. It is used to set up +/// environment variables for the MSVC compiler toolchain. +/// +/// This function requires that [`vswhere`](VsWhere) is available in the `PATH`. It will be used to +/// locate installations of MSVC Build Tools / compiler. +pub async fn retrieve_dev_environment() -> Result> { + let path = vc_vars_all_path().await?; + let platform = Platforms::local()?; + + cmd::compare_env(|command| { + command + .arg(path) + .arg(platform.to_string()) + .set_env(VSCMD_SKIP_SENDTELEMETRY, &true) + // Safety: this can only fail if `true` fails to pretty print, which is not possible. + .unwrap() }) - .await?; - for change in changes { - change.apply()?; + .await +} + +/// Modifies the environment of the current process, as if `vcvarsall.bat` was executed. +pub async fn apply_dev_environment() -> Result { + let modifications = retrieve_dev_environment().await?; + for modification in modifications { + modification.apply()?; } Ok(()) } +/// A major Visual Studio version. +/// /// Serialization follows the VS Where `productLineVersion` format. #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Serialize, Deserialize)] pub enum Version { @@ -43,12 +128,3 @@ pub enum Version { #[serde(rename = "2022")] VS2022, } - -#[tokio::test] -#[ignore] -async fn foo() -> Result { - // let old_vars = dbg!(std::env::vars_os().map(|(name, _)| name).collect_vec()); - apply_dev_environment().await?; - // let new_vars = dbg!(std::env::vars_os().collect_vec()); - Ok(()) -} diff --git a/build/ci_utils/src/programs/vs/redist.rs b/build/ci_utils/src/programs/vs/redist.rs new file mode 100644 index 00000000000..7dc1653f1cc --- /dev/null +++ b/build/ci_utils/src/programs/vs/redist.rs @@ -0,0 +1,56 @@ +//! Support for dealing with Microsoft Visual C++ Redistributables. +//! +//! See for the +//! official documentation. + +use crate::prelude::*; + +use crate::programs::vs; +use crate::programs::vs::VCToolsRedistDir; + + + +/// Get the directory with MSVC redistributable files for a given platform. +/// +/// This requires [`VCToolsRedistDir`] environment variable to be set, e.g. by having invoked +/// [`apply_dev_environment`]. +pub fn locate_dir(platform: vs::Platforms) -> Result { + let redist_path = VCToolsRedistDir.get()?; + let platform_segment = platform.to_string(); + Ok(redist_path.join(platform_segment)) +} + +/// Obtain a list of all Visual C++ runtime (CRT) redistributable DLLs for a given target. +/// +/// This requires [`VCToolsRedistDir`] environment variable to be set, e.g. by having invoked +/// [`apply_dev_environment`]. +/// +/// Note that this does not include the C runtime (UCRT) which is now a part of Windows. +#[context("Failed to locate MSVC CRT redistributables for {platform}.")] +pub async fn list_crt_dlls(platform: vs::Platforms) -> Result> { + let redist_dir = locate_dir(platform)?; + // The first component is "*.CRT" because we only want to redistribute CRT DLLs. If we matched + // all folders, we would also get other libraries, like MFC. + let redist_binaries_glob = redist_dir.join_iter(["*.crt", "**", "*.dll"]); + // Be careful about case! Windows is case-insensitive. + let glob_options = glob::MatchOptions { case_sensitive: false, ..default() }; + glob::glob_with(redist_binaries_glob.as_str(), glob_options)?.try_collect().with_context(|| { + format!("Failed to list CRT redistributables in {}.", redist_binaries_glob.display()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::programs::vs::apply_dev_environment; + + #[tokio::test] + #[ignore] + async fn test_listing_dlls() -> Result { + setup_logging()?; + apply_dev_environment().await?; + let dlls = list_crt_dlls(vs::Platforms::local()?).await?; + dbg!(&dlls); + Ok(()) + } +} diff --git a/build/ci_utils/src/programs/vswhere.rs b/build/ci_utils/src/programs/vswhere.rs index 26a41cbde5e..669a8442563 100644 --- a/build/ci_utils/src/programs/vswhere.rs +++ b/build/ci_utils/src/programs/vswhere.rs @@ -1,34 +1,33 @@ +//! Wrapper for the [`vswhere`](https://github.com/microsoft/vswhere) program. + use crate::prelude::*; +/// Wrapper for the [`vswhere`](https://github.com/microsoft/vswhere) program. +/// +/// It typically is installed as part of the Visual Studio installation. #[derive(Clone, Copy, Debug)] pub struct VsWhere; impl Program for VsWhere { - fn default_locations(&self) -> Vec { - let dir_opt = crate::platform::win::program_files_x86() - .map(|program_files| program_files.join("Microsoft Visual Studio").join("Installer")); - Vec::from_iter(dir_opt) - } - fn executable_name(&self) -> &'static str { "vswhere" } + + fn default_locations(&self) -> Vec { + if let Ok(program_files) = crate::platform::win::program_files_x86() { + let subdir = ["Microsoft Visual Studio", "Installer"]; + vec![program_files.join_iter(subdir)] + } else { + default() + } + } } impl VsWhere { - pub async fn find_all_with(component: Component) -> Result> { - let mut command = VsWhere.cmd()?; - command - .args(Option::Format(Format::Json).format_arguments()) - .args(Option::Required(vec![component]).format_arguments()) - .args(Option::ForceUTF8.format_arguments()); - - let stdout = command.run_stdout().await?; - serde_json::from_str(&stdout).anyhow_err() - } - + /// Finds installation of Visual Studio that have installed the given component. + #[context("Failed to find Visual Studio installation with component {component}.")] pub async fn find_with(component: Component) -> Result { let mut command = VsWhere.cmd()?; command @@ -60,7 +59,7 @@ impl VsWhere { #[serde(rename_all = "camelCase")] pub struct InstanceInfo { pub install_date: chrono::DateTime, - /// Example: C:\\Program Files\\Microsoft Visual Studio\\2022\\Community + /// Example: `C:\Program Files\Microsoft Visual Studio\2022\Community` pub installation_path: PathBuf, pub installation_version: String, pub is_prerelease: bool,