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}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
173pub enum SysrootStatus {
174 AlreadyCached,
176 SysrootBuilt,
178}
179
180const HASH_FILE_NAME: &str = ".rustc-build-sysroot-hash";
182
183impl<'a> SysrootBuilder<'a> {
184 pub fn new(sysroot_dir: &Path, target: impl Into<OsString>) -> Self {
187 let default_flags = &[
188 "-Zforce-unstable-if-unmarked",
190 "-Aunexpected_cfgs",
192 ];
193 SysrootBuilder {
194 sysroot_dir: sysroot_dir.to_owned(),
195 target: target.into(),
196 config: SysrootConfig::WithStd {
197 std_features: vec![],
198 },
199 mode: BuildMode::Build,
200 rustflags: default_flags.iter().map(Into::into).collect(),
201 cargo: None,
202 rustc_version: None,
203 when_build_required: None,
204 }
205 }
206
207 pub fn build_mode(mut self, build_mode: BuildMode) -> Self {
209 self.mode = build_mode;
210 self
211 }
212
213 pub fn sysroot_config(mut self, sysroot_config: SysrootConfig) -> Self {
215 self.config = sysroot_config;
216 self
217 }
218
219 pub fn rustflag(mut self, rustflag: impl Into<OsString>) -> Self {
221 self.rustflags.push(rustflag.into());
222 self
223 }
224
225 pub fn rustflags(mut self, rustflags: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
227 self.rustflags.extend(rustflags.into_iter().map(Into::into));
228 self
229 }
230
231 pub fn cargo(mut self, cargo: Command) -> Self {
236 self.cargo = Some(cargo);
237 self
238 }
239
240 pub fn rustc_version(mut self, rustc_version: rustc_version::VersionMeta) -> Self {
242 self.rustc_version = Some(rustc_version);
243 self
244 }
245
246 pub fn when_build_required(mut self, when_build_required: impl FnOnce() + 'a) -> Self {
249 self.when_build_required = Some(Box::new(when_build_required));
250 self
251 }
252
253 fn target_name(&self) -> &OsStr {
257 let path = Path::new(&self.target);
258 if path.extension().and_then(OsStr::to_str) == Some("json") {
259 path.file_stem().unwrap()
262 } else {
263 &self.target
266 }
267 }
268
269 fn sysroot_target_dir(&self) -> PathBuf {
270 self.sysroot_dir
271 .join("lib")
272 .join("rustlib")
273 .join(self.target_name())
274 }
275
276 fn sysroot_compute_hash(
278 &self,
279 src_dir: &Path,
280 rustc_version: &rustc_version::VersionMeta,
281 ) -> Result<u64> {
282 let mut hasher = DefaultHasher::new();
283
284 src_dir.hash(&mut hasher);
285 hash_recursive(src_dir, &mut hasher)?;
286 self.config.hash(&mut hasher);
287 self.mode.hash(&mut hasher);
288 self.rustflags.hash(&mut hasher);
289 rustc_version.hash(&mut hasher);
290
291 Ok(hasher.finish())
292 }
293
294 fn sysroot_read_hash(&self) -> Option<u64> {
295 let hash_file = self.sysroot_target_dir().join(HASH_FILE_NAME);
296 let hash = fs::read_to_string(&hash_file).ok()?;
297 hash.parse().ok()
298 }
299
300 fn gen_manifest(&self, src_dir: &Path) -> String {
302 let crates = match &self.config {
303 SysrootConfig::NoStd => format!(
304 r#"
305 [dependencies.core]
306 path = {src_dir_core:?}
307 [dependencies.alloc]
308 path = {src_dir_alloc:?}
309 [dependencies.compiler_builtins]
310 path = {src_dir_builtins:?}
311 features = ["compiler-builtins", "mem"]
312 "#,
313 src_dir_core = src_dir.join("core"),
314 src_dir_alloc = src_dir.join("alloc"),
315 src_dir_builtins = src_dir.join("compiler-builtins").join("compiler-builtins"),
316 ),
317 SysrootConfig::WithStd { std_features } => format!(
318 r#"
319 [dependencies.std]
320 features = {std_features:?}
321 path = {src_dir_std:?}
322 [dependencies.sysroot]
323 path = {src_dir_sysroot:?}
324 "#,
325 std_features = std_features,
326 src_dir_std = src_dir.join("std"),
327 src_dir_sysroot = src_dir.join("sysroot"),
328 ),
329 };
330
331 let unneeded_patches = match &self.config {
338 SysrootConfig::NoStd => &["rustc-std-workspace-alloc", "rustc-std-workspace-std"][..],
339 SysrootConfig::WithStd { .. } => &[][..],
340 };
341
342 let mut patches = extract_patches(src_dir);
343 for (repo, repo_patches) in &mut patches {
344 let repo_patches = repo_patches
345 .as_table_mut()
346 .unwrap_or_else(|| panic!("source `{}` is not a table", repo));
347
348 for krate in unneeded_patches {
350 repo_patches.remove(*krate);
351 }
352
353 for (krate, patch) in repo_patches {
355 if let Some(path) = patch.get_mut("path") {
356 let curr_path = path
357 .as_str()
358 .unwrap_or_else(|| panic!("`{}.path` is not a string", krate));
359
360 *path = Value::String(src_dir.join(curr_path).display().to_string());
361 }
362 }
363 }
364
365 let mut table: Table = toml::from_str(&format!(
366 r#"
367 [package]
368 authors = ["rustc-build-sysroot"]
369 name = "custom-local-sysroot"
370 version = "0.0.0"
371 edition = "2018"
372
373 [lib]
374 # empty dummy, just so that things are being built
375 path = "lib.rs"
376
377 [profile.{DEFAULT_SYSROOT_PROFILE}]
378 # We inherit from the local release profile, but then overwrite some
379 # settings to ensure we still get a working sysroot.
380 inherits = "release"
381 panic = 'unwind'
382
383 {crates}
384 "#
385 ))
386 .expect("failed to parse toml");
387
388 table.insert("patch".to_owned(), patches.into());
389 toml::to_string(&table).expect("failed to serialize to toml")
390 }
391
392 pub fn build_from_source(mut self, src_dir: &Path) -> Result<SysrootStatus> {
396 if !src_dir.join("std").join("Cargo.toml").exists() {
398 bail!(
399 "{:?} does not seem to be a rust library source folder: `std/Cargo.toml` not found",
400 src_dir
401 );
402 }
403 let sysroot_target_dir = self.sysroot_target_dir();
404 let target_name = self.target_name().to_owned();
405 let cargo = self.cargo.take().unwrap_or_else(|| {
406 Command::new(env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")))
407 });
408 let rustc_version = match self.rustc_version.take() {
409 Some(v) => v,
410 None => rustc_version::version_meta()?,
411 };
412
413 let cur_hash = self.sysroot_compute_hash(src_dir, &rustc_version)?;
415 if self.sysroot_read_hash() == Some(cur_hash) {
416 return Ok(SysrootStatus::AlreadyCached);
418 }
419
420 if let Some(when_build_required) = self.when_build_required.take() {
422 when_build_required();
423 }
424
425 fs::create_dir_all(&sysroot_target_dir.parent().unwrap())
428 .context("failed to create target directory")?;
429 let unstaging_dir =
435 TempDir::new_in(&self.sysroot_dir).context("failed to create un-staging dir")?;
436 let _ = fs::rename(&sysroot_target_dir, &unstaging_dir); let build_dir = TempDir::new().context("failed to create tempdir")?;
440 let lock_file = build_dir.path().join("Cargo.lock");
442 let lock_file_src = {
443 let new_lock_file_name = src_dir.join("Cargo.lock");
446 if new_lock_file_name.exists() {
447 new_lock_file_name
448 } else {
449 src_dir
451 .parent()
452 .expect("src_dir must have a parent")
453 .join("Cargo.lock")
454 }
455 };
456 fs::copy(lock_file_src, &lock_file)
457 .context("failed to copy lockfile from sysroot source")?;
458 make_writeable(&lock_file).context("failed to make lockfile writeable")?;
459 let manifest_file = build_dir.path().join("Cargo.toml");
461 let manifest = self.gen_manifest(src_dir);
462 fs::write(&manifest_file, manifest.as_bytes()).context("failed to write manifest file")?;
463 let lib_file = build_dir.path().join("lib.rs");
465 let lib = match self.config {
466 SysrootConfig::NoStd => r#"#![no_std]"#,
467 SysrootConfig::WithStd { .. } => "",
468 };
469 fs::write(&lib_file, lib.as_bytes()).context("failed to write lib file")?;
470
471 let mut cmd = cargo;
473 cmd.arg(self.mode.as_str());
474 cmd.arg("--profile");
475 cmd.arg(DEFAULT_SYSROOT_PROFILE);
476 cmd.arg("--manifest-path");
477 cmd.arg(&manifest_file);
478 cmd.arg("--target");
479 cmd.arg(&self.target);
480 cmd.env("CARGO_ENCODED_RUSTFLAGS", encode_rustflags(&self.rustflags));
482 let build_target_dir = build_dir.path().join("target");
485 cmd.env("CARGO_TARGET_DIR", &build_target_dir);
486 cmd.env("CARGO_BUILD_BUILD_DIR", &build_target_dir);
487 cmd.env("__CARGO_DEFAULT_LIB_METADATA", "rustc-build-sysroot");
491
492 let output = cmd
493 .output()
494 .context("failed to execute cargo for sysroot build")?;
495 if !output.status.success() {
496 let stderr = String::from_utf8_lossy(&output.stderr);
497 if stderr.is_empty() {
498 bail!("sysroot build failed");
499 } else {
500 bail!("sysroot build failed; stderr:\n{}", stderr);
501 }
502 }
503
504 fs::create_dir_all(&self.sysroot_dir).context("failed to create sysroot dir")?; let staging_dir =
511 TempDir::new_in(&self.sysroot_dir).context("failed to create staging dir")?;
512 let staging_lib_dir = staging_dir.path().join("lib");
514 fs::create_dir(&staging_lib_dir).context("failed to create staging/lib dir")?;
515 let out_dir = build_target_dir
516 .join(&target_name)
517 .join(DEFAULT_SYSROOT_PROFILE);
518 if out_dir.join("deps").exists() {
519 copy_files(&out_dir.join("deps"), &staging_lib_dir)
521 .context("failed to copy cargo out dir (old layout)")?;
522 } else {
523 for_each_dir(&out_dir.join("build"), |dir| {
525 for_each_dir(dir, |dir| copy_files(&dir.join("out"), &staging_lib_dir))
526 })
527 .context("failed to copy cargo out dir (new layout)")?;
528 }
529
530 fs::write(
532 staging_dir.path().join(HASH_FILE_NAME),
533 cur_hash.to_string().as_bytes(),
534 )
535 .context("failed to write hash file")?;
536
537 if fs::rename(staging_dir.path(), sysroot_target_dir).is_err() {
540 if self.sysroot_read_hash() != Some(cur_hash) {
542 bail!("detected a concurrent sysroot build with different settings");
543 }
544 }
545
546 Ok(SysrootStatus::SysrootBuilt)
547 }
548}
549
550fn copy_files(from: &Path, to: &Path) -> Result<()> {
552 for entry in fs::read_dir(from)? {
553 let entry = entry?;
554 assert!(
555 entry.file_type()?.is_file(),
556 "cargo out dir must not contain directories"
557 );
558 fs::copy(&entry.path(), to.join(entry.file_name()))?;
559 }
560 Ok(())
561}
562
563fn for_each_dir(path: &Path, f: impl Fn(&Path) -> Result<()>) -> Result<()> {
565 for entry in fs::read_dir(path)? {
566 let entry = entry?;
567 if !entry.file_type()?.is_dir() {
568 continue;
569 }
570 f(&entry.path())?;
571 }
572 Ok(())
573}
574
575fn extract_patches(src_dir: &Path) -> Table {
577 let workspace_manifest = src_dir.join("Cargo.toml");
579 let f = fs::read_to_string(&workspace_manifest).unwrap_or_else(|e| {
580 panic!(
581 "unable to read workspace manifest at `{}`: {}",
582 workspace_manifest.display(),
583 e
584 )
585 });
586 let mut t: Table = toml::from_str(&f).expect("invalid sysroot workspace Cargo.toml");
587 t.remove("patch")
589 .map(|v| match v {
590 Value::Table(map) => map,
591 _ => panic!("`patch` is not a table"),
592 })
593 .unwrap_or_default()
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599
600 fn setup_manifest_test(config: SysrootConfig) -> (Table, TempDir) {
602 let workspace_toml = r#"
603 [workspace]
604 foo = "bar"
605
606 [patch.crates-io]
607 foo = { path = "bar" }
608 rustc-std-workspace-core = { path = "core" }
609 rustc-std-workspace-alloc = { path = "alloc" }
610 rustc-std-workspace-std = { path = "std" }
611 "#;
612
613 let sysroot = tempfile::tempdir().unwrap(); let src_dir = tempfile::tempdir().unwrap();
615 let f = src_dir.path().join("Cargo.toml");
616 fs::write(&f, workspace_toml).unwrap();
617
618 let builder = SysrootBuilder::new(sysroot.path(), "sometarget").sysroot_config(config);
619 let manifest: Table = toml::from_str(&builder.gen_manifest(src_dir.path())).unwrap();
620 (manifest, src_dir)
621 }
622
623 #[track_caller]
625 fn check_patch_path(manifest: &Table, krate: &str, path: Option<&Path>) {
626 let patches = &manifest["patch"]["crates-io"];
627 match path {
628 Some(path) => assert_eq!(
629 &patches[krate]["path"].as_str().unwrap(),
630 &path.to_str().unwrap()
631 ),
632 None => assert!(patches.get(krate).is_none()),
633 }
634 }
635
636 #[test]
637 fn check_patches_no_std() {
638 let (manifest, src_dir) = setup_manifest_test(SysrootConfig::NoStd);
639
640 check_patch_path(&manifest, "foo", Some(&src_dir.path().join("bar")));
642 check_patch_path(
643 &manifest,
644 "rustc-std-workspace-core",
645 Some(&src_dir.path().join("core")),
646 );
647
648 check_patch_path(&manifest, "rustc-std-workspace-alloc", None);
650 check_patch_path(&manifest, "rustc-std-workspace-std", None);
651 }
652
653 #[test]
654 fn check_patches_with_std() {
655 let (manifest, src_dir) = setup_manifest_test(SysrootConfig::WithStd {
656 std_features: Vec::new(),
657 });
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 check_patch_path(
667 &manifest,
668 "rustc-std-workspace-alloc",
669 Some(&src_dir.path().join("alloc")),
670 );
671 check_patch_path(
672 &manifest,
673 "rustc-std-workspace-std",
674 Some(&src_dir.path().join("std")),
675 );
676 }
677}