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 walkdir::WalkDir;
18
19const DEFAULT_SYSROOT_PROFILE: &str = "custom_sysroot";
21
22fn rustc_sysroot_dir(mut rustc: Command) -> Result<PathBuf> {
23 let output = rustc
24 .args(["--print", "sysroot"])
25 .output()
26 .context("failed to determine sysroot")?;
27 if !output.status.success() {
28 bail!(
29 "failed to determine sysroot; rustc said:\n{}",
30 String::from_utf8_lossy(&output.stderr).trim_end()
31 );
32 }
33 let sysroot =
34 std::str::from_utf8(&output.stdout).context("sysroot folder is not valid UTF-8")?;
35 let sysroot = PathBuf::from(sysroot.trim_end_matches('\n'));
36 if !sysroot.is_dir() {
37 bail!(
38 "sysroot directory `{}` is not a directory",
39 sysroot.display()
40 );
41 }
42 Ok(sysroot)
43}
44
45pub fn rustc_sysroot_src(rustc: Command) -> Result<PathBuf> {
47 let sysroot = rustc_sysroot_dir(rustc)?;
48 let rustc_src = sysroot
49 .join("lib")
50 .join("rustlib")
51 .join("src")
52 .join("rust")
53 .join("library");
54 let rustc_src = rustc_src.canonicalize().unwrap_or(rustc_src);
57 Ok(rustc_src)
58}
59
60pub fn encode_rustflags(flags: &[OsString]) -> OsString {
62 let mut res = OsString::new();
63 for flag in flags {
64 if !res.is_empty() {
65 res.push(OsStr::new("\x1f"));
66 }
67 let flag = flag.to_str().expect("rustflags must be valid UTF-8");
69 if flag.contains('\x1f') {
70 panic!("rustflags must not contain `\\x1f` separator");
71 }
72 res.push(flag);
73 }
74 res
75}
76
77#[cfg(unix)]
79fn make_writeable(p: &Path) -> Result<()> {
80 use std::fs::Permissions;
83 use std::os::unix::fs::PermissionsExt;
84
85 let perms = fs::metadata(p)?.permissions();
86 let perms = Permissions::from_mode(perms.mode() | 0o600); fs::set_permissions(p, perms).context("cannot set permissions")?;
88 Ok(())
89}
90
91#[cfg(not(unix))]
93fn make_writeable(p: &Path) -> Result<()> {
94 let mut perms = fs::metadata(p)?.permissions();
95 perms.set_readonly(false);
96 fs::set_permissions(p, perms).context("cannot set permissions")?;
97 Ok(())
98}
99
100fn hash_recursive(path: &Path, hasher: &mut DefaultHasher) -> Result<()> {
102 for entry in WalkDir::new(path)
104 .follow_links(true)
105 .sort_by_file_name()
106 .into_iter()
107 {
108 let entry = entry?;
109 if entry.file_type().is_dir() {
112 continue;
113 }
114 let meta = entry.metadata()?;
115 meta.modified()?.hash(hasher);
118 meta.len().hash(hasher);
119 }
120 Ok(())
121}
122
123#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
125pub enum BuildMode {
126 Build,
129 Check,
132}
133
134impl BuildMode {
135 pub fn as_str(&self) -> &str {
137 use BuildMode::*;
138 match self {
139 Build => "build",
140 Check => "check",
141 }
142 }
143}
144
145#[derive(Clone, Debug, PartialEq, Eq, Hash)]
147pub enum SysrootConfig {
148 NoStd,
150 WithStd {
152 std_features: Vec<String>,
154 },
155}
156
157pub struct SysrootBuilder<'a> {
159 sysroot_dir: PathBuf,
160 target: OsString,
161 config: SysrootConfig,
162 mode: BuildMode,
163 rustflags: Vec<OsString>,
164 cargo: Option<Command>,
165 rustc_version: Option<rustc_version::VersionMeta>,
166 when_build_required: Option<Box<dyn FnOnce() + 'a>>,
167}
168
169#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
172pub enum SysrootStatus {
173 AlreadyCached,
175 SysrootBuilt,
177}
178
179const HASH_FILE_NAME: &str = ".rustc-build-sysroot-hash";
181
182impl<'a> SysrootBuilder<'a> {
183 pub fn new(sysroot_dir: &Path, target: impl Into<OsString>) -> Self {
186 let default_flags = &[
187 "-Zforce-unstable-if-unmarked",
189 "-Aunexpected_cfgs",
191 ];
192 SysrootBuilder {
193 sysroot_dir: sysroot_dir.to_owned(),
194 target: target.into(),
195 config: SysrootConfig::WithStd {
196 std_features: vec![],
197 },
198 mode: BuildMode::Build,
199 rustflags: default_flags.iter().map(Into::into).collect(),
200 cargo: None,
201 rustc_version: None,
202 when_build_required: None,
203 }
204 }
205
206 pub fn build_mode(mut self, build_mode: BuildMode) -> Self {
208 self.mode = build_mode;
209 self
210 }
211
212 pub fn sysroot_config(mut self, sysroot_config: SysrootConfig) -> Self {
214 self.config = sysroot_config;
215 self
216 }
217
218 pub fn rustflag(mut self, rustflag: impl Into<OsString>) -> Self {
225 self.rustflags.push(rustflag.into());
226 self
227 }
228
229 pub fn rustflags(mut self, rustflags: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
234 self.rustflags.extend(rustflags.into_iter().map(Into::into));
235 self
236 }
237
238 pub fn cargo(mut self, cargo: Command) -> Self {
243 self.cargo = Some(cargo);
244 self
245 }
246
247 pub fn rustc_version(mut self, rustc_version: rustc_version::VersionMeta) -> Self {
249 self.rustc_version = Some(rustc_version);
250 self
251 }
252
253 pub fn when_build_required(mut self, when_build_required: impl FnOnce() + 'a) -> Self {
256 self.when_build_required = Some(Box::new(when_build_required));
257 self
258 }
259
260 fn target_name(&self) -> &OsStr {
264 let path = Path::new(&self.target);
265 if path.extension().and_then(OsStr::to_str) == Some("json") {
266 path.file_stem().unwrap()
269 } else {
270 &self.target
273 }
274 }
275
276 fn sysroot_target_dir(&self) -> PathBuf {
277 self.sysroot_dir
278 .join("lib")
279 .join("rustlib")
280 .join(self.target_name())
281 }
282
283 fn sysroot_compute_hash(
285 &self,
286 src_dir: &Path,
287 rustc_version: &rustc_version::VersionMeta,
288 ) -> Result<u64> {
289 let mut hasher = DefaultHasher::new();
290
291 src_dir.hash(&mut hasher);
292 hash_recursive(src_dir, &mut hasher)?;
293 self.config.hash(&mut hasher);
294 self.mode.hash(&mut hasher);
295 self.rustflags.hash(&mut hasher);
296 rustc_version.hash(&mut hasher);
297
298 Ok(hasher.finish())
299 }
300
301 fn sysroot_read_hash(&self) -> Option<u64> {
302 let hash_file = self.sysroot_target_dir().join(HASH_FILE_NAME);
303 let hash = fs::read_to_string(&hash_file).ok()?;
304 hash.parse().ok()
305 }
306
307 fn gen_manifest(&self, src_dir: &Path) -> String {
309 let have_sysroot_crate = src_dir.join("sysroot").exists();
310 let crates = match &self.config {
311 SysrootConfig::NoStd => format!(
312 r#"
313[dependencies.core]
314path = {src_dir_core:?}
315[dependencies.alloc]
316path = {src_dir_alloc:?}
317[dependencies.compiler_builtins]
318features = ["rustc-dep-of-std", "mem"]
319version = "*"
320 "#,
321 src_dir_core = src_dir.join("core"),
322 src_dir_alloc = src_dir.join("alloc"),
323 ),
324 SysrootConfig::WithStd { std_features } if have_sysroot_crate => format!(
325 r#"
326[dependencies.std]
327features = {std_features:?}
328path = {src_dir_std:?}
329[dependencies.sysroot]
330path = {src_dir_sysroot:?}
331 "#,
332 std_features = std_features,
333 src_dir_std = src_dir.join("std"),
334 src_dir_sysroot = src_dir.join("sysroot"),
335 ),
336 SysrootConfig::WithStd { std_features } => format!(
338 r#"
339[dependencies.std]
340features = {std_features:?}
341path = {src_dir_std:?}
342[dependencies.test]
343path = {src_dir_test:?}
344 "#,
345 std_features = std_features,
346 src_dir_std = src_dir.join("std"),
347 src_dir_test = src_dir.join("test"),
348 ),
349 };
350
351 let patches = match &self.config {
358 SysrootConfig::NoStd => format!(
359 r#"
360[patch.crates-io.rustc-std-workspace-core]
361path = {src_dir_workspace_core:?}
362 "#,
363 src_dir_workspace_core = src_dir.join("rustc-std-workspace-core"),
364 ),
365 SysrootConfig::WithStd { .. } => format!(
366 r#"
367[patch.crates-io.rustc-std-workspace-core]
368path = {src_dir_workspace_core:?}
369[patch.crates-io.rustc-std-workspace-alloc]
370path = {src_dir_workspace_alloc:?}
371[patch.crates-io.rustc-std-workspace-std]
372path = {src_dir_workspace_std:?}
373 "#,
374 src_dir_workspace_core = src_dir.join("rustc-std-workspace-core"),
375 src_dir_workspace_alloc = src_dir.join("rustc-std-workspace-alloc"),
376 src_dir_workspace_std = src_dir.join("rustc-std-workspace-std"),
377 ),
378 };
379
380 format!(
381 r#"
382[package]
383authors = ["rustc-build-sysroot"]
384name = "custom-local-sysroot"
385version = "0.0.0"
386edition = "2018"
387
388[lib]
389# empty dummy, just so that things are being built
390path = "lib.rs"
391
392[profile.{DEFAULT_SYSROOT_PROFILE}]
393# We inherit from the local release profile, but then overwrite some
394# settings to ensure we still get a working sysroot.
395inherits = "release"
396panic = 'unwind'
397
398{crates}
399
400{patches}
401 "#
402 )
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 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");
484 cmd.env("CARGO_TARGET_DIR", &build_target_dir);
485 cmd.env("__CARGO_DEFAULT_LIB_METADATA", "rustc-build-sysroot");
489
490 let output = cmd
491 .output()
492 .context("failed to execute cargo for sysroot build")?;
493 if !output.status.success() {
494 let stderr = String::from_utf8_lossy(&output.stderr);
495 if stderr.is_empty() {
496 bail!("sysroot build failed");
497 } else {
498 bail!("sysroot build failed; stderr:\n{}", stderr);
499 }
500 }
501
502 fs::create_dir_all(&self.sysroot_dir).context("failed to create sysroot dir")?; let staging_dir =
509 TempDir::new_in(&self.sysroot_dir).context("failed to create staging dir")?;
510 let staging_lib_dir = staging_dir.path().join("lib");
512 fs::create_dir(&staging_lib_dir).context("faiked to create staging/lib dir")?;
513 let out_dir = build_target_dir
514 .join(&target_name)
515 .join(DEFAULT_SYSROOT_PROFILE)
516 .join("deps");
517 for entry in fs::read_dir(&out_dir).context("failed to read cargo out dir")? {
518 let entry = entry.context("failed to read cargo out dir entry")?;
519 assert!(
520 entry.file_type().unwrap().is_file(),
521 "cargo out dir must not contain directories"
522 );
523 let entry = entry.path();
524 fs::copy(&entry, staging_lib_dir.join(entry.file_name().unwrap()))
525 .context("failed to copy cargo out file")?;
526 }
527
528 fs::write(
530 staging_dir.path().join(HASH_FILE_NAME),
531 cur_hash.to_string().as_bytes(),
532 )
533 .context("failed to write hash file")?;
534
535 if sysroot_target_dir.exists() {
537 fs::remove_dir_all(&sysroot_target_dir)
539 .context("failed to clean sysroot target dir")?;
540 }
541 fs::create_dir_all(&sysroot_target_dir.parent().unwrap())
543 .context("failed to create target directory")?;
544 fs::rename(staging_dir.path(), sysroot_target_dir).context("failed installing sysroot")?;
545
546 Ok(SysrootStatus::SysrootBuilt)
547 }
548}