bootstrap/utils/
build_stamp.rs

1//! Module for managing build stamp files.
2//!
3//! Contains the core implementation of how bootstrap utilizes stamp files on build processes.
4
5use std::path::{Path, PathBuf};
6use std::{fs, io};
7
8use sha2::digest::Digest;
9
10use crate::core::builder::Builder;
11use crate::core::config::TargetSelection;
12use crate::utils::helpers::{hex_encode, mtime};
13use crate::{Compiler, Mode, helpers, t};
14
15#[cfg(test)]
16mod tests;
17
18/// Manages a stamp file to track build state. The file is created in the given
19/// directory and can have custom content and name.
20#[derive(Clone)]
21pub struct BuildStamp {
22    path: PathBuf,
23    stamp: String,
24}
25
26impl BuildStamp {
27    /// Creates a new `BuildStamp` for a given directory.
28    ///
29    /// By default, stamp will be an empty file named `.stamp` within the specified directory.
30    pub fn new(dir: &Path) -> Self {
31        // Avoid using `is_dir()` as the directory may not exist yet.
32        // It is more appropriate to assert that the path is not a file.
33        assert!(!dir.is_file(), "can't be a file path");
34        Self { path: dir.join(".stamp"), stamp: String::new() }
35    }
36
37    /// Returns path of the stamp file.
38    pub fn path(&self) -> &Path {
39        &self.path
40    }
41
42    /// Returns the value of the stamp.
43    ///
44    /// Note that this is empty by default and is populated using `BuildStamp::add_stamp`.
45    /// It is not read from an actual file, but rather it holds the value that will be used
46    /// when `BuildStamp::write` is called.
47    pub fn stamp(&self) -> &str {
48        &self.stamp
49    }
50
51    /// Adds specified stamp content to the current value.
52    ///
53    /// This method can be used incrementally e.g., `add_stamp("x").add_stamp("y").add_stamp("z")`.
54    pub fn add_stamp<S: ToString>(mut self, stamp: S) -> Self {
55        self.stamp.push_str(&stamp.to_string());
56        self
57    }
58
59    /// Adds a prefix to stamp's name.
60    ///
61    /// Prefix cannot start or end with a dot (`.`).
62    pub fn with_prefix(mut self, prefix: &str) -> Self {
63        assert!(
64            !prefix.starts_with('.') && !prefix.ends_with('.'),
65            "prefix can not start or end with '.'"
66        );
67
68        let stamp_filename = self.path.file_name().unwrap().to_str().unwrap();
69        let stamp_filename = stamp_filename.strip_prefix('.').unwrap_or(stamp_filename);
70        self.path.set_file_name(format!(".{prefix}-{stamp_filename}"));
71
72        self
73    }
74
75    /// Removes the stamp file if it exists.
76    pub fn remove(&self) -> io::Result<()> {
77        match fs::remove_file(&self.path) {
78            Ok(()) => Ok(()),
79            Err(e) => {
80                if e.kind() == io::ErrorKind::NotFound {
81                    Ok(())
82                } else {
83                    Err(e)
84                }
85            }
86        }
87    }
88
89    /// Creates the stamp file.
90    pub fn write(&self) -> io::Result<()> {
91        fs::write(&self.path, &self.stamp)
92    }
93
94    /// Checks if the stamp file is up-to-date.
95    ///
96    /// It is considered up-to-date if file content matches with the stamp string.
97    pub fn is_up_to_date(&self) -> bool {
98        match fs::read(&self.path) {
99            Ok(h) => self.stamp.as_bytes() == h.as_slice(),
100            Err(e) if e.kind() == io::ErrorKind::NotFound => false,
101            Err(e) => {
102                panic!("failed to read stamp file `{}`: {}", self.path.display(), e);
103            }
104        }
105    }
106}
107
108/// Clear out `dir` if `input` is newer.
109///
110/// After this executes, it will also ensure that `dir` exists.
111pub fn clear_if_dirty(builder: &Builder<'_>, dir: &Path, input: &Path) -> bool {
112    let stamp = BuildStamp::new(dir);
113    let mut cleared = false;
114    if mtime(stamp.path()) < mtime(input) {
115        builder.verbose(|| println!("Dirty - {}", dir.display()));
116        let _ = fs::remove_dir_all(dir);
117        cleared = true;
118    } else if stamp.path().exists() {
119        return cleared;
120    }
121    t!(fs::create_dir_all(dir));
122    t!(fs::File::create(stamp.path()));
123    cleared
124}
125
126/// Cargo's output path for librustc_codegen_llvm in a given stage, compiled by a particular
127/// compiler for the specified target and backend.
128pub fn codegen_backend_stamp(
129    builder: &Builder<'_>,
130    compiler: Compiler,
131    target: TargetSelection,
132    backend: &str,
133) -> BuildStamp {
134    BuildStamp::new(&builder.cargo_out(compiler, Mode::Codegen, target))
135        .with_prefix(&format!("librustc_codegen_{backend}"))
136}
137
138/// Cargo's output path for the standard library in a given stage, compiled
139/// by a particular compiler for the specified target.
140pub fn libstd_stamp(
141    builder: &Builder<'_>,
142    compiler: Compiler,
143    target: TargetSelection,
144) -> BuildStamp {
145    BuildStamp::new(&builder.cargo_out(compiler, Mode::Std, target)).with_prefix("libstd")
146}
147
148/// Cargo's output path for librustc in a given stage, compiled by a particular
149/// compiler for the specified target.
150pub fn librustc_stamp(
151    builder: &Builder<'_>,
152    compiler: Compiler,
153    target: TargetSelection,
154) -> BuildStamp {
155    BuildStamp::new(&builder.cargo_out(compiler, Mode::Rustc, target)).with_prefix("librustc")
156}
157
158/// Computes a hash representing the state of a repository/submodule and additional input.
159///
160/// It uses `git diff` for the actual changes, and `git status` for including the untracked
161/// files in the specified directory. The additional input is also incorporated into the
162/// computation of the hash.
163///
164/// # Parameters
165///
166/// - `dir`: A reference to the directory path of the target repository/submodule.
167/// - `additional_input`: An additional input to be included in the hash.
168///
169/// # Panics
170///
171/// In case of errors during `git` command execution (e.g., in tarball sources), default values
172/// are used to prevent panics.
173pub fn generate_smart_stamp_hash(
174    builder: &Builder<'_>,
175    dir: &Path,
176    additional_input: &str,
177) -> String {
178    let diff = helpers::git(Some(dir))
179        .allow_failure()
180        .arg("diff")
181        .arg(".")
182        .run_capture_stdout(builder)
183        .stdout_if_ok()
184        .unwrap_or_default();
185
186    let status = helpers::git(Some(dir))
187        .allow_failure()
188        .arg("status")
189        .arg(".")
190        .arg("--porcelain")
191        .arg("-z")
192        .arg("--untracked-files=normal")
193        .run_capture_stdout(builder)
194        .stdout_if_ok()
195        .unwrap_or_default();
196
197    let mut hasher = sha2::Sha256::new();
198
199    hasher.update(diff);
200    hasher.update(status);
201    hasher.update(additional_input);
202
203    hex_encode(hasher.finalize().as_slice())
204}