Skip to main content

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