• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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::{
10     cell::RefCell,
11     io, mem,
12     path::{self, PathBuf},
13     process::Command,
14 };
15 
16 use cargo_metadata::{camino::Utf8Path, Message};
17 use itertools::Itertools;
18 use la_arena::ArenaMap;
19 use paths::{AbsPath, AbsPathBuf};
20 use rustc_hash::{FxHashMap, FxHashSet};
21 use semver::Version;
22 use serde::Deserialize;
23 
24 use crate::{
25     cfg_flag::CfgFlag, utf8_stdout, CargoConfig, CargoFeatures, CargoWorkspace, InvocationLocation,
26     InvocationStrategy, Package,
27 };
28 
29 #[derive(Debug, Default, Clone, PartialEq, Eq)]
30 pub struct WorkspaceBuildScripts {
31     outputs: ArenaMap<Package, BuildScriptOutput>,
32     error: Option<String>,
33 }
34 
35 #[derive(Debug, Clone, Default, PartialEq, Eq)]
36 pub(crate) struct BuildScriptOutput {
37     /// List of config flags defined by this package's build script.
38     pub(crate) cfgs: Vec<CfgFlag>,
39     /// List of cargo-related environment variables with their value.
40     ///
41     /// If the package has a build script which defines environment variables,
42     /// they can also be found here.
43     pub(crate) envs: Vec<(String, String)>,
44     /// Directory where a build script might place its output.
45     pub(crate) out_dir: Option<AbsPathBuf>,
46     /// Path to the proc-macro library file if this package exposes proc-macros.
47     pub(crate) proc_macro_dylib_path: Option<AbsPathBuf>,
48 }
49 
50 impl BuildScriptOutput {
is_unchanged(&self) -> bool51     fn is_unchanged(&self) -> bool {
52         self.cfgs.is_empty()
53             && self.envs.is_empty()
54             && self.out_dir.is_none()
55             && self.proc_macro_dylib_path.is_none()
56     }
57 }
58 
59 impl WorkspaceBuildScripts {
build_command( config: &CargoConfig, allowed_features: &FxHashSet<String>, ) -> io::Result<Command>60     fn build_command(
61         config: &CargoConfig,
62         allowed_features: &FxHashSet<String>,
63     ) -> io::Result<Command> {
64         let mut cmd = match config.run_build_script_command.as_deref() {
65             Some([program, args @ ..]) => {
66                 let mut cmd = Command::new(program);
67                 cmd.args(args);
68                 cmd
69             }
70             _ => {
71                 let mut cmd = Command::new(toolchain::cargo());
72 
73                 cmd.args(["check", "--quiet", "--workspace", "--message-format=json"]);
74                 cmd.args(&config.extra_args);
75 
76                 // --all-targets includes tests, benches and examples in addition to the
77                 // default lib and bins. This is an independent concept from the --target
78                 // flag below.
79                 cmd.arg("--all-targets");
80 
81                 if let Some(target) = &config.target {
82                     cmd.args(["--target", target]);
83                 }
84 
85                 match &config.features {
86                     CargoFeatures::All => {
87                         cmd.arg("--all-features");
88                     }
89                     CargoFeatures::Selected { features, no_default_features } => {
90                         if *no_default_features {
91                             cmd.arg("--no-default-features");
92                         }
93                         if !features.is_empty() {
94                             cmd.arg("--features");
95                             cmd.arg(
96                                 features
97                                     .iter()
98                                     .filter(|&feat| allowed_features.contains(feat))
99                                     .join(","),
100                             );
101                         }
102                     }
103                 }
104 
105                 cmd
106             }
107         };
108 
109         cmd.envs(&config.extra_env);
110         if config.wrap_rustc_in_build_scripts {
111             // Setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself. We use
112             // that to compile only proc macros and build scripts during the initial
113             // `cargo check`.
114             let myself = std::env::current_exe()?;
115             cmd.env("RUSTC_WRAPPER", myself);
116             cmd.env("RA_RUSTC_WRAPPER", "1");
117         }
118 
119         Ok(cmd)
120     }
121 
122     /// Runs the build scripts for the given workspace
run_for_workspace( config: &CargoConfig, workspace: &CargoWorkspace, progress: &dyn Fn(String), toolchain: &Option<Version>, ) -> io::Result<WorkspaceBuildScripts>123     pub(crate) fn run_for_workspace(
124         config: &CargoConfig,
125         workspace: &CargoWorkspace,
126         progress: &dyn Fn(String),
127         toolchain: &Option<Version>,
128     ) -> io::Result<WorkspaceBuildScripts> {
129         const RUST_1_62: Version = Version::new(1, 62, 0);
130 
131         let current_dir = match &config.invocation_location {
132             InvocationLocation::Root(root) if config.run_build_script_command.is_some() => {
133                 root.as_path()
134             }
135             _ => workspace.workspace_root(),
136         }
137         .as_ref();
138 
139         let allowed_features = workspace.workspace_features();
140 
141         match Self::run_per_ws(
142             Self::build_command(config, &allowed_features)?,
143             workspace,
144             current_dir,
145             progress,
146         ) {
147             Ok(WorkspaceBuildScripts { error: Some(error), .. })
148                 if toolchain.as_ref().map_or(false, |it| *it >= RUST_1_62) =>
149             {
150                 // building build scripts failed, attempt to build with --keep-going so
151                 // that we potentially get more build data
152                 let mut cmd = Self::build_command(config, &allowed_features)?;
153                 cmd.args(["-Z", "unstable-options", "--keep-going"]).env("RUSTC_BOOTSTRAP", "1");
154                 let mut res = Self::run_per_ws(cmd, workspace, current_dir, progress)?;
155                 res.error = Some(error);
156                 Ok(res)
157             }
158             res => res,
159         }
160     }
161 
162     /// Runs the build scripts by invoking the configured command *once*.
163     /// This populates the outputs for all passed in workspaces.
run_once( config: &CargoConfig, workspaces: &[&CargoWorkspace], progress: &dyn Fn(String), ) -> io::Result<Vec<WorkspaceBuildScripts>>164     pub(crate) fn run_once(
165         config: &CargoConfig,
166         workspaces: &[&CargoWorkspace],
167         progress: &dyn Fn(String),
168     ) -> io::Result<Vec<WorkspaceBuildScripts>> {
169         assert_eq!(config.invocation_strategy, InvocationStrategy::Once);
170 
171         let current_dir = match &config.invocation_location {
172             InvocationLocation::Root(root) => root,
173             InvocationLocation::Workspace => {
174                 return Err(io::Error::new(
175                     io::ErrorKind::Other,
176                     "Cannot run build scripts from workspace with invocation strategy `once`",
177                 ))
178             }
179         };
180         let cmd = Self::build_command(config, &Default::default())?;
181         // NB: Cargo.toml could have been modified between `cargo metadata` and
182         // `cargo check`. We shouldn't assume that package ids we see here are
183         // exactly those from `config`.
184         let mut by_id = FxHashMap::default();
185         // some workspaces might depend on the same crates, so we need to duplicate the outputs
186         // to those collisions
187         let mut collisions = Vec::new();
188         let mut res: Vec<_> = workspaces
189             .iter()
190             .enumerate()
191             .map(|(idx, workspace)| {
192                 let mut res = WorkspaceBuildScripts::default();
193                 for package in workspace.packages() {
194                     res.outputs.insert(package, BuildScriptOutput::default());
195                     if by_id.contains_key(&workspace[package].id) {
196                         collisions.push((&workspace[package].id, idx, package));
197                     } else {
198                         by_id.insert(workspace[package].id.clone(), (package, idx));
199                     }
200                 }
201                 res
202             })
203             .collect();
204 
205         let errors = Self::run_command(
206             cmd,
207             current_dir.as_path().as_ref(),
208             |package, cb| {
209                 if let Some(&(package, workspace)) = by_id.get(package) {
210                     cb(&workspaces[workspace][package].name, &mut res[workspace].outputs[package]);
211                 }
212             },
213             progress,
214         )?;
215         res.iter_mut().for_each(|it| it.error = errors.clone());
216         collisions.into_iter().for_each(|(id, workspace, package)| {
217             if let Some(&(p, w)) = by_id.get(id) {
218                 res[workspace].outputs[package] = res[w].outputs[p].clone();
219             }
220         });
221 
222         if tracing::enabled!(tracing::Level::INFO) {
223             for (idx, workspace) in workspaces.iter().enumerate() {
224                 for package in workspace.packages() {
225                     let package_build_data = &mut res[idx].outputs[package];
226                     if !package_build_data.is_unchanged() {
227                         tracing::info!(
228                             "{}: {:?}",
229                             workspace[package].manifest.parent().display(),
230                             package_build_data,
231                         );
232                     }
233                 }
234             }
235         }
236 
237         Ok(res)
238     }
239 
run_per_ws( cmd: Command, workspace: &CargoWorkspace, current_dir: &path::Path, progress: &dyn Fn(String), ) -> io::Result<WorkspaceBuildScripts>240     fn run_per_ws(
241         cmd: Command,
242         workspace: &CargoWorkspace,
243         current_dir: &path::Path,
244         progress: &dyn Fn(String),
245     ) -> io::Result<WorkspaceBuildScripts> {
246         let mut res = WorkspaceBuildScripts::default();
247         let outputs = &mut res.outputs;
248         // NB: Cargo.toml could have been modified between `cargo metadata` and
249         // `cargo check`. We shouldn't assume that package ids we see here are
250         // exactly those from `config`.
251         let mut by_id: FxHashMap<String, Package> = FxHashMap::default();
252         for package in workspace.packages() {
253             outputs.insert(package, BuildScriptOutput::default());
254             by_id.insert(workspace[package].id.clone(), package);
255         }
256 
257         res.error = Self::run_command(
258             cmd,
259             current_dir,
260             |package, cb| {
261                 if let Some(&package) = by_id.get(package) {
262                     cb(&workspace[package].name, &mut outputs[package]);
263                 }
264             },
265             progress,
266         )?;
267 
268         if tracing::enabled!(tracing::Level::INFO) {
269             for package in workspace.packages() {
270                 let package_build_data = &outputs[package];
271                 if !package_build_data.is_unchanged() {
272                     tracing::info!(
273                         "{}: {:?}",
274                         workspace[package].manifest.parent().display(),
275                         package_build_data,
276                     );
277                 }
278             }
279         }
280 
281         Ok(res)
282     }
283 
run_command( mut cmd: Command, current_dir: &path::Path, mut with_output_for: impl FnMut(&str, &mut dyn FnMut(&str, &mut BuildScriptOutput)), progress: &dyn Fn(String), ) -> io::Result<Option<String>>284     fn run_command(
285         mut cmd: Command,
286         current_dir: &path::Path,
287         // ideally this would be something like:
288         // with_output_for: impl FnMut(&str, dyn FnOnce(&mut BuildScriptOutput)),
289         // but owned trait objects aren't a thing
290         mut with_output_for: impl FnMut(&str, &mut dyn FnMut(&str, &mut BuildScriptOutput)),
291         progress: &dyn Fn(String),
292     ) -> io::Result<Option<String>> {
293         let errors = RefCell::new(String::new());
294         let push_err = |err: &str| {
295             let mut e = errors.borrow_mut();
296             e.push_str(err);
297             e.push('\n');
298         };
299 
300         tracing::info!("Running build scripts in {}: {:?}", current_dir.display(), cmd);
301         cmd.current_dir(current_dir);
302         let output = stdx::process::spawn_with_streaming_output(
303             cmd,
304             &mut |line| {
305                 // Copy-pasted from existing cargo_metadata. It seems like we
306                 // should be using serde_stacker here?
307                 let mut deserializer = serde_json::Deserializer::from_str(line);
308                 deserializer.disable_recursion_limit();
309                 let message = Message::deserialize(&mut deserializer)
310                     .unwrap_or_else(|_| Message::TextLine(line.to_string()));
311 
312                 match message {
313                     Message::BuildScriptExecuted(mut message) => {
314                         with_output_for(&message.package_id.repr, &mut |name, data| {
315                             progress(format!("running build-script: {name}"));
316                             let cfgs = {
317                                 let mut acc = Vec::new();
318                                 for cfg in &message.cfgs {
319                                     match cfg.parse::<CfgFlag>() {
320                                         Ok(it) => acc.push(it),
321                                         Err(err) => {
322                                             push_err(&format!(
323                                                 "invalid cfg from cargo-metadata: {err}"
324                                             ));
325                                             return;
326                                         }
327                                     };
328                                 }
329                                 acc
330                             };
331                             if !message.env.is_empty() {
332                                 data.envs = mem::take(&mut message.env);
333                             }
334                             // cargo_metadata crate returns default (empty) path for
335                             // older cargos, which is not absolute, so work around that.
336                             let out_dir = mem::take(&mut message.out_dir).into_os_string();
337                             if !out_dir.is_empty() {
338                                 let out_dir = AbsPathBuf::assert(PathBuf::from(out_dir));
339                                 // inject_cargo_env(package, package_build_data);
340                                 // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
341                                 if let Some(out_dir) =
342                                     out_dir.as_os_str().to_str().map(|s| s.to_owned())
343                                 {
344                                     data.envs.push(("OUT_DIR".to_string(), out_dir));
345                                 }
346                                 data.out_dir = Some(out_dir);
347                                 data.cfgs = cfgs;
348                             }
349                         });
350                     }
351                     Message::CompilerArtifact(message) => {
352                         with_output_for(&message.package_id.repr, &mut |name, data| {
353                             progress(format!("building proc-macros: {name}"));
354                             if message.target.kind.iter().any(|k| k == "proc-macro") {
355                                 // Skip rmeta file
356                                 if let Some(filename) =
357                                     message.filenames.iter().find(|name| is_dylib(name))
358                                 {
359                                     let filename = AbsPathBuf::assert(PathBuf::from(&filename));
360                                     data.proc_macro_dylib_path = Some(filename);
361                                 }
362                             }
363                         });
364                     }
365                     Message::CompilerMessage(message) => {
366                         progress(message.target.name);
367 
368                         if let Some(diag) = message.message.rendered.as_deref() {
369                             push_err(diag);
370                         }
371                     }
372                     Message::BuildFinished(_) => {}
373                     Message::TextLine(_) => {}
374                     _ => {}
375                 }
376             },
377             &mut |line| {
378                 push_err(line);
379             },
380         )?;
381 
382         let errors = if !output.status.success() {
383             let errors = errors.into_inner();
384             Some(if errors.is_empty() { "cargo check failed".to_string() } else { errors })
385         } else {
386             None
387         };
388         Ok(errors)
389     }
390 
error(&self) -> Option<&str>391     pub fn error(&self) -> Option<&str> {
392         self.error.as_deref()
393     }
394 
get_output(&self, idx: Package) -> Option<&BuildScriptOutput>395     pub(crate) fn get_output(&self, idx: Package) -> Option<&BuildScriptOutput> {
396         self.outputs.get(idx)
397     }
398 
rustc_crates( rustc: &CargoWorkspace, current_dir: &AbsPath, extra_env: &FxHashMap<String, String>, ) -> Self399     pub(crate) fn rustc_crates(
400         rustc: &CargoWorkspace,
401         current_dir: &AbsPath,
402         extra_env: &FxHashMap<String, String>,
403     ) -> Self {
404         let mut bs = WorkspaceBuildScripts::default();
405         for p in rustc.packages() {
406             bs.outputs.insert(p, BuildScriptOutput::default());
407         }
408         let res = (|| {
409             let target_libdir = (|| {
410                 let mut cargo_config = Command::new(toolchain::cargo());
411                 cargo_config.envs(extra_env);
412                 cargo_config
413                     .current_dir(current_dir)
414                     .args(["rustc", "-Z", "unstable-options", "--print", "target-libdir"])
415                     .env("RUSTC_BOOTSTRAP", "1");
416                 if let Ok(it) = utf8_stdout(cargo_config) {
417                     return Ok(it);
418                 }
419                 let mut cmd = Command::new(toolchain::rustc());
420                 cmd.envs(extra_env);
421                 cmd.args(["--print", "target-libdir"]);
422                 utf8_stdout(cmd)
423             })()?;
424 
425             let target_libdir = AbsPathBuf::try_from(PathBuf::from(target_libdir))
426                 .map_err(|_| anyhow::format_err!("target-libdir was not an absolute path"))?;
427             tracing::info!("Loading rustc proc-macro paths from {}", target_libdir.display());
428 
429             let proc_macro_dylibs: Vec<(String, AbsPathBuf)> = std::fs::read_dir(target_libdir)?
430                 .filter_map(|entry| {
431                     let dir_entry = entry.ok()?;
432                     if dir_entry.file_type().ok()?.is_file() {
433                         let path = dir_entry.path();
434                         let extension = path.extension()?;
435                         if extension == std::env::consts::DLL_EXTENSION {
436                             let name = path.file_stem()?.to_str()?.split_once('-')?.0.to_owned();
437                             let path = AbsPathBuf::try_from(path).ok()?;
438                             return Some((name, path));
439                         }
440                     }
441                     None
442                 })
443                 .collect();
444             for p in rustc.packages() {
445                 let package = &rustc[p];
446                 if package.targets.iter().any(|&it| rustc[it].is_proc_macro) {
447                     if let Some((_, path)) = proc_macro_dylibs
448                         .iter()
449                         .find(|(name, _)| *name.trim_start_matches("lib") == package.name)
450                     {
451                         bs.outputs[p].proc_macro_dylib_path = Some(path.clone());
452                     }
453                 }
454             }
455 
456             if tracing::enabled!(tracing::Level::INFO) {
457                 for package in rustc.packages() {
458                     let package_build_data = &bs.outputs[package];
459                     if !package_build_data.is_unchanged() {
460                         tracing::info!(
461                             "{}: {:?}",
462                             rustc[package].manifest.parent().display(),
463                             package_build_data,
464                         );
465                     }
466                 }
467             }
468             Ok(())
469         })();
470         if let Err::<_, anyhow::Error>(e) = res {
471             bs.error = Some(e.to_string());
472         }
473         bs
474     }
475 }
476 
477 // FIXME: Find a better way to know if it is a dylib.
is_dylib(path: &Utf8Path) -> bool478 fn is_dylib(path: &Utf8Path) -> bool {
479     match path.extension().map(|e| e.to_string().to_lowercase()) {
480         None => false,
481         Some(ext) => matches!(ext.as_str(), "dll" | "dylib" | "so"),
482     }
483 }
484