#![warn(missing_docs)]
#![allow(clippy::needless_borrows_for_generic_args)]
use std::collections::hash_map::DefaultHasher;
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{bail, Context, Result};
use tempfile::TempDir;
use toml::{Table, Value};
use walkdir::WalkDir;
const DEFAULT_SYSROOT_PROFILE: &str = "custom_sysroot";
fn rustc_sysroot_dir(mut rustc: Command) -> Result<PathBuf> {
let output = rustc
.args(["--print", "sysroot"])
.output()
.context("failed to determine sysroot")?;
if !output.status.success() {
bail!(
"failed to determine sysroot; rustc said:\n{}",
String::from_utf8_lossy(&output.stderr).trim_end()
);
}
let sysroot =
std::str::from_utf8(&output.stdout).context("sysroot folder is not valid UTF-8")?;
let sysroot = PathBuf::from(sysroot.trim_end_matches('\n'));
if !sysroot.is_dir() {
bail!(
"sysroot directory `{}` is not a directory",
sysroot.display()
);
}
Ok(sysroot)
}
pub fn rustc_sysroot_src(rustc: Command) -> Result<PathBuf> {
let sysroot = rustc_sysroot_dir(rustc)?;
let rustc_src = sysroot
.join("lib")
.join("rustlib")
.join("src")
.join("rust")
.join("library");
let rustc_src = rustc_src.canonicalize().unwrap_or(rustc_src);
Ok(rustc_src)
}
pub fn encode_rustflags(flags: &[OsString]) -> OsString {
let mut res = OsString::new();
for flag in flags {
if !res.is_empty() {
res.push(OsStr::new("\x1f"));
}
let flag = flag.to_str().expect("rustflags must be valid UTF-8");
if flag.contains('\x1f') {
panic!("rustflags must not contain `\\x1f` separator");
}
res.push(flag);
}
res
}
#[cfg(unix)]
fn make_writeable(p: &Path) -> Result<()> {
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
let perms = fs::metadata(p)?.permissions();
let perms = Permissions::from_mode(perms.mode() | 0o600); fs::set_permissions(p, perms).context("cannot set permissions")?;
Ok(())
}
#[cfg(not(unix))]
fn make_writeable(p: &Path) -> Result<()> {
let mut perms = fs::metadata(p)?.permissions();
perms.set_readonly(false);
fs::set_permissions(p, perms).context("cannot set permissions")?;
Ok(())
}
fn hash_recursive(path: &Path, hasher: &mut DefaultHasher) -> Result<()> {
for entry in WalkDir::new(path)
.follow_links(true)
.sort_by_file_name()
.into_iter()
{
let entry = entry?;
if entry.file_type().is_dir() {
continue;
}
let meta = entry.metadata()?;
meta.modified()?.hash(hasher);
meta.len().hash(hasher);
}
Ok(())
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum BuildMode {
Build,
Check,
}
impl BuildMode {
pub fn as_str(&self) -> &str {
use BuildMode::*;
match self {
Build => "build",
Check => "check",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum SysrootConfig {
NoStd,
WithStd {
std_features: Vec<String>,
},
}
pub struct SysrootBuilder<'a> {
sysroot_dir: PathBuf,
target: OsString,
config: SysrootConfig,
mode: BuildMode,
rustflags: Vec<OsString>,
cargo: Option<Command>,
rustc_version: Option<rustc_version::VersionMeta>,
when_build_required: Option<Box<dyn FnOnce() + 'a>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SysrootStatus {
AlreadyCached,
SysrootBuilt,
}
const HASH_FILE_NAME: &str = ".rustc-build-sysroot-hash";
impl<'a> SysrootBuilder<'a> {
pub fn new(sysroot_dir: &Path, target: impl Into<OsString>) -> Self {
let default_flags = &[
"-Zforce-unstable-if-unmarked",
"-Aunexpected_cfgs",
];
SysrootBuilder {
sysroot_dir: sysroot_dir.to_owned(),
target: target.into(),
config: SysrootConfig::WithStd {
std_features: vec![],
},
mode: BuildMode::Build,
rustflags: default_flags.iter().map(Into::into).collect(),
cargo: None,
rustc_version: None,
when_build_required: None,
}
}
pub fn build_mode(mut self, build_mode: BuildMode) -> Self {
self.mode = build_mode;
self
}
pub fn sysroot_config(mut self, sysroot_config: SysrootConfig) -> Self {
self.config = sysroot_config;
self
}
pub fn rustflag(mut self, rustflag: impl Into<OsString>) -> Self {
self.rustflags.push(rustflag.into());
self
}
pub fn rustflags(mut self, rustflags: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
self.rustflags.extend(rustflags.into_iter().map(Into::into));
self
}
pub fn cargo(mut self, cargo: Command) -> Self {
self.cargo = Some(cargo);
self
}
pub fn rustc_version(mut self, rustc_version: rustc_version::VersionMeta) -> Self {
self.rustc_version = Some(rustc_version);
self
}
pub fn when_build_required(mut self, when_build_required: impl FnOnce() + 'a) -> Self {
self.when_build_required = Some(Box::new(when_build_required));
self
}
fn target_name(&self) -> &OsStr {
let path = Path::new(&self.target);
if path.extension().and_then(OsStr::to_str) == Some("json") {
path.file_stem().unwrap()
} else {
&self.target
}
}
fn sysroot_target_dir(&self) -> PathBuf {
self.sysroot_dir
.join("lib")
.join("rustlib")
.join(self.target_name())
}
fn sysroot_compute_hash(
&self,
src_dir: &Path,
rustc_version: &rustc_version::VersionMeta,
) -> Result<u64> {
let mut hasher = DefaultHasher::new();
src_dir.hash(&mut hasher);
hash_recursive(src_dir, &mut hasher)?;
self.config.hash(&mut hasher);
self.mode.hash(&mut hasher);
self.rustflags.hash(&mut hasher);
rustc_version.hash(&mut hasher);
Ok(hasher.finish())
}
fn sysroot_read_hash(&self) -> Option<u64> {
let hash_file = self.sysroot_target_dir().join(HASH_FILE_NAME);
let hash = fs::read_to_string(&hash_file).ok()?;
hash.parse().ok()
}
fn gen_manifest(&self, src_dir: &Path) -> String {
let crates = match &self.config {
SysrootConfig::NoStd => format!(
r#"
[dependencies.core]
path = {src_dir_core:?}
[dependencies.alloc]
path = {src_dir_alloc:?}
[dependencies.compiler_builtins]
path = {src_dir_builtins:?}
features = ["compiler-builtins", "mem"]
"#,
src_dir_core = src_dir.join("core"),
src_dir_alloc = src_dir.join("alloc"),
src_dir_builtins = src_dir.join("compiler-builtins").join("compiler-builtins"),
),
SysrootConfig::WithStd { std_features } => format!(
r#"
[dependencies.std]
features = {std_features:?}
path = {src_dir_std:?}
[dependencies.sysroot]
path = {src_dir_sysroot:?}
"#,
std_features = std_features,
src_dir_std = src_dir.join("std"),
src_dir_sysroot = src_dir.join("sysroot"),
),
};
let unneeded_patches = match &self.config {
SysrootConfig::NoStd => &["rustc-std-workspace-alloc", "rustc-std-workspace-std"][..],
SysrootConfig::WithStd { .. } => &[][..],
};
let mut patches = extract_patches(src_dir);
for (repo, repo_patches) in &mut patches {
let repo_patches = repo_patches
.as_table_mut()
.unwrap_or_else(|| panic!("source `{}` is not a table", repo));
for krate in unneeded_patches {
repo_patches.remove(*krate);
}
for (krate, patch) in repo_patches {
if let Some(path) = patch.get_mut("path") {
let curr_path = path
.as_str()
.unwrap_or_else(|| panic!("`{}.path` is not a string", krate));
*path = Value::String(src_dir.join(curr_path).display().to_string());
}
}
}
let mut table: Table = toml::from_str(&format!(
r#"
[package]
authors = ["rustc-build-sysroot"]
name = "custom-local-sysroot"
version = "0.0.0"
edition = "2018"
[lib]
# empty dummy, just so that things are being built
path = "lib.rs"
[profile.{DEFAULT_SYSROOT_PROFILE}]
# We inherit from the local release profile, but then overwrite some
# settings to ensure we still get a working sysroot.
inherits = "release"
panic = 'unwind'
{crates}
"#
))
.expect("failed to parse toml");
table.insert("patch".to_owned(), patches.into());
toml::to_string(&table).expect("failed to serialize to toml")
}
pub fn build_from_source(mut self, src_dir: &Path) -> Result<SysrootStatus> {
if !src_dir.join("std").join("Cargo.toml").exists() {
bail!(
"{:?} does not seem to be a rust library source folder: `std/Cargo.toml` not found",
src_dir
);
}
let sysroot_target_dir = self.sysroot_target_dir();
let target_name = self.target_name().to_owned();
let cargo = self.cargo.take().unwrap_or_else(|| {
Command::new(env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")))
});
let rustc_version = match self.rustc_version.take() {
Some(v) => v,
None => rustc_version::version_meta()?,
};
let cur_hash = self.sysroot_compute_hash(src_dir, &rustc_version)?;
if self.sysroot_read_hash() == Some(cur_hash) {
return Ok(SysrootStatus::AlreadyCached);
}
if let Some(when_build_required) = self.when_build_required.take() {
when_build_required();
}
fs::create_dir_all(&sysroot_target_dir.parent().unwrap())
.context("failed to create target directory")?;
let unstaging_dir =
TempDir::new_in(&self.sysroot_dir).context("failed to create un-staging dir")?;
let _ = fs::rename(&sysroot_target_dir, &unstaging_dir);
let build_dir = TempDir::new().context("failed to create tempdir")?;
let lock_file = build_dir.path().join("Cargo.lock");
let lock_file_src = {
let new_lock_file_name = src_dir.join("Cargo.lock");
if new_lock_file_name.exists() {
new_lock_file_name
} else {
src_dir
.parent()
.expect("src_dir must have a parent")
.join("Cargo.lock")
}
};
fs::copy(lock_file_src, &lock_file)
.context("failed to copy lockfile from sysroot source")?;
make_writeable(&lock_file).context("failed to make lockfile writeable")?;
let manifest_file = build_dir.path().join("Cargo.toml");
let manifest = self.gen_manifest(src_dir);
fs::write(&manifest_file, manifest.as_bytes()).context("failed to write manifest file")?;
let lib_file = build_dir.path().join("lib.rs");
let lib = match self.config {
SysrootConfig::NoStd => r#"#![no_std]"#,
SysrootConfig::WithStd { .. } => "",
};
fs::write(&lib_file, lib.as_bytes()).context("failed to write lib file")?;
let mut cmd = cargo;
cmd.arg(self.mode.as_str());
cmd.arg("--profile");
cmd.arg(DEFAULT_SYSROOT_PROFILE);
cmd.arg("--manifest-path");
cmd.arg(&manifest_file);
cmd.arg("--target");
cmd.arg(&self.target);
cmd.env("CARGO_ENCODED_RUSTFLAGS", encode_rustflags(&self.rustflags));
let build_target_dir = build_dir.path().join("target");
cmd.env("CARGO_TARGET_DIR", &build_target_dir);
cmd.env("CARGO_BUILD_BUILD_DIR", &build_target_dir);
cmd.env("__CARGO_DEFAULT_LIB_METADATA", "rustc-build-sysroot");
let output = cmd
.output()
.context("failed to execute cargo for sysroot build")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.is_empty() {
bail!("sysroot build failed");
} else {
bail!("sysroot build failed; stderr:\n{}", stderr);
}
}
fs::create_dir_all(&self.sysroot_dir).context("failed to create sysroot dir")?; let staging_dir =
TempDir::new_in(&self.sysroot_dir).context("failed to create staging dir")?;
let staging_lib_dir = staging_dir.path().join("lib");
fs::create_dir(&staging_lib_dir).context("failed to create staging/lib dir")?;
let out_dir = build_target_dir
.join(&target_name)
.join(DEFAULT_SYSROOT_PROFILE);
if out_dir.join("deps").exists() {
copy_files(&out_dir.join("deps"), &staging_lib_dir)
.context("failed to copy cargo out dir (old layout)")?;
} else {
for_each_dir(&out_dir.join("build"), |dir| {
for_each_dir(dir, |dir| copy_files(&dir.join("out"), &staging_lib_dir))
})
.context("failed to copy cargo out dir (new layout)")?;
}
fs::write(
staging_dir.path().join(HASH_FILE_NAME),
cur_hash.to_string().as_bytes(),
)
.context("failed to write hash file")?;
if fs::rename(staging_dir.path(), sysroot_target_dir).is_err() {
if self.sysroot_read_hash() != Some(cur_hash) {
bail!("detected a concurrent sysroot build with different settings");
}
}
Ok(SysrootStatus::SysrootBuilt)
}
}
fn copy_files(from: &Path, to: &Path) -> Result<()> {
for entry in fs::read_dir(from)? {
let entry = entry?;
assert!(
entry.file_type()?.is_file(),
"cargo out dir must not contain directories"
);
fs::copy(&entry.path(), to.join(entry.file_name()))?;
}
Ok(())
}
fn for_each_dir(path: &Path, f: impl Fn(&Path) -> Result<()>) -> Result<()> {
for entry in fs::read_dir(path)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
f(&entry.path())?;
}
Ok(())
}
fn extract_patches(src_dir: &Path) -> Table {
let workspace_manifest = src_dir.join("Cargo.toml");
let f = fs::read_to_string(&workspace_manifest).unwrap_or_else(|e| {
panic!(
"unable to read workspace manifest at `{}`: {}",
workspace_manifest.display(),
e
)
});
let mut t: Table = toml::from_str(&f).expect("invalid sysroot workspace Cargo.toml");
t.remove("patch")
.map(|v| match v {
Value::Table(map) => map,
_ => panic!("`patch` is not a table"),
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn setup_manifest_test(config: SysrootConfig) -> (Table, TempDir) {
let workspace_toml = r#"
[workspace]
foo = "bar"
[patch.crates-io]
foo = { path = "bar" }
rustc-std-workspace-core = { path = "core" }
rustc-std-workspace-alloc = { path = "alloc" }
rustc-std-workspace-std = { path = "std" }
"#;
let sysroot = tempfile::tempdir().unwrap(); let src_dir = tempfile::tempdir().unwrap();
let f = src_dir.path().join("Cargo.toml");
fs::write(&f, workspace_toml).unwrap();
let builder = SysrootBuilder::new(sysroot.path(), "sometarget").sysroot_config(config);
let manifest: Table = toml::from_str(&builder.gen_manifest(src_dir.path())).unwrap();
(manifest, src_dir)
}
#[track_caller]
fn check_patch_path(manifest: &Table, krate: &str, path: Option<&Path>) {
let patches = &manifest["patch"]["crates-io"];
match path {
Some(path) => assert_eq!(
&patches[krate]["path"].as_str().unwrap(),
&path.to_str().unwrap()
),
None => assert!(patches.get(krate).is_none()),
}
}
#[test]
fn check_patches_no_std() {
let (manifest, src_dir) = setup_manifest_test(SysrootConfig::NoStd);
check_patch_path(&manifest, "foo", Some(&src_dir.path().join("bar")));
check_patch_path(
&manifest,
"rustc-std-workspace-core",
Some(&src_dir.path().join("core")),
);
check_patch_path(&manifest, "rustc-std-workspace-alloc", None);
check_patch_path(&manifest, "rustc-std-workspace-std", None);
}
#[test]
fn check_patches_with_std() {
let (manifest, src_dir) = setup_manifest_test(SysrootConfig::WithStd {
std_features: Vec::new(),
});
check_patch_path(&manifest, "foo", Some(&src_dir.path().join("bar")));
check_patch_path(
&manifest,
"rustc-std-workspace-core",
Some(&src_dir.path().join("core")),
);
check_patch_path(
&manifest,
"rustc-std-workspace-alloc",
Some(&src_dir.path().join("alloc")),
);
check_patch_path(
&manifest,
"rustc-std-workspace-std",
Some(&src_dir.path().join("std")),
);
}
}