about summary refs log tree commit diff
path: root/crates/project-model/src/build_scripts.rs
diff options
context:
space:
mode:
authorLukas Wirth <lukastw97@gmail.com>2022-08-27 18:28:09 +0200
committerLukas Wirth <lukastw97@gmail.com>2022-10-19 23:21:29 +0200
commit7e2c41dbd68014acbf91c2657a975c5a4b486da9 (patch)
tree62f814c69442d2f5a9fe47bf22e818f935958bf5 /crates/project-model/src/build_scripts.rs
parent40cbeb5b3d1ec37e8857844e43c75b6980f588e3 (diff)
Implement invocation strategy config for build scripts
Diffstat (limited to 'crates/project-model/src/build_scripts.rs')
-rw-r--r--crates/project-model/src/build_scripts.rs350
1 files changed, 224 insertions, 126 deletions
diff --git a/crates/project-model/src/build_scripts.rs b/crates/project-model/src/build_scripts.rs
index d9f09c03495..6e6654e74e1 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,13 @@ 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, 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 +45,69 @@ 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 +115,15 @@ 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) {
+        match Self::run_per_ws(Self::build_command(config)?, config, workspace, 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, config, workspace, progress)?;
                 res.error = Some(error);
                 Ok(res)
             }
@@ -102,22 +131,79 @@ impl WorkspaceBuildScripts {
         }
     }
 
-    fn run_(
+    /// 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,
+        workspaces: &[&CargoWorkspace],
+        progress: &dyn Fn(String),
+    ) -> io::Result<Vec<WorkspaceBuildScripts>> {
+        assert_eq!(config.invocation_strategy, InvocationStrategy::OnceInRoot);
+        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();
+        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());
+                    by_id.insert(workspace[package].id.clone(), (package, idx));
+                }
+                res
+            })
+            .collect();
+
+        let errors = Self::run_command(
+            cmd,
+            |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());
+
+        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,
+                        );
+                    }
+                }
+            }
+        }
+
+        Ok(res)
+    }
+
+    fn run_per_ws(
         mut cmd: Command,
         config: &CargoConfig,
         workspace: &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");
-        }
+        let workspace_root: &path::Path = &workspace.workspace_root().as_ref();
 
-        cmd.current_dir(workspace.workspace_root());
+        match config.invocation_strategy {
+            InvocationStrategy::OnceInRoot => (),
+            InvocationStrategy::PerWorkspaceWithManifestPath => {
+                cmd.arg("--manifest-path");
+                cmd.arg(workspace_root.join("Cargo.toml"));
+            }
+            InvocationStrategy::PerWorkspace => {
+                cmd.current_dir(workspace_root);
+            }
+        }
 
         let mut res = WorkspaceBuildScripts::default();
         let outputs = &mut res.outputs;
@@ -126,10 +212,44 @@ 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,
+            |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(
+        cmd: Command,
+        // 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();
@@ -149,61 +269,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 +339,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 +353,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,