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