cargo/core/compiler/
output_depinfo.rs

1//! dep-info files for external build system integration.
2//! See [`output_depinfo`] for more.
3
4use cargo_util::paths::normalize_path;
5use std::collections::{BTreeSet, HashSet};
6use std::io::{BufWriter, Write};
7use std::path::{Path, PathBuf};
8
9use super::{BuildRunner, FileFlavor, Unit, fingerprint};
10use crate::util::{CargoResult, internal};
11use cargo_util::paths;
12use tracing::debug;
13
14/// Basically just normalizes a given path and converts it to a string.
15fn render_filename<P: AsRef<Path>>(path: P, basedir: Option<&str>) -> CargoResult<String> {
16    fn wrap_path(path: &Path) -> CargoResult<String> {
17        path.to_str()
18            .ok_or_else(|| internal(format!("path `{:?}` not utf-8", path)))
19            .map(|f| f.replace(" ", "\\ "))
20    }
21
22    let path = path.as_ref();
23    if let Some(basedir) = basedir {
24        let norm_path = normalize_path(path);
25        let norm_basedir = normalize_path(basedir.as_ref());
26        match norm_path.strip_prefix(norm_basedir) {
27            Ok(relpath) => wrap_path(relpath),
28            _ => wrap_path(path),
29        }
30    } else {
31        wrap_path(path)
32    }
33}
34
35/// Collects all dependencies of the `unit` for the output dep info file.
36///
37/// Dependencies will be stored in `deps`, including:
38///
39/// * dependencies from [fingerprint dep-info]
40/// * paths from `rerun-if-changed` build script instruction
41/// * ...and traverse transitive dependencies recursively
42///
43/// [fingerprint dep-info]: super::fingerprint#fingerprint-dep-info-files
44fn add_deps_for_unit(
45    deps: &mut BTreeSet<PathBuf>,
46    build_runner: &mut BuildRunner<'_, '_>,
47    unit: &Unit,
48    visited: &mut HashSet<Unit>,
49) -> CargoResult<()> {
50    if !visited.insert(unit.clone()) {
51        return Ok(());
52    }
53
54    // units representing the execution of a build script don't actually
55    // generate a dep info file, so we just keep on going below
56    if !unit.mode.is_run_custom_build() {
57        // Add dependencies from rustc dep-info output (stored in fingerprint directory)
58        let dep_info_loc = fingerprint::dep_info_loc(build_runner, unit);
59        if let Some(paths) = fingerprint::parse_dep_info(
60            unit.pkg.root(),
61            build_runner.files().host_build_root(),
62            &dep_info_loc,
63        )? {
64            for path in paths.files.into_keys() {
65                deps.insert(path);
66            }
67        } else {
68            debug!(
69                "can't find dep_info for {:?} {}",
70                unit.pkg.package_id(),
71                unit.target
72            );
73            return Err(internal("dep_info missing"));
74        }
75    }
76
77    // Add rerun-if-changed dependencies
78    if let Some(metadata_vec) = build_runner.find_build_script_metadatas(unit) {
79        for metadata in metadata_vec {
80            if let Some(output) = build_runner
81                .build_script_outputs
82                .lock()
83                .unwrap()
84                .get(metadata)
85            {
86                for path in &output.rerun_if_changed {
87                    let package_root = unit.pkg.root();
88
89                    let path = if path.as_os_str().is_empty() {
90                        // Joining with an empty path causes Rust to add a trailing path separator.
91                        // On Windows, this would add an invalid trailing backslash to the .d file.
92                        package_root.to_path_buf()
93                    } else {
94                        // The paths we have saved from the unit are of arbitrary relativeness and
95                        // may be relative to the crate root of the dependency.
96                        package_root.join(path)
97                    };
98
99                    deps.insert(path);
100                }
101            }
102        }
103    }
104
105    // Recursively traverse all transitive dependencies
106    let unit_deps = Vec::from(build_runner.unit_deps(unit)); // Create vec due to mutable borrow.
107    for dep in unit_deps {
108        if dep.unit.is_local() {
109            add_deps_for_unit(deps, build_runner, &dep.unit, visited)?;
110        }
111    }
112    Ok(())
113}
114
115/// Save a `.d` dep-info file for the given unit. This is the third kind of
116/// dep-info mentioned in [`fingerprint`] module.
117///
118/// Argument `unit` is expected to be the root unit, which will be uplifted.
119///
120/// Cargo emits its own dep-info files in the output directory. This is
121/// only done for every "uplifted" artifact. These are intended to be used
122/// with external build systems so that they can detect if Cargo needs to be
123/// re-executed.
124///
125/// It includes all the entries from the `rustc` dep-info file, and extends it
126/// with any `rerun-if-changed` entries from build scripts. It also includes
127/// sources from any path dependencies. Registry dependencies are not included
128/// under the assumption that changes to them can be detected via changes to
129/// `Cargo.lock`.
130///
131/// [`fingerprint`]: super::fingerprint#dep-info-files
132pub fn output_depinfo(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> CargoResult<()> {
133    let bcx = build_runner.bcx;
134    let mut deps = BTreeSet::new();
135    let mut visited = HashSet::new();
136    let success = add_deps_for_unit(&mut deps, build_runner, unit, &mut visited).is_ok();
137    let basedir_string;
138    let basedir = match bcx.gctx.build_config()?.dep_info_basedir.clone() {
139        Some(value) => {
140            basedir_string = value
141                .resolve_path(bcx.gctx)
142                .as_os_str()
143                .to_str()
144                .ok_or_else(|| anyhow::format_err!("build.dep-info-basedir path not utf-8"))?
145                .to_string();
146            Some(basedir_string.as_str())
147        }
148        None => None,
149    };
150    let deps = deps
151        .iter()
152        .map(|f| render_filename(f, basedir))
153        .collect::<CargoResult<Vec<_>>>()?;
154
155    for output in build_runner.outputs(unit)?.iter().filter(|o| {
156        !matches!(
157            o.flavor,
158            FileFlavor::DebugInfo | FileFlavor::Auxiliary | FileFlavor::Sbom
159        )
160    }) {
161        if let Some(ref link_dst) = output.hardlink {
162            let output_path = link_dst.with_extension("d");
163            if success {
164                let target_fn = render_filename(link_dst, basedir)?;
165
166                // If nothing changed don't recreate the file which could alter
167                // its mtime
168                if let Ok(previous) = fingerprint::parse_rustc_dep_info(&output_path) {
169                    if previous
170                        .files
171                        .iter()
172                        .map(|(path, _checksum)| path)
173                        .eq(deps.iter().map(Path::new))
174                    {
175                        continue;
176                    }
177                }
178
179                // Otherwise write it all out
180                let mut outfile = BufWriter::new(paths::create(output_path)?);
181                write!(outfile, "{}:", target_fn)?;
182                for dep in &deps {
183                    write!(outfile, " {}", dep)?;
184                }
185                writeln!(outfile)?;
186
187            // dep-info generation failed, so delete output file. This will
188            // usually cause the build system to always rerun the build
189            // rule, which is correct if inefficient.
190            } else if output_path.exists() {
191                paths::remove_file(output_path)?;
192            }
193        }
194    }
195    Ok(())
196}