cargo-sysroot 0.8.1

Simply and Easily cross-compile the Rust sysroot crates.
Documentation
//! # Cargo-Sysroot
//!
//! Compiles the Rust sysroot crates, core, compiler_builtins, and alloc.
use anyhow::{anyhow, Context, Error, Result};
use cargo_toml2::{
    from_path,
    to_path,
    CargoToml,
    Dependency,
    DependencyFull,
    Package,
    Patches,
    TargetConfig,
    Workspace,
};
use std::{
    collections::BTreeMap,
    env,
    ffi::OsString,
    fs,
    path::{Path, PathBuf},
    process::Command,
};

mod util;

pub use util::get_rust_src;

/// The sysroot crates to build.
///
/// See [`build_sysroot_with`] for details.
#[derive(Debug, Copy, Clone)]
pub enum Sysroot {
    /// The core crate. Provides core functionality.
    ///
    /// This does **not** include [`Sysroot::CompilerBuiltins`],
    /// which is what you probably want unless your target
    /// needs special handling.
    Core,

    /// Compiler-builtins crate.
    ///
    /// This implies [`Sysroot::Core`].
    CompilerBuiltins,

    /// The alloc crate. Gives you a heap, and things to put on it.
    ///
    /// This implies [`Sysroot::Core`], and [`Sysroot::CompilerBuiltins`].
    Alloc,

    /// The standard library. Gives you an operating system.
    ///
    /// This implies [`Sysroot::Alloc`], [`Sysroot::Core`], and
    /// [`Sysroot::CompilerBuiltins`].
    Std,
}

/// Features to enable when building the sysroot crates
///
/// See [`SysrootBuilder::features`] for usage.
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq)]
pub enum Features {
    /// This enables the `mem` feature of [`compiler_builtins`][1],
    /// which will provide memory related intrinsics such as `memcpy`.
    ///
    /// [1]: https://github.com/rust-lang/compiler-builtins
    CompilerBuiltinsMem,

    /// This enables the `c` feature of [`compiler_builtins`][1],
    /// which enables compilation of `C` code and may result in more
    /// optimized implementations, and fills in the rare unimplemented
    /// intrinsics.
    ///
    /// [1]: https://github.com/rust-lang/compiler-builtins
    CompilerBuiltinsC,

    /// This enables the `no-asm` feature of [`compiler_builtins`][1],
    /// which disables any implementations which use
    /// inline assembly and fall back to pure Rust versions (if available).
    ///
    /// [1]: https://github.com/rust-lang/compiler-builtins
    // TODO: Would only work on the [`Sysroot::CompilerBuiltins`] target,
    // as it's not exported through alloc, but could be forced by
    // adding compiler_builtins as an explicit dependency and enabling it,
    // relying on features collapsing.
    CompilerBuiltinsNoAsm,
}

/// A builder interface for constructing the Sysroot
///
/// See the individual methods for more details on what this means
/// and what defaults exist.
#[derive(Debug, Clone)]
pub struct SysrootBuilder {
    /// Manifest to use for cargo profiles
    manifest: Option<PathBuf>,

    /// Output directory, where the built sysroot will be anchored.
    output: PathBuf,

    /// Target triple/json to build for
    target: Option<PathBuf>,

    /// The rust sources to use
    rust_src: Option<PathBuf>,

    /// Which crates to include in the sysroot
    sysroot_crate: Sysroot,

    /// What custom features to enable, if any. See [`Features`] for details.
    features: Vec<Features>,

    /// Custom flags to pass to rustc.
    rustc_flags: Vec<OsString>,
}

impl SysrootBuilder {
    /// New [`SysrootBuilder`].
    ///
    /// `sysroot_crate` specifies which libraries to build as part of
    /// the sysroot. See [`Sysroot`] for more details.
    pub fn new(sysroot_crate: Sysroot) -> Self {
        Self {
            manifest: Default::default(),
            output: PathBuf::from(".").join("target").join("sysroot"),
            target: Default::default(),
            // Set in [`SysrootBuilder::build`] since `new` can't error.
            rust_src: Default::default(),
            sysroot_crate,
            features: Vec::with_capacity(3),
            rustc_flags: Default::default(),
        }
    }

    /// Set path to the `Cargo.toml` of the project requiring a custom sysroot.
    ///
    /// If provided, any [Cargo Profile's][1] in the provided manifest
    /// will be copied into the sysroot crate being compiled.
    ///
    /// If not provided, profiles use their default settings.
    ///
    /// By default this will be `None`.
    ///
    /// [1]: https://doc.rust-lang.org/stable/cargo/reference/profiles.html
    pub fn manifest(&mut self, manifest: PathBuf) -> &mut Self {
        self.manifest = Some(manifest);
        self
    }

    /// Set where the sysroot directory will be placed.
    ///
    /// By default this is `./target/sysroot`.
    pub fn output(&mut self, output: PathBuf) -> &mut Self {
        self.output = output;
        self
    }

    /// The target to compile *for*. This can be a target-triple,
    /// or a [JSON Target Specification][1].
    ///
    /// By default this is `None`, and if not set when
    /// [`SysrootBuilder::build`] is called, will cause an error.
    ///
    /// [1]: https://doc.rust-lang.org/rustc/targets/custom.html
    pub fn target(&mut self, target: PathBuf) -> &mut Self {
        self.target = Some(target);
        self
    }

    /// The rust source directory. These are used to compile the sysroot.
    ///
    /// By default this uses the `rust-src` component from the
    /// current `rustup` toolchain.
    pub fn rust_src(&mut self, rust_src: PathBuf) -> &mut Self {
        self.rust_src = Some(rust_src);
        self
    }

    /// Which features to enable.
    ///
    /// This *adds* to, not *replaces*, any previous calls to this method.
    ///
    /// By default this is empty.
    ///
    /// See [`Features`] for details.
    pub fn features(&mut self, features: &[Features]) -> &mut Self {
        self.features.extend_from_slice(features);
        // TODO: Should?? Not??
        self.features.sort_unstable();
        self.features.dedup();
        self
    }

    /// Custom flags to pass to **all** `rustc` compiler invocations.
    ///
    /// This *adds* to, not *replaces*, any previous calls to this method.
    ///
    /// By default this is empty.
    ///
    /// # Internal
    ///
    /// This will use the `RUSTFLAGS` to ensure flags are set for all
    /// invocations.
    ///
    /// Flags passed to this method will be *appended*
    /// to any existing `RUSTFLAGS`.
    pub fn rustc_flags<I, S>(&mut self, flags: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: Into<OsString>,
    {
        self.rustc_flags.extend(flags.into_iter().map(Into::into));
        self
    }

    /// Build the Sysroot, and return a path suitable to pass to rustc.
    ///
    /// # Errors
    ///
    /// - [`SysrootBuilder::target`] was not called
    /// - If `manifest` is provided and does not exist
    /// - If `target` is a JSON specification, but doesn't exist.
    /// - If the `rust_src` directory does not exist, or could not be detected.
    /// - If the sysroot cannot be setup, or fails to compile
    pub fn build(&self) -> Result<PathBuf> {
        let target = match &self.target {
            Some(t) => t,
            None => return Err(anyhow!("SysrootBuilder::target was not called")),
        };
        if let Some(manifest) = &self.manifest {
            if !manifest.exists() {
                return Err(anyhow!(
                    "Provided manifest did not exist. Path: {}",
                    manifest.display()
                ));
            }
        }
        // If `target` has an extension, assume target spec...
        if target.extension().is_some() {
            // ...and check if it exists.
            if !target.exists() {
                return Err(anyhow!(
                    "Provided JSON Target Specification did not exist: {}",
                    target.display()
                ));
            }
        }
        let rust_src = match &self.rust_src {
            Some(s) => {
                if !s.exists() {
                    return Err(anyhow!(
                        "The provided rust-src directory did not exist: {}",
                        s.display()
                    ));
                }
                s.clone()
            }
            None => {
                let src = util::get_rust_src().context("Could not detect appropriate rust-src")?;
                if !src.exists() {
                    return Err(anyhow!("Rust-src component not installed?"));
                }
                src
            }
        };
        fs::create_dir_all(&self.output).context("Couldn't create sysroot output directory")?;
        fs::create_dir_all(artifact_dir(&self.output, target)?)
            .context("Failed to setup sysroot directory structure")?;

        let sysroot_cargo_toml = generate_sysroot_cargo_toml(&SysrootBuilder {
            // HACK: So it can see auto-detected rust-src.
            rust_src: Some(rust_src),
            ..self.clone()
        })?;
        build_alloc(&sysroot_cargo_toml, &self).context("Failed to build sysroot")?;

        // Copy host tools to the new sysroot, so that stuff like proc-macros and
        // testing can work.
        util::copy_host_tools(&self.output).context("Couldn't copy host tools to sysroot")?;
        Ok(self.output.canonicalize().with_context(|| {
            format!(
                "Couldn't get canonical path to sysroot: {}",
                self.output.display()
            )
        })?)
    }
}

/// Generate a Cargo.toml for building the sysroot crates
///
/// Should ONLY be called by [`SysrootBuilder::build`].
///
/// See [`build_sysroot_with`].
fn generate_sysroot_cargo_toml(builder: &SysrootBuilder) -> Result<PathBuf> {
    fs::write(
        builder.output.join("lib.rs"),
        "#![feature(no_core)]\n#![no_core]",
    )?;
    let toml = CargoToml {
        package: Package {
            name: "Sysroot".into(),
            version: "0.0.0".into(),
            authors: vec!["The Rust Project Developers".into(), "DianaNites".into()],
            edition: Some("2018".into()),
            autotests: Some(false),
            autobenches: Some(false),
            ..Default::default()
        },
        lib: Some(TargetConfig {
            name: Some("sysroot".into()),
            path: Some("lib.rs".into()),
            ..Default::default()
        }),
        workspace: Some(Workspace::default()),
        dependencies: Some({
            let mut deps = BTreeMap::new();
            match builder.sysroot_crate {
                Sysroot::Core => {
                    deps.insert(
                        "core".into(),
                        Dependency::Full(DependencyFull {
                            path: Some(builder.rust_src.as_ref().unwrap().join("core")),
                            ..Default::default()
                        }),
                    );
                }

                Sysroot::CompilerBuiltins => {
                    deps.insert(
                        "compiler_builtins".into(),
                        Dependency::Full(DependencyFull {
                            version: Some("0.1".into()),
                            features: {
                                let mut f = vec!["rustc-dep-of-std".into()];
                                if builder.features.contains(&Features::CompilerBuiltinsMem) {
                                    f.push("mem".into());
                                }
                                Some(f)
                            },
                            ..Default::default()
                        }),
                    );
                }

                Sysroot::Alloc => {
                    deps.insert(
                        "alloc".into(),
                        Dependency::Full(DependencyFull {
                            path: Some(builder.rust_src.as_ref().unwrap().join("alloc")),
                            features: if builder.features.contains(&Features::CompilerBuiltinsMem) {
                                Some(vec!["compiler-builtins-mem".into()])
                            } else {
                                None
                            },
                            ..Default::default()
                        }),
                    );
                }

                Sysroot::Std => {
                    deps.insert(
                        "std".into(),
                        Dependency::Full(DependencyFull {
                            path: Some(builder.rust_src.as_ref().unwrap().join("std")),
                            features: if builder.features.contains(&Features::CompilerBuiltinsMem) {
                                Some(vec!["compiler-builtins-mem".into()])
                            } else {
                                None
                            },
                            ..Default::default()
                        }),
                    );
                }
            }
            deps
        }),
        patch: Some(Patches {
            sources: if let Sysroot::Core = builder.sysroot_crate {
                BTreeMap::new()
            } else {
                let mut sources = BTreeMap::new();
                sources.insert("crates-io".into(), {
                    let mut x = BTreeMap::new();
                    x.insert(
                        "rustc-std-workspace-core".to_string(),
                        Dependency::Full(DependencyFull {
                            path: Some(
                                builder
                                    .rust_src
                                    .as_ref()
                                    .unwrap()
                                    .join("rustc-std-workspace-core"),
                            ),
                            ..Default::default()
                        }),
                    );
                    x
                });
                sources
            },
        }),
        profile: {
            match &builder.manifest {
                Some(manifest) => {
                    let toml: CargoToml =
                        from_path(manifest).with_context(|| manifest.display().to_string())?;
                    toml.profile
                }
                None => None,
            }
        },
        ..Default::default()
    };
    let path = builder.output.join("Cargo.toml");
    to_path(&path, &toml).context("Failed writing sysroot Cargo.toml")?;
    Ok(path)
}

/// The entry-point for building the alloc crate, which builds all the others
///
/// Should ONLY be called by [`SysrootBuilder::build`].
fn build_alloc(alloc_cargo_toml: &Path, builder: &SysrootBuilder) -> Result<()> {
    let path = alloc_cargo_toml;
    let triple = builder.target.as_ref().unwrap();
    let target_dir = builder.output.join("target");

    // TODO: Eat output if up to date? Always? On error?
    let exit = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
        .arg("rustc")
        .arg("--release")
        .arg("--target")
        // If it doesn't work, assume it's a builtin path?
        .arg(&triple.canonicalize().unwrap_or_else(|_| triple.into()))
        .arg("--target-dir")
        .arg(&target_dir)
        .arg("--manifest-path")
        .arg(path)
        .arg("--") // Pass to rustc directly.
        .arg("-Z")
        // The rust build system only passes this for rustc? xbuild passes this for alloc. 🤷‍♀️
        .arg("force-unstable-if-unmarked")
        .env("RUSTFLAGS", {
            let mut env = OsString::new();
            if let Some(exist) = std::env::var_os("RUSTFLAGS") {
                env.push(exist);
            }
            for flag in &builder.rustc_flags {
                env.push(" ");
                env.push(flag)
            }
            env
        })
        // Causes clippy to leak output
        // See #6
        .env_remove("RUSTC_WORKSPACE_WRAPPER")
        .status()
        .context("Couldn't find/run cargo command")?;
    if !exit.success() {
        return Err(anyhow!(
            "Failed to build sysroot: Exit code {}",
            exit.code()
                .map(|i| i.to_string())
                .unwrap_or_else(|| "Killed by signal".to_string())
        ));
    }

    // Copy artifacts to sysroot.
    for entry in fs::read_dir(
        target_dir
            .join(
                &triple
                    .file_stem()
                    .context("Failed to parse target triple")?,
            )
            .join("release")
            .join("deps"),
    )
    .context("Failure to read artifact directory")?
    {
        let entry = entry?;
        let name = entry
            .file_name()
            .into_string()
            .map_err(|e| Error::msg(e.to_string_lossy().to_string()))
            .context("Invalid Unicode in path")?;
        if name.starts_with("lib") {
            let out = artifact_dir(&builder.output, &triple)?.join(name);
            fs::copy(entry.path(), &out).with_context(|| {
                format!(
                    "Copying sysroot artifact from {} to {} failed",
                    entry.path().display(),
                    out.display()
                )
            })?;
        }
    }

    Ok(())
}

/// The output artifact directory
///
/// Not part of the public API.
fn artifact_dir(sysroot_dir: &Path, target: &Path) -> Result<PathBuf> {
    Ok(sysroot_dir
        .join("lib")
        .join("rustlib")
        .join(target.file_stem().context("Invalid Target Specification")?)
        .join("lib"))
}

/// Clean up generated sysroot artifacts.
///
/// Should be called before [`build_sysroot`] if you want this behavior.
pub fn clean_artifacts(sysroot_dir: &Path) -> Result<()> {
    // Clean-up old artifacts
    match remove_dir_all::remove_dir_all(sysroot_dir) {
        Ok(_) => (),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
        e => e.context("Couldn't clean sysroot artifacts")?,
    };
    Ok(())
}