about summary refs log tree commit diff
path: root/crates/project-model/src
diff options
context:
space:
mode:
authorLaurențiu Nicola <lnicola@dend.ro>2022-10-26 17:40:41 +0300
committerLaurențiu Nicola <lnicola@dend.ro>2022-10-26 17:40:41 +0300
commit8807fc4cc358fa2152b303df0caa2f5fc9efaa9d (patch)
treeda701483c74d2d402617c2482626b70aa2fff536 /crates/project-model/src
parent26a413e015b7fef4c0edb51bda0d39ab499f4950 (diff)
:arrow_up: rust-analyzer
Diffstat (limited to 'crates/project-model/src')
-rw-r--r--crates/project-model/src/build_scripts.rs380
-rw-r--r--crates/project-model/src/cargo_workspace.rs52
-rw-r--r--crates/project-model/src/lib.rs18
-rw-r--r--crates/project-model/src/sysroot.rs18
-rw-r--r--crates/project-model/src/workspace.rs69
5 files changed, 368 insertions, 169 deletions
diff --git a/crates/project-model/src/build_scripts.rs b/crates/project-model/src/build_scripts.rs
index d9f09c03495..a26a7c57acf 100644
--- a/crates/project-model/src/build_scripts.rs
+++ b/crates/project-model/src/build_scripts.rs
@@ -6,7 +6,12 @@
 //! This module implements this second part. We use "build script" terminology
 //! here, but it covers procedural macros as well.
 
-use std::{cell::RefCell, io, path::PathBuf, process::Command};
+use std::{
+    cell::RefCell,
+    io, mem,
+    path::{self, PathBuf},
+    process::Command,
+};
 
 use cargo_metadata::{camino::Utf8Path, Message};
 use la_arena::ArenaMap;
@@ -15,11 +20,14 @@ use rustc_hash::FxHashMap;
 use semver::Version;
 use serde::Deserialize;
 
-use crate::{cfg_flag::CfgFlag, CargoConfig, CargoFeatures, CargoWorkspace, Package};
+use crate::{
+    cfg_flag::CfgFlag, CargoConfig, CargoFeatures, CargoWorkspace, InvocationLocation,
+    InvocationStrategy, Package,
+};
 
 #[derive(Debug, Default, Clone, PartialEq, Eq)]
 pub struct WorkspaceBuildScripts {
-    outputs: ArenaMap<Package, Option<BuildScriptOutput>>,
+    outputs: ArenaMap<Package, BuildScriptOutput>,
     error: Option<String>,
 }
 
@@ -38,47 +46,71 @@ pub(crate) struct BuildScriptOutput {
     pub(crate) proc_macro_dylib_path: Option<AbsPathBuf>,
 }
 
-impl WorkspaceBuildScripts {
-    fn build_command(config: &CargoConfig) -> Command {
-        if let Some([program, args @ ..]) = config.run_build_script_command.as_deref() {
-            let mut cmd = Command::new(program);
-            cmd.args(args);
-            cmd.envs(&config.extra_env);
-            return cmd;
-        }
+impl BuildScriptOutput {
+    fn is_unchanged(&self) -> bool {
+        self.cfgs.is_empty()
+            && self.envs.is_empty()
+            && self.out_dir.is_none()
+            && self.proc_macro_dylib_path.is_none()
+    }
+}
 
-        let mut cmd = Command::new(toolchain::cargo());
-        cmd.envs(&config.extra_env);
-        cmd.args(&["check", "--quiet", "--workspace", "--message-format=json"]);
+impl WorkspaceBuildScripts {
+    fn build_command(config: &CargoConfig) -> io::Result<Command> {
+        let mut cmd = match config.run_build_script_command.as_deref() {
+            Some([program, args @ ..]) => {
+                let mut cmd = Command::new(program);
+                cmd.args(args);
+                cmd
+            }
+            _ => {
+                let mut cmd = Command::new(toolchain::cargo());
 
-        // --all-targets includes tests, benches and examples in addition to the
-        // default lib and bins. This is an independent concept from the --targets
-        // flag below.
-        cmd.arg("--all-targets");
+                cmd.args(&["check", "--quiet", "--workspace", "--message-format=json"]);
 
-        if let Some(target) = &config.target {
-            cmd.args(&["--target", target]);
-        }
+                // --all-targets includes tests, benches and examples in addition to the
+                // default lib and bins. This is an independent concept from the --targets
+                // flag below.
+                cmd.arg("--all-targets");
 
-        match &config.features {
-            CargoFeatures::All => {
-                cmd.arg("--all-features");
-            }
-            CargoFeatures::Selected { features, no_default_features } => {
-                if *no_default_features {
-                    cmd.arg("--no-default-features");
+                if let Some(target) = &config.target {
+                    cmd.args(&["--target", target]);
                 }
-                if !features.is_empty() {
-                    cmd.arg("--features");
-                    cmd.arg(features.join(" "));
+
+                match &config.features {
+                    CargoFeatures::All => {
+                        cmd.arg("--all-features");
+                    }
+                    CargoFeatures::Selected { features, no_default_features } => {
+                        if *no_default_features {
+                            cmd.arg("--no-default-features");
+                        }
+                        if !features.is_empty() {
+                            cmd.arg("--features");
+                            cmd.arg(features.join(" "));
+                        }
+                    }
                 }
+
+                cmd
             }
+        };
+
+        cmd.envs(&config.extra_env);
+        if config.wrap_rustc_in_build_scripts {
+            // Setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself. We use
+            // that to compile only proc macros and build scripts during the initial
+            // `cargo check`.
+            let myself = std::env::current_exe()?;
+            cmd.env("RUSTC_WRAPPER", myself);
+            cmd.env("RA_RUSTC_WRAPPER", "1");
         }
 
-        cmd
+        Ok(cmd)
     }
 
-    pub(crate) fn run(
+    /// Runs the build scripts for the given workspace
+    pub(crate) fn run_for_workspace(
         config: &CargoConfig,
         workspace: &CargoWorkspace,
         progress: &dyn Fn(String),
@@ -86,15 +118,23 @@ impl WorkspaceBuildScripts {
     ) -> io::Result<WorkspaceBuildScripts> {
         const RUST_1_62: Version = Version::new(1, 62, 0);
 
-        match Self::run_(Self::build_command(config), config, workspace, progress) {
+        let current_dir = match &config.invocation_location {
+            InvocationLocation::Root(root) if config.run_build_script_command.is_some() => {
+                root.as_path()
+            }
+            _ => &workspace.workspace_root(),
+        }
+        .as_ref();
+
+        match Self::run_per_ws(Self::build_command(config)?, workspace, current_dir, progress) {
             Ok(WorkspaceBuildScripts { error: Some(error), .. })
                 if toolchain.as_ref().map_or(false, |it| *it >= RUST_1_62) =>
             {
                 // building build scripts failed, attempt to build with --keep-going so
                 // that we potentially get more build data
-                let mut cmd = Self::build_command(config);
+                let mut cmd = Self::build_command(config)?;
                 cmd.args(&["-Z", "unstable-options", "--keep-going"]).env("RUSTC_BOOTSTRAP", "1");
-                let mut res = Self::run_(cmd, config, workspace, progress)?;
+                let mut res = Self::run_per_ws(cmd, workspace, current_dir, progress)?;
                 res.error = Some(error);
                 Ok(res)
             }
@@ -102,23 +142,90 @@ impl WorkspaceBuildScripts {
         }
     }
 
-    fn run_(
-        mut cmd: Command,
+    /// Runs the build scripts by invoking the configured command *once*.
+    /// This populates the outputs for all passed in workspaces.
+    pub(crate) fn run_once(
         config: &CargoConfig,
-        workspace: &CargoWorkspace,
+        workspaces: &[&CargoWorkspace],
         progress: &dyn Fn(String),
-    ) -> io::Result<WorkspaceBuildScripts> {
-        if config.wrap_rustc_in_build_scripts {
-            // Setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself. We use
-            // that to compile only proc macros and build scripts during the initial
-            // `cargo check`.
-            let myself = std::env::current_exe()?;
-            cmd.env("RUSTC_WRAPPER", myself);
-            cmd.env("RA_RUSTC_WRAPPER", "1");
+    ) -> io::Result<Vec<WorkspaceBuildScripts>> {
+        assert_eq!(config.invocation_strategy, InvocationStrategy::Once);
+
+        let current_dir = match &config.invocation_location {
+            InvocationLocation::Root(root) => root,
+            InvocationLocation::Workspace => {
+                return Err(io::Error::new(
+                    io::ErrorKind::Other,
+                    "Cannot run build scripts from workspace with invocation strategy `once`",
+                ))
+            }
+        };
+        let cmd = Self::build_command(config)?;
+        // NB: Cargo.toml could have been modified between `cargo metadata` and
+        // `cargo check`. We shouldn't assume that package ids we see here are
+        // exactly those from `config`.
+        let mut by_id = FxHashMap::default();
+        // some workspaces might depend on the same crates, so we need to duplicate the outputs
+        // to those collisions
+        let mut collisions = Vec::new();
+        let mut res: Vec<_> = workspaces
+            .iter()
+            .enumerate()
+            .map(|(idx, workspace)| {
+                let mut res = WorkspaceBuildScripts::default();
+                for package in workspace.packages() {
+                    res.outputs.insert(package, BuildScriptOutput::default());
+                    if by_id.contains_key(&workspace[package].id) {
+                        collisions.push((&workspace[package].id, idx, package));
+                    } else {
+                        by_id.insert(workspace[package].id.clone(), (package, idx));
+                    }
+                }
+                res
+            })
+            .collect();
+
+        let errors = Self::run_command(
+            cmd,
+            current_dir.as_path().as_ref(),
+            |package, cb| {
+                if let Some(&(package, workspace)) = by_id.get(package) {
+                    cb(&workspaces[workspace][package].name, &mut res[workspace].outputs[package]);
+                }
+            },
+            progress,
+        )?;
+        res.iter_mut().for_each(|it| it.error = errors.clone());
+        collisions.into_iter().for_each(|(id, workspace, package)| {
+            if let Some(&(p, w)) = by_id.get(id) {
+                res[workspace].outputs[package] = res[w].outputs[p].clone();
+            }
+        });
+
+        if tracing::enabled!(tracing::Level::INFO) {
+            for (idx, workspace) in workspaces.iter().enumerate() {
+                for package in workspace.packages() {
+                    let package_build_data = &mut res[idx].outputs[package];
+                    if !package_build_data.is_unchanged() {
+                        tracing::info!(
+                            "{}: {:?}",
+                            workspace[package].manifest.parent().display(),
+                            package_build_data,
+                        );
+                    }
+                }
+            }
         }
 
-        cmd.current_dir(workspace.workspace_root());
+        Ok(res)
+    }
 
+    fn run_per_ws(
+        cmd: Command,
+        workspace: &CargoWorkspace,
+        current_dir: &path::Path,
+        progress: &dyn Fn(String),
+    ) -> io::Result<WorkspaceBuildScripts> {
         let mut res = WorkspaceBuildScripts::default();
         let outputs = &mut res.outputs;
         // NB: Cargo.toml could have been modified between `cargo metadata` and
@@ -126,10 +233,46 @@ impl WorkspaceBuildScripts {
         // exactly those from `config`.
         let mut by_id: FxHashMap<String, Package> = FxHashMap::default();
         for package in workspace.packages() {
-            outputs.insert(package, None);
+            outputs.insert(package, BuildScriptOutput::default());
             by_id.insert(workspace[package].id.clone(), package);
         }
 
+        res.error = Self::run_command(
+            cmd,
+            current_dir,
+            |package, cb| {
+                if let Some(&package) = by_id.get(package) {
+                    cb(&workspace[package].name, &mut outputs[package]);
+                }
+            },
+            progress,
+        )?;
+
+        if tracing::enabled!(tracing::Level::INFO) {
+            for package in workspace.packages() {
+                let package_build_data = &mut outputs[package];
+                if !package_build_data.is_unchanged() {
+                    tracing::info!(
+                        "{}: {:?}",
+                        workspace[package].manifest.parent().display(),
+                        package_build_data,
+                    );
+                }
+            }
+        }
+
+        Ok(res)
+    }
+
+    fn run_command(
+        mut cmd: Command,
+        current_dir: &path::Path,
+        // ideally this would be something like:
+        // with_output_for: impl FnMut(&str, dyn FnOnce(&mut BuildScriptOutput)),
+        // but owned trait objects aren't a thing
+        mut with_output_for: impl FnMut(&str, &mut dyn FnMut(&str, &mut BuildScriptOutput)),
+        progress: &dyn Fn(String),
+    ) -> io::Result<Option<String>> {
         let errors = RefCell::new(String::new());
         let push_err = |err: &str| {
             let mut e = errors.borrow_mut();
@@ -137,7 +280,8 @@ impl WorkspaceBuildScripts {
             e.push('\n');
         };
 
-        tracing::info!("Running build scripts: {:?}", cmd);
+        tracing::info!("Running build scripts in {}: {:?}", current_dir.display(), cmd);
+        cmd.current_dir(current_dir);
         let output = stdx::process::spawn_with_streaming_output(
             cmd,
             &mut |line| {
@@ -149,61 +293,58 @@ impl WorkspaceBuildScripts {
                     .unwrap_or_else(|_| Message::TextLine(line.to_string()));
 
                 match message {
-                    Message::BuildScriptExecuted(message) => {
-                        let package = match by_id.get(&message.package_id.repr) {
-                            Some(&it) => it,
-                            None => return,
-                        };
-                        progress(format!("running build-script: {}", workspace[package].name));
-
-                        let cfgs = {
-                            let mut acc = Vec::new();
-                            for cfg in message.cfgs {
-                                match cfg.parse::<CfgFlag>() {
-                                    Ok(it) => acc.push(it),
-                                    Err(err) => {
-                                        push_err(&format!(
-                                            "invalid cfg from cargo-metadata: {}",
-                                            err
-                                        ));
-                                        return;
-                                    }
-                                };
+                    Message::BuildScriptExecuted(mut message) => {
+                        with_output_for(&message.package_id.repr, &mut |name, data| {
+                            progress(format!("running build-script: {}", name));
+                            let cfgs = {
+                                let mut acc = Vec::new();
+                                for cfg in &message.cfgs {
+                                    match cfg.parse::<CfgFlag>() {
+                                        Ok(it) => acc.push(it),
+                                        Err(err) => {
+                                            push_err(&format!(
+                                                "invalid cfg from cargo-metadata: {}",
+                                                err
+                                            ));
+                                            return;
+                                        }
+                                    };
+                                }
+                                acc
+                            };
+                            if !message.env.is_empty() {
+                                data.envs = mem::take(&mut message.env);
                             }
-                            acc
-                        };
-                        // cargo_metadata crate returns default (empty) path for
-                        // older cargos, which is not absolute, so work around that.
-                        let out_dir = message.out_dir.into_os_string();
-                        if !out_dir.is_empty() {
-                            let data = outputs[package].get_or_insert_with(Default::default);
-                            data.out_dir = Some(AbsPathBuf::assert(PathBuf::from(out_dir)));
-                            data.cfgs = cfgs;
-                        }
-                        if !message.env.is_empty() {
-                            outputs[package].get_or_insert_with(Default::default).envs =
-                                message.env;
-                        }
+                            // cargo_metadata crate returns default (empty) path for
+                            // older cargos, which is not absolute, so work around that.
+                            let out_dir = mem::take(&mut message.out_dir).into_os_string();
+                            if !out_dir.is_empty() {
+                                let out_dir = AbsPathBuf::assert(PathBuf::from(out_dir));
+                                // inject_cargo_env(package, package_build_data);
+                                // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
+                                if let Some(out_dir) =
+                                    out_dir.as_os_str().to_str().map(|s| s.to_owned())
+                                {
+                                    data.envs.push(("OUT_DIR".to_string(), out_dir));
+                                }
+                                data.out_dir = Some(out_dir);
+                                data.cfgs = cfgs;
+                            }
+                        });
                     }
                     Message::CompilerArtifact(message) => {
-                        let package = match by_id.get(&message.package_id.repr) {
-                            Some(it) => *it,
-                            None => return,
-                        };
-
-                        progress(format!("building proc-macros: {}", message.target.name));
-
-                        if message.target.kind.iter().any(|k| k == "proc-macro") {
-                            // Skip rmeta file
-                            if let Some(filename) =
-                                message.filenames.iter().find(|name| is_dylib(name))
-                            {
-                                let filename = AbsPathBuf::assert(PathBuf::from(&filename));
-                                outputs[package]
-                                    .get_or_insert_with(Default::default)
-                                    .proc_macro_dylib_path = Some(filename);
+                        with_output_for(&message.package_id.repr, &mut |name, data| {
+                            progress(format!("building proc-macros: {}", name));
+                            if message.target.kind.iter().any(|k| k == "proc-macro") {
+                                // Skip rmeta file
+                                if let Some(filename) =
+                                    message.filenames.iter().find(|name| is_dylib(name))
+                                {
+                                    let filename = AbsPathBuf::assert(PathBuf::from(&filename));
+                                    data.proc_macro_dylib_path = Some(filename);
+                                }
                             }
-                        }
+                        });
                     }
                     Message::CompilerMessage(message) => {
                         progress(message.target.name);
@@ -222,32 +363,13 @@ impl WorkspaceBuildScripts {
             },
         )?;
 
-        for package in workspace.packages() {
-            if let Some(package_build_data) = &mut outputs[package] {
-                tracing::info!(
-                    "{}: {:?}",
-                    workspace[package].manifest.parent().display(),
-                    package_build_data,
-                );
-                // inject_cargo_env(package, package_build_data);
-                if let Some(out_dir) = &package_build_data.out_dir {
-                    // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
-                    if let Some(out_dir) = out_dir.as_os_str().to_str().map(|s| s.to_owned()) {
-                        package_build_data.envs.push(("OUT_DIR".to_string(), out_dir));
-                    }
-                }
-            }
-        }
-
-        let mut errors = errors.into_inner();
-        if !output.status.success() {
-            if errors.is_empty() {
-                errors = "cargo check failed".to_string();
-            }
-            res.error = Some(errors);
-        }
-
-        Ok(res)
+        let errors = if !output.status.success() {
+            let errors = errors.into_inner();
+            Some(if errors.is_empty() { "cargo check failed".to_string() } else { errors })
+        } else {
+            None
+        };
+        Ok(errors)
     }
 
     pub fn error(&self) -> Option<&str> {
@@ -255,11 +377,11 @@ impl WorkspaceBuildScripts {
     }
 
     pub(crate) fn get_output(&self, idx: Package) -> Option<&BuildScriptOutput> {
-        self.outputs.get(idx)?.as_ref()
+        self.outputs.get(idx)
     }
 }
 
-// FIXME: File a better way to know if it is a dylib.
+// FIXME: Find a better way to know if it is a dylib.
 fn is_dylib(path: &Utf8Path) -> bool {
     match path.extension().map(|e| e.to_string().to_lowercase()) {
         None => false,
diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs
index 8e690f1125a..b4c2ba43677 100644
--- a/crates/project-model/src/cargo_workspace.rs
+++ b/crates/project-model/src/cargo_workspace.rs
@@ -14,8 +14,8 @@ use rustc_hash::FxHashMap;
 use serde::Deserialize;
 use serde_json::from_value;
 
-use crate::CfgOverrides;
-use crate::{utf8_stdout, ManifestPath};
+use crate::{utf8_stdout, InvocationLocation, ManifestPath};
+use crate::{CfgOverrides, InvocationStrategy};
 
 /// [`CargoWorkspace`] represents the logical structure of, well, a Cargo
 /// workspace. It pretty closely mirrors `cargo metadata` output.
@@ -106,6 +106,8 @@ pub struct CargoConfig {
     pub run_build_script_command: Option<Vec<String>>,
     /// Extra env vars to set when invoking the cargo command
     pub extra_env: FxHashMap<String, String>,
+    pub invocation_strategy: InvocationStrategy,
+    pub invocation_location: InvocationLocation,
 }
 
 impl CargoConfig {
@@ -283,8 +285,6 @@ impl CargoWorkspace {
             }
             CargoFeatures::Selected { features, no_default_features } => {
                 if *no_default_features {
-                    // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures`
-                    // https://github.com/oli-obk/cargo_metadata/issues/79
                     meta.features(CargoOpt::NoDefaultFeatures);
                 }
                 if !features.is_empty() {
@@ -329,18 +329,21 @@ impl CargoWorkspace {
         let ws_members = &meta.workspace_members;
 
         meta.packages.sort_by(|a, b| a.id.cmp(&b.id));
-        for meta_pkg in &meta.packages {
+        for meta_pkg in meta.packages {
             let cargo_metadata::Package {
-                id,
-                edition,
                 name,
-                manifest_path,
                 version,
-                metadata,
+                id,
+                source,
+                targets: meta_targets,
+                features,
+                manifest_path,
                 repository,
+                edition,
+                metadata,
                 ..
             } = meta_pkg;
-            let meta = from_value::<PackageMetadata>(metadata.clone()).unwrap_or_default();
+            let meta = from_value::<PackageMetadata>(metadata).unwrap_or_default();
             let edition = match edition {
                 cargo_metadata::Edition::E2015 => Edition::Edition2015,
                 cargo_metadata::Edition::E2018 => Edition::Edition2018,
@@ -352,35 +355,36 @@ impl CargoWorkspace {
             };
             // We treat packages without source as "local" packages. That includes all members of
             // the current workspace, as well as any path dependency outside the workspace.
-            let is_local = meta_pkg.source.is_none();
-            let is_member = ws_members.contains(id);
+            let is_local = source.is_none();
+            let is_member = ws_members.contains(&id);
 
             let pkg = packages.alloc(PackageData {
                 id: id.repr.clone(),
-                name: name.clone(),
-                version: version.clone(),
-                manifest: AbsPathBuf::assert(PathBuf::from(&manifest_path)).try_into().unwrap(),
+                name,
+                version,
+                manifest: AbsPathBuf::assert(manifest_path.into()).try_into().unwrap(),
                 targets: Vec::new(),
                 is_local,
                 is_member,
                 edition,
-                repository: repository.clone(),
+                repository,
                 dependencies: Vec::new(),
-                features: meta_pkg.features.clone().into_iter().collect(),
+                features: features.into_iter().collect(),
                 active_features: Vec::new(),
                 metadata: meta.rust_analyzer.unwrap_or_default(),
             });
             let pkg_data = &mut packages[pkg];
             pkg_by_id.insert(id, pkg);
-            for meta_tgt in &meta_pkg.targets {
-                let is_proc_macro = meta_tgt.kind.as_slice() == ["proc-macro"];
+            for meta_tgt in meta_targets {
+                let cargo_metadata::Target { name, kind, required_features, src_path, .. } =
+                    meta_tgt;
                 let tgt = targets.alloc(TargetData {
                     package: pkg,
-                    name: meta_tgt.name.clone(),
-                    root: AbsPathBuf::assert(PathBuf::from(&meta_tgt.src_path)),
-                    kind: TargetKind::new(meta_tgt.kind.as_slice()),
-                    is_proc_macro,
-                    required_features: meta_tgt.required_features.clone(),
+                    name,
+                    root: AbsPathBuf::assert(src_path.into()),
+                    kind: TargetKind::new(&kind),
+                    is_proc_macro: &*kind == ["proc-macro"],
+                    required_features,
                 });
                 pkg_data.targets.push(tgt);
             }
diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs
index ce78ce85697..575581fa543 100644
--- a/crates/project-model/src/lib.rs
+++ b/crates/project-model/src/lib.rs
@@ -67,7 +67,7 @@ impl ProjectManifest {
         if path.file_name().unwrap_or_default() == "Cargo.toml" {
             return Ok(ProjectManifest::CargoToml(path));
         }
-        bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display())
+        bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display());
     }
 
     pub fn discover_single(path: &AbsPath) -> Result<ProjectManifest> {
@@ -78,7 +78,7 @@ impl ProjectManifest {
         };
 
         if !candidates.is_empty() {
-            bail!("more than one project")
+            bail!("more than one project");
         }
         Ok(res)
     }
@@ -157,3 +157,17 @@ fn utf8_stdout(mut cmd: Command) -> Result<String> {
     let stdout = String::from_utf8(output.stdout)?;
     Ok(stdout.trim().to_string())
 }
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub enum InvocationStrategy {
+    Once,
+    #[default]
+    PerWorkspace,
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub enum InvocationLocation {
+    Root(AbsPathBuf),
+    #[default]
+    Workspace,
+}
diff --git a/crates/project-model/src/sysroot.rs b/crates/project-model/src/sysroot.rs
index bc37e3d132a..fa8d76f3f45 100644
--- a/crates/project-model/src/sysroot.rs
+++ b/crates/project-model/src/sysroot.rs
@@ -64,14 +64,15 @@ impl Sysroot {
         self.by_name("proc_macro")
     }
 
-    pub fn crates<'a>(&'a self) -> impl Iterator<Item = SysrootCrate> + ExactSizeIterator + 'a {
+    pub fn crates(&self) -> impl Iterator<Item = SysrootCrate> + ExactSizeIterator + '_ {
         self.crates.iter().map(|(id, _data)| id)
     }
 }
 
 impl Sysroot {
+    /// Attempts to discover the toolchain's sysroot from the given `dir`.
     pub fn discover(dir: &AbsPath, extra_env: &FxHashMap<String, String>) -> Result<Sysroot> {
-        tracing::debug!("Discovering sysroot for {}", dir.display());
+        tracing::debug!("discovering sysroot for {}", dir.display());
         let sysroot_dir = discover_sysroot_dir(dir, extra_env)?;
         let sysroot_src_dir =
             discover_sysroot_src_dir_or_add_component(&sysroot_dir, dir, extra_env)?;
@@ -83,11 +84,10 @@ impl Sysroot {
         cargo_toml: &ManifestPath,
         extra_env: &FxHashMap<String, String>,
     ) -> Option<ManifestPath> {
-        tracing::debug!("Discovering rustc source for {}", cargo_toml.display());
+        tracing::debug!("discovering rustc source for {}", cargo_toml.display());
         let current_dir = cargo_toml.parent();
-        discover_sysroot_dir(current_dir, extra_env)
-            .ok()
-            .and_then(|sysroot_dir| get_rustc_src(&sysroot_dir))
+        let sysroot_dir = discover_sysroot_dir(current_dir, extra_env).ok()?;
+        get_rustc_src(&sysroot_dir)
     }
 
     pub fn with_sysroot_dir(sysroot_dir: AbsPathBuf) -> Result<Sysroot> {
@@ -189,6 +189,7 @@ fn discover_sysroot_src_dir(sysroot_path: &AbsPathBuf) -> Option<AbsPathBuf> {
 
     get_rust_src(sysroot_path)
 }
+
 fn discover_sysroot_src_dir_or_add_component(
     sysroot_path: &AbsPathBuf,
     current_dir: &AbsPath,
@@ -199,6 +200,7 @@ fn discover_sysroot_src_dir_or_add_component(
             let mut rustup = Command::new(toolchain::rustup());
             rustup.envs(extra_env);
             rustup.current_dir(current_dir).args(&["component", "add", "rust-src"]);
+            tracing::info!("adding rust-src component by {:?}", rustup);
             utf8_stdout(rustup).ok()?;
             get_rust_src(sysroot_path)
         })
@@ -217,7 +219,7 @@ try installing the Rust source the same way you installed rustc",
 fn get_rustc_src(sysroot_path: &AbsPath) -> Option<ManifestPath> {
     let rustc_src = sysroot_path.join("lib/rustlib/rustc-src/rust/compiler/rustc/Cargo.toml");
     let rustc_src = ManifestPath::try_from(rustc_src).ok()?;
-    tracing::debug!("Checking for rustc source code: {}", rustc_src.display());
+    tracing::debug!("checking for rustc source code: {}", rustc_src.display());
     if fs::metadata(&rustc_src).is_ok() {
         Some(rustc_src)
     } else {
@@ -227,7 +229,7 @@ fn get_rustc_src(sysroot_path: &AbsPath) -> Option<ManifestPath> {
 
 fn get_rust_src(sysroot_path: &AbsPath) -> Option<AbsPathBuf> {
     let rust_src = sysroot_path.join("lib/rustlib/src/rust/library");
-    tracing::debug!("Checking sysroot: {}", rust_src.display());
+    tracing::debug!("checking sysroot library: {}", rust_src.display());
     if fs::metadata(&rust_src).is_ok() {
         Some(rust_src)
     } else {
diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs
index 72ddf809288..2780c62ed11 100644
--- a/crates/project-model/src/workspace.rs
+++ b/crates/project-model/src/workspace.rs
@@ -2,7 +2,7 @@
 //! metadata` or `rust-project.json`) into representation stored in the salsa
 //! database -- `CrateGraph`.
 
-use std::{collections::VecDeque, fmt, fs, process::Command};
+use std::{collections::VecDeque, fmt, fs, process::Command, sync::Arc};
 
 use anyhow::{format_err, Context, Result};
 use base_db::{
@@ -21,8 +21,8 @@ use crate::{
     cfg_flag::CfgFlag,
     rustc_cfg,
     sysroot::SysrootCrate,
-    utf8_stdout, CargoConfig, CargoWorkspace, ManifestPath, Package, ProjectJson, ProjectManifest,
-    Sysroot, TargetKind, WorkspaceBuildScripts,
+    utf8_stdout, CargoConfig, CargoWorkspace, InvocationStrategy, ManifestPath, Package,
+    ProjectJson, ProjectManifest, Sysroot, TargetKind, WorkspaceBuildScripts,
 };
 
 /// A set of cfg-overrides per crate.
@@ -209,6 +209,9 @@ impl ProjectWorkspace {
                     ),
                     None => None,
                 };
+                if let Some(sysroot) = &sysroot {
+                    tracing::info!(src_root = %sysroot.src_root().display(), root = %sysroot.root().display(), "Using sysroot");
+                }
 
                 let rustc_dir = match &config.rustc_source {
                     Some(RustcSource::Path(path)) => ManifestPath::try_from(path.clone()).ok(),
@@ -217,6 +220,9 @@ impl ProjectWorkspace {
                     }
                     None => None,
                 };
+                if let Some(rustc_dir) = &rustc_dir {
+                    tracing::info!(rustc_dir = %rustc_dir.display(), "Using rustc source");
+                }
 
                 let rustc = match rustc_dir {
                     Some(rustc_dir) => Some({
@@ -277,6 +283,9 @@ impl ProjectWorkspace {
             }
             (None, None) => None,
         };
+        if let Some(sysroot) = &sysroot {
+            tracing::info!(src_root = %sysroot.src_root().display(), root = %sysroot.root().display(), "Using sysroot");
+        }
 
         let rustc_cfg = rustc_cfg::get(None, target, extra_env);
         Ok(ProjectWorkspace::Json { project: project_json, sysroot, rustc_cfg })
@@ -294,6 +303,7 @@ impl ProjectWorkspace {
         Ok(ProjectWorkspace::DetachedFiles { files: detached_files, sysroot, rustc_cfg })
     }
 
+    /// Runs the build scripts for this [`ProjectWorkspace`].
     pub fn run_build_scripts(
         &self,
         config: &CargoConfig,
@@ -301,9 +311,13 @@ impl ProjectWorkspace {
     ) -> Result<WorkspaceBuildScripts> {
         match self {
             ProjectWorkspace::Cargo { cargo, toolchain, .. } => {
-                WorkspaceBuildScripts::run(config, cargo, progress, toolchain).with_context(|| {
-                    format!("Failed to run build scripts for {}", &cargo.workspace_root().display())
-                })
+                WorkspaceBuildScripts::run_for_workspace(config, cargo, progress, toolchain)
+                    .with_context(|| {
+                        format!(
+                            "Failed to run build scripts for {}",
+                            &cargo.workspace_root().display()
+                        )
+                    })
             }
             ProjectWorkspace::Json { .. } | ProjectWorkspace::DetachedFiles { .. } => {
                 Ok(WorkspaceBuildScripts::default())
@@ -311,6 +325,49 @@ impl ProjectWorkspace {
         }
     }
 
+    /// Runs the build scripts for the given [`ProjectWorkspace`]s. Depending on the invocation
+    /// strategy this may run a single build process for all project workspaces.
+    pub fn run_all_build_scripts(
+        workspaces: &[ProjectWorkspace],
+        config: &CargoConfig,
+        progress: &dyn Fn(String),
+    ) -> Vec<Result<WorkspaceBuildScripts>> {
+        if matches!(config.invocation_strategy, InvocationStrategy::PerWorkspace)
+            || config.run_build_script_command.is_none()
+        {
+            return workspaces.iter().map(|it| it.run_build_scripts(config, progress)).collect();
+        }
+
+        let cargo_ws: Vec<_> = workspaces
+            .iter()
+            .filter_map(|it| match it {
+                ProjectWorkspace::Cargo { cargo, .. } => Some(cargo),
+                _ => None,
+            })
+            .collect();
+        let ref mut outputs = match WorkspaceBuildScripts::run_once(config, &cargo_ws, progress) {
+            Ok(it) => Ok(it.into_iter()),
+            // io::Error is not Clone?
+            Err(e) => Err(Arc::new(e)),
+        };
+
+        workspaces
+            .iter()
+            .map(|it| match it {
+                ProjectWorkspace::Cargo { cargo, .. } => match outputs {
+                    Ok(outputs) => Ok(outputs.next().unwrap()),
+                    Err(e) => Err(e.clone()).with_context(|| {
+                        format!(
+                            "Failed to run build scripts for {}",
+                            &cargo.workspace_root().display()
+                        )
+                    }),
+                },
+                _ => Ok(WorkspaceBuildScripts::default()),
+            })
+            .collect()
+    }
+
     pub fn set_build_scripts(&mut self, bs: WorkspaceBuildScripts) {
         match self {
             ProjectWorkspace::Cargo { build_scripts, .. } => *build_scripts = bs,