1#![warn(missing_docs)]
3#![allow(clippy::needless_borrows_for_generic_args)]
6
7use std::collections::hash_map::DefaultHasher;
8use std::env;
9use std::ffi::{OsStr, OsString};
10use std::fs;
11use std::hash::{Hash, Hasher};
12use std::path::{Path, PathBuf};
13use std::process::Command;
14
15use anyhow::{bail, Context, Result};
16use tempfile::TempDir;
17use toml::{Table, Value};
18use walkdir::WalkDir;
19
20const DEFAULT_SYSROOT_PROFILE: &str = "custom_sysroot";
22
23fn rustc_sysroot_dir(mut rustc: Command) -> Result<PathBuf> {
24 let output = rustc
25 .args(["--print", "sysroot"])
26 .output()
27 .context("failed to determine sysroot")?;
28 if !output.status.success() {
29 bail!(
30 "failed to determine sysroot; rustc said:\n{}",
31 String::from_utf8_lossy(&output.stderr).trim_end()
32 );
33 }
34 let sysroot =
35 std::str::from_utf8(&output.stdout).context("sysroot folder is not valid UTF-8")?;
36 let sysroot = PathBuf::from(sysroot.trim_end_matches('\n'));
37 if !sysroot.is_dir() {
38 bail!(
39 "sysroot directory `{}` is not a directory",
40 sysroot.display()
41 );
42 }
43 Ok(sysroot)
44}
45
46pub fn rustc_sysroot_src(rustc: Command) -> Result<PathBuf> {
48 let sysroot = rustc_sysroot_dir(rustc)?;
49 let rustc_src = sysroot
50 .join("lib")
51 .join("rustlib")
52 .join("src")
53 .join("rust")
54 .join("library");
55 let rustc_src = rustc_src.canonicalize().unwrap_or(rustc_src);
58 Ok(rustc_src)
59}
60
61pub fn encode_rustflags(flags: &[OsString]) -> OsString {
63 let mut res = OsString::new();
64 for flag in flags {
65 if !res.is_empty() {
66 res.push(OsStr::new("\x1f"));
67 }
68 let flag = flag.to_str().expect("rustflags must be valid UTF-8");
70 if flag.contains('\x1f') {
71 panic!("rustflags must not contain `\\x1f` separator");
72 }
73 res.push(flag);
74 }
75 res
76}
77
78#[cfg(unix)]
80fn make_writeable(p: &Path) -> Result<()> {
81 use std::fs::Permissions;
84 use std::os::unix::fs::PermissionsExt;
85
86 let perms = fs::metadata(p)?.permissions();
87 let perms = Permissions::from_mode(perms.mode() | 0o600); fs::set_permissions(p, perms).context("cannot set permissions")?;
89 Ok(())
90}
91
92#[cfg(not(unix))]
94fn make_writeable(p: &Path) -> Result<()> {
95 let mut perms = fs::metadata(p)?.permissions();
96 perms.set_readonly(false);
97 fs::set_permissions(p, perms).context("cannot set permissions")?;
98 Ok(())
99}
100
101fn hash_recursive(path: &Path, hasher: &mut DefaultHasher) -> Result<()> {
103 for entry in WalkDir::new(path)
105 .follow_links(true)
106 .sort_by_file_name()
107 .into_iter()
108 {
109 let entry = entry?;
110 if entry.file_type().is_dir() {
113 continue;
114 }
115 let meta = entry.metadata()?;
116 meta.modified()?.hash(hasher);
119 meta.len().hash(hasher);
120 }
121 Ok(())
122}
123
124#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
126pub enum BuildMode {
127 Build,
130 Check,
133}
134
135impl BuildMode {
136 pub fn as_str(&self) -> &str {
138 use BuildMode::*;
139 match self {
140 Build => "build",
141 Check => "check",
142 }
143 }
144}
145
146#[derive(Clone, Debug, PartialEq, Eq, Hash)]
148pub enum SysrootConfig {
149 NoStd,
151 WithStd {
153 std_features: Vec<String>,
155 },
156}
157
158pub struct SysrootBuilder<'a> {
160 sysroot_dir: PathBuf,
161 target: OsString,
162 config: SysrootConfig,
163 mode: BuildMode,
164 rustflags: Vec<OsString>,
165 cargo: Option<Command>,
166 rustc_version: Option<rustc_version::VersionMeta>,
167 when_build_required: Option<Box<dyn FnOnce() + 'a>>,
168 keep_build_dir: bool,
169}
170
171#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
174pub enum SysrootStatus {
175 AlreadyCached,
177 SysrootBuilt,
179}
180
181const HASH_FILE_NAME: &str = ".rustc-build-sysroot-hash";
183
184impl<'a> SysrootBuilder<'a> {
185 pub fn new(sysroot_dir: &Path, target: impl Into<OsString>) -> Self {
188 let default_flags = &[
189 "-Zforce-unstable-if-unmarked",
191 "-Aunexpected_cfgs",
193 ];
194 SysrootBuilder {
195 sysroot_dir: sysroot_dir.to_owned(),
196 target: target.into(),
197 config: SysrootConfig::WithStd {
198 std_features: vec![],
199 },
200 mode: BuildMode::Build,
201 rustflags: default_flags.iter().map(Into::into).collect(),
202 cargo: None,
203 rustc_version: None,
204 when_build_required: None,
205 keep_build_dir: false,
206 }
207 }
208
209 pub fn build_mode(mut self, build_mode: BuildMode) -> Self {
211 self.mode = build_mode;
212 self
213 }
214
215 pub fn sysroot_config(mut self, sysroot_config: SysrootConfig) -> Self {
217 self.config = sysroot_config;
218 self
219 }
220
221 pub fn rustflag(mut self, rustflag: impl Into<OsString>) -> Self {
223 self.rustflags.push(rustflag.into());
224 self
225 }
226
227 pub fn rustflags(mut self, rustflags: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
229 self.rustflags.extend(rustflags.into_iter().map(Into::into));
230 self
231 }
232
233 pub fn cargo(mut self, cargo: Command) -> Self {
238 self.cargo = Some(cargo);
239 self
240 }
241
242 pub fn rustc_version(mut self, rustc_version: rustc_version::VersionMeta) -> Self {
244 self.rustc_version = Some(rustc_version);
245 self
246 }
247
248 pub fn when_build_required(mut self, when_build_required: impl FnOnce() + 'a) -> Self {
251 self.when_build_required = Some(Box::new(when_build_required));
252 self
253 }
254
255 pub fn keep_build_dir(mut self, keep: bool) -> Self {
257 self.keep_build_dir = keep;
258 self
259 }
260
261 fn target_name(&self) -> &OsStr {
265 let path = Path::new(&self.target);
266 if path.extension().and_then(OsStr::to_str) == Some("json") {
267 path.file_stem().unwrap()
270 } else {
271 &self.target
274 }
275 }
276
277 fn sysroot_target_dir(&self) -> PathBuf {
278 self.sysroot_dir
279 .join("lib")
280 .join("rustlib")
281 .join(self.target_name())
282 }
283
284 fn sysroot_compute_hash(
286 &self,
287 src_dir: &Path,
288 rustc_version: &rustc_version::VersionMeta,
289 ) -> Result<u64> {
290 let mut hasher = DefaultHasher::new();
291
292 src_dir.hash(&mut hasher);
293 hash_recursive(src_dir, &mut hasher)?;
294 self.config.hash(&mut hasher);
295 self.mode.hash(&mut hasher);
296 self.rustflags.hash(&mut hasher);
297 rustc_version.hash(&mut hasher);
298
299 Ok(hasher.finish())
300 }
301
302 fn sysroot_read_hash(&self) -> Option<u64> {
303 let hash_file = self.sysroot_target_dir().join(HASH_FILE_NAME);
304 let hash = fs::read_to_string(&hash_file).ok()?;
305 hash.parse().ok()
306 }
307
308 fn gen_manifest(&self, src_dir: &Path) -> String {
310 let crates = match &self.config {
311 SysrootConfig::NoStd => format!(
312 r#"
313 [dependencies.core]
314 path = {src_dir_core:?}
315 [dependencies.alloc]
316 path = {src_dir_alloc:?}
317 [dependencies.compiler_builtins]
318 path = {src_dir_builtins:?}
319 features = ["compiler-builtins", "mem"]
320 "#,
321 src_dir_core = src_dir.join("core"),
322 src_dir_alloc = src_dir.join("alloc"),
323 src_dir_builtins = src_dir.join("compiler-builtins").join("compiler-builtins"),
324 ),
325 SysrootConfig::WithStd { std_features } => format!(
326 r#"
327 [dependencies.std]
328 features = {std_features:?}
329 path = {src_dir_std:?}
330 [dependencies.sysroot]
331 path = {src_dir_sysroot:?}
332 "#,
333 std_features = std_features,
334 src_dir_std = src_dir.join("std"),
335 src_dir_sysroot = src_dir.join("sysroot"),
336 ),
337 };
338
339 let unneeded_patches = match &self.config {
346 SysrootConfig::NoStd => &["rustc-std-workspace-alloc", "rustc-std-workspace-std"][..],
347 SysrootConfig::WithStd { .. } => &[][..],
348 };
349
350 let mut patches = extract_patches(src_dir);
351 for (repo, repo_patches) in &mut patches {
352 let repo_patches = repo_patches
353 .as_table_mut()
354 .unwrap_or_else(|| panic!("source `{}` is not a table", repo));
355
356 for krate in unneeded_patches {
358 repo_patches.remove(*krate);
359 }
360
361 for (krate, patch) in repo_patches {
363 if let Some(path) = patch.get_mut("path") {
364 let curr_path = path
365 .as_str()
366 .unwrap_or_else(|| panic!("`{}.path` is not a string", krate));
367
368 *path = Value::String(src_dir.join(curr_path).display().to_string());
369 }
370 }
371 }
372
373 let mut table: Table = toml::from_str(&format!(
374 r#"
375 [package]
376 authors = ["rustc-build-sysroot"]
377 name = "custom-local-sysroot"
378 version = "0.0.0"
379 edition = "2018"
380
381 [lib]
382 # empty dummy, just so that things are being built
383 path = "lib.rs"
384
385 [profile.{DEFAULT_SYSROOT_PROFILE}]
386 # We inherit from the local release profile, but then overwrite some
387 # settings to ensure we still get a working sysroot.
388 inherits = "release"
389 panic = 'unwind'
390
391 [lints.cargo]
392 # The dependencies are not actually used.
393 unused_dependencies = "allow"
394
395 {crates}
396 "#
397 ))
398 .expect("failed to parse toml");
399
400 table.insert("patch".to_owned(), patches.into());
401 toml::to_string(&table).expect("failed to serialize to toml")
402 }
403
404 pub fn build_from_source(mut self, src_dir: &Path) -> Result<SysrootStatus> {
408 if !src_dir.join("std").join("Cargo.toml").exists() {
410 bail!(
411 "{:?} does not seem to be a rust library source folder: `std/Cargo.toml` not found",
412 src_dir
413 );
414 }
415 let sysroot_target_dir = self.sysroot_target_dir();
416 let target_name = self.target_name().to_owned();
417 let cargo = self.cargo.take().unwrap_or_else(|| {
418 Command::new(env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")))
419 });
420 let rustc_version = match self.rustc_version.take() {
421 Some(v) => v,
422 None => rustc_version::version_meta()?,
423 };
424
425 let cur_hash = self.sysroot_compute_hash(src_dir, &rustc_version)?;
427 if self.sysroot_read_hash() == Some(cur_hash) {
428 return Ok(SysrootStatus::AlreadyCached);
430 }
431
432 if let Some(when_build_required) = self.when_build_required.take() {
434 when_build_required();
435 }
436
437 fs::create_dir_all(&sysroot_target_dir.parent().unwrap())
440 .context("failed to create target directory")?;
441 let unstaging_dir =
447 TempDir::new_in(&self.sysroot_dir).context("failed to create un-staging dir")?;
448 let _ = fs::rename(&sysroot_target_dir, &unstaging_dir); let build_dir = tempfile::Builder::new()
452 .prefix("rustc-build-sysroot-")
453 .disable_cleanup(self.keep_build_dir)
454 .tempdir()
455 .context("failed to create tempdir")?;
456 let lock_file = build_dir.path().join("Cargo.lock");
458 let lock_file_src = {
459 let new_lock_file_name = src_dir.join("Cargo.lock");
462 if new_lock_file_name.exists() {
463 new_lock_file_name
464 } else {
465 src_dir
467 .parent()
468 .expect("src_dir must have a parent")
469 .join("Cargo.lock")
470 }
471 };
472 fs::copy(lock_file_src, &lock_file)
473 .context("failed to copy lockfile from sysroot source")?;
474 make_writeable(&lock_file).context("failed to make lockfile writeable")?;
475 let manifest_file = build_dir.path().join("Cargo.toml");
477 let manifest = self.gen_manifest(src_dir);
478 fs::write(&manifest_file, manifest.as_bytes()).context("failed to write manifest file")?;
479 let lib_file = build_dir.path().join("lib.rs");
481 let lib = match self.config {
482 SysrootConfig::NoStd => r#"#![no_std]"#,
483 SysrootConfig::WithStd { .. } => "",
484 };
485 fs::write(&lib_file, lib.as_bytes()).context("failed to write lib file")?;
486
487 let mut cmd = cargo;
489 cmd.arg(self.mode.as_str());
490 cmd.arg("--profile");
491 cmd.arg(DEFAULT_SYSROOT_PROFILE);
492 cmd.arg("--manifest-path");
493 cmd.arg(&manifest_file);
494 cmd.arg("--target");
495 cmd.arg(&self.target);
496 cmd.env("CARGO_ENCODED_RUSTFLAGS", encode_rustflags(&self.rustflags));
498 let build_target_dir = build_dir.path().join("target");
501 cmd.env("CARGO_TARGET_DIR", &build_target_dir);
502 cmd.env("CARGO_BUILD_BUILD_DIR", &build_target_dir);
503 cmd.env("__CARGO_DEFAULT_LIB_METADATA", "rustc-build-sysroot");
507
508 let output = cmd
509 .output()
510 .context("failed to execute cargo for sysroot build")?;
511 if !output.status.success() {
512 let stderr = String::from_utf8_lossy(&output.stderr);
513 if stderr.is_empty() {
514 bail!("sysroot build failed\ncommand: {cmd:?}");
515 } else {
516 bail!("sysroot build failed\ncommand: {cmd:?}\nstderr:\n{stderr}");
517 }
518 }
519
520 fs::create_dir_all(&self.sysroot_dir).context("failed to create sysroot dir")?; let staging_dir =
527 TempDir::new_in(&self.sysroot_dir).context("failed to create staging dir")?;
528 let staging_lib_dir = staging_dir.path().join("lib");
530 fs::create_dir(&staging_lib_dir).context("failed to create staging/lib dir")?;
531 let out_dir = build_target_dir
532 .join(&target_name)
533 .join(DEFAULT_SYSROOT_PROFILE);
534 if out_dir.join("deps").exists() {
535 copy_files(&out_dir.join("deps"), &staging_lib_dir)
537 .context("failed to copy cargo out dir (old layout)")?;
538 } else {
539 for_each_dir(&out_dir.join("build"), |dir| {
541 for_each_dir(dir, |dir| copy_files(&dir.join("out"), &staging_lib_dir))
542 })
543 .context("failed to copy cargo out dir (new layout)")?;
544 }
545
546 fs::write(
548 staging_dir.path().join(HASH_FILE_NAME),
549 cur_hash.to_string().as_bytes(),
550 )
551 .context("failed to write hash file")?;
552
553 if fs::rename(staging_dir.path(), sysroot_target_dir).is_err() {
556 if self.sysroot_read_hash() != Some(cur_hash) {
558 bail!("detected a concurrent sysroot build with different settings");
559 }
560 }
561
562 Ok(SysrootStatus::SysrootBuilt)
563 }
564}
565
566fn copy_files(from: &Path, to: &Path) -> Result<()> {
568 for entry in fs::read_dir(from)? {
569 let entry = entry?;
570 let file_type = entry.file_type()?;
571 assert!(
572 file_type.is_file(),
573 "cargo out dir must only contain regular files, found {:?} which has file type {:?}",
574 entry.path(),
575 file_type
576 );
577 fs::copy(&entry.path(), to.join(entry.file_name()))?;
578 }
579 Ok(())
580}
581
582fn for_each_dir(path: &Path, f: impl Fn(&Path) -> Result<()>) -> Result<()> {
584 for entry in fs::read_dir(path)? {
585 let entry = entry?;
586 if !entry.file_type()?.is_dir() {
587 continue;
588 }
589 f(&entry.path())?;
590 }
591 Ok(())
592}
593
594fn extract_patches(src_dir: &Path) -> Table {
596 let workspace_manifest = src_dir.join("Cargo.toml");
598 let f = fs::read_to_string(&workspace_manifest).unwrap_or_else(|e| {
599 panic!(
600 "unable to read workspace manifest at `{}`: {}",
601 workspace_manifest.display(),
602 e
603 )
604 });
605 let mut t: Table = toml::from_str(&f).expect("invalid sysroot workspace Cargo.toml");
606 t.remove("patch")
608 .map(|v| match v {
609 Value::Table(map) => map,
610 _ => panic!("`patch` is not a table"),
611 })
612 .unwrap_or_default()
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 fn setup_manifest_test(config: SysrootConfig) -> (Table, TempDir) {
621 let workspace_toml = r#"
622 [workspace]
623 foo = "bar"
624
625 [patch.crates-io]
626 foo = { path = "bar" }
627 rustc-std-workspace-core = { path = "core" }
628 rustc-std-workspace-alloc = { path = "alloc" }
629 rustc-std-workspace-std = { path = "std" }
630 "#;
631
632 let sysroot = tempfile::tempdir().unwrap(); let src_dir = tempfile::tempdir().unwrap();
634 let f = src_dir.path().join("Cargo.toml");
635 fs::write(&f, workspace_toml).unwrap();
636
637 let builder = SysrootBuilder::new(sysroot.path(), "sometarget").sysroot_config(config);
638 let manifest: Table = toml::from_str(&builder.gen_manifest(src_dir.path())).unwrap();
639 (manifest, src_dir)
640 }
641
642 #[track_caller]
644 fn check_patch_path(manifest: &Table, krate: &str, path: Option<&Path>) {
645 let patches = &manifest["patch"]["crates-io"];
646 match path {
647 Some(path) => assert_eq!(
648 &patches[krate]["path"].as_str().unwrap(),
649 &path.to_str().unwrap()
650 ),
651 None => assert!(patches.get(krate).is_none()),
652 }
653 }
654
655 #[test]
656 fn check_patches_no_std() {
657 let (manifest, src_dir) = setup_manifest_test(SysrootConfig::NoStd);
658
659 check_patch_path(&manifest, "foo", Some(&src_dir.path().join("bar")));
661 check_patch_path(
662 &manifest,
663 "rustc-std-workspace-core",
664 Some(&src_dir.path().join("core")),
665 );
666
667 check_patch_path(&manifest, "rustc-std-workspace-alloc", None);
669 check_patch_path(&manifest, "rustc-std-workspace-std", None);
670 }
671
672 #[test]
673 fn check_patches_with_std() {
674 let (manifest, src_dir) = setup_manifest_test(SysrootConfig::WithStd {
675 std_features: Vec::new(),
676 });
677
678 check_patch_path(&manifest, "foo", Some(&src_dir.path().join("bar")));
680 check_patch_path(
681 &manifest,
682 "rustc-std-workspace-core",
683 Some(&src_dir.path().join("core")),
684 );
685 check_patch_path(
686 &manifest,
687 "rustc-std-workspace-alloc",
688 Some(&src_dir.path().join("alloc")),
689 );
690 check_patch_path(
691 &manifest,
692 "rustc-std-workspace-std",
693 Some(&src_dir.path().join("std")),
694 );
695 }
696}