]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/project-model/src/build_scripts.rs
Rollup merge of #102073 - andrewpollack:add-execvp-call-ignore, r=tmandry
[rust.git] / src / tools / rust-analyzer / crates / project-model / src / build_scripts.rs
1 //! Workspace information we get from cargo consists of two pieces. The first is
2 //! the output of `cargo metadata`. The second is the output of running
3 //! `build.rs` files (`OUT_DIR` env var, extra cfg flags) and compiling proc
4 //! macro.
5 //!
6 //! This module implements this second part. We use "build script" terminology
7 //! here, but it covers procedural macros as well.
8
9 use std::{cell::RefCell, io, path::PathBuf, process::Command};
10
11 use cargo_metadata::{camino::Utf8Path, Message};
12 use la_arena::ArenaMap;
13 use paths::AbsPathBuf;
14 use rustc_hash::FxHashMap;
15 use semver::Version;
16 use serde::Deserialize;
17
18 use crate::{cfg_flag::CfgFlag, CargoConfig, CargoWorkspace, Package};
19
20 #[derive(Debug, Default, Clone, PartialEq, Eq)]
21 pub struct WorkspaceBuildScripts {
22     outputs: ArenaMap<Package, Option<BuildScriptOutput>>,
23     error: Option<String>,
24 }
25
26 #[derive(Debug, Clone, Default, PartialEq, Eq)]
27 pub(crate) struct BuildScriptOutput {
28     /// List of config flags defined by this package's build script.
29     pub(crate) cfgs: Vec<CfgFlag>,
30     /// List of cargo-related environment variables with their value.
31     ///
32     /// If the package has a build script which defines environment variables,
33     /// they can also be found here.
34     pub(crate) envs: Vec<(String, String)>,
35     /// Directory where a build script might place its output.
36     pub(crate) out_dir: Option<AbsPathBuf>,
37     /// Path to the proc-macro library file if this package exposes proc-macros.
38     pub(crate) proc_macro_dylib_path: Option<AbsPathBuf>,
39 }
40
41 impl WorkspaceBuildScripts {
42     fn build_command(config: &CargoConfig) -> Command {
43         if let Some([program, args @ ..]) = config.run_build_script_command.as_deref() {
44             let mut cmd = Command::new(program);
45             cmd.args(args);
46             cmd.envs(&config.extra_env);
47             return cmd;
48         }
49
50         let mut cmd = Command::new(toolchain::cargo());
51         cmd.envs(&config.extra_env);
52
53         cmd.args(&["check", "--quiet", "--workspace", "--message-format=json"]);
54
55         // --all-targets includes tests, benches and examples in addition to the
56         // default lib and bins. This is an independent concept from the --targets
57         // flag below.
58         cmd.arg("--all-targets");
59
60         if let Some(target) = &config.target {
61             cmd.args(&["--target", target]);
62         }
63
64         if config.all_features {
65             cmd.arg("--all-features");
66         } else {
67             if config.no_default_features {
68                 cmd.arg("--no-default-features");
69             }
70             if !config.features.is_empty() {
71                 cmd.arg("--features");
72                 cmd.arg(config.features.join(" "));
73             }
74         }
75
76         cmd
77     }
78
79     pub(crate) fn run(
80         config: &CargoConfig,
81         workspace: &CargoWorkspace,
82         progress: &dyn Fn(String),
83         toolchain: &Option<Version>,
84     ) -> io::Result<WorkspaceBuildScripts> {
85         const RUST_1_62: Version = Version::new(1, 62, 0);
86
87         match Self::run_(Self::build_command(config), config, workspace, progress) {
88             Ok(WorkspaceBuildScripts { error: Some(error), .. })
89                 if toolchain.as_ref().map_or(false, |it| *it >= RUST_1_62) =>
90             {
91                 // building build scripts failed, attempt to build with --keep-going so
92                 // that we potentially get more build data
93                 let mut cmd = Self::build_command(config);
94                 cmd.args(&["-Z", "unstable-options", "--keep-going"]).env("RUSTC_BOOTSTRAP", "1");
95                 let mut res = Self::run_(cmd, config, workspace, progress)?;
96                 res.error = Some(error);
97                 Ok(res)
98             }
99             res => res,
100         }
101     }
102
103     fn run_(
104         mut cmd: Command,
105         config: &CargoConfig,
106         workspace: &CargoWorkspace,
107         progress: &dyn Fn(String),
108     ) -> io::Result<WorkspaceBuildScripts> {
109         if config.wrap_rustc_in_build_scripts {
110             // Setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself. We use
111             // that to compile only proc macros and build scripts during the initial
112             // `cargo check`.
113             let myself = std::env::current_exe()?;
114             cmd.env("RUSTC_WRAPPER", myself);
115             cmd.env("RA_RUSTC_WRAPPER", "1");
116         }
117
118         cmd.current_dir(workspace.workspace_root());
119
120         let mut res = WorkspaceBuildScripts::default();
121         let outputs = &mut res.outputs;
122         // NB: Cargo.toml could have been modified between `cargo metadata` and
123         // `cargo check`. We shouldn't assume that package ids we see here are
124         // exactly those from `config`.
125         let mut by_id: FxHashMap<String, Package> = FxHashMap::default();
126         for package in workspace.packages() {
127             outputs.insert(package, None);
128             by_id.insert(workspace[package].id.clone(), package);
129         }
130
131         let errors = RefCell::new(String::new());
132         let push_err = |err: &str| {
133             let mut e = errors.borrow_mut();
134             e.push_str(err);
135             e.push('\n');
136         };
137
138         tracing::info!("Running build scripts: {:?}", cmd);
139         let output = stdx::process::spawn_with_streaming_output(
140             cmd,
141             &mut |line| {
142                 // Copy-pasted from existing cargo_metadata. It seems like we
143                 // should be using serde_stacker here?
144                 let mut deserializer = serde_json::Deserializer::from_str(line);
145                 deserializer.disable_recursion_limit();
146                 let message = Message::deserialize(&mut deserializer)
147                     .unwrap_or_else(|_| Message::TextLine(line.to_string()));
148
149                 match message {
150                     Message::BuildScriptExecuted(message) => {
151                         let package = match by_id.get(&message.package_id.repr) {
152                             Some(&it) => it,
153                             None => return,
154                         };
155                         let cfgs = {
156                             let mut acc = Vec::new();
157                             for cfg in message.cfgs {
158                                 match cfg.parse::<CfgFlag>() {
159                                     Ok(it) => acc.push(it),
160                                     Err(err) => {
161                                         push_err(&format!(
162                                             "invalid cfg from cargo-metadata: {}",
163                                             err
164                                         ));
165                                         return;
166                                     }
167                                 };
168                             }
169                             acc
170                         };
171                         // cargo_metadata crate returns default (empty) path for
172                         // older cargos, which is not absolute, so work around that.
173                         let out_dir = message.out_dir.into_os_string();
174                         if !out_dir.is_empty() {
175                             let data = outputs[package].get_or_insert_with(Default::default);
176                             data.out_dir = Some(AbsPathBuf::assert(PathBuf::from(out_dir)));
177                             data.cfgs = cfgs;
178                         }
179                         if !message.env.is_empty() {
180                             outputs[package].get_or_insert_with(Default::default).envs =
181                                 message.env;
182                         }
183                     }
184                     Message::CompilerArtifact(message) => {
185                         let package = match by_id.get(&message.package_id.repr) {
186                             Some(it) => *it,
187                             None => return,
188                         };
189
190                         progress(format!("metadata {}", message.target.name));
191
192                         if message.target.kind.iter().any(|k| k == "proc-macro") {
193                             // Skip rmeta file
194                             if let Some(filename) =
195                                 message.filenames.iter().find(|name| is_dylib(name))
196                             {
197                                 let filename = AbsPathBuf::assert(PathBuf::from(&filename));
198                                 outputs[package]
199                                     .get_or_insert_with(Default::default)
200                                     .proc_macro_dylib_path = Some(filename);
201                             }
202                         }
203                     }
204                     Message::CompilerMessage(message) => {
205                         progress(message.target.name);
206
207                         if let Some(diag) = message.message.rendered.as_deref() {
208                             push_err(diag);
209                         }
210                     }
211                     Message::BuildFinished(_) => {}
212                     Message::TextLine(_) => {}
213                     _ => {}
214                 }
215             },
216             &mut |line| {
217                 push_err(line);
218             },
219         )?;
220
221         for package in workspace.packages() {
222             if let Some(package_build_data) = &mut outputs[package] {
223                 tracing::info!(
224                     "{}: {:?}",
225                     workspace[package].manifest.parent().display(),
226                     package_build_data,
227                 );
228                 // inject_cargo_env(package, package_build_data);
229                 if let Some(out_dir) = &package_build_data.out_dir {
230                     // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
231                     if let Some(out_dir) = out_dir.as_os_str().to_str().map(|s| s.to_owned()) {
232                         package_build_data.envs.push(("OUT_DIR".to_string(), out_dir));
233                     }
234                 }
235             }
236         }
237
238         let mut errors = errors.into_inner();
239         if !output.status.success() {
240             if errors.is_empty() {
241                 errors = "cargo check failed".to_string();
242             }
243             res.error = Some(errors);
244         }
245
246         Ok(res)
247     }
248
249     pub fn error(&self) -> Option<&str> {
250         self.error.as_deref()
251     }
252
253     pub(crate) fn get_output(&self, idx: Package) -> Option<&BuildScriptOutput> {
254         self.outputs.get(idx)?.as_ref()
255     }
256 }
257
258 // FIXME: File a better way to know if it is a dylib.
259 fn is_dylib(path: &Utf8Path) -> bool {
260     match path.extension().map(|e| e.to_string().to_lowercase()) {
261         None => false,
262         Some(ext) => matches!(ext.as_str(), "dll" | "dylib" | "so"),
263     }
264 }