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