Skip to content

Commit ac575b6

Browse files
committed
Auto merge of rust-lang#82653 - jyn514:submodules-on-demand, r=Mark-Simulacrum
Update all submodules that rustbuild doesn't depend on lazily This only updates the submodules the first time they're needed, instead of unconditionally the first time you run x.py. Ideally, this would move *all* submodules to rustbuild and not exclude some tools and backtrace. Unfortunately, cargo requires all `Cargo.toml` files in the whole workspace to be present to build any crate. On my machine, this takes the time for an initial submodule clone (for `x.py --help`) from 55.70 to 15.87 seconds. Helps with rust-lang#76653. Builds on rust-lang#86015 and should not be merged before (only the last commit is relevant).
2 parents fabf502 + 2ac0e9b commit ac575b6

File tree

8 files changed

+173
-113
lines changed

8 files changed

+173
-113
lines changed

src/bootstrap/bootstrap.py

+18-9
Original file line numberDiff line numberDiff line change
@@ -989,21 +989,30 @@ def update_submodules(self):
989989
slow_submodules = self.get_toml('fast-submodules') == "false"
990990
start_time = time()
991991
if slow_submodules:
992-
print('Unconditionally updating all submodules')
992+
print('Unconditionally updating submodules')
993993
else:
994994
print('Updating only changed submodules')
995995
default_encoding = sys.getdefaultencoding()
996-
submodules = [s.split(' ', 1)[1] for s in subprocess.check_output(
997-
["git", "config", "--file",
998-
os.path.join(self.rust_root, ".gitmodules"),
999-
"--get-regexp", "path"]
1000-
).decode(default_encoding).splitlines()]
996+
# Only update submodules that are needed to build bootstrap. These are needed because Cargo
997+
# currently requires everything in a workspace to be "locally present" when starting a
998+
# build, and will give a hard error if any Cargo.toml files are missing.
999+
# FIXME: Is there a way to avoid cloning these eagerly? Bootstrap itself doesn't need to
1000+
# share a workspace with any tools - maybe it could be excluded from the workspace?
1001+
# That will still require cloning the submodules the second you check the standard
1002+
# library, though...
1003+
# FIXME: Is there a way to avoid hard-coding the submodules required?
1004+
# WARNING: keep this in sync with the submodules hard-coded in bootstrap/lib.rs
1005+
submodules = [
1006+
"src/tools/rust-installer",
1007+
"src/tools/cargo",
1008+
"src/tools/rls",
1009+
"src/tools/miri",
1010+
"library/backtrace",
1011+
"library/stdarch"
1012+
]
10011013
filtered_submodules = []
10021014
submodules_names = []
10031015
for module in submodules:
1004-
# This is handled by native::Llvm in rustbuild, not here
1005-
if module.endswith("llvm-project"):
1006-
continue
10071016
check = self.check_submodule(module, slow_submodules)
10081017
filtered_submodules.append((module, check))
10091018
submodules_names.append(module)

src/bootstrap/check.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::config::TargetSelection;
77
use crate::tool::{prepare_tool_cargo, SourceType};
88
use crate::INTERNER;
99
use crate::{Compiler, Mode, Subcommand};
10-
use std::path::PathBuf;
10+
use std::path::{Path, PathBuf};
1111

1212
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1313
pub struct Std {
@@ -72,6 +72,8 @@ impl Step for Std {
7272
}
7373

7474
fn run(self, builder: &Builder<'_>) {
75+
builder.update_submodule(&Path::new("library").join("stdarch"));
76+
7577
let target = self.target;
7678
let compiler = builder.compiler(builder.top_stage, builder.config.build);
7779

src/bootstrap/compile.rs

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ impl Step for Std {
7979
return;
8080
}
8181

82+
builder.update_submodule(&Path::new("library").join("stdarch"));
83+
8284
let mut target_deps = builder.ensure(StartupObjects { compiler, target });
8385

8486
let compiler_to_use = builder.compiler_for(compiler.stage, compiler.host, target);

src/bootstrap/doc.rs

+29-10
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,17 @@ use crate::config::{Config, TargetSelection};
2222
use crate::tool::{self, prepare_tool_cargo, SourceType, Tool};
2323
use crate::util::symlink_dir;
2424

25+
macro_rules! submodule_helper {
26+
($path:expr, submodule) => {
27+
$path
28+
};
29+
($path:expr, submodule = $submodule:literal) => {
30+
$submodule
31+
};
32+
}
33+
2534
macro_rules! book {
26-
($($name:ident, $path:expr, $book_name:expr;)+) => {
35+
($($name:ident, $path:expr, $book_name:expr $(, submodule $(= $submodule:literal)? )? ;)+) => {
2736
$(
2837
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
2938
pub struct $name {
@@ -46,6 +55,10 @@ macro_rules! book {
4655
}
4756

4857
fn run(self, builder: &Builder<'_>) {
58+
$(
59+
let path = Path::new(submodule_helper!( $path, submodule $( = $submodule )? ));
60+
builder.update_submodule(&path);
61+
)?
4962
builder.ensure(RustbookSrc {
5063
target: self.target,
5164
name: INTERNER.intern_str($book_name),
@@ -59,13 +72,16 @@ macro_rules! book {
5972

6073
// NOTE: When adding a book here, make sure to ALSO build the book by
6174
// adding a build step in `src/bootstrap/builder.rs`!
75+
// NOTE: Make sure to add the corresponding submodule when adding a new book.
76+
// FIXME: Make checking for a submodule automatic somehow (maybe by having a list of all submodules
77+
// and checking against it?).
6278
book!(
63-
CargoBook, "src/tools/cargo/src/doc", "cargo";
64-
EditionGuide, "src/doc/edition-guide", "edition-guide";
65-
EmbeddedBook, "src/doc/embedded-book", "embedded-book";
66-
Nomicon, "src/doc/nomicon", "nomicon";
67-
Reference, "src/doc/reference", "reference";
68-
RustByExample, "src/doc/rust-by-example", "rust-by-example";
79+
CargoBook, "src/tools/cargo/src/doc", "cargo", submodule = "src/tools/cargo";
80+
EditionGuide, "src/doc/edition-guide", "edition-guide", submodule;
81+
EmbeddedBook, "src/doc/embedded-book", "embedded-book", submodule;
82+
Nomicon, "src/doc/nomicon", "nomicon", submodule;
83+
Reference, "src/doc/reference", "reference", submodule;
84+
RustByExample, "src/doc/rust-by-example", "rust-by-example", submodule;
6985
RustdocBook, "src/doc/rustdoc", "rustdoc";
7086
);
7187

@@ -197,22 +213,25 @@ impl Step for TheBook {
197213
/// * Index page
198214
/// * Redirect pages
199215
fn run(self, builder: &Builder<'_>) {
216+
let relative_path = Path::new("src").join("doc").join("book");
217+
builder.update_submodule(&relative_path);
218+
200219
let compiler = self.compiler;
201220
let target = self.target;
202221

203222
// build book
204223
builder.ensure(RustbookSrc {
205224
target,
206225
name: INTERNER.intern_str("book"),
207-
src: INTERNER.intern_path(builder.src.join("src/doc/book")),
226+
src: INTERNER.intern_path(builder.src.join(&relative_path)),
208227
});
209228

210229
// building older edition redirects
211230
for edition in &["first-edition", "second-edition", "2018-edition"] {
212231
builder.ensure(RustbookSrc {
213232
target,
214233
name: INTERNER.intern_string(format!("book/{}", edition)),
215-
src: INTERNER.intern_path(builder.src.join("src/doc/book").join(edition)),
234+
src: INTERNER.intern_path(builder.src.join(&relative_path).join(edition)),
216235
});
217236
}
218237

@@ -221,7 +240,7 @@ impl Step for TheBook {
221240

222241
// build the redirect pages
223242
builder.info(&format!("Documenting book redirect pages ({})", target));
224-
for file in t!(fs::read_dir(builder.src.join("src/doc/book/redirects"))) {
243+
for file in t!(fs::read_dir(builder.src.join(&relative_path).join("redirects"))) {
225244
let file = t!(file);
226245
let path = file.path();
227246
let path = path.to_str().unwrap();

src/bootstrap/lib.rs

+108-5
Original file line numberDiff line numberDiff line change
@@ -477,17 +477,120 @@ impl Build {
477477
slice::from_ref(&self.build.triple)
478478
}
479479

480+
// modified from `check_submodule` and `update_submodule` in bootstrap.py
481+
/// Given a path to the directory of a submodule, update it.
482+
///
483+
/// `relative_path` should be relative to the root of the git repository, not an absolute path.
484+
pub(crate) fn update_submodule(&self, relative_path: &Path) {
485+
fn dir_is_empty(dir: &Path) -> bool {
486+
t!(std::fs::read_dir(dir)).next().is_none()
487+
}
488+
489+
if !self.config.submodules {
490+
return;
491+
}
492+
493+
let absolute_path = self.config.src.join(relative_path);
494+
495+
// NOTE: The check for the empty directory is here because when running x.py the first time,
496+
// the submodule won't be checked out. Check it out now so we can build it.
497+
if !channel::GitInfo::new(false, relative_path).is_git() && !dir_is_empty(&absolute_path) {
498+
return;
499+
}
500+
501+
// check_submodule
502+
if self.config.fast_submodules {
503+
let checked_out_hash = output(
504+
Command::new("git").args(&["rev-parse", "HEAD"]).current_dir(&absolute_path),
505+
);
506+
// update_submodules
507+
let recorded = output(
508+
Command::new("git")
509+
.args(&["ls-tree", "HEAD"])
510+
.arg(relative_path)
511+
.current_dir(&self.config.src),
512+
);
513+
let actual_hash = recorded
514+
.split_whitespace()
515+
.nth(2)
516+
.unwrap_or_else(|| panic!("unexpected output `{}`", recorded));
517+
518+
// update_submodule
519+
if actual_hash == checked_out_hash.trim_end() {
520+
// already checked out
521+
return;
522+
}
523+
}
524+
525+
println!("Updating submodule {}", relative_path.display());
526+
self.run(
527+
Command::new("git")
528+
.args(&["submodule", "-q", "sync"])
529+
.arg(relative_path)
530+
.current_dir(&self.config.src),
531+
);
532+
533+
// Try passing `--progress` to start, then run git again without if that fails.
534+
let update = |progress: bool| {
535+
let mut git = Command::new("git");
536+
git.args(&["submodule", "update", "--init", "--recursive"]);
537+
if progress {
538+
git.arg("--progress");
539+
}
540+
git.arg(relative_path).current_dir(&self.config.src);
541+
git
542+
};
543+
// NOTE: doesn't use `try_run` because this shouldn't print an error if it fails.
544+
if !update(true).status().map_or(false, |status| status.success()) {
545+
self.run(&mut update(false));
546+
}
547+
548+
self.run(Command::new("git").args(&["reset", "-q", "--hard"]).current_dir(&absolute_path));
549+
self.run(Command::new("git").args(&["clean", "-qdfx"]).current_dir(absolute_path));
550+
}
551+
552+
/// If any submodule has been initialized already, sync it unconditionally.
553+
/// This avoids contributors checking in a submodule change by accident.
554+
pub fn maybe_update_submodules(&self) {
555+
// WARNING: keep this in sync with the submodules hard-coded in bootstrap.py
556+
const BOOTSTRAP_SUBMODULES: &[&str] = &[
557+
"src/tools/rust-installer",
558+
"src/tools/cargo",
559+
"src/tools/rls",
560+
"src/tools/miri",
561+
"library/backtrace",
562+
"library/stdarch",
563+
];
564+
// Avoid running git when there isn't a git checkout.
565+
if !self.config.submodules {
566+
return;
567+
}
568+
let output = output(
569+
Command::new("git")
570+
.args(&["config", "--file"])
571+
.arg(&self.config.src.join(".gitmodules"))
572+
.args(&["--get-regexp", "path"]),
573+
);
574+
for line in output.lines() {
575+
// Look for `submodule.$name.path = $path`
576+
// Sample output: `submodule.src/rust-installer.path src/tools/rust-installer`
577+
let submodule = Path::new(line.splitn(2, ' ').nth(1).unwrap());
578+
// avoid updating submodules twice
579+
if !BOOTSTRAP_SUBMODULES.iter().any(|&p| Path::new(p) == submodule)
580+
&& channel::GitInfo::new(false, submodule).is_git()
581+
{
582+
self.update_submodule(submodule);
583+
}
584+
}
585+
}
586+
480587
/// Executes the entire build, as configured by the flags and configuration.
481588
pub fn build(&mut self) {
482589
unsafe {
483590
job::setup(self);
484591
}
485592

486-
// If the LLVM submodule has been initialized already, sync it unconditionally. This avoids
487-
// contributors checking in a submodule change by accident.
488-
if self.in_tree_llvm_info.is_git() {
489-
native::update_llvm_submodule(self);
490-
}
593+
self.maybe_update_submodules();
491594

492595
if let Subcommand::Format { check, paths } = &self.config.cmd {
493596
return format::format(self, *check, &paths);

src/bootstrap/native.rs

+2-84
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use build_helper::{output, t};
2121
use crate::builder::{Builder, RunConfig, ShouldRun, Step};
2222
use crate::config::TargetSelection;
2323
use crate::util::{self, exe};
24-
use crate::{Build, GitRepo};
24+
use crate::GitRepo;
2525
use build_helper::up_to_date;
2626

2727
pub struct Meta {
@@ -91,86 +91,6 @@ pub fn prebuilt_llvm_config(
9191
Err(Meta { stamp, build_llvm_config, out_dir, root: root.into() })
9292
}
9393

94-
// modified from `check_submodule` and `update_submodule` in bootstrap.py
95-
pub(crate) fn update_llvm_submodule(build: &Build) {
96-
let llvm_project = &Path::new("src").join("llvm-project");
97-
98-
fn dir_is_empty(dir: &Path) -> bool {
99-
t!(std::fs::read_dir(dir)).next().is_none()
100-
}
101-
102-
if !build.config.submodules {
103-
return;
104-
}
105-
106-
// NOTE: The check for the empty directory is here because when running x.py
107-
// the first time, the llvm submodule won't be checked out. Check it out
108-
// now so we can build it.
109-
if !build.in_tree_llvm_info.is_git() && !dir_is_empty(&build.config.src.join(llvm_project)) {
110-
return;
111-
}
112-
113-
// check_submodule
114-
if build.config.fast_submodules {
115-
let checked_out_hash = output(
116-
Command::new("git")
117-
.args(&["rev-parse", "HEAD"])
118-
.current_dir(build.config.src.join(llvm_project)),
119-
);
120-
// update_submodules
121-
let recorded = output(
122-
Command::new("git")
123-
.args(&["ls-tree", "HEAD"])
124-
.arg(llvm_project)
125-
.current_dir(&build.config.src),
126-
);
127-
let actual_hash = recorded
128-
.split_whitespace()
129-
.nth(2)
130-
.unwrap_or_else(|| panic!("unexpected output `{}`", recorded));
131-
132-
// update_submodule
133-
if actual_hash == checked_out_hash.trim_end() {
134-
// already checked out
135-
return;
136-
}
137-
}
138-
139-
println!("Updating submodule {}", llvm_project.display());
140-
build.run(
141-
Command::new("git")
142-
.args(&["submodule", "-q", "sync"])
143-
.arg(llvm_project)
144-
.current_dir(&build.config.src),
145-
);
146-
147-
// Try passing `--progress` to start, then run git again without if that fails.
148-
let update = |progress: bool| {
149-
let mut git = Command::new("git");
150-
git.args(&["submodule", "update", "--init", "--recursive"]);
151-
if progress {
152-
git.arg("--progress");
153-
}
154-
git.arg(llvm_project).current_dir(&build.config.src);
155-
git
156-
};
157-
// NOTE: doesn't use `try_run` because this shouldn't print an error if it fails.
158-
if !update(true).status().map_or(false, |status| status.success()) {
159-
build.run(&mut update(false));
160-
}
161-
162-
build.run(
163-
Command::new("git")
164-
.args(&["reset", "-q", "--hard"])
165-
.current_dir(build.config.src.join(llvm_project)),
166-
);
167-
build.run(
168-
Command::new("git")
169-
.args(&["clean", "-qdfx"])
170-
.current_dir(build.config.src.join(llvm_project)),
171-
);
172-
}
173-
17494
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
17595
pub struct Llvm {
17696
pub target: TargetSelection,
@@ -208,9 +128,7 @@ impl Step for Llvm {
208128
Err(m) => m,
209129
};
210130

211-
if !builder.config.dry_run {
212-
update_llvm_submodule(builder);
213-
}
131+
builder.update_submodule(&Path::new("src").join("llvm-project"));
214132
if builder.config.llvm_link_shared
215133
&& (target.contains("windows") || target.contains("apple-darwin"))
216134
{

0 commit comments

Comments
 (0)