#[cfg(target_arch = "wasm32")]
compile_error!("The `compiler` feature is not supported on wasm32 targets.");
use std::{
io,
path::{Path, PathBuf},
process::{Command, Output, Stdio},
};
#[cfg(not(target_arch = "wasm32"))]
static TOOLCHAIN_STABLE: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/toolchain-stable.tar.xz"));
#[cfg(not(target_arch = "wasm32"))]
static TOOLCHAIN_NIGHTLY: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/toolchain-nightly.tar.xz"));
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ToolchainChannel {
#[default]
Stable,
Nightly,
}
impl ToolchainChannel {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Stable => "stable",
Self::Nightly => "nightly",
}
}
}
pub enum Input {
Dir(PathBuf),
File {
path: PathBuf,
deps: Vec<Dependency>,
},
}
impl Input {
pub fn dir(p: impl AsRef<Path>) -> Self {
Self::Dir(p.as_ref().to_path_buf())
}
pub fn file(p: impl AsRef<Path>) -> Self {
Self::File { path: p.as_ref().to_path_buf(), deps: vec![] }
}
pub fn file_with_deps(p: impl AsRef<Path>, deps: Vec<Dependency>) -> Self {
Self::File { path: p.as_ref().to_path_buf(), deps }
}
}
#[derive(Debug, Clone, Default)]
pub struct Dependency {
pub name: String,
pub version: Option<String>,
pub path: Option<PathBuf>,
pub git: Option<String>,
pub branch: Option<String>,
pub tag: Option<String>,
pub rev: Option<String>,
pub features: Vec<String>,
pub default_features: Option<bool>,
pub optional: Option<bool>,
pub package: Option<String>,
}
impl Dependency {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into(), ..Default::default() }
}
pub fn version(mut self, v: impl Into<String>) -> Self {
self.version = Some(v.into());
self
}
pub fn path(mut self, p: impl AsRef<Path>) -> Self {
self.path = Some(p.as_ref().to_path_buf());
self
}
pub fn git(mut self, url: impl Into<String>) -> Self {
self.git = Some(url.into());
self
}
pub fn branch(mut self, b: impl Into<String>) -> Self {
self.branch = Some(b.into());
self
}
pub fn tag(mut self, t: impl Into<String>) -> Self {
self.tag = Some(t.into());
self
}
pub fn rev(mut self, r: impl Into<String>) -> Self {
self.rev = Some(r.into());
self
}
pub fn feature(mut self, f: impl Into<String>) -> Self {
self.features.push(f.into());
self
}
pub fn default_features(mut self, enabled: bool) -> Self {
self.default_features = Some(enabled);
self
}
pub fn optional(mut self, opt: bool) -> Self {
self.optional = Some(opt);
self
}
pub fn package(mut self, pkg: impl Into<String>) -> Self {
self.package = Some(pkg.into());
self
}
fn to_toml_value(&self) -> String {
let mut parts: Vec<String> = vec![];
if let Some(v) = &self.version {
parts.push(format!("version = \"{}\"", v));
}
if let Some(p) = &self.path {
parts.push(format!("path = \"{}\"", p.display()));
}
if let Some(g) = &self.git {
parts.push(format!("git = \"{}\"", g));
}
if let Some(b) = &self.branch {
parts.push(format!("branch = \"{}\"", b));
}
if let Some(t) = &self.tag {
parts.push(format!("tag = \"{}\"", t));
}
if let Some(r) = &self.rev {
parts.push(format!("rev = \"{}\"", r));
}
if !self.features.is_empty() {
let fs: Vec<String> = self.features.iter().map(|f| format!("\"{}\"", f)).collect();
parts.push(format!("features = [{}]", fs.join(", ")));
}
if let Some(df) = self.default_features {
parts.push(format!("default-features = {}", df));
}
if let Some(opt) = self.optional {
parts.push(format!("optional = {}", opt));
}
if let Some(pkg) = &self.package {
parts.push(format!("package = \"{}\"", pkg));
}
if parts.is_empty() {
"\"*\"".to_string()
} else {
format!("{{ {} }}", parts.join(", "))
}
}
}
#[derive(Debug)]
pub struct CompileOutput {
pub success: bool,
pub stdout: String,
pub stderr: String,
}
#[derive(Debug)]
pub enum CompileError {
Io(io::Error),
InvalidProject(String),
BuildFailed(CompileOutput),
}
impl std::fmt::Display for CompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::InvalidProject(s) => write!(f, "invalid project: {s}"),
Self::BuildFailed(o) => write!(f, "cargo build failed:\n{}", o.stderr),
}
}
}
impl std::error::Error for CompileError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for CompileError {
fn from(e: io::Error) -> Self { Self::Io(e) }
}
#[derive(Debug, Clone, clap::ValueEnum)]
pub enum CrossTarget {
#[value(name = "LINUX_X64")] LinuxX64,
#[value(name = "LINUX_X86")] LinuxX86,
#[value(name = "LINUX_ARM64")] LinuxArm64,
#[value(name = "LINUX_ARM")] LinuxArm,
#[value(name = "LINUX_MUSL_X64")] LinuxMuslX64,
#[value(name = "LINUX_MUSL_ARM64")] LinuxMuslArm64,
#[value(name = "WIN_X64")] WinX64,
#[value(name = "WIN_X86")] WinX86,
#[value(name = "WIN_X64_GNU")] WinX64Gnu,
#[value(name = "WIN_ARM64")] WinArm64,
#[value(name = "MAC_X64")] MacX64,
#[value(name = "MAC_ARM64")] MacArm64,
#[value(name = "WASM")] Wasm,
#[value(name = "WASM_WASI")] WasmWasi,
#[value(name = "ANDROID_ARM64")] AndroidArm64,
#[value(name = "ANDROID_X64")] AndroidX64,
#[value(name = "ANDROID_ARM")] AndroidArm,
#[value(name = "ANDROID_X86")] AndroidX86,
#[value(name = "FREEBSD_X64")] FreebsdX64,
#[value(name = "IOS_ARM64")] IosArm64,
}
impl CrossTarget {
pub fn triple(&self) -> &'static str {
match self {
Self::LinuxX64 => "x86_64-unknown-linux-gnu",
Self::LinuxX86 => "i686-unknown-linux-gnu",
Self::LinuxArm64 => "aarch64-unknown-linux-gnu",
Self::LinuxArm => "armv7-unknown-linux-gnueabihf",
Self::LinuxMuslX64 => "x86_64-unknown-linux-musl",
Self::LinuxMuslArm64 => "aarch64-unknown-linux-musl",
Self::WinX64 => "x86_64-pc-windows-msvc",
Self::WinX86 => "i686-pc-windows-msvc",
Self::WinX64Gnu => "x86_64-pc-windows-gnu",
Self::WinArm64 => "aarch64-pc-windows-msvc",
Self::MacX64 => "x86_64-apple-darwin",
Self::MacArm64 => "aarch64-apple-darwin",
Self::Wasm => "wasm32-unknown-unknown",
Self::WasmWasi => "wasm32-wasip1",
Self::AndroidArm64 => "aarch64-linux-android",
Self::AndroidX64 => "x86_64-linux-android",
Self::AndroidArm => "armv7-linux-androideabi",
Self::AndroidX86 => "i686-linux-android",
Self::FreebsdX64 => "x86_64-unknown-freebsd",
Self::IosArm64 => "aarch64-apple-ios",
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn compile(input: Input, release: bool, live_output: bool, show_progress: bool, cross_target: Option<CrossTarget>, channel: ToolchainChannel) -> Result<Option<CompileOutput>, CompileError> {
match &input {
Input::Dir(p) => validate_project(p)?,
Input::File { path, .. } => {
if !path.exists() {
return Err(CompileError::Io(io::Error::new(
io::ErrorKind::NotFound,
format!("{}: file not found", path.display()),
)));
}
}
}
let tarball = match channel {
ToolchainChannel::Stable => TOOLCHAIN_STABLE,
ToolchainChannel::Nightly => TOOLCHAIN_NIGHTLY,
};
let toolchain_tmp = tempdir()?;
extract_tarball(tarball, toolchain_tmp.path(), show_progress, "toolchain")?;
let toolchain_root = find_single_subdir(toolchain_tmp.path())
.ok_or_else(|| CompileError::Io(io::Error::new(
io::ErrorKind::NotFound,
"could not locate toolchain root inside the extracted tarball",
)))?;
let rustc_bin = toolchain_root.join("rustc/bin/rustc");
let cargo_bin = toolchain_root.join("cargo/bin/cargo");
merge_std_into_sysroot(&toolchain_root)?;
if let Some(ref ct) = cross_target {
fetch_cross_std(ct.triple(), &toolchain_root, show_progress, channel)?;
};
let path_with_toolchain = format!(
"{}:{}:{}",
toolchain_root.join("rustc/bin").display(),
toolchain_root.join("cargo/bin").display(),
std::env::var("PATH").unwrap_or_default(),
);
let (project_dir, _project_tmp) = match &input {
Input::Dir(p) => {
(p.clone(), None::<TempDir>)
}
Input::File { path, deps } => {
let tmp = tempdir()?;
let proj = synthesise_project(path, deps, tmp.path())?;
(proj, Some(tmp))
}
};
let mut cmd = Command::new(&cargo_bin);
cmd.arg("build");
if release {
cmd.arg("--release");
}
if let Some(ref ct) = cross_target {
cmd.arg("--target").arg(ct.triple());
}
cmd.env("RUSTC", &rustc_bin)
.env("PATH", &path_with_toolchain)
.env_remove("RUSTFLAGS")
.env_remove("CARGO_ENCODED_RUSTFLAGS")
.current_dir(&project_dir);
let result = if live_output {
let status = cmd
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
if status.success() {
Ok(None)
} else {
Err(CompileError::BuildFailed(CompileOutput {
success: false,
stdout: String::new(),
stderr: String::new(),
}))
}
} else {
let compile_pb = if show_progress {
use indicatif::{ProgressBar, ProgressStyle};
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::with_template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]),
);
pb.set_message("Compiling\u{2026}");
pb.tick(); pb.enable_steady_tick(std::time::Duration::from_millis(80));
Some(pb)
} else {
None
};
let output: Output = cmd.output()?;
if let Some(pb) = compile_pb { pb.finish_and_clear(); }
let res = CompileOutput {
success: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
};
if res.success { Ok(Some(res)) } else { Err(CompileError::BuildFailed(res)) }
};
if show_progress {
use indicatif::{ProgressBar, ProgressStyle};
let tp = toolchain_tmp.into_path();
let pp = _project_tmp.map(|t| t.into_path());
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::with_template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]),
);
pb.set_message("Cleaning up\u{2026}");
pb.tick(); pb.enable_steady_tick(std::time::Duration::from_millis(80));
let _ = std::fs::remove_dir_all(&tp);
if let Some(p) = pp { let _ = std::fs::remove_dir_all(&p); }
pb.finish_with_message("Done");
}
result
}
#[cfg(not(target_arch = "wasm32"))]
struct TempDir(PathBuf);
#[cfg(not(target_arch = "wasm32"))]
impl TempDir {
fn path(&self) -> &Path { &self.0 }
fn into_path(self) -> PathBuf {
let path = self.0.clone();
std::mem::forget(self);
path
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[cfg(not(target_arch = "wasm32"))]
fn tempdir() -> io::Result<TempDir> {
use std::time::{SystemTime, UNIX_EPOCH};
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!("toolkit-zero-compiler-{}-{}", std::process::id(), nonce));
std::fs::create_dir_all(&dir)?;
Ok(TempDir(dir))
}
#[cfg(not(target_arch = "wasm32"))]
fn extract_tarball(bytes: &[u8], dest: &Path, show_progress: bool, label: &str) -> Result<(), CompileError> {
if show_progress {
use indicatif::{ProgressBar, ProgressStyle};
let total = bytes.len() as u64;
let pb = ProgressBar::new(total);
pb.set_style(
ProgressStyle::with_template(
"{spinner:.cyan} {msg} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})"
)
.unwrap()
.progress_chars("=>-"),
);
pb.set_message(format!("decompressing {label}"));
let tracked = pb.wrap_read(bytes);
let xz = xz2::read::XzDecoder::new(tracked);
let mut archive = tar::Archive::new(xz);
archive.unpack(dest).map_err(CompileError::Io)?;
pb.finish_with_message(format!("decompressed {label}"));
} else {
let xz = xz2::read::XzDecoder::new(bytes);
let mut archive = tar::Archive::new(xz);
archive.unpack(dest).map_err(CompileError::Io)?;
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn merge_std_components(src_root: &Path, dest_rustlib: &Path) -> Result<(), CompileError> {
for entry in std::fs::read_dir(src_root).map_err(CompileError::Io)? {
let entry = entry.map_err(CompileError::Io)?;
let name = entry.file_name();
if name.to_string_lossy().starts_with("rust-std-") {
let std_rustlib = entry.path().join("lib/rustlib");
if std_rustlib.exists() {
copy_dir_merge(&std_rustlib, dest_rustlib).map_err(CompileError::Io)?;
}
}
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn merge_std_into_sysroot(toolchain_root: &Path) -> Result<(), CompileError> {
let rustc_rustlib = toolchain_root.join("rustc/lib/rustlib");
merge_std_components(toolchain_root, &rustc_rustlib)
}
#[cfg(not(target_arch = "wasm32"))]
fn merge_cross_std(cross_tmp: &Path, toolchain_root: &Path) -> Result<(), CompileError> {
let outer = find_single_subdir(cross_tmp)
.ok_or_else(|| CompileError::Io(io::Error::new(
io::ErrorKind::NotFound,
"cross-std tarball has unexpected structure (no single top-level dir)",
)))?;
let dest_rustlib = toolchain_root.join("rustc/lib/rustlib");
merge_std_components(&outer, &dest_rustlib)
}
#[cfg(not(target_arch = "wasm32"))]
fn copy_dir_merge(src: &Path, dst: &Path) -> io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let dst_path = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_dir_merge(&entry.path(), &dst_path)?;
} else {
std::fs::copy(entry.path(), &dst_path)?;
}
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn cross_std_cache_dir(triple: &str, channel: &str) -> PathBuf {
let cargo_home = std::env::var("CARGO_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".cargo".to_string());
PathBuf::from(home).join(".cargo")
});
cargo_home.join("toolchain-cache").join(triple).join(channel)
}
#[cfg(not(target_arch = "wasm32"))]
fn parse_manifest_section_rt(text: &str, section: &str) -> Option<(String, String)> {
let mut in_section = false;
let mut xz_url: Option<String> = None;
let mut xz_hash: Option<String> = None;
for line in text.lines() {
let line = line.trim();
if line.starts_with('[') {
if line == section { in_section = true; }
else if in_section { break; }
continue;
}
if !in_section { continue; }
if let Some(rest) = line.strip_prefix("xz_url = \"") {
xz_url = Some(rest.trim_end_matches('"').to_string());
} else if let Some(rest) = line.strip_prefix("xz_hash = \"") {
xz_hash = Some(rest.trim_end_matches('"').to_string());
}
if xz_url.is_some() && xz_hash.is_some() { break; }
}
Some((xz_url?, xz_hash?))
}
#[cfg(not(target_arch = "wasm32"))]
fn fetch_cross_std(triple: &str, toolchain_root: &Path, _show_progress: bool, channel: ToolchainChannel) -> Result<(), CompileError> {
use sha2::{Digest, Sha256};
use std::io::Read;
use indicatif::{ProgressBar, ProgressStyle};
let cache_dir = cross_std_cache_dir(triple, channel.as_str());
let cache_file = cache_dir.join("rust-std.tar.xz");
let tarball_bytes: Vec<u8> = if cache_file.exists() {
std::fs::read(&cache_file).map_err(CompileError::Io)?
} else {
let dl_pb = ProgressBar::new_spinner();
dl_pb.set_style(
ProgressStyle::with_template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]),
);
let manifest_url = format!("https://static.rust-lang.org/dist/channel-rust-{}.toml", channel.as_str());
dl_pb.set_message(format!("fetching manifest for {triple}…"));
dl_pb.tick();
dl_pb.enable_steady_tick(std::time::Duration::from_millis(80));
let mut manifest_bytes = Vec::new();
ureq::get(&manifest_url)
.call()
.map_err(|e| CompileError::Io(io::Error::new(io::ErrorKind::Other, e.to_string())))?
.into_reader()
.read_to_end(&mut manifest_bytes)
.map_err(CompileError::Io)?;
let manifest = String::from_utf8(manifest_bytes)
.map_err(|e| CompileError::Io(io::Error::new(io::ErrorKind::InvalidData, e.to_string())))?;
let section = format!("[pkg.rust-std.target.{triple}]");
let (xz_url, xz_hash) = parse_manifest_section_rt(&manifest, §ion)
.ok_or_else(|| CompileError::Io(io::Error::new(
io::ErrorKind::NotFound,
format!("target '{triple}' has no rust-std entry in the {} manifest", channel.as_str()),
)))?;
dl_pb.set_message(format!("downloading rust-std for {triple}…"));
let response = ureq::get(&xz_url)
.call()
.map_err(|e| CompileError::Io(io::Error::new(io::ErrorKind::Other, e.to_string())))?;
let total_opt: Option<u64> = response
.header("content-length")
.and_then(|v| v.parse().ok());
dl_pb.finish_and_clear();
let dl_pb2 = if let Some(total) = total_opt {
let pb = ProgressBar::new(total);
pb.set_style(
ProgressStyle::with_template(
"{spinner:.cyan} {msg} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})"
)
.unwrap()
.progress_chars("=>-"),
);
pb.set_message(format!("downloading rust-std for {triple}"));
pb
} else {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::with_template("{spinner:.cyan} {msg} {bytes}")
.unwrap()
.tick_strings(&["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]),
);
pb.set_message(format!("downloading rust-std for {triple}…"));
pb
};
dl_pb2.tick();
dl_pb2.enable_steady_tick(std::time::Duration::from_millis(80));
let mut reader = dl_pb2.wrap_read(response.into_reader());
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).map_err(CompileError::Io)?;
dl_pb2.finish_with_message(format!("downloaded rust-std for {triple}"));
let expected = xz_hash.strip_prefix("sha256:").unwrap_or(&xz_hash).to_string();
let hash = Sha256::digest(&bytes);
let actual: String = hash.iter().map(|b| format!("{b:02x}")).collect();
if actual != expected {
return Err(CompileError::Io(io::Error::new(
io::ErrorKind::InvalidData,
format!("SHA-256 mismatch for rust-std-{triple}: expected {expected}, got {actual}"),
)));
}
std::fs::create_dir_all(&cache_dir).map_err(CompileError::Io)?;
std::fs::write(&cache_file, &bytes).map_err(CompileError::Io)?;
bytes
};
let cross_tmp = tempdir()?;
extract_tarball(&tarball_bytes, cross_tmp.path(), true, &format!("rust-std for {triple}"))?;
merge_cross_std(cross_tmp.path(), toolchain_root)?;
Ok(())
}
fn find_single_subdir(parent: &Path) -> Option<PathBuf> {
let mut entries: Vec<PathBuf> = std::fs::read_dir(parent)
.ok()?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
if entries.len() == 1 { entries.pop() } else { None }
}
fn validate_project(dir: &Path) -> Result<(), CompileError> {
if !dir.join("Cargo.toml").exists() {
return Err(CompileError::InvalidProject(format!(
"{}: missing Cargo.toml", dir.display()
)));
}
let src = dir.join("src");
if !src.join("main.rs").exists() && !src.join("lib.rs").exists() {
return Err(CompileError::InvalidProject(format!(
"{}: missing src/main.rs or src/lib.rs", dir.display()
)));
}
Ok(())
}
fn synthesise_project(
src_file: &Path,
deps: &[Dependency],
tmp: &Path,
) -> Result<PathBuf, CompileError> {
let stem = src_file
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("project");
let project_name: String = stem
.chars()
.map(|c| if c.is_alphanumeric() || c == '-' { c } else { '_' })
.collect();
let project_dir = tmp.join(&project_name);
let src_dir = project_dir.join("src");
std::fs::create_dir_all(&src_dir)?;
std::fs::copy(src_file, src_dir.join("main.rs"))?;
let mut toml = format!(
"[package]\n\
name = \"{}\"\n\
version = \"0.1.0\"\n\
edition = \"2021\"\n",
project_name
);
if !deps.is_empty() {
toml.push_str("\n[dependencies]\n");
for dep in deps {
toml.push_str(&format!("{} = {}\n", dep.name, dep.to_toml_value()));
}
}
std::fs::write(project_dir.join("Cargo.toml"), &toml)?;
Ok(project_dir)
}