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 have_builtins_crate = src_dir.join("compiler-builtins").exists();
303 let crates = match &self.config {
304 SysrootConfig::NoStd if have_builtins_crate => format!(
307 r#"
308 [dependencies.core]
309 path = {src_dir_core:?}
310 [dependencies.alloc]
311 path = {src_dir_alloc:?}
312 [dependencies.compiler_builtins]
313 path = {src_dir_builtins:?}
314 features = ["rustc-dep-of-std", "mem"]
315 "#,
316 src_dir_core = src_dir.join("core"),
317 src_dir_alloc = src_dir.join("alloc"),
318 src_dir_builtins = src_dir.join("compiler-builtins").join("compiler-builtins"),
319 ),
320 SysrootConfig::NoStd => format!(
322 r#"
323 [dependencies.core]
324 path = {src_dir_core:?}
325 [dependencies.alloc]
326 path = {src_dir_alloc:?}
327 [dependencies.compiler_builtins]
328 version = "*"
329 features = ["rustc-dep-of-std", "mem"]
330 "#,
331 src_dir_core = src_dir.join("core"),
332 src_dir_alloc = src_dir.join("alloc"),
333 ),
334 SysrootConfig::WithStd { std_features } => format!(
335 r#"
336 [dependencies.std]
337 features = {std_features:?}
338 path = {src_dir_std:?}
339 [dependencies.sysroot]
340 path = {src_dir_sysroot:?}
341 "#,
342 std_features = std_features,
343 src_dir_std = src_dir.join("std"),
344 src_dir_sysroot = src_dir.join("sysroot"),
345 ),
346 };
347
348 let unneeded_patches = match &self.config {
355 SysrootConfig::NoStd => &["rustc-std-workspace-alloc", "rustc-std-workspace-std"][..],
356 SysrootConfig::WithStd { .. } => &[][..],
357 };
358
359 let mut patches = extract_patches(src_dir);
360 for (repo, repo_patches) in &mut patches {
361 let repo_patches = repo_patches
362 .as_table_mut()
363 .unwrap_or_else(|| panic!("source `{}` is not a table", repo));
364
365 for krate in unneeded_patches {
367 repo_patches.remove(*krate);
368 }
369
370 for (krate, patch) in repo_patches {
372 if let Some(path) = patch.get_mut("path") {
373 let curr_path = path
374 .as_str()
375 .unwrap_or_else(|| panic!("`{}.path` is not a string", krate));
376
377 *path = Value::String(src_dir.join(curr_path).display().to_string());
378 }
379 }
380 }
381
382 let mut table: Table = toml::from_str(&format!(
383 r#"
384 [package]
385 authors = ["rustc-build-sysroot"]
386 name = "custom-local-sysroot"
387 version = "0.0.0"
388 edition = "2018"
389
390 [lib]
391 # empty dummy, just so that things are being built
392 path = "lib.rs"
393
394 [profile.{DEFAULT_SYSROOT_PROFILE}]
395 # We inherit from the local release profile, but then overwrite some
396 # settings to ensure we still get a working sysroot.
397 inherits = "release"
398 panic = 'unwind'
399
400 {crates}
401 "#
402 ))
403 .expect("failed to parse toml");
404
405 table.insert("patch".to_owned(), patches.into());
406 toml::to_string(&table).expect("failed to serialize to toml")
407 }
408
409 pub fn build_from_source(mut self, src_dir: &Path) -> Result<SysrootStatus> {
413 if !src_dir.join("std").join("Cargo.toml").exists() {
415 bail!(
416 "{:?} does not seem to be a rust library source folder: `src/Cargo.toml` not found",
417 src_dir
418 );
419 }
420 let sysroot_target_dir = self.sysroot_target_dir();
421 let target_name = self.target_name().to_owned();
422 let cargo = self.cargo.take().unwrap_or_else(|| {
423 Command::new(env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")))
424 });
425 let rustc_version = match self.rustc_version.take() {
426 Some(v) => v,
427 None => rustc_version::version_meta()?,
428 };
429
430 let cur_hash = self.sysroot_compute_hash(src_dir, &rustc_version)?;
432 if self.sysroot_read_hash() == Some(cur_hash) {
433 return Ok(SysrootStatus::AlreadyCached);
435 }
436
437 if let Some(when_build_required) = self.when_build_required.take() {
439 when_build_required();
440 }
441
442 fs::create_dir_all(&sysroot_target_dir.parent().unwrap())
445 .context("failed to create target directory")?;
446 let unstaging_dir =
452 TempDir::new_in(&self.sysroot_dir).context("failed to create un-staging dir")?;
453 let _ = fs::rename(&sysroot_target_dir, &unstaging_dir); let build_dir = TempDir::new().context("failed to create tempdir")?;
457 let lock_file = build_dir.path().join("Cargo.lock");
459 let lock_file_src = {
460 let new_lock_file_name = src_dir.join("Cargo.lock");
463 if new_lock_file_name.exists() {
464 new_lock_file_name
465 } else {
466 src_dir
468 .parent()
469 .expect("src_dir must have a parent")
470 .join("Cargo.lock")
471 }
472 };
473 fs::copy(lock_file_src, &lock_file)
474 .context("failed to copy lockfile from sysroot source")?;
475 make_writeable(&lock_file).context("failed to make lockfile writeable")?;
476 let manifest_file = build_dir.path().join("Cargo.toml");
478 let manifest = self.gen_manifest(src_dir);
479 fs::write(&manifest_file, manifest.as_bytes()).context("failed to write manifest file")?;
480 let lib_file = build_dir.path().join("lib.rs");
482 let lib = match self.config {
483 SysrootConfig::NoStd => r#"#![no_std]"#,
484 SysrootConfig::WithStd { .. } => "",
485 };
486 fs::write(&lib_file, lib.as_bytes()).context("failed to write lib file")?;
487
488 let mut cmd = cargo;
490 cmd.arg(self.mode.as_str());
491 cmd.arg("--profile");
492 cmd.arg(DEFAULT_SYSROOT_PROFILE);
493 cmd.arg("--manifest-path");
494 cmd.arg(&manifest_file);
495 cmd.arg("--target");
496 cmd.arg(&self.target);
497 cmd.env("CARGO_ENCODED_RUSTFLAGS", encode_rustflags(&self.rustflags));
499 let build_target_dir = build_dir.path().join("target");
502 cmd.env("CARGO_TARGET_DIR", &build_target_dir);
503 cmd.env("CARGO_BUILD_BUILD_DIR", &build_target_dir);
504 cmd.env("__CARGO_DEFAULT_LIB_METADATA", "rustc-build-sysroot");
508
509 let output = cmd
510 .output()
511 .context("failed to execute cargo for sysroot build")?;
512 if !output.status.success() {
513 let stderr = String::from_utf8_lossy(&output.stderr);
514 if stderr.is_empty() {
515 bail!("sysroot build failed");
516 } else {
517 bail!("sysroot build failed; stderr:\n{}", stderr);
518 }
519 }
520
521 fs::create_dir_all(&self.sysroot_dir).context("failed to create sysroot dir")?; let staging_dir =
528 TempDir::new_in(&self.sysroot_dir).context("failed to create staging dir")?;
529 let staging_lib_dir = staging_dir.path().join("lib");
531 fs::create_dir(&staging_lib_dir).context("failed to create staging/lib dir")?;
532 let out_dir = build_target_dir
533 .join(&target_name)
534 .join(DEFAULT_SYSROOT_PROFILE)
535 .join("deps");
536 for entry in fs::read_dir(&out_dir).context("failed to read cargo out dir")? {
537 let entry = entry.context("failed to read cargo out dir entry")?;
538 assert!(
539 entry.file_type().unwrap().is_file(),
540 "cargo out dir must not contain directories"
541 );
542 let entry = entry.path();
543 fs::copy(&entry, staging_lib_dir.join(entry.file_name().unwrap()))
544 .context("failed to copy cargo out file")?;
545 }
546
547 fs::write(
549 staging_dir.path().join(HASH_FILE_NAME),
550 cur_hash.to_string().as_bytes(),
551 )
552 .context("failed to write hash file")?;
553
554 if fs::rename(staging_dir.path(), sysroot_target_dir).is_err() {
557 if self.sysroot_read_hash() != Some(cur_hash) {
559 bail!("detected a concurrent sysroot build with different settings");
560 }
561 }
562
563 Ok(SysrootStatus::SysrootBuilt)
564 }
565}
566
567fn extract_patches(src_dir: &Path) -> Table {
569 let workspace_manifest = src_dir.join("Cargo.toml");
571 let f = fs::read_to_string(&workspace_manifest).unwrap_or_else(|e| {
572 panic!(
573 "unable to read workspace manifest at `{}`: {}",
574 workspace_manifest.display(),
575 e
576 )
577 });
578 let mut t: Table = toml::from_str(&f).expect("invalid sysroot workspace Cargo.toml");
579 t.remove("patch")
581 .map(|v| match v {
582 Value::Table(map) => map,
583 _ => panic!("`patch` is not a table"),
584 })
585 .unwrap_or_default()
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 fn setup_manifest_test(config: SysrootConfig) -> (Table, TempDir) {
594 let workspace_toml = r#"
595 [workspace]
596 foo = "bar"
597
598 [patch.crates-io]
599 foo = { path = "bar" }
600 rustc-std-workspace-core = { path = "core" }
601 rustc-std-workspace-alloc = { path = "alloc" }
602 rustc-std-workspace-std = { path = "std" }
603 "#;
604
605 let sysroot = tempfile::tempdir().unwrap(); let src_dir = tempfile::tempdir().unwrap();
607 let f = src_dir.path().join("Cargo.toml");
608 fs::write(&f, workspace_toml).unwrap();
609
610 let builder = SysrootBuilder::new(sysroot.path(), "sometarget").sysroot_config(config);
611 let manifest: Table = toml::from_str(&builder.gen_manifest(src_dir.path())).unwrap();
612 (manifest, src_dir)
613 }
614
615 #[track_caller]
617 fn check_patch_path(manifest: &Table, krate: &str, path: Option<&Path>) {
618 let patches = &manifest["patch"]["crates-io"];
619 match path {
620 Some(path) => assert_eq!(
621 &patches[krate]["path"].as_str().unwrap(),
622 &path.to_str().unwrap()
623 ),
624 None => assert!(patches.get(krate).is_none()),
625 }
626 }
627
628 #[test]
629 fn check_patches_no_std() {
630 let (manifest, src_dir) = setup_manifest_test(SysrootConfig::NoStd);
631
632 check_patch_path(&manifest, "foo", Some(&src_dir.path().join("bar")));
634 check_patch_path(
635 &manifest,
636 "rustc-std-workspace-core",
637 Some(&src_dir.path().join("core")),
638 );
639
640 check_patch_path(&manifest, "rustc-std-workspace-alloc", None);
642 check_patch_path(&manifest, "rustc-std-workspace-std", None);
643 }
644
645 #[test]
646 fn check_patches_with_std() {
647 let (manifest, src_dir) = setup_manifest_test(SysrootConfig::WithStd {
648 std_features: Vec::new(),
649 });
650
651 check_patch_path(&manifest, "foo", Some(&src_dir.path().join("bar")));
653 check_patch_path(
654 &manifest,
655 "rustc-std-workspace-core",
656 Some(&src_dir.path().join("core")),
657 );
658 check_patch_path(
659 &manifest,
660 "rustc-std-workspace-alloc",
661 Some(&src_dir.path().join("alloc")),
662 );
663 check_patch_path(
664 &manifest,
665 "rustc-std-workspace-std",
666 Some(&src_dir.path().join("std")),
667 );
668 }
669}