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
6 //! This module implements this second part. We use "build script" terminology
7 //! here, but it covers procedural macros as well.
12 path
::{self, PathBuf}
,
16 use cargo_metadata
::{camino::Utf8Path, Message}
;
17 use la_arena
::ArenaMap
;
18 use paths
::AbsPathBuf
;
19 use rustc_hash
::FxHashMap
;
21 use serde
::Deserialize
;
24 cfg_flag
::CfgFlag
, CargoConfig
, CargoFeatures
, CargoWorkspace
, InvocationLocation
,
25 InvocationStrategy
, Package
,
28 #[derive(Debug, Default, Clone, PartialEq, Eq)]
29 pub struct WorkspaceBuildScripts
{
30 outputs
: ArenaMap
<Package
, BuildScriptOutput
>,
31 error
: Option
<String
>,
34 #[derive(Debug, Clone, Default, PartialEq, Eq)]
35 pub(crate) struct BuildScriptOutput
{
36 /// List of config flags defined by this package's build script.
37 pub(crate) cfgs
: Vec
<CfgFlag
>,
38 /// List of cargo-related environment variables with their value.
40 /// If the package has a build script which defines environment variables,
41 /// they can also be found here.
42 pub(crate) envs
: Vec
<(String
, String
)>,
43 /// Directory where a build script might place its output.
44 pub(crate) out_dir
: Option
<AbsPathBuf
>,
45 /// Path to the proc-macro library file if this package exposes proc-macros.
46 pub(crate) proc_macro_dylib_path
: Option
<AbsPathBuf
>,
49 impl BuildScriptOutput
{
50 fn is_unchanged(&self) -> bool
{
52 && self.envs
.is_empty()
53 && self.out_dir
.is_none()
54 && self.proc_macro_dylib_path
.is_none()
58 impl WorkspaceBuildScripts
{
59 fn build_command(config
: &CargoConfig
) -> io
::Result
<Command
> {
60 let mut cmd
= match config
.run_build_script_command
.as_deref() {
61 Some([program
, args @
..]) => {
62 let mut cmd
= Command
::new(program
);
67 let mut cmd
= Command
::new(toolchain
::cargo());
69 cmd
.args(["check", "--quiet", "--workspace", "--message-format=json"]);
71 // --all-targets includes tests, benches and examples in addition to the
72 // default lib and bins. This is an independent concept from the --target
74 cmd
.arg("--all-targets");
76 if let Some(target
) = &config
.target
{
77 cmd
.args(["--target", target
]);
80 match &config
.features
{
81 CargoFeatures
::All
=> {
82 cmd
.arg("--all-features");
84 CargoFeatures
::Selected { features, no_default_features }
=> {
85 if *no_default_features
{
86 cmd
.arg("--no-default-features");
88 if !features
.is_empty() {
89 cmd
.arg("--features");
90 cmd
.arg(features
.join(" "));
99 cmd
.envs(&config
.extra_env
);
100 if config
.wrap_rustc_in_build_scripts
{
101 // Setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself. We use
102 // that to compile only proc macros and build scripts during the initial
104 let myself
= std
::env
::current_exe()?
;
105 cmd
.env("RUSTC_WRAPPER", myself
);
106 cmd
.env("RA_RUSTC_WRAPPER", "1");
112 /// Runs the build scripts for the given workspace
113 pub(crate) fn run_for_workspace(
114 config
: &CargoConfig
,
115 workspace
: &CargoWorkspace
,
116 progress
: &dyn Fn(String
),
117 toolchain
: &Option
<Version
>,
118 ) -> io
::Result
<WorkspaceBuildScripts
> {
119 const RUST_1_62
: Version
= Version
::new(1, 62, 0);
121 let current_dir
= match &config
.invocation_location
{
122 InvocationLocation
::Root(root
) if config
.run_build_script_command
.is_some() => {
125 _
=> workspace
.workspace_root(),
129 match Self::run_per_ws(Self::build_command(config
)?
, workspace
, current_dir
, progress
) {
130 Ok(WorkspaceBuildScripts { error: Some(error), .. }
)
131 if toolchain
.as_ref().map_or(false, |it
| *it
>= RUST_1_62
) =>
133 // building build scripts failed, attempt to build with --keep-going so
134 // that we potentially get more build data
135 let mut cmd
= Self::build_command(config
)?
;
136 cmd
.args(["-Z", "unstable-options", "--keep-going"]).env("RUSTC_BOOTSTRAP", "1");
137 let mut res
= Self::run_per_ws(cmd
, workspace
, current_dir
, progress
)?
;
138 res
.error
= Some(error
);
145 /// Runs the build scripts by invoking the configured command *once*.
146 /// This populates the outputs for all passed in workspaces.
147 pub(crate) fn run_once(
148 config
: &CargoConfig
,
149 workspaces
: &[&CargoWorkspace
],
150 progress
: &dyn Fn(String
),
151 ) -> io
::Result
<Vec
<WorkspaceBuildScripts
>> {
152 assert_eq
!(config
.invocation_strategy
, InvocationStrategy
::Once
);
154 let current_dir
= match &config
.invocation_location
{
155 InvocationLocation
::Root(root
) => root
,
156 InvocationLocation
::Workspace
=> {
157 return Err(io
::Error
::new(
158 io
::ErrorKind
::Other
,
159 "Cannot run build scripts from workspace with invocation strategy `once`",
163 let cmd
= Self::build_command(config
)?
;
164 // NB: Cargo.toml could have been modified between `cargo metadata` and
165 // `cargo check`. We shouldn't assume that package ids we see here are
166 // exactly those from `config`.
167 let mut by_id
= FxHashMap
::default();
168 // some workspaces might depend on the same crates, so we need to duplicate the outputs
169 // to those collisions
170 let mut collisions
= Vec
::new();
171 let mut res
: Vec
<_
> = workspaces
174 .map(|(idx
, workspace
)| {
175 let mut res
= WorkspaceBuildScripts
::default();
176 for package
in workspace
.packages() {
177 res
.outputs
.insert(package
, BuildScriptOutput
::default());
178 if by_id
.contains_key(&workspace
[package
].id
) {
179 collisions
.push((&workspace
[package
].id
, idx
, package
));
181 by_id
.insert(workspace
[package
].id
.clone(), (package
, idx
));
188 let errors
= Self::run_command(
190 current_dir
.as_path().as_ref(),
192 if let Some(&(package
, workspace
)) = by_id
.get(package
) {
193 cb(&workspaces
[workspace
][package
].name
, &mut res
[workspace
].outputs
[package
]);
198 res
.iter_mut().for_each(|it
| it
.error
= errors
.clone());
199 collisions
.into_iter().for_each(|(id
, workspace
, package
)| {
200 if let Some(&(p
, w
)) = by_id
.get(id
) {
201 res
[workspace
].outputs
[package
] = res
[w
].outputs
[p
].clone();
205 if tracing
::enabled
!(tracing
::Level
::INFO
) {
206 for (idx
, workspace
) in workspaces
.iter().enumerate() {
207 for package
in workspace
.packages() {
208 let package_build_data
= &mut res
[idx
].outputs
[package
];
209 if !package_build_data
.is_unchanged() {
212 workspace
[package
].manifest
.parent().display(),
225 workspace
: &CargoWorkspace
,
226 current_dir
: &path
::Path
,
227 progress
: &dyn Fn(String
),
228 ) -> io
::Result
<WorkspaceBuildScripts
> {
229 let mut res
= WorkspaceBuildScripts
::default();
230 let outputs
= &mut res
.outputs
;
231 // NB: Cargo.toml could have been modified between `cargo metadata` and
232 // `cargo check`. We shouldn't assume that package ids we see here are
233 // exactly those from `config`.
234 let mut by_id
: FxHashMap
<String
, Package
> = FxHashMap
::default();
235 for package
in workspace
.packages() {
236 outputs
.insert(package
, BuildScriptOutput
::default());
237 by_id
.insert(workspace
[package
].id
.clone(), package
);
240 res
.error
= Self::run_command(
244 if let Some(&package
) = by_id
.get(package
) {
245 cb(&workspace
[package
].name
, &mut outputs
[package
]);
251 if tracing
::enabled
!(tracing
::Level
::INFO
) {
252 for package
in workspace
.packages() {
253 let package_build_data
= &mut outputs
[package
];
254 if !package_build_data
.is_unchanged() {
257 workspace
[package
].manifest
.parent().display(),
269 current_dir
: &path
::Path
,
270 // ideally this would be something like:
271 // with_output_for: impl FnMut(&str, dyn FnOnce(&mut BuildScriptOutput)),
272 // but owned trait objects aren't a thing
273 mut with_output_for
: impl FnMut(&str, &mut dyn FnMut(&str, &mut BuildScriptOutput
)),
274 progress
: &dyn Fn(String
),
275 ) -> io
::Result
<Option
<String
>> {
276 let errors
= RefCell
::new(String
::new());
277 let push_err
= |err
: &str| {
278 let mut e
= errors
.borrow_mut();
283 tracing
::info
!("Running build scripts in {}: {:?}", current_dir
.display(), cmd
);
284 cmd
.current_dir(current_dir
);
285 let output
= stdx
::process
::spawn_with_streaming_output(
288 // Copy-pasted from existing cargo_metadata. It seems like we
289 // should be using serde_stacker here?
290 let mut deserializer
= serde_json
::Deserializer
::from_str(line
);
291 deserializer
.disable_recursion_limit();
292 let message
= Message
::deserialize(&mut deserializer
)
293 .unwrap_or_else(|_
| Message
::TextLine(line
.to_string()));
296 Message
::BuildScriptExecuted(mut message
) => {
297 with_output_for(&message
.package_id
.repr
, &mut |name
, data
| {
298 progress(format
!("running build-script: {name}"));
300 let mut acc
= Vec
::new();
301 for cfg
in &message
.cfgs
{
302 match cfg
.parse
::<CfgFlag
>() {
303 Ok(it
) => acc
.push(it
),
306 "invalid cfg from cargo-metadata: {err}"
314 if !message
.env
.is_empty() {
315 data
.envs
= mem
::take(&mut message
.env
);
317 // cargo_metadata crate returns default (empty) path for
318 // older cargos, which is not absolute, so work around that.
319 let out_dir
= mem
::take(&mut message
.out_dir
).into_os_string();
320 if !out_dir
.is_empty() {
321 let out_dir
= AbsPathBuf
::assert(PathBuf
::from(out_dir
));
322 // inject_cargo_env(package, package_build_data);
323 // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
324 if let Some(out_dir
) =
325 out_dir
.as_os_str().to_str().map(|s
| s
.to_owned())
327 data
.envs
.push(("OUT_DIR".to_string(), out_dir
));
329 data
.out_dir
= Some(out_dir
);
334 Message
::CompilerArtifact(message
) => {
335 with_output_for(&message
.package_id
.repr
, &mut |name
, data
| {
336 progress(format
!("building proc-macros: {name}"));
337 if message
.target
.kind
.iter().any(|k
| k
== "proc-macro") {
339 if let Some(filename
) =
340 message
.filenames
.iter().find(|name
| is_dylib(name
))
342 let filename
= AbsPathBuf
::assert(PathBuf
::from(&filename
));
343 data
.proc_macro_dylib_path
= Some(filename
);
348 Message
::CompilerMessage(message
) => {
349 progress(message
.target
.name
);
351 if let Some(diag
) = message
.message
.rendered
.as_deref() {
355 Message
::BuildFinished(_
) => {}
356 Message
::TextLine(_
) => {}
365 let errors
= if !output
.status
.success() {
366 let errors
= errors
.into_inner();
367 Some(if errors
.is_empty() { "cargo check failed".to_string() }
else { errors }
)
374 pub fn error(&self) -> Option
<&str> {
375 self.error
.as_deref()
378 pub(crate) fn get_output(&self, idx
: Package
) -> Option
<&BuildScriptOutput
> {
379 self.outputs
.get(idx
)
383 // FIXME: Find a better way to know if it is a dylib.
384 fn is_dylib(path
: &Utf8Path
) -> bool
{
385 match path
.extension().map(|e
| e
.to_string().to_lowercase()) {
387 Some(ext
) => matches
!(ext
.as_str(), "dll" | "dylib" | "so"),