use crate::meta::{self, Metadata, Package, Target};
use crate::store::{sha256_hex, Store};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{Condvar, Mutex, OnceLock};
use std::time::Instant;
const TOOL_VERSION: &str = "corgi/0.21";
#[derive(Clone, Copy, PartialEq)]
pub enum Mode {
Build,
Run,
Check,
Clippy,
Test,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Kind {
Lib,
Bsc, Bsr, Bin,
Test, }
struct UnitDep {
unit: usize,
extern_name: Option<String>,
}
struct Unit {
pkg: usize,
kind: Kind,
host: bool,
is_root: bool,
target: Target,
features: Vec<String>,
deps: Vec<UnitDep>,
profile: meta::UgProfile,
}
#[derive(Clone, Serialize, Deserialize, Default)]
struct BuildScriptOut {
cfgs: Vec<String>,
envs: Vec<(String, String)>,
link_libs: Vec<String>,
link_search: Vec<String>,
link_args: Vec<String>,
metadata: Vec<(String, String)>,
stdout: String,
}
#[derive(Clone, Serialize, Deserialize)]
struct OutputFile {
name: String,
hash: String,
exe: bool,
}
#[derive(Clone, Serialize, Deserialize, Default)]
struct ActionResult {
#[serde(default)]
outputs: Vec<OutputFile>,
#[serde(default)]
stderr: String,
#[serde(default)]
bs: Option<BuildScriptOut>,
}
#[derive(Clone)]
struct MetaOut {
file: String,
hash: String,
}
fn unit_crate_type(unit: &Unit) -> String {
match unit.kind {
Kind::Lib => {
if unit.target.kind.iter().any(|k| k == "proc-macro") {
"proc-macro".to_string()
} else if unit.is_root && unit.target.crate_types.iter().any(|c| c == "cdylib") {
unit.target.crate_types.join(",")
} else {
"lib".to_string()
}
}
Kind::Bsc | Kind::Bin | Kind::Test => "bin".to_string(),
Kind::Bsr => String::new(),
}
}
fn unit_pipelined(unit: &Unit) -> bool {
matches!(unit.kind, Kind::Lib) && unit_crate_type(unit) == "lib"
}
fn unit_links(unit: &Unit) -> bool {
match unit.kind {
Kind::Bin | Kind::Bsc | Kind::Test => true,
Kind::Lib => unit_crate_type(unit) != "lib",
Kind::Bsr => false,
}
}
fn is_checked(ctx: &Ctx, idx: usize) -> bool {
ctx.check_mode.get(idx).copied().unwrap_or(false)
}
fn is_pipelined(ctx: &Ctx, idx: usize) -> bool {
unit_pipelined(&ctx.units[idx]) || is_checked(ctx, idx)
}
fn is_linking(ctx: &Ctx, idx: usize) -> bool {
unit_links(&ctx.units[idx]) && !is_checked(ctx, idx)
}
struct ToolRt {
name: String,
version: String,
env: String,
value: String,
id: String,
bin: String,
packages: Vec<String>,
}
#[derive(Default, Clone, Copy)]
struct Phases {
key_ns: u64,
cache_ns: u64,
rustc_ns: u64,
validate_ns: u64,
ingest_ns: u64,
ingest_bytes: u64,
finish_ns: u64,
}
struct UnitResult {
key: String,
cached: bool,
res: ActionResult,
main: Option<OutputFile>,
phases: Phases,
}
pub struct Ctx {
store: Store,
verbose: bool,
rustc: String,
rustc_version: String,
host: String,
cfg_env: Vec<(String, String)>,
meta: Metadata,
units: Vec<Unit>,
pool: PathBuf,
pool_logical: PathBuf,
cargo: String,
cargo_home: String,
base_env: Vec<(String, String)>,
workspace_root: String,
sysroot: String,
rustup_home: String,
devdir: String,
sandbox: bool,
darwin_dirs: Vec<String>,
sdkroot: String,
src_hash_memo: Mutex<HashMap<usize, String>>,
src_hash_nanos: std::sync::atomic::AtomicU64,
file_names_memo: Mutex<HashMap<(String, bool), Vec<String>>>,
profile_name: String,
toolchain: String,
tools: Vec<ToolRt>,
env_probes: Vec<(String, String, Vec<String>, Vec<String>)>,
target: Option<String>,
timings: bool,
incremental: bool,
jobserver: jobserver::Client,
idents: Vec<String>,
check_mode: Vec<bool>,
lints: Vec<LintFlags>,
clippy: bool,
clippy_driver: String,
clippy_id: String,
clippy_conf: Option<PathBuf>,
target_std_libdir: Option<String>,
cfg_env_target: Vec<(String, String)>,
target_rustflags: Vec<String>,
host_rustflags: Vec<String>,
config_env: Vec<(String, String)>,
extra_inputs: ExtraInputs,
}
impl Ctx {
fn extra_inputs_for(&self, pkg: &Package) -> &[String] {
self.extra_inputs.get(&pkg.name).map(Vec::as_slice).unwrap_or(&[])
}
}
#[derive(Serialize)]
struct CompileKey<'a> {
kind: &'a str,
tool: &'a str,
rustc: &'a str,
host: &'a str,
pkg: [&'a str; 3],
src_hash: &'a str,
crate_name: &'a str,
edition: &'a str,
crate_type: &'a str,
src_rel: &'a str,
features: &'a [String],
externs: &'a [(String, String, String)],
link_closure: &'a [(String, String)],
cfgs: &'a [String],
renvs: &'a [(String, String)],
link_libs: &'a [String],
link_search: &'a [String],
link_args: &'a [String],
out_key: &'a str,
profile: &'a [String],
lints: &'a [String],
clippy: &'a str,
incr: bool,
ident: &'a str,
oso: &'a str,
env: &'a [(String, String)],
cap_lints: bool,
rustflags: &'a [String],
toolchain: &'a str,
tgt: &'a str,
}
#[derive(Serialize)]
struct RunKey<'a> {
kind: &'a str,
tool: &'a str,
rustc: &'a str,
host: &'a str,
pkg: [&'a str; 3],
src_hash: &'a str,
script: [&'a str; 2],
env: &'a [(String, String)],
dep_env: &'a [(String, String)],
toolchain: &'a str,
tools: &'a [String],
}
#[derive(serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct ToolSpec {
#[serde(skip)]
name: String,
version: String,
url: String,
sha256: String,
#[serde(default)]
bin: String,
#[serde(default)]
path: String,
env: String,
#[serde(default)]
packages: Vec<String>,
#[serde(default)]
auth: String,
}
#[derive(serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct EnvProbe {
#[serde(skip)]
name: String,
command: String,
packages: Vec<String>,
#[serde(default)]
profiles: Vec<String>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct UniverseDef {
packages: Vec<String>,
}
#[derive(serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct CorgiToml {
#[serde(default)]
tools: std::collections::BTreeMap<String, ToolSpec>,
#[serde(default)]
env: std::collections::BTreeMap<String, EnvProbe>,
#[serde(default)]
universe: std::collections::BTreeMap<String, UniverseDef>,
#[serde(default, rename = "extra-inputs")]
extra_inputs: std::collections::BTreeMap<String, Vec<String>>,
}
type Universes = Vec<(String, Vec<String>)>;
type ExtraInputs = std::collections::BTreeMap<String, Vec<String>>;
type CorgiManifest = (Vec<ToolSpec>, Vec<EnvProbe>, Universes, ExtraInputs);
fn read_corgi_toml(dir: &Path) -> Result<Option<CorgiManifest>> {
let mut found: Option<PathBuf> = None;
let mut cur = Some(dir);
while let Some(d) = cur {
let p = d.join("corgi.toml");
if p.exists() {
found = Some(p);
break;
}
cur = d.parent();
}
let Some(p) = found else { return Ok(None) };
let text = fs::read_to_string(&p)?;
let parsed: CorgiToml =
toml::from_str(&text).with_context(|| format!("parsing {}", p.display()))?;
let mut specs: Vec<ToolSpec> = Vec::new();
for (name, mut t) in parsed.tools {
t.name = name;
if t.bin.is_empty() == t.path.is_empty() {
bail!(
"tool `{}` in {} needs exactly one of `bin` (executable) or `path` (file/dir)",
t.name,
p.display()
);
}
specs.push(t);
}
let mut probes: Vec<EnvProbe> = Vec::new();
for (name, mut e) in parsed.env {
e.name = name;
if e.command.is_empty() || e.packages.is_empty() {
bail!("env `{}` in {} needs a command and a non-empty packages list", e.name, p.display());
}
probes.push(e);
}
let universes: Universes = parsed.universe.into_iter().map(|(k, v)| (k, v.packages)).collect();
Ok(Some((specs, probes, universes, parsed.extra_inputs)))
}
#[derive(Default, Clone)]
struct LintFlags {
rustc_only: Vec<String>,
with_clippy: Vec<String>,
}
type LintEntry = (i64, String, String, String);
fn lint_entries_from_table(table: &toml::Table) -> Result<Vec<LintEntry>> {
let mut entries = Vec::new();
for (tool, lints) in table {
let Some(lints) = lints.as_table() else {
bail!("[lints.{tool}] is not a table");
};
for (lint, value) in lints {
let (level, priority) = match value {
toml::Value::String(level) => (level.clone(), 0),
toml::Value::Table(cfg) => {
if cfg.contains_key("check-cfg") {
bail!("[lints] `{tool}::{lint}`: check-cfg configuration is not supported yet");
}
let level = cfg
.get("level")
.and_then(|v| v.as_str())
.with_context(|| format!("[lints] `{tool}::{lint}` needs a `level`"))?;
let priority = cfg.get("priority").and_then(|v| v.as_integer()).unwrap_or(0);
(level.to_string(), priority)
}
other => bail!("[lints] `{tool}::{lint}`: unsupported value {other:?}"),
};
entries.push((priority, tool.clone(), lint.clone(), level));
}
}
Ok(entries)
}
fn lint_entries_to_flags(entries: &[LintEntry]) -> Result<LintFlags> {
let mut sorted = entries.to_vec();
sorted.sort();
let mut out = LintFlags::default();
for (_, tool, lint, level) in &sorted {
if tool == "rustdoc" {
continue; }
let name = if tool == "rust" { lint.clone() } else { format!("{tool}::{lint}") };
let flag = match level.as_str() {
"allow" => format!("-A{name}"),
"warn" => format!("-W{name}"),
"deny" => format!("-D{name}"),
"forbid" => format!("-F{name}"),
"force-warn" => format!("--force-warn={name}"),
other => bail!("[lints] `{name}`: unknown level `{other}`"),
};
if tool == "rust" {
out.rustc_only.push(flag.clone());
}
out.with_clippy.push(flag);
}
Ok(out)
}
fn resolve_lints(meta: &Metadata) -> Result<Vec<LintFlags>> {
let members: std::collections::HashSet<&str> =
meta.workspace_members.iter().map(|s| s.as_str()).collect();
let ws_path = Path::new(&meta.workspace_root).join("Cargo.toml");
let ws_text = fs::read_to_string(&ws_path).unwrap_or_default();
let ws_doc: toml::Table =
toml::from_str(&ws_text).with_context(|| format!("parsing {}", ws_path.display()))?;
let ws_entries = match ws_doc.get("workspace").and_then(|w| w.get("lints")).and_then(|l| l.as_table()) {
Some(t) => lint_entries_from_table(t)?,
None => Vec::new(),
};
let ws_flags = lint_entries_to_flags(&ws_entries)?;
let mut out = vec![LintFlags::default(); meta.packages.len()];
for (i, pkg) in meta.packages.iter().enumerate() {
if !members.contains(pkg.id.as_str()) {
continue;
}
let text = fs::read_to_string(&pkg.manifest_path)
.with_context(|| format!("reading manifest of {}", pkg.name))?;
let doc: toml::Table =
toml::from_str(&text).with_context(|| format!("parsing manifest of {}", pkg.name))?;
let Some(lints) = doc.get("lints").and_then(|l| l.as_table()) else {
continue;
};
let uses_workspace = lints.get("workspace").and_then(|v| v.as_bool()).unwrap_or(false);
if uses_workspace {
if lints.len() > 1 {
bail!("{}: [lints] mixes `workspace = true` with inline tables", pkg.name);
}
out[i] = ws_flags.clone();
} else {
out[i] = lint_entries_to_flags(&lint_entries_from_table(lints)?)?;
}
}
Ok(out)
}
fn ensure_tool_shims(store: &Store, tools: &[&ToolRt]) -> Result<Option<PathBuf>> {
let with_bin: Vec<&&ToolRt> = tools.iter().filter(|t| !t.bin.is_empty()).collect();
if with_bin.is_empty() {
return Ok(None);
}
let mut ids: Vec<&str> = with_bin.iter().map(|t| t.id.as_str()).collect();
ids.sort();
let subset = crate::store::sha256_hex(ids.join("\n").as_bytes());
let dir = store.root.join("toolsets").join(&subset[..16]);
if dir.exists() {
Store::touch_used(&dir);
return Ok(Some(dir));
}
let tmp = store.tmp_path("shims");
fs::create_dir_all(&tmp)?;
for t in &with_bin {
let target = store
.root
.join("tools")
.join(format!("{}-{}", t.name, t.version))
.join(&t.bin);
std::os::unix::fs::symlink(&target, tmp.join(&t.name)).ok();
}
fs::create_dir_all(dir.parent().unwrap())?;
match fs::rename(&tmp, &dir) {
Ok(()) => {}
Err(_) if dir.exists() => {
fs::remove_dir_all(&tmp).ok();
}
Err(e) => return Err(e).context("publishing tool shims"),
}
Ok(Some(dir))
}
fn ensure_tool(store: &Store, t: &ToolSpec) -> Result<PathBuf> {
let exported = if !t.bin.is_empty() { &t.bin } else { &t.path };
let dest = store.root.join("tools").join(format!("{}-{}", t.name, t.version));
if dest.join(exported).exists() {
touch_tool_marker(&dest);
return Ok(dest.join(exported));
}
eprintln!("corgi: installing tool {} {} (sha256-pinned)", t.name, t.version);
let work = store.tmp_path("tool");
let unpack = work.join("unpack");
fs::create_dir_all(&unpack)?;
let archive = work.join("archive");
match t.auth.as_str() {
"" => {
let st =
Command::new("curl").args(["-sSfL", "-o"]).arg(&archive).arg(&t.url).status()?;
if !st.success() {
bail!("download failed: {}", t.url);
}
}
"github" => {
let (repo, tag, asset) = parse_github_release_url(&t.url).with_context(|| {
format!("tool {}: auth = \"github\" requires a github.com release-asset url", t.name)
})?;
let st = Command::new("gh")
.args(["release", "download", &tag, "-R", &repo, "--pattern", &asset, "--output"])
.arg(&archive)
.status()
.with_context(|| {
format!(
"tool {}: running gh (auth = \"github\" needs the GitHub CLI, logged in)",
t.name
)
})?;
if !st.success() {
bail!(
"tool {}: gh release download failed for {} (is `gh auth status` ok?)",
t.name,
t.url
);
}
}
other => bail!("tool {}: unknown auth scheme `{other}` (supported: \"github\")", t.name),
}
let actual = crate::store::sha256_file(&archive)?;
if actual != t.sha256 {
bail!("sha256 mismatch for tool {}: manifest pins {}, archive is {actual}", t.name, t.sha256);
}
let st = Command::new("tar").arg("-xf").arg(&archive).arg("-C").arg(&unpack).status()?;
if !st.success() {
bail!("unpack failed for tool {}", t.name);
}
if !unpack.join(exported).exists() {
bail!("tool {}: `{exported}` not found inside the archive", t.name);
}
fs::create_dir_all(dest.parent().unwrap())?;
match fs::rename(&unpack, &dest) {
Ok(()) => {}
Err(_) if dest.join(exported).exists() => {}
Err(e) => return Err(e).context("publishing tool"),
}
touch_tool_marker(&dest);
fs::remove_dir_all(&work).ok();
Ok(dest.join(exported))
}
fn parse_github_release_url(url: &str) -> Result<(String, String, String)> {
let rest = url
.strip_prefix("https://github.com/")
.with_context(|| format!("not a github.com url: {url}"))?;
let parts: Vec<&str> = rest.split('/').collect();
match parts.as_slice() {
[owner, repo, "releases", "download", tag, asset]
if !owner.is_empty() && !repo.is_empty() && !tag.is_empty() && !asset.is_empty() =>
{
Ok((format!("{owner}/{repo}"), tag.to_string(), asset.to_string()))
}
_ => bail!("not a release-asset url (expected .../releases/download/<tag>/<asset>): {url}"),
}
}
fn host_triple() -> Result<String> {
let arch = std::env::consts::ARCH;
let os = match std::env::consts::OS {
"macos" => "apple-darwin",
"linux" => "unknown-linux-gnu", o => bail!("unsupported host OS {o}"),
};
Ok(format!("{arch}-{os}"))
}
fn read_toolchain_pin(dir: &Path) -> Result<String> {
let mut found: Option<PathBuf> = None;
let mut cur = Some(dir);
while let Some(d) = cur {
if d.join("rust-toolchain.toml").exists() || d.join("rust-toolchain").exists() {
found = Some(d.to_path_buf());
break;
}
cur = d.parent();
}
let dir = found.as_deref().unwrap_or(dir);
let toml_p = dir.join("rust-toolchain.toml");
let legacy = dir.join("rust-toolchain");
let channel = if toml_p.exists() {
let text = fs::read_to_string(&toml_p)?;
let doc: toml::Table =
toml::from_str(&text).with_context(|| format!("parsing {}", toml_p.display()))?;
doc.get("toolchain")
.and_then(|t| t.get("channel"))
.and_then(|c| c.as_str())
.map(str::to_string)
.with_context(|| format!("no `channel` key in {}", toml_p.display()))?
} else if legacy.exists() {
fs::read_to_string(&legacy)?.trim().to_string()
} else {
bail!(
"corgi requires a pinned toolchain: create rust-toolchain.toml with \
`[toolchain]\nchannel = \"<exact version>\"` (e.g. \"1.94.1\" or \"nightly-2026-03-25\")"
);
};
if !is_concrete_channel(&channel) {
bail!(
"floating toolchain channel `{channel}` is not allowed; \
pin an exact version like \"1.94.1\" or \"nightly-2026-03-25\""
);
}
Ok(channel)
}
fn is_concrete_channel(c: &str) -> bool {
let semver = {
let parts: Vec<&str> = c.split('.').collect();
parts.len() == 3 && parts.iter().all(|p| !p.is_empty() && p.chars().all(|ch| ch.is_ascii_digit()))
};
let dated = c
.strip_prefix("nightly-")
.or_else(|| c.strip_prefix("beta-"))
.map(|d| {
d.len() == 10
&& d.chars()
.enumerate()
.all(|(i, ch)| if i == 4 || i == 7 { ch == '-' } else { ch.is_ascii_digit() })
})
.unwrap_or(false);
semver || dated
}
pub fn gc(store: &Store) -> Result<()> {
let days: u64 = std::env::var("CORGI_GC_TTL_DAYS").ok().and_then(|v| v.parse().ok()).unwrap_or(5);
let (files, dirs, bytes) = gc_trim(store, std::time::Duration::from_secs(days * 24 * 3600))?;
eprintln!(
"corgi: gc removed {files} files and {dirs} dirs ({:.1} MB) older than {days} days",
bytes as f64 / 1e6
);
Ok(())
}
fn maybe_auto_gc(store: &Store) {
let marker = store.root.join("cache").join("trim.txt");
if let Ok(md) = fs::metadata(&marker) {
if let Ok(age) = md.modified().and_then(|m| m.elapsed().map_err(std::io::Error::other)) {
if age < std::time::Duration::from_secs(24 * 3600) {
return;
}
} else {
return; }
}
let days: u64 = std::env::var("CORGI_GC_TTL_DAYS").ok().and_then(|v| v.parse().ok()).unwrap_or(5);
if let Err(e) = gc_trim(store, std::time::Duration::from_secs(days * 24 * 3600)) {
eprintln!("corgi: warning: gc trim failed: {e:#}");
}
let _ = store.write_atomic(&marker, b"trimmed\n");
}
fn gc_trim(store: &Store, ttl: std::time::Duration) -> Result<(u64, u64, u64)> {
let now = std::time::SystemTime::now();
let cutoff = now.checked_sub(ttl).context("ttl too large")?;
let stale = |p: &Path| -> bool {
fs::metadata(p).and_then(|m| m.modified()).map(|m| m < cutoff).unwrap_or(false)
};
let mut files = 0u64;
let mut dirs = 0u64;
let mut bytes = 0u64;
for shard in read_dir_paths(&store.root.join("cache"))? {
if !shard.is_dir() {
continue;
}
for f in read_dir_paths(&shard)? {
if stale(&f) {
bytes += fs::metadata(&f).map(|m| m.len()).unwrap_or(0);
if fs::remove_file(&f).is_ok() {
files += 1;
}
}
}
}
for f in read_dir_paths(&store.root.join("pool"))? {
if stale(&f) && fs::remove_file(&f).is_ok() {
files += 1;
}
}
for p in read_dir_paths(&store.root.join("outdirs"))? {
if p.extension().is_some_and(|e| e == "lock") {
let entry = p.with_extension("");
if !entry.exists() && stale(&p) && fs::remove_file(&p).is_ok() {
files += 1;
}
continue;
}
if !p.is_dir() {
continue;
}
let ok = p.join(".ok");
let verdict = if ok.exists() { stale(&ok) } else { stale(&p) };
if verdict && fs::remove_dir_all(&p).is_ok() {
dirs += 1;
}
}
if store.root.join("hints").exists() {
for f in read_dir_paths(&store.root.join("hints"))? {
if stale(&f) && fs::remove_file(&f).is_ok() {
files += 1;
}
}
}
for p in read_dir_paths(&store.root.join("tools"))? {
if !p.is_dir() {
continue;
}
let marker = p.join(".corgi-used");
if !marker.exists() {
let _ = fs::write(&marker, b"used\n");
continue;
}
if stale(&marker) && fs::remove_dir_all(&p).is_ok() {
dirs += 1;
}
}
for p in read_dir_paths(&store.root.join("toolsets"))? {
if p.is_dir() && stale(&p) && fs::remove_dir_all(&p).is_ok() {
dirs += 1;
}
}
for p in read_dir_paths(&store.root.join("incr"))? {
if p.is_dir() {
if stale(&p) && fs::remove_dir_all(&p).is_ok() {
dirs += 1;
}
} else if stale(&p) && fs::remove_file(&p).is_ok() {
files += 1;
}
}
let cargo_home = store.root.join("cargo-home");
for index_dir in read_dir_paths(&cargo_home.join("registry/src"))? {
for pkg_dir in read_dir_paths(&index_dir)? {
if !pkg_dir.is_dir() {
continue;
}
let ok = pkg_dir.join(".cargo-ok");
let verdict = if ok.exists() { stale(&ok) } else { stale(&pkg_dir) };
if verdict && fs::remove_dir_all(&pkg_dir).is_ok() {
dirs += 1;
}
}
}
for index_dir in read_dir_paths(&cargo_home.join("registry/cache"))? {
for f in read_dir_paths(&index_dir)? {
if f.is_file() && stale(&f) {
let n = fs::metadata(&f).map(|m| m.len()).unwrap_or(0);
if fs::remove_file(&f).is_ok() {
files += 1;
bytes += n;
}
}
}
}
for repo_dir in read_dir_paths(&cargo_home.join("git/checkouts"))? {
for checkout_dir in read_dir_paths(&repo_dir)? {
if !checkout_dir.is_dir() {
continue;
}
let ok = checkout_dir.join(".cargo-ok");
let verdict = if ok.exists() { stale(&ok) } else { stale(&checkout_dir) };
if verdict && fs::remove_dir_all(&checkout_dir).is_ok() {
dirs += 1;
}
}
}
for db_dir in read_dir_paths(&cargo_home.join("git/db"))? {
if db_dir.is_dir() && stale(&db_dir) && fs::remove_dir_all(&db_dir).is_ok() {
dirs += 1;
}
}
let day = now.checked_sub(std::time::Duration::from_secs(24 * 3600)).unwrap_or(cutoff);
for p in read_dir_paths(&store.root.join("tmp"))? {
let old = fs::metadata(&p).and_then(|m| m.modified()).map(|m| m < day).unwrap_or(false);
if old {
if p.is_dir() {
if fs::remove_dir_all(&p).is_ok() {
dirs += 1;
}
} else if fs::remove_file(&p).is_ok() {
files += 1;
}
}
}
Ok((files, dirs, bytes))
}
fn read_dir_paths(dir: &Path) -> Result<Vec<PathBuf>> {
let mut out = Vec::new();
if let Ok(rd) = fs::read_dir(dir) {
for e in rd {
out.push(e?.path());
}
}
Ok(out)
}
fn touch_tool_marker(dir: &Path) {
let marker = dir.join(".corgi-used");
if !marker.exists() {
let _ = fs::write(&marker, b"used\n");
return;
}
Store::touch_used(&marker);
}
fn ensure_toolchain(store: &Store, channel: &str, triple: &str) -> Result<PathBuf> {
let dest = store.root.join("tools").join(format!("rust-{channel}-{triple}"));
let bin = dest.join("bin");
if bin.join("rustc").is_file() && bin.join("cargo").is_file() {
touch_tool_marker(&dest);
return Ok(bin);
}
eprintln!("corgi: installing toolchain {channel}-{triple} into {}", dest.display());
let (base, ver) = if let Some(d) = channel.strip_prefix("nightly-") {
(format!("https://static.rust-lang.org/dist/{d}"), "nightly".to_string())
} else if let Some(d) = channel.strip_prefix("beta-") {
(format!("https://static.rust-lang.org/dist/{d}"), "beta".to_string())
} else {
("https://static.rust-lang.org/dist".to_string(), channel.to_string())
};
let work = store.tmp_path("toolchain");
let install = work.join("install");
fs::create_dir_all(&install)?;
for (comp, payload) in [
("rustc", "rustc".to_string()),
("rust-std", format!("rust-std-{triple}")),
("cargo", "cargo".to_string()),
] {
let name = format!("{comp}-{ver}-{triple}");
let url = format!("{base}/{name}.tar.xz");
let tarball = work.join(format!("{name}.tar.xz"));
let st = Command::new("curl")
.args(["-sSfL", "-o"])
.arg(&tarball)
.arg(&url)
.status()
.context("running curl")?;
if !st.success() {
bail!("download failed: {url}");
}
let expected = capture(Command::new("curl").args(["-sSfL", &format!("{url}.sha256")]), "fetching sha256")?;
let expected = expected.split_whitespace().next().unwrap_or("").to_string();
let actual = crate::store::sha256_file(&tarball)?;
if actual != expected {
bail!("sha256 mismatch for {name}: expected {expected}, got {actual}");
}
let st = Command::new("tar").arg("-xf").arg(&tarball).arg("-C").arg(&work).status()?;
if !st.success() {
bail!("unpack failed: {name}");
}
let payload_dir = work.join(&name).join(&payload);
let st = Command::new("cp")
.arg("-R")
.arg(format!("{}/.", payload_dir.display()))
.arg(&install)
.status()?;
if !st.success() {
bail!("copying component {comp} failed");
}
eprintln!("corgi: {comp} {ver} verified (sha256 {}…)", &expected[..12]);
}
fs::create_dir_all(dest.parent().unwrap())?;
match fs::rename(&install, &dest) {
Ok(()) => {}
Err(_) if dest.join("bin/rustc").is_file() => {} Err(e) => return Err(e).context("publishing toolchain"),
}
touch_tool_marker(&dest);
fs::remove_dir_all(&work).ok();
Ok(dest.join("bin"))
}
fn ensure_rust_src(store: &Store, channel: &str) -> Result<()> {
let dest = store.root.join("tools").join(format!("rust-src-{channel}"));
if dest.join("lib/rustlib/src/rust/library").exists() {
touch_tool_marker(&dest);
return Ok(());
}
eprintln!("corgi: installing rust-src {channel} (sha256-pinned)");
let (base, ver) = if let Some(d) = channel.strip_prefix("nightly-") {
(format!("https://static.rust-lang.org/dist/{d}"), "nightly".to_string())
} else if let Some(d) = channel.strip_prefix("beta-") {
(format!("https://static.rust-lang.org/dist/{d}"), "beta".to_string())
} else {
("https://static.rust-lang.org/dist".to_string(), channel.to_string())
};
let name = format!("rust-src-{ver}");
let work = store.tmp_path("rust-src");
fs::create_dir_all(&work)?;
let tarball = work.join("t.tar.xz");
let url = format!("{base}/{name}.tar.xz");
let st = Command::new("curl").args(["-sSfL", "-o"]).arg(&tarball).arg(&url).status()?;
if !st.success() {
bail!("download failed: {url}");
}
let expected = capture(Command::new("curl").args(["-sSfL", &format!("{url}.sha256")]), "sha256")?;
let expected = expected.split_whitespace().next().unwrap_or("").to_string();
let actual = crate::store::sha256_file(&tarball)?;
if actual != expected {
bail!("sha256 mismatch for {name}");
}
let st = Command::new("tar").arg("-xf").arg(&tarball).arg("-C").arg(&work).status()?;
if !st.success() {
bail!("unpack failed: {name}");
}
let payload = work.join(&name).join("rust-src");
fs::create_dir_all(dest.parent().unwrap())?;
match fs::rename(&payload, &dest) {
Ok(()) => {}
Err(_) if dest.join("lib/rustlib/src/rust/library").exists() => {}
Err(e) => return Err(e).context("publishing rust-src component"),
}
touch_tool_marker(&dest);
fs::remove_dir_all(&work).ok();
Ok(())
}
fn ensure_clippy(store: &Store, channel: &str, triple: &str) -> Result<()> {
let toolchain = store.root.join("tools").join(format!("rust-{channel}-{triple}"));
let driver = toolchain.join("bin/clippy-driver");
if driver.is_file() {
return Ok(());
}
eprintln!("corgi: installing clippy {channel} (sha256-pinned)");
let (base, ver) = if let Some(d) = channel.strip_prefix("nightly-") {
(format!("https://static.rust-lang.org/dist/{d}"), "nightly".to_string())
} else if let Some(d) = channel.strip_prefix("beta-") {
(format!("https://static.rust-lang.org/dist/{d}"), "beta".to_string())
} else {
("https://static.rust-lang.org/dist".to_string(), channel.to_string())
};
let name = format!("clippy-{ver}-{triple}");
let work = store.tmp_path("clippy");
fs::create_dir_all(&work)?;
let tarball = work.join("t.tar.xz");
let url = format!("{base}/{name}.tar.xz");
let st = Command::new("curl").args(["-sSfL", "-o"]).arg(&tarball).arg(&url).status()?;
if !st.success() {
bail!("download failed: {url}");
}
let expected = capture(Command::new("curl").args(["-sSfL", &format!("{url}.sha256")]), "sha256")?;
let expected = expected.split_whitespace().next().unwrap_or("").to_string();
let actual = crate::store::sha256_file(&tarball)?;
if actual != expected {
bail!("sha256 mismatch for {name}");
}
let st = Command::new("tar").arg("-xf").arg(&tarball).arg("-C").arg(&work).status()?;
if !st.success() {
bail!("unpack failed: {name}");
}
let payload = work.join(&name).join("clippy-preview/bin/clippy-driver");
match fs::rename(&payload, &driver) {
Ok(()) => {}
Err(_) if driver.is_file() => {} Err(e) => return Err(e).context("publishing clippy-driver"),
}
fs::remove_dir_all(&work).ok();
Ok(())
}
fn ensure_target_std(store: &Store, channel: &str, target: &str) -> Result<()> {
let dest = store.root.join("tools").join(format!("rust-std-{channel}-{target}"));
if dest.join("lib/rustlib").join(target).join("lib").exists() {
touch_tool_marker(&dest);
return Ok(());
}
eprintln!("corgi: installing rust-std for {target} (sha256-pinned)");
let (base, ver) = if let Some(d) = channel.strip_prefix("nightly-") {
(format!("https://static.rust-lang.org/dist/{d}"), "nightly".to_string())
} else if let Some(d) = channel.strip_prefix("beta-") {
(format!("https://static.rust-lang.org/dist/{d}"), "beta".to_string())
} else {
("https://static.rust-lang.org/dist".to_string(), channel.to_string())
};
let name = format!("rust-std-{ver}-{target}");
let work = store.tmp_path("std");
fs::create_dir_all(&work)?;
let tarball = work.join("t.tar.xz");
let url = format!("{base}/{name}.tar.xz");
let st = Command::new("curl").args(["-sSfL", "-o"]).arg(&tarball).arg(&url).status()?;
if !st.success() {
bail!("download failed: {url}");
}
let expected = capture(Command::new("curl").args(["-sSfL", &format!("{url}.sha256")]), "sha256")?;
let expected = expected.split_whitespace().next().unwrap_or("").to_string();
let actual = crate::store::sha256_file(&tarball)?;
if actual != expected {
bail!("sha256 mismatch for {name}");
}
let st = Command::new("tar").arg("-xf").arg(&tarball).arg("-C").arg(&work).status()?;
if !st.success() {
bail!("unpack failed: {name}");
}
let payload = work.join(&name).join(format!("rust-std-{target}"));
fs::create_dir_all(dest.parent().unwrap())?;
match fs::rename(&payload, &dest) {
Ok(()) => {}
Err(_) if dest.join("lib/rustlib").join(target).join("lib").exists() => {}
Err(e) => return Err(e).context("publishing rust-std"),
}
touch_tool_marker(&dest);
fs::remove_dir_all(&work).ok();
Ok(())
}
pub struct BuildOpts {
pub verbose: bool,
pub release: bool,
pub workspace: bool,
pub package: Option<String>,
pub target: Option<String>,
pub mode: Mode,
pub timings: bool,
pub no_incremental: bool,
pub test_filter: Option<String>,
pub exec_args: Vec<String>,
}
pub fn build(store: Store, dir: &Path, opts: BuildOpts) -> Result<()> {
let BuildOpts {
verbose,
release,
workspace,
package,
target,
mode,
timings,
no_incremental,
test_filter,
exec_args,
} = opts;
let t0 = Instant::now();
let dir = dir
.canonicalize()
.with_context(|| format!("bad directory {}", dir.display()))?;
let manifest = dir.join("Cargo.toml");
if !manifest.exists() {
bail!("no Cargo.toml in {}", dir.display());
}
for var in ["RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", "CARGO_BUILD_RUSTFLAGS"] {
if std::env::var_os(var).is_some_and(|v| !v.is_empty()) {
bail!("{var} is set; corgi only honors rustflags from .cargo/config.toml");
}
}
let (cargo_config, config_dir) = crate::config::discover(&dir)?;
let channel = read_toolchain_pin(&dir)?;
let host_guess = host_triple()?;
ensure_toolchain(&store, &channel, &host_guess)?;
if let Err(e) = ensure_rust_src(&store, &channel) {
eprintln!("corgi: warning: rust-src install failed ({e}); std source display in debuggers unavailable");
}
let toolchain_logical = store
.logical_root()
.join("tools")
.join(format!("rust-{channel}-{host_guess}"));
let rustc = toolchain_logical.join("bin/rustc").display().to_string();
let cargo_bin = toolchain_logical.join("bin/cargo");
let rustc_version = capture(Command::new(&rustc).arg("-vV"), "rustc -vV")?;
let host = rustc_version
.lines()
.find_map(|l| l.strip_prefix("host: "))
.context("rustc -vV: no host line")?
.trim()
.to_string();
if host != host_guess {
bail!("host triple mismatch: corgi resolved {host_guess}, pinned rustc reports {host}");
}
let target_rustflags =
cargo_config.rustflags_for(target.as_deref().unwrap_or(&host_guess))?;
let host_rustflags: Vec<String> =
if target.is_some() { Vec::new() } else { target_rustflags.clone() };
let config_env = cargo_config.env;
let mut cfg_probe = Command::new(&rustc);
cfg_probe.args(["--print", "cfg"]);
cfg_probe.args(&host_rustflags);
let cfg_out = capture(&mut cfg_probe, "rustc --print cfg")?;
let sysroot = toolchain_logical.display().to_string();
let rust_src = Path::new(&sysroot).join("lib/rustlib/src/rust").exists();
let rustc_version = format!("{rustc_version}rust-src: {rust_src}\n");
let cfg_env = cargo_cfg_env(&cfg_out);
let mut target_std_libdir: Option<String> = None;
if let Some(t) = &target {
if t != &host_guess {
ensure_target_std(&store, &channel, t)?;
target_std_libdir = Some(
store
.logical_root()
.join("tools")
.join(format!("rust-std-{channel}-{t}"))
.join("lib/rustlib")
.join(t)
.join("lib")
.display()
.to_string(),
);
}
}
let cfg_env_target = if let Some(t) = &target {
let mut probe = Command::new(&rustc);
probe.args(["--print", "cfg", "--target", t]);
probe.args(&target_rustflags);
let o = capture(&mut probe, "rustc --print cfg --target")?;
cargo_cfg_env(&o)
} else {
cfg_env.clone()
};
let cargo_home = store.logical_root().join("cargo-home").display().to_string();
let t_setup_done = Instant::now();
let (universes, extra_inputs) = match read_corgi_toml(&dir)? {
Some((_, _, u, x)) => (u, x),
None => (Universes::new(), ExtraInputs::new()),
};
let universe_id = sha256_hex(format!("{universes:?}").as_bytes());
let plan_kind = if matches!(mode, Mode::Test) { "test" } else { "build" };
let plan_ptr = sha256_hex(
format!(
"plan-ptr\0{TOOL_VERSION}\0{plan_kind}\0{channel}\0{host_guess}\0{}\0{release}\0{}\0{}",
target.as_deref().unwrap_or(""),
sha256_hex(&fs::read(&manifest)?),
universe_id,
)
.as_bytes(),
);
let mut plan: Option<(String, String)> = None; if let Some(bytes) = store.load_action(&plan_ptr) {
if let Ok(entry) = serde_json::from_slice::<PlanEntry>(&bytes) {
if let Ok(ws_root) = dir.join(&entry.ws_root_rel).canonicalize() {
if plan_fingerprint(&ws_root, &entry.files, &entry.glob_dirs) == entry.fingerprint {
let meta_text =
fs::read(store.cache_path(&entry.meta_blob)).ok().and_then(|b| String::from_utf8(b).ok());
let ug_text =
fs::read(store.cache_path(&entry.ug_blob)).ok().and_then(|b| String::from_utf8(b).ok());
if let (Some(mut m), Some(mut u)) = (meta_text, ug_text) {
let new_root = ws_root.display().to_string();
if entry.ws_root_abs != new_root {
m = m.replace(&entry.ws_root_abs, &new_root);
u = u.replace(&entry.ws_root_abs, &new_root);
}
plan = Some((m, u));
}
}
}
}
}
let resolve_now = || -> Result<(String, String)> {
{
eprintln!("corgi: resolving/fetching dependencies via cargo (metadata only)");
if capture_with_live_stderr(
Command::new(&cargo_bin)
.args(["fetch", "--locked", "--manifest-path"])
.arg(&manifest)
.env("CARGO_HOME", &cargo_home)
.env("RUSTC", &rustc)
.current_dir(&dir),
"cargo fetch --locked",
)
.is_err()
{
eprintln!("corgi: Cargo.lock is missing or stale; letting cargo update it");
capture_with_live_stderr(
Command::new(&cargo_bin)
.args(["fetch", "--manifest-path"])
.arg(&manifest)
.env("CARGO_HOME", &cargo_home)
.env("RUSTC", &rustc)
.current_dir(&dir),
"cargo fetch",
)?;
}
let mut meta_cmd = Command::new(&cargo_bin);
meta_cmd.args(["metadata", "--format-version", "1", "--locked"]);
meta_cmd.env("CARGO_HOME", &cargo_home);
meta_cmd.env("RUSTC", &rustc);
meta_cmd.current_dir(&dir);
meta_cmd.arg("--manifest-path").arg(&manifest);
let meta_json = capture_with_live_stderr(&mut meta_cmd, "cargo metadata")?;
let meta: Metadata = serde_json::from_str(&meta_json).context("parsing cargo metadata")?;
let ws_manifest = Path::new(&meta.workspace_root).join("Cargo.toml");
let mut ug_cmd = Command::new(&cargo_bin);
ug_cmd.env("RUSTC_BOOTSTRAP", "1"); ug_cmd.env("CARGO_HOME", &cargo_home);
ug_cmd.env("RUSTC", &rustc);
ug_cmd.current_dir(&dir);
ug_cmd.args(["build", "--unit-graph", "-Zunstable-options", "--locked"]);
if matches!(mode, Mode::Test) {
ug_cmd.arg("--tests");
}
if release {
ug_cmd.arg("--release");
}
if let Some(t) = &target {
ug_cmd.args(["--target", t]);
}
match target.as_ref().and_then(|t| universes.iter().find(|(k, _)| k == t)) {
Some((_, members)) => {
for m in members {
ug_cmd.args(["-p", m]);
}
}
None => {
ug_cmd.arg("--workspace");
}
}
ug_cmd.arg("--manifest-path").arg(&ws_manifest);
let ug_json = capture_with_live_stderr(&mut ug_cmd, "cargo build --unit-graph")?;
save_plan(&store, &plan_ptr, &dir, &meta, &meta_json, &ug_json)?;
Ok((meta_json, ug_json))
}
};
let from_cache = plan.is_some();
let (mut meta_json, mut ug_json) = match plan {
Some(cached) => {
eprintln!("corgi: plan unchanged (cached resolve; skipping cargo)");
cached
}
None => resolve_now()?,
};
let mut meta: Metadata = serde_json::from_str(&meta_json).context("parsing cargo metadata")?;
if from_cache && meta.packages.iter().any(|p| p.source.is_some() && !p.root().exists()) {
eprintln!("corgi: dependency sources missing from the store; re-fetching");
let (m, u) = resolve_now()?;
meta_json = m;
ug_json = u;
meta = serde_json::from_str(&meta_json).context("parsing cargo metadata")?;
}
for pkg in &meta.packages {
if pkg.source.is_none() {
continue;
}
let root = pkg.root();
let mut marker: Option<PathBuf> = None;
let mut cursor: Option<&Path> = Some(root.as_path());
while let Some(c) = cursor {
if !c.starts_with(&cargo_home) {
break;
}
let ok = c.join(".cargo-ok");
if ok.exists() {
marker = Some(ok);
break;
}
cursor = c.parent();
}
Store::touch_used(marker.as_deref().unwrap_or(root.as_path()));
}
let mut pkgs = HashMap::new();
for (i, p) in meta.packages.iter().enumerate() {
pkgs.insert(p.id.clone(), i);
}
let root_pi = select_root_package(&meta, &pkgs, workspace, package.as_deref(), mode)?;
let ug: meta::UnitGraph = serde_json::from_str(&ug_json).context("parsing unit-graph")?;
let units = translate_unit_graph(&ug, &pkgs, root_pi)?;
let profile_name = units
.iter()
.find(|u| u.is_root)
.map(|u| u.profile.dir_name())
.unwrap_or_else(|| if release { "release".into() } else { "debug".into() });
let idents: Vec<String> = {
let mut memo: Vec<Option<String>> = vec![None; units.len()];
fn ident_of(i: usize, units: &[Unit], meta: &Metadata, memo: &mut Vec<Option<String>>) -> String {
if let Some(v) = &memo[i] {
return v.clone();
}
let u = &units[i];
let mut dep_ids: Vec<String> =
u.deps.iter().map(|d| ident_of(d.unit, units, meta, memo)).collect();
dep_ids.sort();
let mut features = u.features.clone();
features.sort();
let prof = &u.profile;
let ident = sha256_hex(
format!(
"ident\0{}\0{}\0{}\0{:?}\0{}\0{}\0{}\0{}\0{:?}\0{}\0{}\0{}\0{:?}\0{:?}",
TOOL_VERSION,
ws_relative_pkg_id(&meta.packages[u.pkg].id, &meta.workspace_root),
u.target.name,
u.target.kind,
u.host,
prof.name,
prof.opt_level,
prof.debuginfo_flag(),
prof.codegen_units,
prof.panic,
prof.debug_assertions,
prof.overflow_checks,
features,
dep_ids
)
.as_bytes(),
)[..16]
.to_string();
memo[i] = Some(ident.clone());
ident
}
(0..units.len()).map(|i| ident_of(i, &units, &meta, &mut memo)).collect()
};
if units.iter().any(|u| u.profile.lto_enabled()) {
eprintln!("corgi: warning: profile requests lto; not supported yet, building without");
}
if units.iter().any(|u| u.profile.rpath) {
eprintln!("corgi: warning: profile requests rpath; ignored");
}
if units.iter().any(|u| {
u.profile.debuginfo_flag() != "0"
&& matches!(u.profile.split_debuginfo.as_deref(), Some("packed") | Some("off"))
}) {
eprintln!(
"corgi: warning: split-debuginfo=packed/off requested; darwin linking units use unpacked (determinism requires it)"
);
}
let mut check_mode: Vec<bool> = Vec::new();
if matches!(mode, Mode::Check | Mode::Clippy) {
let mut codegen = vec![false; units.len()];
let mut stack: Vec<usize> = (0..units.len())
.filter(|&i| {
matches!(units[i].kind, Kind::Bsc | Kind::Bsr)
|| (matches!(units[i].kind, Kind::Lib)
&& unit_crate_type(&units[i]) == "proc-macro")
})
.collect();
while let Some(i) = stack.pop() {
if codegen[i] {
continue;
}
codegen[i] = true;
for d in &units[i].deps {
stack.push(d.unit);
}
}
check_mode = (0..units.len()).map(|i| !codegen[i]).collect();
}
let t_plan_done = Instant::now();
let home = std::env::var("HOME").unwrap_or_default();
let rustup_home = std::env::var("RUSTUP_HOME").unwrap_or_else(|_| format!("{home}/.rustup"));
let devdir = capture(Command::new("/usr/bin/xcode-select").arg("-p"), "xcode-select -p")
.map(|s| s.trim().to_string())
.unwrap_or_else(|_| "/Library/Developer/CommandLineTools".to_string());
let cc_v = capture(Command::new("cc").arg("--version"), "cc --version")
.ok()
.and_then(|o| o.lines().next().map(str::to_string))
.unwrap_or_default();
let ld_v = Command::new("ld")
.arg("-v")
.output()
.map(|o| {
let all = format!(
"{}{}",
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr)
);
all.lines().next().unwrap_or("").to_string()
})
.unwrap_or_default();
let sdk_v = if host.contains("apple") {
capture(Command::new("/usr/bin/xcrun").arg("--show-sdk-version"), "xcrun --show-sdk-version")
.map(|s| s.trim().to_string())
.unwrap_or_default()
} else {
String::new()
};
let xcode_v = if host.contains("apple") {
capture(Command::new("/usr/bin/xcodebuild").arg("-version"), "xcodebuild -version")
.map(|s| s.split_whitespace().collect::<Vec<_>>().join(" "))
.unwrap_or_default()
} else {
String::new()
};
let toolchain = format!("cc: {cc_v}\nld: {ld_v}\nsdk: {sdk_v}\nxcode: {xcode_v}");
let sandbox = host.contains("apple") && Path::new("/usr/bin/sandbox-exec").is_file();
if sandbox {
eprintln!("corgi: hermetic sandbox enabled (seatbelt)");
}
let mut darwin_dirs = Vec::new();
for key in ["DARWIN_USER_TEMP_DIR", "DARWIN_USER_CACHE_DIR"] {
if let Ok(d) = capture(Command::new("/usr/bin/getconf").arg(key), "getconf") {
let d = d.trim().trim_end_matches('/').to_string();
if !d.is_empty() {
let canon = if d.starts_with("/var/") { format!("/private{d}") } else { d };
darwin_dirs.push(canon);
}
}
}
let sdkroot = if host.contains("apple") {
capture(Command::new("/usr/bin/xcrun").arg("--show-sdk-path"), "xcrun --show-sdk-path")
.map(|s| s.trim().to_string())
.unwrap_or_default()
} else {
String::new()
};
let cargo = cargo_bin.display().to_string();
let lints = resolve_lints(&meta)?;
let mut clippy_driver = String::new();
let mut clippy_id = String::new();
let mut clippy_conf: Option<PathBuf> = None;
if matches!(mode, Mode::Clippy) {
ensure_clippy(&store, &channel, &host_guess)?;
clippy_driver = format!("{}/bin/clippy-driver", toolchain_logical.display());
let version = capture(Command::new(&clippy_driver).arg("-V"), "clippy-driver -V")?;
let mut conf_hash = String::new();
for name in ["clippy.toml", ".clippy.toml"] {
let candidate = Path::new(&meta.workspace_root).join(name);
if candidate.is_file() {
conf_hash = crate::store::sha256_file(&candidate)?;
clippy_conf = Some(candidate);
break;
}
}
clippy_id = format!("{}|{conf_hash}", version.trim());
}
let mut tools_rt: Vec<ToolRt> = Vec::new();
let mut env_probes: Vec<(String, String, Vec<String>, Vec<String>)> = Vec::new();
if let Some((specs, probes, _, _)) = read_corgi_toml(&dir)? {
for t in &specs {
ensure_tool(&store, t)?;
let exported = if !t.bin.is_empty() { &t.bin } else { &t.path };
let logical = store
.logical_root()
.join("tools")
.join(format!("{}-{}", t.name, t.version))
.join(exported);
let scope = if t.packages.is_empty() {
String::new()
} else {
format!(" (packages {:?})", t.packages)
};
eprintln!("corgi: tool {} {} -> ${}{scope}", t.name, t.version, t.env);
let id = sha256_hex(
format!(
"tool\0{}\0{}\0{}\0{}\0{}\0{}\0{}",
t.name, t.version, t.url, t.sha256, t.bin, t.path, t.env
)
.as_bytes(),
);
tools_rt.push(ToolRt {
name: t.name.clone(),
version: t.version.clone(),
env: t.env.clone(),
value: logical.display().to_string(),
id,
bin: t.bin.clone(),
packages: t.packages.clone(),
});
}
for probe in &probes {
let active = units.iter().any(|u| {
let pkg_name = &meta.packages[u.pkg].name;
probe.packages.iter().any(|p| p == pkg_name)
&& (probe.profiles.is_empty()
|| probe.profiles.iter().any(|pr| pr == &u.profile.name))
});
if !active {
continue;
}
let parts: Vec<&str> = probe.command.split_whitespace().collect();
if parts.is_empty() {
bail!("env {} has an empty command", probe.name);
}
let out = capture(
Command::new(parts[0]).args(&parts[1..]).current_dir(Path::new(&meta.workspace_root)),
&format!("env probe {}", probe.name),
)?;
let val = out.trim().to_string();
eprintln!(
"corgi: env {}={} (packages {:?}{})",
probe.name,
val,
probe.packages,
if probe.profiles.is_empty() {
String::new()
} else {
format!(", profiles {:?}", probe.profiles)
}
);
env_probes.push((probe.name.clone(), val, probe.packages.clone(), probe.profiles.clone()));
}
}
let mut base_env = vec![("PATH".to_string(), "/usr/bin:/bin".to_string())];
if let Ok(v) = std::env::var("HOME") {
base_env.push(("HOME".to_string(), v));
}
{
let building = match root_pi {
Some(pi) => {
let root_pkg = &meta.packages[pi];
format!("{} v{}", root_pkg.name, root_pkg.version)
}
None => "workspace".to_string(),
};
eprintln!(
"corgi: building {building} — {} units (store {})",
units.len(),
store.root.display()
);
}
let pool = store.root.join("pool");
let pool_logical = store.logical_root().join("pool");
let file_names_memo = Mutex::new(HashMap::new());
let workspace_root = meta.workspace_root.clone();
if let Some(config_location) = &config_dir {
if config_location != Path::new(&workspace_root) {
bail!(
".cargo/config found at {} but the workspace root is {}; corgi only honors the workspace's own config",
config_location.display(),
workspace_root
);
}
}
let ctx = Ctx {
store,
verbose,
rustc,
rustc_version,
host,
cfg_env,
meta,
units,
pool,
pool_logical,
cargo,
cargo_home,
base_env,
workspace_root,
sysroot,
rustup_home,
devdir,
sandbox,
darwin_dirs,
sdkroot,
src_hash_memo: Mutex::new(HashMap::new()),
src_hash_nanos: std::sync::atomic::AtomicU64::new(0),
file_names_memo,
profile_name,
toolchain,
tools: tools_rt,
env_probes,
target,
timings,
incremental: !no_incremental,
jobserver: jobserver::Client::new(
std::thread::available_parallelism().map(|p| p.get()).unwrap_or(4),
)
.context("creating jobserver")?,
idents,
check_mode,
lints,
clippy: matches!(mode, Mode::Clippy),
clippy_driver,
clippy_id,
clippy_conf,
target_std_libdir,
cfg_env_target,
target_rustflags,
host_rustflags,
config_env,
extra_inputs,
};
let t_ctx_done = Instant::now();
let results: Vec<OnceLock<UnitResult>> = (0..ctx.units.len()).map(|_| OnceLock::new()).collect();
let (executed, cached) = schedule(&ctx, &results)?;
let t_sched_done = Instant::now();
if matches!(mode, Mode::Test) {
fs::create_dir_all("/tmp/corgi/target-tmp").context("creating CARGO_TARGET_TMPDIR")?;
}
let dtarget = Path::new(&ctx.workspace_root).join("target");
if matches!(mode, Mode::Test) {
let mut failures: Vec<String> = Vec::new();
for (i, u) in ctx.units.iter().enumerate() {
if !matches!(u.kind, Kind::Test) || !u.is_root {
continue;
}
let r = results[i].get().context("test harness not built")?;
let m = r.main.as_ref().context("test artifact missing")?;
let pkg = &ctx.meta.packages[u.pkg];
let dest = dtarget.join(&ctx.profile_name).join("deps").join(&m.name);
ctx.store.export(&m.hash, &dest, true)?;
for o in &r.res.outputs {
if o.name.ends_with(".rcgu.o") {
let odest = dtarget.join(&ctx.profile_name).join(&o.name);
ctx.store.export(&o.hash, &odest, false)?;
}
}
eprintln!("corgi: running tests: {} ({})", u.target.name, dest.display());
let mut c = Command::new(&dest);
c.current_dir(pkg.root());
if let Some(filter) = &test_filter {
c.arg(filter);
}
c.args(&exec_args);
let st = c.status().with_context(|| format!("running test {}", u.target.name))?;
if !st.success() {
failures.push(u.target.name.clone());
}
}
if !failures.is_empty() {
eprintln!("corgi: finished in {:.2}s", t0.elapsed().as_secs_f64());
bail!("{} test target(s) failed: {}", failures.len(), failures.join(", "));
}
}
for (i, u) in ctx.units.iter().enumerate() {
if !matches!(mode, Mode::Build | Mode::Run) {
break;
}
if matches!(u.kind, Kind::Bin) && u.is_root {
let t = &u.target;
let r = results[i].get().context("bin not built")?;
let m = r.main.as_ref().context("bin artifact missing")?;
let dest = dtarget.join(&ctx.profile_name).join(&t.name);
ctx.store.export(&m.hash, &dest, true)?;
eprintln!("corgi: bin {} (sha256 {}…)", dest.display(), &m.hash[..12]);
for o in &r.res.outputs {
if o.name.ends_with(".rcgu.o") {
let odest = dtarget.join(&ctx.profile_name).join(&o.name);
ctx.store.export(&o.hash, &odest, false)?;
}
}
}
if matches!(u.kind, Kind::Lib) && u.is_root && !u.host {
if let (Some(tgt), Some(r)) = (ctx.target.as_deref(), results[i].get()) {
let k16 = &r.key[..16];
for o in &r.res.outputs {
if o.name.ends_with(".wasm") || o.name.ends_with(".dylib") || o.name.ends_with(".so") {
let clean = o.name.replace(&format!("-{k16}"), "");
let dest = dtarget.join(tgt).join(&ctx.profile_name).join(&clean);
ctx.store.export(&o.hash, &dest, true)?;
eprintln!("corgi: cdylib {} (sha256 {}…)", dest.display(), &o.hash[..12]);
}
}
}
}
}
if std::env::var_os("CORGI_TIMING").is_some() {
use std::sync::atomic::Ordering::Relaxed;
eprintln!(
"corgi-timing: setup {:.3}s | plan {:.3}s | identity+tools {:.3}s | schedule {:.3}s (src-hash sum {:.3}s across workers) | export {:.3}s",
(t_setup_done - t0).as_secs_f64(),
(t_plan_done - t_setup_done).as_secs_f64(),
(t_ctx_done - t_plan_done).as_secs_f64(),
(t_sched_done - t_ctx_done).as_secs_f64(),
ctx.src_hash_nanos.load(Relaxed) as f64 / 1e9,
t_sched_done.elapsed().as_secs_f64(),
);
eprintln!(
"corgi-timing: hinted dirs {}, files statted {}, files content-rehashed {}, immutable src-hash hits {}, export-check bytes hashed {}",
crate::store::HINTED_DIRS.load(Relaxed),
crate::store::STAT_FILES.load(Relaxed),
crate::store::REHASHED_FILES.load(Relaxed),
crate::store::IMMUTABLE_HITS.load(Relaxed),
crate::store::EXPORT_CHECK_BYTES.load(Relaxed),
);
}
eprintln!(
"corgi: finished in {:.2}s — {executed} executed, {cached} cached",
t0.elapsed().as_secs_f64()
);
maybe_auto_gc(&ctx.store);
if matches!(mode, Mode::Run) {
let root_bins: Vec<usize> = ctx
.units
.iter()
.enumerate()
.filter(|(_, u)| matches!(u.kind, Kind::Bin) && u.is_root)
.map(|(i, _)| i)
.collect();
let package = root_bins
.first()
.map(|&i| &ctx.meta.packages[ctx.units[i].pkg])
.context("selected package has no runnable binary target")?;
let bin_index = select_run_binary(
package.default_run.as_deref(),
root_bins.iter().map(|&i| (i, ctx.units[i].target.name.as_str())),
)?;
let dest = dtarget.join(&ctx.profile_name).join(&ctx.units[bin_index].target.name);
eprintln!("corgi: running {}", dest.display());
let status = Command::new(&dest)
.args(&exec_args)
.status()
.with_context(|| format!("running {}", dest.display()))?;
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(signal) = status.signal() {
std::process::exit(128 + signal);
}
}
std::process::exit(status.code().unwrap_or(1));
}
Ok(())
}
#[derive(Serialize, Deserialize)]
struct PlanEntry {
ws_root_rel: String,
ws_root_abs: String,
files: Vec<String>,
glob_dirs: Vec<String>,
fingerprint: String,
meta_blob: String,
ug_blob: String,
}
fn plan_fingerprint(ws_root: &Path, files: &[String], glob_dirs: &[String]) -> String {
let mut buf: Vec<u8> = Vec::new();
for f in files {
buf.extend_from_slice(f.as_bytes());
buf.push(0);
if let Ok(b) = fs::read(plan_abs(ws_root, f)) {
buf.extend_from_slice(&b);
} else {
buf.extend_from_slice(b"<absent>");
}
buf.push(0xff);
}
for d in glob_dirs {
buf.extend_from_slice(d.as_bytes());
buf.push(0);
let mut names: Vec<String> = Vec::new();
if let Ok(rd) = fs::read_dir(plan_abs(ws_root, d)) {
for e in rd.flatten() {
if e.path().join("Cargo.toml").is_file() {
names.push(e.file_name().to_string_lossy().into_owned());
}
}
}
names.sort();
for n in &names {
buf.extend_from_slice(n.as_bytes());
buf.push(0);
}
buf.push(0xff);
}
sha256_hex(&buf)
}
fn plan_abs(ws_root: &Path, rel: &str) -> PathBuf {
if rel.starts_with('/') {
PathBuf::from(rel)
} else {
ws_root.join(rel)
}
}
fn rel_path(from: &Path, to: &Path) -> String {
let from_parts: Vec<_> = from.components().collect();
let to_parts: Vec<_> = to.components().collect();
let mut common = 0;
while common < from_parts.len() && common < to_parts.len() && from_parts[common] == to_parts[common]
{
common += 1;
}
let mut parts: Vec<String> = vec!["..".to_string(); from_parts.len() - common];
for c in &to_parts[common..] {
parts.push(c.as_os_str().to_string_lossy().into_owned());
}
if parts.is_empty() {
".".to_string()
} else {
parts.join("/")
}
}
fn save_plan(
store: &Store,
plan_ptr: &str,
dir: &Path,
meta: &Metadata,
meta_json: &str,
ug_json: &str,
) -> Result<()> {
let ws_root = Path::new(&meta.workspace_root)
.canonicalize()
.context("canonicalizing workspace root")?;
let mut files: BTreeSet<String> = BTreeSet::new();
files.insert("Cargo.toml".to_string());
files.insert("Cargo.lock".to_string());
for name in [".cargo/config.toml", ".cargo/config"] {
files.insert(name.to_string());
files.insert(rel_path(&ws_root, &dir.join(name)));
}
let mut glob_dirs: BTreeSet<String> = BTreeSet::new();
for p in &meta.packages {
if p.source.is_some() {
continue; }
let mp = Path::new(&p.manifest_path);
files.insert(rel_path(&ws_root, mp));
if let Some(container) = mp.parent().and_then(Path::parent) {
if container.starts_with(&ws_root) {
glob_dirs.insert(rel_path(&ws_root, container));
}
}
}
let files: Vec<String> = files.into_iter().collect();
let glob_dirs: Vec<String> = glob_dirs.into_iter().collect();
let entry = PlanEntry {
ws_root_rel: rel_path(dir, &ws_root),
ws_root_abs: meta.workspace_root.clone(),
fingerprint: plan_fingerprint(&ws_root, &files, &glob_dirs),
files,
glob_dirs,
meta_blob: store.insert_bytes(meta_json.as_bytes())?,
ug_blob: store.insert_bytes(ug_json.as_bytes())?,
};
store.save_action(plan_ptr, serde_json::to_string(&entry)?.as_bytes())
}
fn capture(cmd: &mut Command, what: &str) -> Result<String> {
let out = cmd.output().with_context(|| format!("running {what}"))?;
if !out.status.success() {
bail!("{what} failed:\n{}", String::from_utf8_lossy(&out.stderr));
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
fn capture_with_live_stderr(cmd: &mut Command, what: &str) -> Result<String> {
let out = cmd
.stderr(Stdio::inherit())
.output()
.with_context(|| format!("running {what}"))?;
if !out.status.success() {
bail!("{what} failed with {}", out.status);
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
fn find_in_path(name: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
for d in std::env::split_paths(&path) {
let c = d.join(name);
if c.is_file() {
return Some(c);
}
}
None
}
fn cargo_cfg_env(cfg_out: &str) -> Vec<(String, String)> {
let mut map: BTreeMap<String, Vec<String>> = BTreeMap::new();
for line in cfg_out.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some((k, v)) = line.split_once('=') {
map.entry(k.to_string()).or_default().push(v.trim_matches('"').to_string());
} else {
map.entry(line.to_string()).or_default();
}
}
map.remove("debug_assertions");
map.into_iter()
.map(|(k, mut vs)| {
vs.sort();
(format!("CARGO_CFG_{}", k.to_uppercase().replace('-', "_")), vs.join(","))
})
.collect()
}
fn select_root_package(
metadata: &Metadata,
package_indices: &HashMap<String, usize>,
workspace: bool,
package: Option<&str>,
mode: Mode,
) -> Result<Option<usize>> {
if workspace {
return Ok(None);
}
if let Some(package) = package {
let workspace_members: BTreeSet<&str> =
metadata.workspace_members.iter().map(String::as_str).collect();
let matches: Vec<usize> = metadata
.packages
.iter()
.enumerate()
.filter(|(_, candidate)| {
workspace_members.contains(candidate.id.as_str()) && candidate.name == package
})
.map(|(index, _)| index)
.collect();
return match matches.as_slice() {
[index] => Ok(Some(*index)),
[] => bail!("package `{package}` is not a workspace member"),
_ => bail!("package specification `{package}` is ambiguous"),
};
}
if let Some(root_id) = &metadata.resolve.root {
return package_indices
.get(root_id)
.copied()
.map(Some)
.with_context(|| format!("root package {root_id} missing from metadata"));
}
if matches!(mode, Mode::Run) {
let default_members: Vec<usize> = metadata
.workspace_default_members
.iter()
.filter_map(|id| package_indices.get(id).copied())
.collect();
return match default_members.as_slice() {
[index] => Ok(Some(*index)),
[] => bail!("virtual workspace has no default package; use `-p PACKAGE`"),
_ => bail!(
"virtual workspace has multiple default packages: [{}]; use `-p PACKAGE`",
default_members
.iter()
.map(|&index| metadata.packages[index].name.as_str())
.collect::<Vec<_>>()
.join(", ")
),
};
}
bail!("no root package (build from a member directory, or pass --workspace)")
}
fn select_run_binary<'a>(
default_run: Option<&str>,
binaries: impl Iterator<Item = (usize, &'a str)>,
) -> Result<usize> {
let binaries: Vec<(usize, &str)> = binaries.collect();
if let Some(default_run) = default_run {
return binaries
.iter()
.find(|(_, name)| *name == default_run)
.map(|(index, _)| *index)
.with_context(|| {
format!(
"default-run target `{default_run}` is not available; check its required features"
)
});
}
match binaries.as_slice() {
[(index, _)] => Ok(*index),
_ => bail!(
"`corgi run` could not determine which binary to run; found {}: [{}]",
binaries.len(),
binaries.iter().map(|(_, name)| *name).collect::<Vec<_>>().join(", ")
),
}
}
fn translate_unit_graph(
g: &meta::UnitGraph,
pkgs: &HashMap<String, usize>,
root_pi: Option<usize>,
) -> Result<Vec<Unit>> {
let mut units: Vec<Unit> = Vec::with_capacity(g.units.len());
for u in &g.units {
let pi = *pkgs
.get(&u.pkg_id)
.with_context(|| format!("unit-graph package {} missing from metadata", u.pkg_id))?;
let is_bs_target = u.target.kind.iter().any(|k| k == "custom-build");
let kind = if u.mode == "run-custom-build" {
Kind::Bsr
} else if is_bs_target {
Kind::Bsc
} else if u.mode == "test" {
Kind::Test
} else if u.target.kind.iter().any(|k| k == "bin") {
Kind::Bin
} else {
Kind::Lib
};
units.push(Unit {
pkg: pi,
kind,
host: u.platform.is_none(),
is_root: false,
target: u.target.clone(),
features: u.features.clone(),
deps: vec![],
profile: u.profile.clone(),
});
}
let kinds: Vec<Kind> = units.iter().map(|u| u.kind).collect();
for (i, u) in g.units.iter().enumerate() {
let mut deps = Vec::new();
for d in &u.dependencies {
let extern_name = if matches!(kinds[d.index], Kind::Lib) && !matches!(kinds[i], Kind::Bsr) {
Some(d.extern_crate_name.clone())
} else {
None
};
deps.push(UnitDep { unit: d.index, extern_name });
}
units[i].deps = deps;
}
for i in 0..units.len() {
if matches!(units[i].kind, Kind::Bsr) && units[i].features.is_empty() {
if let Some(b) = units[i].deps.iter().find(|d| matches!(kinds[d.unit], Kind::Bsc)) {
units[i].features = units[b.unit].features.clone();
}
}
}
let mut stack: Vec<usize> = g
.roots
.iter()
.copied()
.filter(|&r| root_pi.is_none_or(|pi| units[r].pkg == pi))
.collect();
if stack.is_empty() {
bail!("requested package has no buildable roots in the unit graph");
}
for &r in &stack {
units[r].is_root = true;
}
let mut keep = vec![false; units.len()];
while let Some(i) = stack.pop() {
if keep[i] {
continue;
}
keep[i] = true;
for d in &units[i].deps {
stack.push(d.unit);
}
}
let mut map = vec![usize::MAX; units.len()];
let mut kept: Vec<Unit> = Vec::new();
for (i, u) in units.into_iter().enumerate() {
if keep[i] {
map[i] = kept.len();
kept.push(u);
}
}
for u in &mut kept {
for d in &mut u.deps {
d.unit = map[d.unit];
}
}
Ok(kept)
}
impl Ctx {
fn out_dir_logical(&self, key: &str) -> PathBuf {
self.store.logical_root().join("outdirs").join(key).join("out")
}
fn materialize(&self, action_key: &str, res: &ActionResult) -> Result<()> {
for o in &res.outputs {
let pool_name = Store::pool_file_name(&o.name, action_key);
self.store.materialize_pool(&o.hash, &pool_name, o.exe)?;
}
Ok(())
}
fn try_cache_hit(&self, key: &str) -> Result<Option<ActionResult>> {
let Some(bytes) = self.store.load_action(key) else {
return Ok(None);
};
let Ok(res) = serde_json::from_slice::<ActionResult>(&bytes) else {
return Ok(None);
};
for o in &res.outputs {
let p = self.store.cache_path(&o.hash);
if !p.exists() {
return Ok(None); }
Store::touch_used(&p);
}
if res.bs.is_some() {
let ok = self.store.root.join("outdirs").join(key).join(".ok");
if !ok.exists() {
return Ok(None);
}
Store::touch_used(&ok);
}
self.materialize(key, &res)?;
Ok(Some(res))
}
fn pkg_src_hash(&self, pi: usize) -> Result<String> {
let started = Instant::now();
let result = self.pkg_src_hash_inner(pi);
self.src_hash_nanos
.fetch_add(started.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
result
}
fn pkg_src_hash_inner(&self, pi: usize) -> Result<String> {
if let Some(h) = self.src_hash_memo.lock().unwrap().get(&pi) {
return Ok(h.clone());
}
let pkg = &self.meta.packages[pi];
let immutable = pkg
.source
.as_ref()
.map(|src| format!("{src}|{}|{}", pkg.name, pkg.version));
let mut h = self
.store
.hash_dir_cached(&pkg.root(), immutable.as_deref())
.with_context(|| format!("hashing sources of {} v{}", pkg.name, pkg.version))?;
let extras = self.extra_inputs_for(pkg);
if !extras.is_empty() {
let mut acc = h;
for e in extras {
let p = pkg.root().join(e).canonicalize().with_context(|| {
format!("extra-input `{e}` of {} does not exist", pkg.name)
})?;
if !p.starts_with(Path::new(&self.workspace_root)) {
bail!("extra-input `{e}` of {} escapes the workspace", pkg.name);
}
let eh = if p.is_dir() {
self.store.hash_dir_cached(&p, None)?
} else {
crate::store::sha256_file(&p)?
};
acc.push_str(&format!("|{e}\0{eh}"));
}
h = sha256_hex(acc.as_bytes());
}
self.src_hash_memo.lock().unwrap().insert(pi, h.clone());
Ok(h)
}
fn pkg_env(&self, pkg: &Package) -> Vec<(String, String)> {
let v = &pkg.version;
let no_build = v.split('+').next().unwrap_or(v);
let (main, pre) = match no_build.split_once('-') {
Some((m, p)) => (m, p),
None => (no_build, ""),
};
let mut it = main.split('.');
let major = it.next().unwrap_or("0");
let minor = it.next().unwrap_or("0");
let patch = it.next().unwrap_or("0");
let mut e: Vec<(String, String)> = vec![
("CARGO_PKG_NAME".into(), pkg.name.clone()),
("CARGO_PKG_VERSION".into(), v.clone()),
("CARGO_PKG_VERSION_MAJOR".into(), major.into()),
("CARGO_PKG_VERSION_MINOR".into(), minor.into()),
("CARGO_PKG_VERSION_PATCH".into(), patch.into()),
("CARGO_PKG_VERSION_PRE".into(), pre.into()),
("CARGO_PKG_AUTHORS".into(), pkg.authors.join(":")),
("CARGO_PKG_DESCRIPTION".into(), pkg.description.clone().unwrap_or_default()),
("CARGO_PKG_HOMEPAGE".into(), pkg.homepage.clone().unwrap_or_default()),
("CARGO_PKG_REPOSITORY".into(), pkg.repository.clone().unwrap_or_default()),
("CARGO_PKG_LICENSE".into(), pkg.license.clone().unwrap_or_default()),
("CARGO_PKG_LICENSE_FILE".into(), pkg.license_file.clone().unwrap_or_default()),
("CARGO_PKG_README".into(), pkg.readme.clone().unwrap_or_default()),
("CARGO_PKG_RUST_VERSION".into(), pkg.rust_version.clone().unwrap_or_default()),
];
e.sort();
e
}
}
fn sandboxed_command(ctx: &Ctx, program: &str, extra_reads: &[&Path], writes: &[&Path]) -> Command {
if !ctx.sandbox {
return Command::new(program);
}
let mut prof = String::from(concat!(
"(version 1)\n",
"(deny default)\n",
"(allow process-fork)\n",
"(allow process-info*)\n",
"(allow file-map-executable)\n",
"(allow signal (target same-sandbox))\n",
"(allow sysctl-read)\n",
"(allow mach-lookup)\n",
"(allow file-read-metadata)\n",
));
prof.push_str("(allow process-exec*\n");
prof.push_str(" (literal \"/usr/bin/cc\")\n");
let mut exec_lits = vec![
format!("{}/bin/rustc", ctx.cargo_home),
format!("{}/bin/rustc", ctx.sysroot),
];
if let Some(r) = find_in_path(&ctx.rustc) {
exec_lits.push(r.display().to_string());
}
let toolchain_bin = Path::new(&ctx.sysroot).join("bin");
if let Ok(canon) = fs::canonicalize(&toolchain_bin) {
prof.push_str(&format!(" (subpath \"{}\")\n", canon.display()));
}
let rustlib = Path::new(&ctx.sysroot).join("lib/rustlib");
if let Ok(canon) = fs::canonicalize(&rustlib) {
prof.push_str(&format!(" (subpath \"{}\")\n", canon.display()));
}
for p in exec_lits {
let canon = fs::canonicalize(&p).map(|c| c.display().to_string()).unwrap_or(p);
prof.push_str(&format!(" (literal \"{canon}\")\n"));
}
prof.push_str(&format!(" (subpath \"{}/Toolchains\")\n", ctx.devdir));
for p in [
"/usr/bin/ar",
"/usr/bin/ranlib",
"/usr/bin/clang",
"/usr/bin/clang++",
"/usr/bin/c++",
"/usr/bin/xcrun",
"/usr/bin/xcodebuild",
"/usr/bin/xcode-select",
"/bin/sh",
] {
prof.push_str(&format!(" (literal \"{p}\")\n"));
}
prof.push_str(" (subpath \"/private/var/run/com.apple.security.cryptexd\")\n");
prof.push_str(&format!(" (subpath \"{}\")\n", ctx.store.root.join("tools").display()));
for w in writes {
prof.push_str(&format!(" (subpath \"{}\")\n", w.display()));
}
prof.push_str(&format!(" (subpath \"{}\")\n", ctx.pool.display()));
prof.push_str(")\n");
prof.push_str("(allow file-read*\n (literal \"/\")\n (literal \"/dev/null\")\n (literal \"/dev/urandom\")\n (literal \"/dev/random\")\n (literal \"/dev/zero\")\n");
prof.push_str(&format!(" (literal \"{}\")\n", ctx.workspace_root));
for p in ["/usr", "/bin", "/sbin", "/System", "/Library", "/Applications", "/opt", "/private/etc", "/private/var/db", "/private/preboot", "/private/var/run/com.apple.security.cryptexd"] {
prof.push_str(&format!(" (subpath \"{p}\")\n"));
}
let mut reads: Vec<String> = vec![
ctx.sysroot.clone(),
ctx.cargo_home.clone(),
ctx.rustup_home.clone(),
ctx.devdir.clone(),
ctx.store.root.display().to_string(),
];
for d in &ctx.darwin_dirs {
reads.push(d.clone());
}
for r in extra_reads {
reads.push(r.display().to_string());
}
for r in reads {
prof.push_str(&format!(" (subpath \"{r}\")\n"));
}
prof.push_str(")\n");
prof.push_str("(deny file-read* file-read-metadata\n");
let mut deny_roots: Vec<String> = vec![ctx.workspace_root.clone()];
for r in extra_reads {
deny_roots.push(r.display().to_string());
}
deny_roots.sort();
deny_roots.dedup();
for r in &deny_roots {
for d in [".git", "target", "dtarget"] {
prof.push_str(&format!(" (subpath \"{r}/{d}\")\n"));
}
prof.push_str(&format!(" (literal \"{r}/Cargo.lock\")\n"));
}
prof.push_str(")\n(allow file-write*\n (literal \"/dev/null\")\n");
for d in &ctx.darwin_dirs {
prof.push_str(&format!(" (subpath \"{d}\")\n"));
}
for w in writes {
prof.push_str(&format!(" (subpath \"{}\")\n", w.display()));
}
prof.push_str(")\n");
let mut c = Command::new("/usr/bin/sandbox-exec");
c.arg("-p").arg(prof).arg(program);
c
}
fn describe(ctx: &Ctx, idx: usize) -> String {
let u = &ctx.units[idx];
let p = &ctx.meta.packages[u.pkg];
let what = match u.kind {
Kind::Lib => {
if meta::is_proc_macro(p) { "proc-macro" } else { "lib" }.to_string()
}
Kind::Bsc => "build.rs compile".to_string(),
Kind::Bsr => "build.rs run".to_string(),
Kind::Bin => format!("bin \"{}\"", u.target.name),
Kind::Test => format!("test \"{}\"", u.target.name),
};
let plat = if !u.host {
ctx.target.as_deref().map(|t| format!(" → {t}")).unwrap_or_default()
} else {
String::new()
};
format!("{} v{} ({what}{plat})", p.name, p.version)
}
struct SchedState {
ready: Vec<usize>,
indeg: Vec<usize>,
done: usize,
in_flight: usize,
errors: Vec<String>,
executed: usize,
cached: usize,
}
fn schedule(ctx: &Ctx, results: &[OnceLock<UnitResult>]) -> Result<(usize, usize)> {
let n = ctx.units.len();
let mut rdeps_meta: Vec<Vec<usize>> = vec![vec![]; n];
let mut rdeps_full: Vec<Vec<usize>> = vec![vec![]; n];
let mut indeg = vec![0usize; n];
for (i, u) in ctx.units.iter().enumerate() {
let mut required: std::collections::HashSet<(usize, bool)> = std::collections::HashSet::new();
let self_meta_ok = !is_linking(ctx, i);
for d in &u.deps {
let meta_edge =
self_meta_ok && d.extern_name.is_some() && is_pipelined(ctx, d.unit);
required.insert((d.unit, !meta_edge));
}
let _ = u;
if is_linking(ctx, i) {
let mut stack: Vec<usize> = u.deps.iter().map(|d| d.unit).collect();
let mut seen = vec![false; n];
while let Some(j) = stack.pop() {
if seen[j] {
continue;
}
seen[j] = true;
required.insert((j, true));
for d in &ctx.units[j].deps {
stack.push(d.unit);
}
}
}
indeg[i] = required.len();
for (j, full) in required {
if full {
rdeps_full[j].push(i);
} else {
rdeps_meta[j].push(i);
}
}
}
let metas: Vec<OnceLock<MetaOut>> = (0..n).map(|_| OnceLock::new()).collect();
let t_sched = Instant::now();
use std::sync::atomic::{AtomicBool as TBool, AtomicU64 as TNs, Ordering::Relaxed};
let t_start: Vec<TNs> = (0..n).map(|_| TNs::new(0)).collect();
let t_meta: Vec<TNs> = (0..n).map(|_| TNs::new(0)).collect();
let t_end: Vec<TNs> = (0..n).map(|_| TNs::new(0)).collect();
let t_cached: Vec<TBool> = (0..n).map(|_| TBool::new(false)).collect();
let phase_slots: Vec<OnceLock<Phases>> = (0..n).map(|_| OnceLock::new()).collect();
let ready: Vec<usize> = (0..n).filter(|&i| indeg[i] == 0).collect();
let state = Mutex::new(SchedState { ready, indeg, done: 0, in_flight: 0, errors: Vec::new(), executed: 0, cached: 0 });
let cv = Condvar::new();
let meta_fired: Vec<std::sync::atomic::AtomicBool> =
(0..n).map(|_| std::sync::atomic::AtomicBool::new(false)).collect();
let fire_meta = |idx: usize, m: MetaOut| {
let _ = metas[idx].set(m);
if meta_fired[idx].swap(true, std::sync::atomic::Ordering::SeqCst) {
return;
}
t_meta[idx].store(t_sched.elapsed().as_nanos() as u64, Relaxed);
let mut st = state.lock().unwrap();
for &j in &rdeps_meta[idx] {
st.indeg[j] -= 1;
if st.indeg[j] == 0 {
st.ready.push(j);
}
}
drop(st);
cv.notify_all();
};
let workers = std::thread::available_parallelism().map(|p| p.get()).unwrap_or(4).min(n.max(1));
std::thread::scope(|scope| {
for _ in 0..workers {
scope.spawn(|| loop {
let idx = {
let mut st = state.lock().unwrap();
loop {
if let Some(i) = st.ready.pop() {
st.in_flight += 1;
break i;
}
if st.in_flight == 0 {
return;
}
st = cv.wait(st).unwrap();
}
};
t_start[idx].store(t_sched.elapsed().as_nanos() as u64, Relaxed);
let res = run_unit(ctx, idx, results, &metas, &fire_meta);
t_end[idx].store(t_sched.elapsed().as_nanos() as u64, Relaxed);
let mut st = state.lock().unwrap();
st.in_flight -= 1;
match res {
Ok(ur) => {
let _ = phase_slots[idx].set(ur.phases);
let verb = if ur.cached {
"Cached"
} else if matches!(ctx.units[idx].kind, Kind::Bsr) {
"Ran"
} else {
"Compiled"
};
eprintln!("{verb:>9} {}", describe(ctx, idx));
if ur.cached {
t_cached[idx].store(true, Relaxed);
st.cached += 1;
} else {
st.executed += 1;
}
if !meta_fired[idx].swap(true, std::sync::atomic::Ordering::SeqCst) {
if let Some(rm) = ur.res.outputs.iter().find(|o| o.name.ends_with(".rmeta")) {
let _ = metas[idx].set(MetaOut {
file: Store::pool_file_name(&rm.name, &ur.key),
hash: rm.hash.clone(),
});
}
for &j in &rdeps_meta[idx] {
st.indeg[j] -= 1;
if st.indeg[j] == 0 {
st.ready.push(j);
}
}
}
let _ = results[idx].set(ur);
st.done += 1;
for &j in &rdeps_full[idx] {
st.indeg[j] -= 1;
if st.indeg[j] == 0 {
st.ready.push(j);
}
}
}
Err(e) => {
eprintln!(" FAILED {} — dependents skipped", describe(ctx, idx));
st.errors.push(format!("[{}] {e:#}", describe(ctx, idx)));
st.done += 1;
}
}
drop(st);
cv.notify_all();
});
}
});
let st = state.into_inner().unwrap();
if !st.errors.is_empty() {
eprintln!("\ncorgi: ===== {} units failed =====", st.errors.len());
for (i, e) in st.errors.iter().enumerate() {
let lines: Vec<&str> = e.lines().collect();
let tail = lines.len().saturating_sub(22);
let short: String = lines[..2.min(lines.len())]
.iter()
.chain(lines[tail.max(2.min(lines.len()))..].iter())
.cloned()
.collect::<Vec<_>>()
.join("\n ");
eprintln!(" {}. {short}\n", i + 1);
}
bail!("{} units failed (skipped dependents not counted)", st.errors.len());
}
if ctx.timings {
let wall = t_sched.elapsed();
let mut rows: Vec<TimingRow> = Vec::new();
let mut cached_walk_ns: u64 = 0;
for i in 0..n {
let end = t_end[i].load(Relaxed);
if end == 0 || t_cached[i].load(Relaxed) {
if end > 0 {
cached_walk_ns += end - t_start[i].load(Relaxed);
}
continue;
}
let start = t_start[i].load(Relaxed);
let meta = t_meta[i].load(Relaxed);
rows.push(TimingRow {
label: describe(ctx, i),
start_ns: start,
meta_ns: if meta > start && meta < end { meta } else { 0 },
end_ns: end,
linking: is_linking(ctx, i),
bsr: matches!(ctx.units[i].kind, Kind::Bsr),
phases: phase_slots[i].get().copied().unwrap_or_default(),
});
}
if let Err(e) = write_timings_report(ctx, &rows, wall, st.executed, st.cached, cached_walk_ns) {
eprintln!("corgi: warning: could not write timings report: {e:#}");
}
}
Ok((st.executed, st.cached))
}
struct TimingRow {
label: String,
start_ns: u64,
meta_ns: u64,
end_ns: u64,
linking: bool,
bsr: bool,
phases: Phases,
}
fn write_timings_report(
ctx: &Ctx,
rows: &[TimingRow],
wall: std::time::Duration,
executed: usize,
cached: usize,
cached_walk_ns: u64,
) -> Result<()> {
let wall_ns = wall.as_nanos().max(1) as u64;
let cpu_ns: u64 = rows.iter().map(|r| r.end_ns - r.start_ns).sum();
let mut sorted: Vec<&TimingRow> = rows.iter().collect();
sorted.sort_by_key(|r| r.start_ns);
let secs = |ns: u64| ns as f64 / 1e9;
let mut gantt = String::new();
for r in &sorted {
let left = r.start_ns as f64 / wall_ns as f64 * 100.0;
let width = ((r.end_ns - r.start_ns) as f64 / wall_ns as f64 * 100.0).max(0.05);
let class = if r.bsr {
"bsr"
} else if r.linking {
"link"
} else {
"lib"
};
let meta_html = if r.meta_ns > 0 {
let mw = (r.meta_ns - r.start_ns) as f64 / (r.end_ns - r.start_ns) as f64 * 100.0;
format!("<i style=\"width:{mw:.1}%\"></i>")
} else {
String::new()
};
let title = if r.meta_ns > 0 {
format!(
"{} — {:.2}s at {:.2}s (rmeta after {:.2}s)",
r.label,
secs(r.end_ns - r.start_ns),
secs(r.start_ns),
secs(r.meta_ns - r.start_ns)
)
} else {
format!("{} — {:.2}s at {:.2}s", r.label, secs(r.end_ns - r.start_ns), secs(r.start_ns))
};
gantt.push_str(&format!(
"<div class=\"row\"><span class=\"lbl\">{}</span><div class=\"bar {class}\" style=\"margin-left:{left:.2}%;width:{width:.2}%\" title=\"{title}\">{meta_html}</div></div>\n",
r.label
));
}
let mut by_dur: Vec<&TimingRow> = rows.iter().collect();
by_dur.sort_by_key(|r| std::cmp::Reverse(r.end_ns - r.start_ns));
let mut table = String::new();
for r in by_dur.iter().take(30) {
let fe = if r.meta_ns > 0 {
format!("{:.2}s", secs(r.meta_ns - r.start_ns))
} else {
"—".to_string()
};
let ph = &r.phases;
let total = r.end_ns - r.start_ns;
let accounted =
ph.key_ns + ph.cache_ns + ph.rustc_ns + ph.validate_ns + ph.ingest_ns + ph.finish_ns;
table.push_str(&format!(
"<tr><td>{}</td><td>{:.2}s</td><td>{:.2}s</td><td>{fe}</td><td>{:.2}s ({:.0} MB)</td><td>{:.0}ms</td><td>{:.0}ms</td><td>{:.0}ms</td><td>{:.0}ms</td><td>{:.2}s</td></tr>\n",
r.label,
secs(total),
secs(ph.rustc_ns),
secs(ph.ingest_ns),
ph.ingest_bytes as f64 / 1e6,
ph.key_ns as f64 / 1e6,
ph.cache_ns as f64 / 1e6,
ph.validate_ns as f64 / 1e6,
ph.finish_ns as f64 / 1e6,
secs(total.saturating_sub(accounted)),
));
}
let sum_rustc: u64 = rows.iter().map(|r| r.phases.rustc_ns).sum();
let sum_ingest: u64 = rows.iter().map(|r| r.phases.ingest_ns).sum();
let sum_bytes: u64 = rows.iter().map(|r| r.phases.ingest_bytes).sum();
let sum_key: u64 = rows.iter().map(|r| r.phases.key_ns).sum();
let sum_finish: u64 = rows.iter().map(|r| r.phases.finish_ns).sum();
let html = format!(
"<!doctype html><meta charset=utf-8><title>corgi timings</title><style>\
body{{font:13px system-ui;margin:20px}}h1{{font-size:18px}}\
.row{{display:flex;align-items:center;height:14px}}\
.lbl{{width:340px;flex:none;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}}\
.bar{{height:11px;border-radius:2px;position:relative;min-width:1px}}\
.bar.lib{{background:#7cb3e8}}.bar.link{{background:#b07ce8}}.bar.bsr{{background:#e8a67c}}\
.bar i{{display:block;position:absolute;left:0;top:0;bottom:0;background:#2c6cb0;border-radius:2px 0 0 2px}}\
table{{border-collapse:collapse;margin-top:24px}}td,th{{border:1px solid #ccc;padding:2px 8px;text-align:left;font-size:12px}}\
</style>\n<h1>corgi timings</h1>\n<p>wall {:.2}s · {} executed · {} cached · cpu {:.1}s · parallelism {:.1}x</p>\n\
<div>{gantt}</div>\n<h2>slowest units</h2><table><tr><th>unit</th><th>total</th><th>rustc</th><th>front-end</th><th>ingest</th><th>key</th><th>cache</th><th>validate</th><th>finish</th><th>other</th></tr>{table}</table>\n\
<p>phase totals across executed units: rustc {:.1}s · ingest {:.1}s ({:.2} GB hashed) · key {:.1}s · finish {:.1}s · cache-hit walk {:.1}s</p>\n",
wall.as_secs_f64(),
executed,
cached,
cpu_ns as f64 / 1e9,
cpu_ns as f64 / wall_ns as f64,
sum_rustc as f64 / 1e9,
sum_ingest as f64 / 1e9,
sum_bytes as f64 / 1e9,
sum_key as f64 / 1e9,
sum_finish as f64 / 1e9,
cached_walk_ns as f64 / 1e9,
);
let dir = Path::new(&ctx.workspace_root).join("target/corgi-timings");
fs::create_dir_all(&dir)?;
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let path = dir.join(format!("corgi-timing-{stamp}.html"));
fs::write(&path, &html)?;
fs::write(dir.join("corgi-timing.html"), &html)?;
eprintln!("corgi: timings report: {}", path.display());
for r in by_dur.iter().take(5) {
eprintln!(
"corgi: slow: {:>7.2}s {}",
(r.end_ns - r.start_ns) as f64 / 1e9,
r.label
);
}
Ok(())
}
fn run_unit(
ctx: &Ctx,
idx: usize,
results: &[OnceLock<UnitResult>],
metas: &[OnceLock<MetaOut>],
fire_meta: &(dyn Fn(usize, MetaOut) + Sync),
) -> Result<UnitResult> {
match ctx.units[idx].kind {
Kind::Bsr => run_build_script(ctx, idx, results),
_ => compile(ctx, idx, results, metas, fire_meta),
}
}
fn expected_outputs(
ctx: &Ctx,
crate_name: &str,
k16: &str,
crate_types: &str,
host: bool,
) -> Result<Vec<String>> {
let memo_key = (crate_types.to_string(), host);
let cached = ctx.file_names_memo.lock().unwrap().get(&memo_key).cloned();
let pattern = match cached {
Some(p) => p,
None => {
let plat = if host { ctx.host.as_str() } else { ctx.target.as_deref().unwrap_or(ctx.host.as_str()) };
let probe_key = sha256_hex(
format!("probe-file-names\0{TOOL_VERSION}\0{}\0{plat}\0{crate_types}", ctx.rustc_version).as_bytes(),
);
let from_store = ctx
.store
.load_action(&probe_key)
.and_then(|b| serde_json::from_slice::<Vec<String>>(&b).ok());
let p = match from_store {
Some(p) => p,
None => {
let mut cmd = Command::new(&ctx.rustc);
cmd.args([
"--print",
"file-names",
"--crate-name",
"corgiprobe",
"--crate-type",
crate_types,
"-Cextra-filename=-XCORGIX",
]);
if !host {
if let Some(t) = &ctx.target {
cmd.args(["--target", t]);
}
}
cmd.arg("-");
cmd.stdin(std::process::Stdio::null());
let names = capture(&mut cmd, "rustc --print file-names")?;
let p: Vec<String> = names
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
if p.is_empty() {
bail!("rustc --print file-names reported nothing for {crate_types}");
}
ctx.store.save_action(&probe_key, &serde_json::to_vec(&p)?)?;
p
}
};
ctx.file_names_memo.lock().unwrap().insert(memo_key, p.clone());
p
}
};
let mut out: Vec<String> = pattern
.iter()
.map(|n| n.replace("corgiprobe", crate_name).replace("-XCORGIX", &format!("-{k16}")))
.collect();
if crate_types == "lib" {
out.push(format!("lib{crate_name}-{k16}.rmeta"));
}
Ok(out)
}
fn run_rustc_streaming(
ctx: &Ctx,
cmd: &mut Command,
uidx: usize,
pkg_name: &str,
action_key: &str,
fire_meta: &(dyn Fn(usize, MetaOut) + Sync),
) -> Result<(bool, String)> {
use std::io::BufRead;
let mut child = cmd
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("spawning rustc for {pkg_name}"))?;
let mut rendered = String::new();
let reader = std::io::BufReader::new(child.stderr.take().unwrap());
for line in reader.lines() {
let line = line?;
match serde_json::from_str::<serde_json::Value>(&line) {
Ok(v) => {
let artifact = v.get("artifact").and_then(|a| a.as_str());
let emit = v.get("emit").and_then(|e| e.as_str());
if let (Some(path), Some("metadata")) = (artifact, emit) {
let bytes = fs::read(path).with_context(|| format!("reading rmeta {path}"))?;
let hash = ctx.store.insert_bytes(&bytes)?;
let file = Path::new(path)
.file_name()
.map(|f| f.to_string_lossy().into_owned())
.unwrap_or_default();
let pool_name = Store::pool_file_name(&file, action_key);
ctx.store.materialize_pool(&hash, &pool_name, false)?;
fire_meta(uidx, MetaOut { file: pool_name, hash });
} else if let Some(r) = v.get("rendered").and_then(|r| r.as_str()) {
rendered.push_str(r);
}
}
Err(_) => {
rendered.push_str(&line);
rendered.push('\n');
}
}
}
let status = child.wait()?;
Ok((status.success(), rendered))
}
fn ws_relative_pkg_id(id: &str, workspace_root: &str) -> String {
if let Some(rest) = id.strip_prefix("path+file://") {
let (dir, suffix) = rest.split_once('#').unwrap_or((rest, ""));
if let Ok(rel) = Path::new(dir).strip_prefix(workspace_root) {
return format!("path+{}#{suffix}", rel.display());
}
}
id.to_string()
}
#[cfg(test)]
mod run_selection_tests {
use super::{select_root_package, select_run_binary, Mode};
use crate::meta::Metadata;
use std::collections::HashMap;
#[test]
fn run_uses_the_single_workspace_default_member() {
let metadata = metadata();
let package_indices = metadata
.packages
.iter()
.enumerate()
.map(|(index, package)| (package.id.clone(), index))
.collect::<HashMap<_, _>>();
let selected =
select_root_package(&metadata, &package_indices, false, None, Mode::Run).unwrap();
assert_eq!(selected, Some(0));
assert_eq!(metadata.packages[selected.unwrap()].name, "delta");
}
#[test]
fn explicit_package_overrides_the_workspace_default_member() {
let metadata = metadata();
let package_indices = metadata
.packages
.iter()
.enumerate()
.map(|(index, package)| (package.id.clone(), index))
.collect::<HashMap<_, _>>();
let selected =
select_root_package(&metadata, &package_indices, false, Some("helper"), Mode::Run)
.unwrap();
assert_eq!(selected, Some(1));
}
#[test]
fn default_run_selects_among_multiple_binaries() {
let selected =
select_run_binary(Some("delta"), [(4, "delta-cli"), (9, "delta")].into_iter())
.unwrap();
assert_eq!(selected, 9);
assert!(select_run_binary(None, [(4, "delta-cli"), (9, "delta")].into_iter()).is_err());
}
fn metadata() -> Metadata {
serde_json::from_value(serde_json::json!({
"workspace_members": ["delta 0.1.0 (path+file:///workspace/crates/delta)", "helper 0.1.0 (path+file:///workspace/crates/helper)"],
"workspace_default_members": ["delta 0.1.0 (path+file:///workspace/crates/delta)"],
"packages": [
{
"name": "delta",
"version": "0.1.0",
"id": "delta 0.1.0 (path+file:///workspace/crates/delta)",
"source": null,
"manifest_path": "/workspace/crates/delta/Cargo.toml",
"edition": "2021",
"default_run": "delta",
"targets": [],
"metadata": {}
},
{
"name": "helper",
"version": "0.1.0",
"id": "helper 0.1.0 (path+file:///workspace/crates/helper)",
"source": null,
"manifest_path": "/workspace/crates/helper/Cargo.toml",
"edition": "2021",
"targets": [],
"metadata": {}
}
],
"resolve": {"root": null},
"workspace_root": "/workspace"
}))
.unwrap()
}
}
#[cfg(test)]
mod tool_url_tests {
use super::parse_github_release_url;
#[test]
fn release_asset_urls_split_into_repo_tag_and_asset() {
let (repo, tag, asset) = parse_github_release_url(
"https://github.com/zed-industries/delta-terminal/releases/download/build-abc123/ex-terminal-aarch64-macos.tar.gz",
)
.unwrap();
assert_eq!(repo, "zed-industries/delta-terminal");
assert_eq!(tag, "build-abc123");
assert_eq!(asset, "ex-terminal-aarch64-macos.tar.gz");
}
#[test]
fn non_release_urls_are_rejected() {
assert!(parse_github_release_url("https://example.com/a/b/releases/download/t/x").is_err());
assert!(parse_github_release_url("https://github.com/o/r/archive/main.tar.gz").is_err());
assert!(parse_github_release_url("https://github.com/o/r/releases/download/tag").is_err());
}
}
#[cfg(test)]
mod ident_tests {
use super::ws_relative_pkg_id;
#[test]
fn path_package_ids_are_workspace_relative() {
let a = ws_relative_pkg_id("path+file:///w/one/crates/editor#0.1.0", "/w/one");
let b = ws_relative_pkg_id("path+file:///w/two/crates/editor#0.1.0", "/w/two");
assert_eq!(a, "path+crates/editor#0.1.0");
assert_eq!(a, b);
assert_eq!(
ws_relative_pkg_id("path+file:///w/crates/foo#bar@1.2.3", "/w"),
"path+crates/foo#bar@1.2.3"
);
let registry = "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.0";
assert_eq!(ws_relative_pkg_id(registry, "/w"), registry);
let outside = "path+file:///elsewhere/dep#0.1.0";
assert_eq!(ws_relative_pkg_id(outside, "/w"), outside);
}
}
fn finish_compile(
expected: &[String],
key: String,
cached: bool,
res: ActionResult,
phases: Phases,
) -> Result<UnitResult> {
for e in expected {
if !res.outputs.iter().any(|o| &o.name == e) {
bail!(
"rustc-reported output {e} missing (got {:?})",
res.outputs.iter().map(|o| &o.name).collect::<Vec<_>>()
);
}
}
let main_name = expected
.iter()
.find(|e| e.ends_with(".rlib"))
.or_else(|| expected.first())
.context("no expected outputs")?;
let main = res.outputs.iter().find(|o| &o.name == main_name).cloned();
Ok(UnitResult { key, cached, res, main, phases })
}
fn compile(
ctx: &Ctx,
uidx: usize,
results: &[OnceLock<UnitResult>],
metas: &[OnceLock<MetaOut>],
fire_meta: &(dyn Fn(usize, MetaOut) + Sync),
) -> Result<UnitResult> {
let unit = &ctx.units[uidx];
let pkg = &ctx.meta.packages[unit.pkg];
let pkg_root = pkg.root();
let target: &Target = &unit.target;
let (crate_name, crate_type): (String, String) = match unit.kind {
Kind::Lib => {
let ct = if target.kind.iter().any(|k| k == "proc-macro") {
"proc-macro".to_string()
} else if unit.is_root && target.crate_types.iter().any(|c| c == "cdylib") {
target.crate_types.join(",")
} else {
"lib".to_string()
};
(target.name.replace('-', "_"), ct)
}
Kind::Bsc | Kind::Bin | Kind::Test => (target.name.replace('-', "_"), "bin".to_string()),
Kind::Bsr => unreachable!(),
};
let crate_type = crate_type.as_str();
let src_rel = Path::new(&target.src_path)
.strip_prefix(&ctx.workspace_root)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| target.src_path.clone());
let mut features: Vec<String> = unit.features.clone();
features.sort();
let self_checked = is_checked(ctx, uidx);
let self_pipelined = is_pipelined(ctx, uidx);
let default_bs = BuildScriptOut::default();
let mut bs: &BuildScriptOut = &default_bs;
let mut out_key = String::new();
let mut externs: Vec<(String, String, String)> = Vec::new();
for d in &unit.deps {
if let Some(name) = &d.extern_name {
if self_pipelined && is_pipelined(ctx, d.unit) {
let m = metas[d.unit].get().context("dependency rmeta missing")?;
externs.push((name.clone(), m.file.clone(), m.hash.clone()));
} else {
let r = results[d.unit].get().context("dependency result missing")?;
let m = r.main.as_ref().context("dependency artifact missing")?;
externs.push((name.clone(), Store::pool_file_name(&m.name, &r.key), m.hash.clone()));
}
} else if matches!(ctx.units[d.unit].kind, Kind::Bsr) && ctx.units[d.unit].pkg == unit.pkg {
let r = results[d.unit].get().context("dependency result missing")?;
if let Some(b) = &r.res.bs {
bs = b;
out_key = r.key.clone();
}
}
}
externs.sort();
let mut link_closure: Vec<(String, String)> = Vec::new();
if is_linking(ctx, uidx) {
let mut seen = vec![false; ctx.units.len()];
let mut stack: Vec<usize> = unit.deps.iter().map(|d| d.unit).collect();
while let Some(i) = stack.pop() {
if seen[i] {
continue;
}
seen[i] = true;
if let Some(r) = results[i].get() {
if let Some(m) = &r.main {
link_closure.push((m.name.clone(), m.hash.clone()));
}
}
for d in &ctx.units[i].deps {
stack.push(d.unit);
}
}
link_closure.sort();
link_closure.dedup();
}
let mut link_search: Vec<String> = bs.link_search.clone();
let mut link_args: Vec<String> = Vec::new();
if matches!(unit.kind, Kind::Bin) {
link_args = bs.link_args.clone();
let mut seen = vec![false; ctx.units.len()];
let mut stack: Vec<usize> = unit.deps.iter().map(|d| d.unit).collect();
while let Some(i) = stack.pop() {
if seen[i] {
continue;
}
seen[i] = true;
if let Some(r) = results[i].get() {
if let Some(b) = &r.res.bs {
for s in &b.link_search {
if !link_search.contains(s) {
link_search.push(s.clone());
}
}
}
}
for d in &ctx.units[i].deps {
stack.push(d.unit);
}
}
}
let mut env = ctx.pkg_env(pkg);
env.extend(ctx.config_env.iter().cloned());
if matches!(unit.kind, Kind::Bin) {
env.push(("CARGO_BIN_NAME".to_string(), target.name.clone()));
}
if matches!(unit.kind, Kind::Test) && target.kind.iter().any(|k| k == "test" || k == "bench") {
env.push(("CARGO_TARGET_TMPDIR".to_string(), "/tmp/corgi/target-tmp".to_string()));
}
let unit_rustflags: &[String] =
if unit.host { &ctx.host_rustflags } else { &ctx.target_rustflags };
let t_phase = Instant::now();
let mut phases = Phases::default();
let src_hash = ctx.pkg_src_hash(unit.pkg)?;
let cap_lints = pkg.source.is_some();
let prof = &unit.profile;
let debuginfo = if self_checked { "0".to_string() } else { prof.debuginfo_flag() };
let mut pflags: Vec<String> = vec![
format!(
"-Copt-level={}",
if prof.opt_level.is_empty() { "0" } else { prof.opt_level.as_str() }
),
format!("-Cdebug-assertions={}", if prof.debug_assertions { "on" } else { "off" }),
format!("-Coverflow-checks={}", if prof.overflow_checks { "on" } else { "off" }),
format!("-Cdebuginfo={debuginfo}"),
format!("-Cstrip={}", prof.strip_flag()),
"-Cembed-bitcode=no".to_string(),
];
if let Some(n) = prof.codegen_units {
pflags.push(format!("-Ccodegen-units={n}"));
}
if !prof.panic.is_empty() && prof.panic != "unwind" && !matches!(unit.kind, Kind::Test) {
pflags.push(format!("-Cpanic={}", prof.panic));
}
let clippy_action = ctx.clippy && pkg.source.is_none();
let lint_flags: &[String] = if clippy_action {
&ctx.lints[unit.pkg].with_clippy
} else {
&ctx.lints[unit.pkg].rustc_only
};
let unit_platform = if unit.host {
ctx.host.as_str()
} else {
ctx.target.as_deref().unwrap_or(ctx.host.as_str())
};
let incr_action = ctx.incremental && prof.incremental;
let oso_split = debuginfo != "0" && is_linking(ctx, uidx) && unit_platform.contains("apple");
let oso_rel = if oso_split { format!("target/{}/", ctx.profile_name) } else { String::new() };
let key_json = serde_json::to_string(&CompileKey {
kind: if clippy_action {
if self_checked { "clippy" } else { "clippy-compile" }
} else if self_checked {
"check"
} else if matches!(unit.kind, Kind::Test) {
"compile-test"
} else {
"compile"
},
tool: TOOL_VERSION,
rustc: &ctx.rustc_version,
host: &ctx.host,
pkg: [&pkg.name, &pkg.version, pkg.source.as_deref().unwrap_or("local")],
src_hash: &src_hash,
crate_name: &crate_name,
edition: &target.edition,
crate_type,
src_rel: &src_rel,
features: &features,
externs: &externs,
link_closure: &link_closure,
cfgs: &bs.cfgs,
renvs: &bs.envs,
link_libs: &bs.link_libs,
link_search: &link_search,
link_args: &link_args,
out_key: &out_key,
profile: &pflags,
lints: lint_flags,
clippy: if clippy_action { &ctx.clippy_id } else { "" },
incr: incr_action,
ident: &ctx.idents[uidx],
oso: &oso_rel,
env: &env,
cap_lints,
rustflags: unit_rustflags,
toolchain: if crate_type == "lib" || self_checked { "" } else { &ctx.toolchain },
tgt: if unit.host { "" } else { ctx.target.as_deref().unwrap_or("") },
})?;
let key = sha256_hex(key_json.as_bytes());
let k16: String = key[..16].to_string();
let ef16: String = if incr_action { ctx.idents[uidx].clone() } else { k16.clone() };
phases.key_ns = t_phase.elapsed().as_nanos() as u64;
let t_cache = Instant::now();
let hit = ctx.try_cache_hit(&key)?;
phases.cache_ns = t_cache.elapsed().as_nanos() as u64;
if let Some(res) = hit {
let expected: Vec<String> = if self_checked {
res.outputs.iter().map(|o| o.name.clone()).collect()
} else {
expected_outputs(ctx, &crate_name, &ef16, crate_type, unit.host)?
};
if expected.iter().all(|e| res.outputs.iter().any(|o| &o.name == e)) {
if !res.stderr.is_empty() && pkg.source.is_none() {
eprint!("{}", res.stderr);
}
return finish_compile(&expected, key, true, res, phases);
}
eprintln!(
"corgi: discarding corrupt action record for {} ({crate_name})",
pkg.name
);
fs::remove_file(ctx.store.action_path(&key)).ok();
}
let mut incr_lock: Option<fs::File> = None;
let incr_dir: Option<PathBuf> = if incr_action {
let kind_tag = match unit.kind {
Kind::Bin => "bin",
Kind::Lib => "lib",
Kind::Bsc => "bsc",
Kind::Bsr => "bsr",
Kind::Test => "test",
};
let mode_tag = if clippy_action {
"clippy"
} else if self_checked {
"check"
} else {
"full"
};
let identity = sha256_hex(
format!(
"incr\0{}\0{}\0{}\0{}\0{}\0{}\0{}\0{}\0{}",
ctx.workspace_root,
pkg.name,
crate_name,
crate_type,
kind_tag,
mode_tag,
unit_platform,
ctx.profile_name,
ctx.idents[uidx]
)
.as_bytes(),
);
let dir = ctx.store.root.join("incr").join(&identity[..16]);
fs::create_dir_all(&dir)?;
let lock = fs::File::create(ctx.store.root.join("incr").join(format!("{}.lock", &identity[..16])))?;
lock.lock().with_context(|| format!("locking incremental state for {crate_name}"))?;
incr_lock = Some(lock);
Some(dir)
} else {
None
};
let stage_root = if let Some(d) = &incr_dir {
let out = d.join("out");
fs::remove_dir_all(&out).ok();
out
} else {
ctx.store.tmp_path("rustc")
};
let outdir = if oso_split { stage_root.join(&oso_rel) } else { stage_root.clone() };
fs::create_dir_all(&outdir)?;
let scratch = if let Some(d) = &incr_dir { d.join("tmp") } else { ctx.store.tmp_path("scratch") };
fs::create_dir_all(&scratch)?;
let extra_in: Vec<PathBuf> = ctx
.extra_inputs_for(pkg)
.iter()
.filter_map(|e| pkg_root.join(e).canonicalize().ok())
.collect();
let mut reads: Vec<&Path> = vec![&pkg_root];
reads.extend(extra_in.iter().map(|p| p.as_path()));
if clippy_action {
if let Some(conf) = &ctx.clippy_conf {
reads.push(conf.as_path());
}
}
let executor = if clippy_action { ctx.clippy_driver.as_str() } else { ctx.rustc.as_str() };
let mut writes: Vec<&Path> = vec![&outdir, &scratch];
if let Some(d) = &incr_dir {
writes.push(d.as_path());
}
let mut cmd = sandboxed_command(ctx, executor, &reads, &writes);
cmd.current_dir(&ctx.workspace_root);
cmd.env_clear();
cmd.env("TMPDIR", &scratch);
if !ctx.sdkroot.is_empty() {
cmd.env("SDKROOT", &ctx.sdkroot);
}
for (k, v) in &ctx.base_env {
cmd.env(k, v);
}
for (k, v) in &env {
cmd.env(k, v);
}
cmd.env("CARGO_CRATE_NAME", &crate_name);
cmd.env("CARGO_MANIFEST_DIR", &pkg_root);
cmd.env("CARGO_MANIFEST_PATH", &pkg.manifest_path);
cmd.env("CARGO", &ctx.cargo);
for (k, v) in &bs.envs {
cmd.env(k, v);
}
if !out_key.is_empty() {
cmd.env("OUT_DIR", ctx.out_dir_logical(&out_key));
}
cmd.arg("--sysroot").arg(&ctx.sysroot);
cmd.arg("--crate-name").arg(&crate_name);
cmd.arg("--edition").arg(&target.edition);
cmd.arg(&src_rel);
cmd.arg("--crate-type").arg(crate_type);
if self_checked {
cmd.arg("--emit=metadata,dep-info");
cmd.arg("--error-format=json");
cmd.arg("--json=artifacts");
} else if self_pipelined {
cmd.arg("--emit=metadata,link,dep-info");
cmd.arg("--error-format=json");
cmd.arg("--json=artifacts");
} else {
cmd.arg("--emit=link,dep-info");
}
if matches!(unit.kind, Kind::Test) {
cmd.arg("--test");
}
if !unit.host {
if let Some(t) = &ctx.target {
cmd.arg("--target").arg(t);
if let Some(libdir) = &ctx.target_std_libdir {
cmd.arg("-L").arg(libdir);
}
}
}
for f in &pflags {
cmd.arg(f);
}
for f in lint_flags {
cmd.arg(f);
}
if clippy_action {
cmd.arg("--cfg").arg("clippy");
cmd.env("CLIPPY_CONF_DIR", &ctx.workspace_root);
}
if let Some(d) = &incr_dir {
cmd.arg(format!("-Cincremental={}", d.display()));
if std::env::var_os("CORGI_INCR_INFO").is_some() {
cmd.arg("-Zincremental-info");
cmd.env("RUSTC_BOOTSTRAP", "1");
}
}
if oso_split {
cmd.arg("-Csplit-debuginfo=unpacked");
cmd.arg(format!("-Clink-arg=-Wl,-oso_prefix,{}/", stage_root.display()));
}
cmd.arg(format!("-Cmetadata={}", ctx.idents[uidx]));
cmd.arg(format!("-Cextra-filename=-{ef16}"));
cmd.arg("--out-dir").arg(&outdir);
cmd.arg("-L").arg(format!("dependency={}", ctx.pool_logical.display()));
for (name, file, _) in &externs {
cmd.arg("--extern").arg(format!("{name}={}", ctx.pool_logical.join(file).display()));
}
if crate_type == "proc-macro" || target.kind.iter().any(|k| k == "proc-macro") {
cmd.arg("--extern").arg("proc_macro");
}
if crate_type == "proc-macro" && ctx.host.contains("apple") {
cmd.arg(format!("-Clink-arg=-Wl,-install_name,/dc/lib{crate_name}-{ef16}.dylib"));
}
for f in &features {
cmd.arg("--cfg").arg(format!("feature=\"{f}\""));
}
for c in &bs.cfgs {
cmd.arg("--cfg").arg(c);
}
for l in &bs.link_libs {
cmd.arg("-l").arg(l);
}
for s in &link_search {
cmd.arg("-L").arg(s);
}
for a in &link_args {
cmd.arg(format!("-Clink-arg={a}"));
}
if cap_lints {
cmd.arg("--cap-lints").arg("allow");
}
cmd.arg("--remap-path-prefix").arg(format!("{}=/dc/sysroot", ctx.sysroot));
cmd.arg("--remap-path-prefix").arg(format!("{}=.", ctx.workspace_root));
if !pkg_root.starts_with(&ctx.workspace_root) && !pkg_root.starts_with(&ctx.cargo_home) {
cmd.arg("--remap-path-prefix")
.arg(format!("{}=/dc/pkg/{}-{}", pkg_root.display(), pkg.name, pkg.version));
}
for flag in unit_rustflags {
cmd.arg(flag);
}
if ctx.verbose {
eprintln!("corgi: exec {cmd:?}");
}
ctx.jobserver.configure(&mut cmd);
let job_token = ctx.jobserver.acquire().context("acquiring jobserver token")?;
let t_rustc = Instant::now();
let (success, stderr) = if self_pipelined {
run_rustc_streaming(ctx, &mut cmd, uidx, &pkg.name, &key, fire_meta)?
} else {
let out = cmd.output().with_context(|| format!("spawning rustc for {}", pkg.name))?;
(out.status.success(), String::from_utf8_lossy(&out.stderr).into_owned())
};
phases.rustc_ns = t_rustc.elapsed().as_nanos() as u64;
drop(job_token); fs::remove_dir_all(&scratch).ok();
if !success {
fs::remove_dir_all(&stage_root).ok();
bail!("rustc failed for {} v{} ({}):\n{}", pkg.name, pkg.version, crate_name, stderr);
}
if !stderr.trim().is_empty() {
eprint!("{stderr}");
}
let dep_file = outdir.join(format!("{crate_name}-{ef16}.d"));
if let Ok(d) = fs::read_to_string(&dep_file) {
let mut allowed: Vec<PathBuf> = vec![
ctx.store.root.clone(),
ctx.store.logical_root().to_path_buf(),
PathBuf::from(&ctx.sysroot),
];
if clippy_action {
if let Some(conf) = &ctx.clippy_conf {
allowed.push(conf.clone());
}
}
for e in ctx.extra_inputs_for(pkg) {
if let Ok(p) = pkg_root.join(e).canonicalize() {
allowed.push(p); }
}
let t_val = Instant::now();
let workspace_relative_pkg = pkg_root.strip_prefix(&ctx.workspace_root).ok();
validate_dep_info(&d, &pkg_root, workspace_relative_pkg, &allowed).with_context(|| {
format!("hermeticity violation compiling {} v{}", pkg.name, pkg.version)
})?;
phases.validate_ns = t_val.elapsed().as_nanos() as u64;
fs::remove_file(&dep_file).ok(); }
let mut outputs = Vec::new();
let mut entries: Vec<PathBuf> = fs::read_dir(&outdir)?
.map(|e| e.map(|e| e.path()))
.collect::<std::io::Result<_>>()?;
entries.sort();
let t_ingest = Instant::now();
for p in entries {
let name = p.file_name().unwrap().to_string_lossy().into_owned();
let exe = !name.contains('.')
|| name.ends_with(".dylib")
|| name.ends_with(".so")
|| name.ends_with(".dll");
phases.ingest_bytes += fs::metadata(&p).map(|m| m.len()).unwrap_or(0);
let (hash, leaked) =
ctx.store.insert_file_scan(&p, ctx.workspace_root.as_bytes())?;
if leaked {
bail!(
"output {name} embeds the workspace path ({}); artifacts must be \
location-free — resolve paths at runtime instead of baking them in at build time",
ctx.workspace_root
);
}
outputs.push(OutputFile { name, hash, exe });
}
phases.ingest_ns = t_ingest.elapsed().as_nanos() as u64;
fs::remove_dir_all(&stage_root).ok();
incr_lock.take();
let t_finish = Instant::now();
let res = ActionResult { outputs, stderr, bs: None };
let expected: Vec<String> = if self_checked {
res.outputs.iter().map(|o| o.name.clone()).collect()
} else {
expected_outputs(ctx, &crate_name, &ef16, crate_type, unit.host)?
};
for e in &expected {
if !res.outputs.iter().any(|o| &o.name == e) {
bail!(
"rustc-reported output {e} missing (got {:?})",
res.outputs.iter().map(|o| &o.name).collect::<Vec<_>>()
);
}
}
ctx.store.save_action(&key, &serde_json::to_vec(&res)?)?;
ctx.materialize(&key, &res)?;
phases.finish_ns = t_finish.elapsed().as_nanos() as u64;
finish_compile(&expected, key, false, res, phases)
}
fn run_build_script(ctx: &Ctx, uidx: usize, results: &[OnceLock<UnitResult>]) -> Result<UnitResult> {
let unit = &ctx.units[uidx];
let pkg = &ctx.meta.packages[unit.pkg];
let pkg_root = pkg.root();
let mut script: Option<OutputFile> = None;
let mut script_key = String::new();
let mut dep_env: Vec<(String, String)> = Vec::new();
for d in &unit.deps {
let r = results[d.unit].get().context("dep result missing")?;
match ctx.units[d.unit].kind {
Kind::Bsc => {
script = r.main.clone();
script_key = r.key.clone();
}
Kind::Bsr => {
let dpkg = &ctx.meta.packages[ctx.units[d.unit].pkg];
if let (Some(links), Some(b)) = (&dpkg.links, &r.res.bs) {
let l = links.to_uppercase().replace('-', "_");
for (k, v) in &b.metadata {
dep_env.push((
format!("DEP_{l}_{}", k.to_uppercase().replace('-', "_")),
v.clone(),
));
}
}
}
_ => {}
}
}
let script = script.context("build script binary missing")?;
let mut env = ctx.pkg_env(pkg);
let plat_triple = if unit.host {
ctx.host.clone()
} else {
ctx.target.clone().unwrap_or_else(|| ctx.host.clone())
};
env.push(("TARGET".into(), plat_triple));
env.push(("HOST".into(), ctx.host.clone()));
env.push(("PROFILE".into(), unit.profile.env_name().into()));
env.push((
"OPT_LEVEL".into(),
if unit.profile.opt_level.is_empty() {
"0".into()
} else {
unit.profile.opt_level.clone()
},
));
env.push(("DEBUG".into(), "false".into()));
env.push(("NUM_JOBS".into(), "4".into()));
env.push(("RUSTC".into(), ctx.rustc.clone()));
env.push(("RUSTDOC".into(), "rustdoc".into()));
env.push(("CARGO".into(), ctx.cargo.clone()));
env.push((
"CARGO_ENCODED_RUSTFLAGS".into(),
(if unit.host { &ctx.host_rustflags } else { &ctx.target_rustflags }).join("\x1f"),
));
env.extend(ctx.config_env.iter().cloned());
if let Some(links) = &pkg.links {
env.push(("CARGO_MANIFEST_LINKS".into(), links.clone()));
}
let visible_tools: Vec<&ToolRt> = ctx
.tools
.iter()
.filter(|t| t.packages.is_empty() || t.packages.iter().any(|p| p == &pkg.name))
.collect();
for t in &visible_tools {
env.push((t.env.clone(), t.value.clone()));
}
for (name, value, pkgs, profiles) in &ctx.env_probes {
if pkgs.iter().any(|p| p == &pkg.name)
&& (profiles.is_empty() || profiles.iter().any(|pr| pr == &unit.profile.name))
{
env.push((name.clone(), value.clone())); }
}
let mut tool_ids: Vec<String> = visible_tools.iter().map(|t| t.id.clone()).collect();
tool_ids.sort();
let plat_cfg = if unit.host { &ctx.cfg_env } else { &ctx.cfg_env_target };
for (k, v) in plat_cfg {
env.push((k.clone(), v.clone()));
}
let mut features = unit.features.clone();
features.sort();
for f in &features {
env.push((format!("CARGO_FEATURE_{}", f.to_uppercase().replace('-', "_")), "1".into()));
}
env.sort();
dep_env.sort();
let src_hash = ctx.pkg_src_hash(unit.pkg)?;
let key_json = serde_json::to_string(&RunKey {
kind: "run-build-script",
tool: TOOL_VERSION,
rustc: &ctx.rustc_version,
host: &ctx.host,
pkg: [&pkg.name, &pkg.version, pkg.source.as_deref().unwrap_or("local")],
src_hash: &src_hash,
script: [&script.name, &script.hash],
env: &env,
dep_env: &dep_env,
toolchain: &ctx.toolchain,
tools: &tool_ids,
})?;
let key = sha256_hex(key_json.as_bytes());
if let Some(res) = ctx.try_cache_hit(&key)? {
return Ok(UnitResult { key, cached: true, res, main: None, phases: Phases::default() });
}
let final_parent = ctx.store.root.join("outdirs").join(&key);
fs::create_dir_all(ctx.store.root.join("outdirs"))?;
let lock_file = fs::File::create(ctx.store.root.join("outdirs").join(format!("{key}.lock")))?;
lock_file.lock().with_context(|| format!("locking OUT_DIR for {}", pkg.name))?;
if let Some(res) = ctx.try_cache_hit(&key)? {
return Ok(UnitResult { key, cached: true, res, main: None, phases: Phases::default() });
}
if final_parent.exists() {
fs::remove_dir_all(&final_parent).context("clearing partial OUT_DIR")?;
}
let stage_out = final_parent.join("out");
fs::create_dir_all(&stage_out)?;
let stage_logical = ctx.out_dir_logical(&key);
let script_path = ctx.pool.join(Store::pool_file_name(&script.name, &script_key));
let scratch = ctx.store.tmp_path("scratch");
fs::create_dir_all(&scratch)?;
let extra_in: Vec<PathBuf> = ctx
.extra_inputs_for(pkg)
.iter()
.filter_map(|e| pkg_root.join(e).canonicalize().ok())
.collect();
let mut reads: Vec<&Path> = vec![&pkg_root];
reads.extend(extra_in.iter().map(|p| p.as_path()));
let writes: Vec<&Path> = vec![&final_parent, &scratch];
let mut cmd = sandboxed_command(ctx, &script_path.to_string_lossy(), &reads, &writes);
cmd.current_dir(&pkg_root);
cmd.env_clear();
cmd.env("TMPDIR", &scratch);
if !ctx.sdkroot.is_empty() {
cmd.env("SDKROOT", &ctx.sdkroot);
}
for (k, v) in &ctx.base_env {
cmd.env(k, v);
}
if let Some(shims) = ensure_tool_shims(&ctx.store, &visible_tools)? {
cmd.env("PATH", format!("{}:/usr/bin:/bin", shims.display()));
}
for (k, v) in &env {
cmd.env(k, v);
}
for (k, v) in &dep_env {
cmd.env(k, v);
}
cmd.env("OUT_DIR", &stage_logical);
cmd.env("CARGO_MANIFEST_DIR", &pkg_root);
cmd.env("CARGO_MANIFEST_PATH", &pkg.manifest_path);
ctx.jobserver.configure(&mut cmd);
let _job_token = ctx.jobserver.acquire().context("acquiring jobserver token")?;
if ctx.verbose {
eprintln!("corgi: exec {cmd:?}");
}
let out = cmd.output().with_context(|| format!("running build script for {}", pkg.name))?;
fs::remove_dir_all(&scratch).ok();
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
if !out.status.success() {
fs::remove_dir_all(&final_parent).ok();
bail!(
"build script failed for {} v{}:\n--- stdout\n{}--- stderr\n{}",
pkg.name,
pkg.version,
stdout,
stderr
);
}
let mut warnings = Vec::new();
let bs = parse_directives(&stdout, &mut warnings)?;
for w in warnings {
eprintln!("corgi: warning ({} build script): {w}", pkg.name);
}
scan_dir_for_workspace_path(&stage_out, &ctx.workspace_root)?;
let res = ActionResult { outputs: vec![], stderr, bs: Some(bs) };
ctx.store.write_atomic(&final_parent.join(".ok"), b"ok\n")?;
ctx.store.save_action(&key, &serde_json::to_vec(&res)?)?;
Ok(UnitResult { key, cached: false, res, main: None, phases: Phases::default() })
}
fn scan_dir_for_workspace_path(dir: &Path, workspace_root: &str) -> Result<()> {
for entry in fs::read_dir(dir)? {
let path = entry?.path();
if path.is_dir() {
scan_dir_for_workspace_path(&path, workspace_root)?;
} else if path.is_file() {
let (_, leaked) = crate::store::sha256_file_scan(&path, workspace_root.as_bytes())?;
if leaked {
bail!(
"build script output {} embeds the workspace path ({workspace_root}); \
outputs must be location-free — resolve paths at runtime instead of \
baking them in at build time",
path.display()
);
}
}
}
Ok(())
}
fn validate_dep_info(
dep: &str,
pkg_root: &Path,
workspace_relative_pkg: Option<&Path>,
allowed_abs: &[PathBuf],
) -> Result<()> {
for line in dep.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((_, rest)) = line.split_once(':') else { continue };
for tok in rest.split_whitespace() {
let p = Path::new(tok);
if p.is_absolute() {
if !(p.starts_with(pkg_root) || allowed_abs.iter().any(|a| p.starts_with(a))) {
bail!(
"undeclared input read during compilation: {tok}\n\
this file is outside the package and OUT_DIR, so it is not part of\n\
the action key; caching it would be unsound"
);
}
} else {
match workspace_relative_pkg {
Some(prefix) if p.starts_with(prefix) => {}
_ => bail!("undeclared input outside the package: {tok}"),
}
}
}
}
Ok(())
}
fn parse_directives(stdout: &str, warnings: &mut Vec<String>) -> Result<BuildScriptOut> {
let mut bs = BuildScriptOut { stdout: stdout.to_string(), ..Default::default() };
for line in stdout.lines() {
let line = line.trim();
let rest = if let Some(r) = line.strip_prefix("cargo::") {
r
} else if let Some(r) = line.strip_prefix("cargo:") {
r
} else {
continue;
};
let Some((k, v)) = rest.split_once('=') else { continue };
match k {
"rustc-cfg" => bs.cfgs.push(v.to_string()),
"rustc-env" => {
if let Some((ek, ev)) = v.split_once('=') {
bs.envs.push((ek.to_string(), ev.to_string()));
}
}
"rustc-link-lib" => bs.link_libs.push(v.to_string()),
"rustc-link-search" => bs.link_search.push(v.to_string()),
"rustc-flags" => {
let toks: Vec<&str> = v.split_whitespace().collect();
let mut i = 0;
while i < toks.len() {
let t = toks[i];
if (t == "-l" || t == "-L") && i + 1 < toks.len() {
i += 1;
if t == "-l" {
bs.link_libs.push(toks[i].to_string());
} else {
bs.link_search.push(toks[i].to_string());
}
} else if let Some(rest) = t.strip_prefix("-l") {
bs.link_libs.push(rest.to_string());
} else if let Some(rest) = t.strip_prefix("-L") {
bs.link_search.push(rest.to_string());
}
i += 1;
}
}
"rustc-link-arg" | "rustc-link-arg-bins" => bs.link_args.push(v.to_string()),
"warning" => warnings.push(v.to_string()),
"error" => bail!("build script error: {v}"),
"metadata" => {
if let Some((mk, mv)) = v.split_once('=') {
bs.metadata.push((mk.to_string(), mv.to_string()));
}
}
"rerun-if-changed" | "rerun-if-env-changed" | "rustc-check-cfg" | "rustc-cdylib-link-arg" => {}
other => bs.metadata.push((other.to_string(), v.to_string())),
}
}
Ok(bs)
}