Skip to main content

bootstrap/core/build_steps/
clean.rs

1//! `./x.py clean`
2//!
3//! Responsible for cleaning out a build directory of all old and stale
4//! artifacts to prepare for a fresh build. Currently doesn't remove the
5//! `build/cache` directory (download cache) or the `build/$target/llvm`
6//! directory unless the `--all` flag is present.
7
8use std::fs;
9use std::io::{self, ErrorKind};
10use std::path::Path;
11
12use crate::core::builder::{
13    Builder, CommandLineStep, Kind, RunConfig, ShouldRun, crate_description,
14};
15use crate::core::compiler::Compiler;
16use crate::core::config::flags::Subcommand;
17use crate::utils::build_stamp::BuildStamp;
18use crate::utils::helpers::t;
19use crate::{Build, Mode};
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct CleanAll {}
23
24impl CommandLineStep for CleanAll {
25    type Output = ();
26
27    fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
28        // Normally this step is invoked implicitly via `./x clean`, but all
29        // steps are required to register at least one explicit path/alias.
30        run.alias("default")
31    }
32
33    fn is_default_step(_builder: &Builder<'_>) -> bool {
34        true
35    }
36
37    fn make_run(run: RunConfig<'_>) {
38        run.builder.ensure(CleanAll {})
39    }
40
41    fn run(self, builder: &Builder<'_>) -> Self::Output {
42        let Subcommand::Clean { all, stage } = builder.config.cmd else {
43            unreachable!("wrong subcommand?")
44        };
45
46        if all && stage.is_some() {
47            panic!("--all and --stage can't be used at the same time for `x clean`");
48        }
49
50        clean(builder.build, all, stage)
51    }
52}
53
54macro_rules! clean_crate_tree {
55    ( $( $name:ident, $mode:path, $root_crate:literal);+ $(;)? ) => { $(
56        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
57        pub struct $name {
58            compiler: Compiler,
59            crates: Vec<String>,
60        }
61
62        impl CommandLineStep for $name {
63            type Output = ();
64
65            fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
66                run.crate_or_deps($root_crate)
67            }
68
69            fn make_run(run: RunConfig<'_>) {
70                let builder = run.builder;
71                let compiler = builder.compiler(builder.top_stage, run.target);
72                builder.ensure(Self { crates: run.cargo_crates_in_set(), compiler });
73            }
74
75            fn run(self, builder: &Builder<'_>) -> Self::Output {
76                let compiler = self.compiler;
77                let target = compiler.host;
78                let mut cargo = builder.bare_cargo(compiler, $mode, target, Kind::Clean);
79
80                // Since https://github.com/rust-lang/rust/pull/111076 enables
81                // unstable cargo feature (`public-dependency`), we need to ensure
82                // that unstable features are enabled before reading libstd Cargo.toml.
83                cargo.env("RUSTC_BOOTSTRAP", "1");
84
85                for krate in &*self.crates {
86                    cargo.arg("-p");
87                    cargo.arg(krate);
88                }
89
90                builder.info(&format!(
91                    "Cleaning{} stage{} {} artifacts ({} -> {})",
92                    crate_description(&self.crates), compiler.stage, stringify!($name).to_lowercase(), &compiler.host, target,
93                ));
94
95                // NOTE: doesn't use `run_cargo` because we don't want to save a stamp file,
96                // and doesn't use `stream_cargo` to avoid passing `--message-format` which `clean` doesn't accept.
97                cargo.run(builder);
98            }
99        }
100    )+ }
101}
102
103clean_crate_tree! {
104    Rustc, Mode::Rustc, "rustc-main";
105    Std, Mode::Std, "sysroot";
106}
107
108fn clean(build: &Build, all: bool, stage: Option<u32>) {
109    if build.config.dry_run() {
110        return;
111    }
112
113    rm_rf("tmp".as_ref());
114
115    // Clean the entire build directory
116    if all {
117        rm_rf(&build.out);
118        return;
119    }
120
121    // Clean the target stage artifacts
122    if let Some(stage) = stage {
123        clean_specific_stage(build, stage);
124        return;
125    }
126
127    // Follow the default behaviour
128    clean_default(build);
129}
130
131fn clean_specific_stage(build: &Build, stage: u32) {
132    for host in &build.hosts {
133        let entries = match build.out.join(host).read_dir() {
134            Ok(iter) => iter,
135            Err(_) => continue,
136        };
137
138        for entry in entries {
139            let entry = t!(entry);
140            let stage_prefix = format!("stage{}", stage + 1);
141
142            // if current entry is not related with the target stage, continue
143            if !entry.file_name().to_str().unwrap_or("").contains(&stage_prefix) {
144                continue;
145            }
146
147            let path = t!(entry.path().canonicalize());
148            rm_rf(&path);
149        }
150    }
151}
152
153fn clean_default(build: &Build) {
154    rm_rf(&build.out.join("tmp"));
155    rm_rf(&build.out.join("dist"));
156    rm_rf(&build.out.join("bootstrap").join(".last-warned-change-id"));
157    rm_rf(&build.out.join("bootstrap-shims-dump"));
158    rm_rf(BuildStamp::new(&build.out).with_prefix("rustfmt").path());
159
160    let mut hosts: Vec<_> = build.hosts.iter().map(|t| build.out.join(t)).collect();
161    // After cross-compilation, artifacts of the host architecture (which may differ from build.host)
162    // might not get removed.
163    // Adding its path (linked one for easier accessibility) will solve this problem.
164    hosts.push(build.out.join("host"));
165
166    for host in hosts {
167        let entries = match host.read_dir() {
168            Ok(iter) => iter,
169            Err(_) => continue,
170        };
171
172        for entry in entries {
173            let entry = t!(entry);
174            if entry.file_name().to_str() == Some("llvm") {
175                continue;
176            }
177            let path = t!(entry.path().canonicalize());
178            rm_rf(&path);
179        }
180    }
181}
182
183fn rm_rf(path: &Path) {
184    match fs::remove_dir_all(path) {
185        Ok(()) => return,
186        // Already deleted, nothing for us to do.
187        Err(e) if e.kind() == ErrorKind::NotFound => return,
188        _ => {}
189    }
190
191    // If remove_dir_all fails then retry.
192    // We do so manually so we can provide better diagnostics,
193    // e.g. pointing to the exact file that failed.
194    match path.symlink_metadata() {
195        Err(e) => {
196            if e.kind() == ErrorKind::NotFound {
197                return;
198            }
199            panic!("failed to get metadata for file {}: {}", path.display(), e);
200        }
201        Ok(metadata) => {
202            if !metadata.file_type().is_dir() {
203                do_op(path, "remove file", |p| match fs::remove_file(p) {
204                    #[cfg(windows)]
205                    Err(e)
206                        if e.kind() == std::io::ErrorKind::PermissionDenied
207                            && p.file_name().and_then(std::ffi::OsStr::to_str)
208                                == Some("bootstrap.exe") =>
209                    {
210                        eprintln!("WARNING: failed to delete '{}'.", p.display());
211                        Ok(())
212                    }
213                    r => r,
214                });
215
216                return;
217            }
218
219            for file in t!(fs::read_dir(path)) {
220                rm_rf(&t!(file).path());
221            }
222
223            do_op(path, "remove dir", |p| match fs::remove_dir(p) {
224                // Check for dir not empty on Windows
225                #[cfg(windows)]
226                Err(e) if e.kind() == ErrorKind::DirectoryNotEmpty => Ok(()),
227                r => r,
228            });
229        }
230    };
231}
232
233fn do_op<F>(path: &Path, desc: &str, mut f: F)
234where
235    F: FnMut(&Path) -> io::Result<()>,
236{
237    match f(path) {
238        Ok(()) => {}
239        // On windows we can't remove a readonly file, and git will often clone files as readonly.
240        // As a result, we have some special logic to remove readonly files on windows.
241        // This is also the reason that we can't use things like fs::remove_dir_all().
242        #[cfg(windows)]
243        Err(ref e) if e.kind() == ErrorKind::PermissionDenied => {
244            let m = t!(path.symlink_metadata());
245            let mut p = m.permissions();
246            // this os not unix, so clippy gives FP
247            #[expect(clippy::permissions_set_readonly_false)]
248            p.set_readonly(false);
249            t!(fs::set_permissions(path, p));
250            f(path).unwrap_or_else(|e| {
251                // Delete symlinked directories on Windows
252                if fs::remove_dir(path).is_ok() {
253                    return;
254                }
255                panic!("failed to {} {}: {}", desc, path.display(), e);
256            });
257        }
258        Err(e) => {
259            panic!("failed to {} {}: {}", desc, path.display(), e);
260        }
261    }
262}