mod args;
mod blob_heal;
mod build_script;
mod build_script_inputs;
use kache_store::atomic;
mod build_intent;
mod cache_fs;
mod cache_key;
mod cargo_env;
mod cargo_layout;
mod cargo_proxy;
mod checked_regions;
mod cli;
mod compile;
mod compiler;
mod config;
mod config_tui;
mod daemon;
mod daemon_publish;
mod demand;
mod disk_recovery;
mod events;
mod explain;
mod extra_inputs;
mod fallback;
mod fallback_planner;
mod heartbeat;
mod identity;
mod incremental_policy;
mod index_compact;
#[cfg(unix)]
mod init_shell;
mod key_env;
use kache_store::link;
mod cache_remote;
mod link_probe;
mod machine;
mod maintenance;
mod miss_chain;
mod native_archive;
mod native_link_key;
mod notice;
mod opcounts;
mod otel;
mod out_dir_alias;
mod path_normalizer;
mod phase_trace;
mod planner_auth;
mod planner_client;
mod platform;
mod policy;
mod prediction_share;
mod prestage;
mod probe;
mod probe_memo;
mod remote;
mod remote_backend;
mod remote_layout;
mod remote_pack;
mod remote_plan;
mod remote_resilience;
mod report;
mod run_diff;
mod savings;
mod scheduler;
mod service;
mod shards;
mod since;
#[cfg_attr(not(unix), allow(dead_code))]
use kache_store::sharing;
mod compiler_store;
use compiler_store as store;
mod store_view;
mod target_cleanup;
mod target_dedup;
mod target_liveness;
mod target_seed;
mod target_use;
mod term;
mod test_runner;
#[cfg(test)]
mod test_support;
mod timeline;
mod timeline_client;
mod toolchain_dylib;
mod transport;
mod tree_stamp;
mod tui;
mod tui_sessions;
mod unit_prune;
mod verify_compare;
mod volume_gc;
mod worktree_discovery;
mod wrapper;
mod wrapper_config;
#[cfg(target_os = "macos")]
#[used]
#[unsafe(link_section = "__TEXT,__info_plist")]
static MACOS_INFO_PLIST: [u8; include_bytes!("../assets/macos/Info.plist").len()] =
*include_bytes!("../assets/macos/Info.plist");
use anyhow::{Context, Result};
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
use std::io::IsTerminal;
use std::path::PathBuf;
pub const VERSION: &str = {
const RAW: &str = match option_env!("KACHE_VERSION") {
Some(v) => v,
None => env!("CARGO_PKG_VERSION"),
};
let b = RAW.as_bytes();
if b.len() > 1 && b[0] == b'v' {
unsafe { core::str::from_utf8_unchecked(b.split_at(1).1) }
} else {
RAW
}
};
#[derive(Parser)]
#[command(name = "kache", version = VERSION, styles = term::clap_styles(), after_help = "Start with `kache init`. Keep using cargo as usual.\nUse `kache stats` for a summary or `kache monitor` for live activity.")]
pub(crate) struct Cli {
#[arg(long, global = true)]
json: bool,
#[command(subcommand)]
pub(crate) command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
#[command(hide = true)]
Cargo {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<std::ffi::OsString>,
},
#[command(display_order = 22)]
List {
crate_name: Option<String>,
#[arg(long, value_enum, default_value_t = ListSort::Name)]
sort: ListSort,
#[arg(long)]
no_pager: bool,
},
#[command(hide = true)]
Gc {
#[arg(long, conflicts_with = "stale_schema")]
max_age: Option<String>,
#[arg(long)]
stale_schema: bool,
},
#[command(hide = true)]
Purge {
#[arg(long)]
crate_name: Option<String>,
},
#[command(display_order = 11)]
Clean {
#[arg(value_name = "DIR", conflicts_with_all = ["cache", "crate_name", "stale", "orphans"])]
path: Option<PathBuf>,
#[arg(long, visible_alias = "artifacts", conflicts_with_all = ["cache", "crate_name", "stale", "orphans", "stale_schema"])]
units: bool,
#[arg(long, short = 'n')]
dry_run: bool,
#[arg(long, short = 'y')]
yes: bool,
#[arg(long, value_name = "DURATION")]
stale: Option<String>,
#[arg(long)]
cache: bool,
#[arg(long, conflicts_with_all = ["stale", "cache", "crate_name"])]
orphans: bool,
#[arg(long, hide = true, requires = "cache", conflicts_with_all = ["stale", "stale_schema"])]
all: bool,
#[arg(long = "crate", hide = true, value_name = "NAME", conflicts_with_all = ["cache", "stale"])]
crate_name: Option<String>,
#[arg(long, hide = true, requires = "cache", conflicts_with = "stale")]
stale_schema: bool,
#[arg(long, hide = true)]
tracked: bool,
},
#[command(hide = true, subcommand_required = false)]
Targets {
#[command(subcommand)]
command: Option<TargetCommands>,
},
#[command(display_order = 1)]
Init {
#[arg(long, short = 'y')]
yes: bool,
#[arg(long)]
no_service: bool,
#[arg(long)]
no_shell: bool,
#[arg(long, visible_alias = "dry-run", short = 'n')]
check: bool,
#[arg(
long,
value_name = "DIR",
num_args = 0..=1,
conflicts_with_all = ["check", "no_service", "no_shell"]
)]
shims: Option<Option<PathBuf>>,
#[arg(long, requires = "shims")]
from_path: bool,
#[arg(long, requires = "shims", conflicts_with = "from_path")]
canonical_only: bool,
#[arg(long, requires = "shims")]
force: bool,
},
#[command(display_order = 2)]
Doctor {
#[arg(long)]
fix: bool,
#[arg(long, requires = "fix")]
purge_sccache: bool,
#[arg(long)]
verify: bool,
#[arg(long)]
checksums: bool,
#[arg(long)]
repair: bool,
},
#[command(display_order = 30)]
Sync {
#[arg(long)]
manifest_path: Option<String>,
#[arg(long, conflicts_with = "push")]
pull: bool,
#[arg(long)]
push: bool,
#[arg(long, short = 'n')]
dry_run: bool,
#[arg(long)]
all: bool,
#[arg(long, conflicts_with = "all")]
workspace: bool,
#[arg(long)]
allow_partial: bool,
},
#[command(hide = true)]
SaveManifest {
#[arg(long)]
manifest_key: Option<String>,
#[arg(long)]
namespace: Option<String>,
},
#[command(subcommand_required = false, display_order = 40)]
Daemon {
#[command(subcommand)]
command: Option<DaemonCommands>,
},
#[command(display_order = 12)]
Monitor {
#[arg(long)]
since: Option<String>,
},
#[command(display_order = 10, visible_alias = "status")]
Stats {
#[arg(long, default_value = "24h")]
since: String,
#[arg(long, conflicts_with = "since")]
last_build: bool,
#[arg(long)]
root: Option<PathBuf>,
#[arg(long)]
full: bool,
#[arg(long, value_name = "FORMAT", value_enum)]
format: Option<cli::ReportFormat>,
#[arg(long, short)]
output: Option<PathBuf>,
#[arg(long)]
top: Option<usize>,
#[arg(long)]
record: bool,
#[arg(long)]
redact: bool,
},
#[command(hide = true)]
Telemetry {
#[command(subcommand)]
command: TelemetryCommands,
},
#[command(display_order = 20, alias = "why-miss")]
Explain {
crate_name: Option<String>,
#[arg(long, conflicts_with = "crate_name")]
root: Option<PathBuf>,
},
#[command(hide = true)]
Diff {
#[arg(long)]
root: Option<PathBuf>,
},
#[command(hide = true)]
Report {
#[arg(long, value_enum, default_value_t = cli::ReportFormat::Text)]
format: cli::ReportFormat,
#[arg(long, default_value = "24h")]
since: String,
#[arg(long, conflicts_with = "since")]
last_build: bool,
#[arg(long)]
root: Option<PathBuf>,
#[arg(long, short)]
output: Option<PathBuf>,
#[arg(long, default_value = "10")]
top: usize,
#[arg(long)]
record: bool,
#[arg(long)]
redact: bool,
},
#[command(display_order = 3)]
Config,
#[command(display_order = 31)]
Login {
#[arg(long)]
device: bool,
#[arg(long)]
retrust: bool,
},
#[command(display_order = 32)]
Logout,
#[command(hide = true)]
InstallShims {
#[arg(value_name = "DIR")]
dir: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
from_path: bool,
#[arg(long, conflicts_with = "from_path")]
canonical_only: bool,
},
#[command(display_order = 41)]
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
enum ListSort {
Name,
Size,
Hits,
Age,
}
impl ListSort {
fn as_str(self) -> &'static str {
match self {
ListSort::Name => "name",
ListSort::Size => "size",
ListSort::Hits => "hits",
ListSort::Age => "age",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
enum BenchPhase {
Cold,
WarmSameTree,
Warm,
Pull,
}
impl BenchPhase {
fn as_str(self) -> &'static str {
match self {
BenchPhase::Cold => "cold",
BenchPhase::WarmSameTree => "warm-same-tree",
BenchPhase::Warm => "warm",
BenchPhase::Pull => "pull",
}
}
}
#[derive(Subcommand)]
enum TargetCommands {
Share {
#[arg(long)]
apply: bool,
#[arg(value_name = "TARGET")]
paths: Vec<PathBuf>,
},
}
#[derive(Subcommand)]
enum DaemonCommands {
Status,
Run,
Start,
Stop,
Restart,
Install,
Uninstall,
Log,
}
#[derive(Subcommand)]
enum TelemetryCommands {
Write {
dir: PathBuf,
#[arg(long)]
scenario: Option<String>,
#[arg(long, value_enum)]
phase: Option<BenchPhase>,
},
Push {
#[arg(long, conflicts_with = "session")]
all: bool,
#[arg(long)]
session: Option<String>,
#[arg(long = "label", value_name = "KEY=VALUE")]
labels: Vec<String>,
#[arg(long, short = 'n')]
dry_run: bool,
},
}
fn command_supports_json(command: &Option<Commands>) -> bool {
let Some(command) = command else {
return false;
};
match command {
Commands::List { .. }
| Commands::Gc { .. }
| Commands::Clean { .. }
| Commands::Targets { .. }
| Commands::Doctor { .. }
| Commands::Stats { .. }
| Commands::Explain { .. }
| Commands::Diff { .. }
| Commands::Report { .. } => true,
Commands::Init { shims, .. } => shims.is_none(),
Commands::Daemon { command } => match command {
None | Some(DaemonCommands::Status) => true,
Some(
DaemonCommands::Run
| DaemonCommands::Start
| DaemonCommands::Stop
| DaemonCommands::Restart
| DaemonCommands::Install
| DaemonCommands::Uninstall
| DaemonCommands::Log,
) => false,
},
Commands::Cargo { .. }
| Commands::Config
| Commands::Monitor { .. }
| Commands::Login { .. }
| Commands::Completions { .. } => false,
Commands::Purge { .. }
| Commands::Sync { .. }
| Commands::SaveManifest { .. }
| Commands::Telemetry { .. }
| Commands::Logout
| Commands::InstallShims { .. } => false,
}
}
fn config_without_terminal(path: &std::path::Path) -> String {
format!(
"`kache config` needs a terminal. Edit {} directly, or set KACHE_* environment variables.",
path.display()
)
}
fn without_hidden(command: &clap::Command) -> clap::Command {
let mut out = clap::Command::new(command.get_name().to_string())
.visible_aliases(command.get_visible_aliases().map(str::to_string));
if let Some(version) = command.get_version() {
out = out.version(version.to_string());
}
if let Some(about) = command.get_about() {
out = out.about(about.clone());
}
for arg in command.get_arguments().filter(|arg| !arg.is_hide_set()) {
out = out.arg(
arg.clone()
.conflicts_with(clap::builder::Resettable::Reset)
.requires(clap::builder::Resettable::Reset),
);
}
for sub in command.get_subcommands().filter(|sub| !sub.is_hide_set()) {
out = out.subcommand(without_hidden(sub));
}
out
}
fn json_refusal(command: &Option<Commands>) -> Option<&'static str> {
if command_supports_json(command) {
return None;
}
Some(match command {
Some(Commands::Config) => "`kache config` is interactive and has no JSON form.",
Some(Commands::Login { .. }) => "`kache login` is interactive and has no JSON form.",
Some(Commands::Monitor { .. }) => {
"`kache monitor` is interactive; use `kache stats --json`."
}
Some(Commands::Purge { .. }) => {
"`kache purge` has no JSON form yet; run it without `--json`."
}
Some(Commands::Init { .. }) => "`kache init --shims` has no JSON form.",
_ => {
"`--json` is supported on init, stats, clean, doctor, explain, list, and daemon status."
}
})
}
fn doctor_json_is_read_only(
fix: bool,
purge_sccache: bool,
verify: bool,
checksums: bool,
repair: bool,
) -> bool {
![fix, purge_sccache, verify, checksums, repair]
.into_iter()
.any(|enabled| enabled)
}
pub(crate) fn diagnostic_log_path() -> PathBuf {
if cfg!(target_os = "macos") {
dirs::home_dir()
.unwrap_or_default()
.join("Library/Logs/kache/kache.log")
} else {
config::default_cache_dir().join("kache.log")
}
}
const MAX_LOG_BYTES: u64 = 5 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LogMode {
Wrapper,
Cli,
TerminalUi,
}
fn detect_log_mode(env_args: &[String]) -> LogMode {
let rustc = std::env::var_os("RUSTC");
detect_log_mode_with_rustc(env_args, rustc.as_deref())
}
fn detect_log_mode_with_rustc(
env_args: &[String],
configured_rustc: Option<&std::ffi::OsStr>,
) -> LogMode {
if env_args.len() >= 2 {
let after = &env_args[1..];
if compiler::is_passthrough_compiler_invocation_with(after, configured_rustc)
|| compiler::is_workspace_wrapper_chain(after)
|| compiler::cc::CcCompiler::recognizes_family_probe(after)
|| compiler::detect_compiler(after).is_some()
{
return LogMode::Wrapper;
}
}
match env_args.get(1).map(String::as_str) {
Some("monitor" | "config") => LogMode::TerminalUi,
_ => LogMode::Cli,
}
}
fn init_logging(mode: LogMode) {
use std::sync::Mutex;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, fmt};
let stderr_layer = if mode == LogMode::TerminalUi {
None
} else {
let default_filter = if mode == LogMode::Wrapper {
"off"
} else {
"kache=warn"
};
let stderr_filter = EnvFilter::try_from_env("KACHE_LOG")
.unwrap_or_else(|_| default_filter.parse().unwrap());
Some(
fmt::layer()
.with_writer(std::io::stderr)
.with_filter(stderr_filter),
)
};
let file_layer = {
let wrapper_opted_in =
mode == LogMode::Wrapper && std::env::var_os("KACHE_LOG_FILE").is_some();
let enable_file_layer = mode != LogMode::Wrapper || wrapper_opted_in;
if !enable_file_layer {
None
} else {
(|| -> Option<_> {
let path = std::env::var_os("KACHE_LOG_FILE_PATH")
.map(PathBuf::from)
.unwrap_or_else(diagnostic_log_path);
std::fs::create_dir_all(path.parent()?).ok()?;
if std::env::var_os("KACHE_LOG_FILE_PATH").is_none()
&& std::fs::metadata(&path).is_ok_and(|m| m.len() > MAX_LOG_BYTES)
{
let _ = std::fs::write(&path, b"--- log rotated ---\n");
}
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.ok()?;
let file_filter = EnvFilter::try_from_env("KACHE_LOG_FILE")
.unwrap_or_else(|_| "kache=info".parse().unwrap());
Some(
fmt::layer()
.with_ansi(false)
.with_writer(Mutex::new(file))
.with_filter(file_filter),
)
})()
}
};
tracing_subscriber::registry()
.with(stderr_layer)
.with(file_layer)
.init();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EntryRoute {
SelfSpawn,
Shim,
Argv,
}
fn entry_route(argv: &[String], self_spawn: Option<&std::ffi::OsStr>) -> EntryRoute {
if platform::is_self_spawn(argv, self_spawn) {
return EntryRoute::SelfSpawn;
}
if argv
.first()
.is_some_and(|arg0| compiler::shim::invoked_as_compiler(arg0))
{
return EntryRoute::Shim;
}
EntryRoute::Argv
}
fn test_runner_args(raw_args: &[std::ffi::OsString]) -> Option<&[std::ffi::OsString]> {
match raw_args.get(1) {
Some(arg)
if arg == "test-runner"
&& !raw_args.first().is_some_and(|arg0| {
compiler::shim::invoked_as_compiler(&arg0.to_string_lossy())
}) =>
{
Some(&raw_args[2..])
}
_ => None,
}
}
fn main() -> std::process::ExitCode {
match run_main() {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(error) => std::process::ExitCode::from(report_main_error(&error)),
}
}
fn report_main_error(error: &anyhow::Error) -> u8 {
if machine::failure_exit(error).is_none() {
return 0;
}
eprintln!("{} {error:#}", term::paint("error:", term::Style::Error));
1
}
fn run_main() -> Result<()> {
opcounts::mark_process_start();
let mut readiness = unsafe { kunobi_daemon::readiness::channel::Notifier::from_env() }
.context("opening daemon readiness channel")?;
if let Some(notifier) = &mut readiness {
let _ = notifier.progress("starting kache");
}
let raw_args: Vec<std::ffi::OsString> = std::env::args_os().collect();
if raw_args.first().is_some_and(|arg| {
std::path::Path::new(arg)
.file_name()
.is_some_and(|name| name == "cargo")
}) && std::env::var_os(platform::SELF_SPAWN_ENV).is_none()
{
init_logging(LogMode::Cli);
return cargo_proxy::run_shim(raw_args[1..].to_vec());
}
if let Some(args) = test_runner_args(&raw_args) {
init_logging(LogMode::Wrapper);
std::process::exit(test_runner::run(args));
}
if std::env::var_os("KACHE_FAMILY_PROBE_ACTIVE").is_some() {
return Ok(());
}
if build_script::is_shim_invocation() {
init_logging(LogMode::Wrapper);
std::process::exit(build_script::run_shim());
}
let env_args: Option<Vec<String>> = raw_args
.iter()
.cloned()
.map(|arg| arg.into_string())
.collect::<Result<_, _>>()
.ok();
let lossy_args;
let detection_args = if let Some(args) = env_args.as_deref() {
args
} else {
lossy_args = raw_args
.iter()
.map(|arg| arg.to_string_lossy().into_owned())
.collect::<Vec<_>>();
&lossy_args
};
let self_spawn = std::env::var_os(platform::SELF_SPAWN_ENV);
let shim_args = match entry_route(detection_args, self_spawn.as_deref()) {
EntryRoute::Shim => compiler::shim::wrapper_args(detection_args),
EntryRoute::SelfSpawn => {
unsafe { std::env::remove_var(platform::SELF_SPAWN_ENV) };
None
}
EntryRoute::Argv => None,
};
if let Some(shim_args) = shim_args {
init_logging(LogMode::Wrapper);
let shim_args = shim_args.map_err(anyhow::Error::msg)?;
if env_args.is_none() {
anyhow::bail!(
"compiler wrapper arguments contain non-UTF-8 bytes and cannot be cached safely"
);
}
return run_wrapper_mode(&shim_args);
}
let log_mode = detect_log_mode(detection_args);
let is_wrapper = log_mode == LogMode::Wrapper;
init_logging(log_mode);
if is_wrapper {
if let Some(env_args) = env_args {
return run_wrapper_mode(&env_args[1..]);
}
anyhow::bail!(
"compiler wrapper arguments contain non-UTF-8 bytes and cannot be cached safely"
);
}
let matches = match Cli::command().try_get_matches_from(&raw_args) {
Ok(matches) => matches,
Err(error) => {
let json = raw_args
.iter()
.skip(1)
.take_while(|arg| *arg != "--")
.any(|arg| arg == "--json");
if json && error.kind() == clap::error::ErrorKind::DisplayHelp {
machine::emit_help(Cli::command())?;
return Ok(());
}
if json && error.kind() == clap::error::ErrorKind::DisplayVersion {
machine::emit(
"version",
serde_json::json!({ "version": VERSION }),
Vec::new(),
)?;
return Ok(());
}
if json && error.use_stderr() {
let _ = machine::emit_error(
"cli",
"invalid_arguments",
&anyhow::anyhow!(error.to_string()),
);
eprint!("{error}");
std::process::exit(2);
}
error.exit();
}
};
let cli = Cli::from_arg_matches(&matches)?;
let json = cli.json;
let mut names = Vec::new();
let mut matched = &matches;
while let Some((name, args)) = matched.subcommand() {
names.push(name);
matched = args;
}
let command = if names.is_empty() {
"cli".into()
} else {
names.join("-")
};
machine::configure(json, command.clone());
if let Err(error) = run_cli(cli, readiness) {
let Some((code, exit)) = machine::failure_exit(&error) else {
return Ok(());
};
if json && error.downcast_ref::<machine::Reported>().is_none() {
let _ = machine::emit_error(&command, code, &error);
}
eprintln!("{} {error:#}", term::paint("error:", term::Style::Error));
std::process::exit(exit);
}
Ok(())
}
fn run_cli(cli: Cli, readiness: Option<kunobi_daemon::readiness::channel::Notifier>) -> Result<()> {
let json = cli.json;
if json && let Some(refusal) = json_refusal(&cli.command) {
return Err(machine::invalid_arguments(refusal));
}
match &cli.command {
Some(Commands::Config) => {
if !std::io::stdout().is_terminal() {
anyhow::bail!(config_without_terminal(&config::resolve_config_path()));
}
return config_tui::run_config_editor();
}
Some(Commands::Login { device, retrust }) => {
return cli::login::login(*device, *retrust);
}
Some(Commands::Logout) => return cli::login::logout(),
Some(Commands::Cargo { args }) => return cargo_proxy::run(args.clone()),
Some(Commands::Completions { shell }) => {
use clap_complete::generate;
let mut cmd = without_hidden(&Cli::command());
let name = cmd.get_name().to_string();
generate(*shell, &mut cmd, name, &mut std::io::stdout());
return Ok(());
}
_ => {}
}
let (config, config_provenance) = config::Config::load_with_provenance()?;
if !matches!(
cli.command,
Some(Commands::Doctor { .. } | Commands::Config)
) && let Some((path, reason)) = config::unusable_config_file()
{
eprintln!(
"{} ignoring {}: {reason}. Using the defaults.",
term::paint("warning:", term::Style::Warning),
path.display()
);
}
match cli.command {
Some(Commands::List {
crate_name,
sort,
no_pager,
}) => cli::list(
&config,
crate_name.as_deref(),
sort.as_str(),
no_pager,
json,
),
Some(Commands::Gc {
max_age,
stale_schema,
}) => {
let hours = max_age
.as_deref()
.map(|value| parse_age_hours("--max-age", value))
.transpose()?;
cli::gc(&config, hours, stale_schema, json)
}
Some(Commands::Purge { crate_name }) => cli::purge(&config, crate_name.as_deref()),
Some(Commands::Clean {
path,
dry_run,
yes,
stale,
orphans,
cache,
all,
crate_name,
stale_schema,
units,
tracked: _,
}) => {
if units {
return cli::clean_units(&config, path, dry_run, yes, json);
}
let stale_hours = stale
.as_deref()
.map(|value| parse_age_hours("--stale", value))
.transpose()?;
if let Some(name) = crate_name {
return cli::clean_cache(&config, cli::CacheClean::Crate(name), dry_run, yes, json);
}
if cache {
let _ = all;
let what = cache_clean_choice(stale_schema, stale_hours);
return cli::clean_cache(&config, what, dry_run, yes, json);
}
let scope = clean_scope(path, orphans, stale_hours);
cli::clean(&config, dry_run, yes, json, scope)
}
Some(Commands::Targets { command: None }) => cli::targets(&config, json),
Some(Commands::Targets {
command: Some(TargetCommands::Share { apply, paths }),
}) => target_dedup::run(&config, &paths, apply, json),
#[cfg(unix)]
Some(Commands::Init {
shims: Some(dir),
from_path,
canonical_only,
force,
..
}) => install_shims(dir, force, from_path, canonical_only),
#[cfg(not(unix))]
Some(Commands::Init { shims: Some(_), .. }) => Err(anyhow::anyhow!(UNIX_ONLY_SHIMS)),
Some(Commands::Init {
yes,
no_service,
no_shell,
check,
..
}) => {
if json && !yes && !check {
return Err(machine::invalid_arguments(
"`kache init --json` needs `--check` to preview or `--yes` to apply setup.",
));
}
cli::init(yes, no_service, no_shell, check)
}
Some(Commands::Doctor {
fix,
purge_sccache,
verify,
checksums,
repair,
}) => {
if json && !doctor_json_is_read_only(fix, purge_sccache, verify, checksums, repair) {
return Err(machine::invalid_arguments(
"`kache doctor --json` reports diagnostics only; run repair options without `--json`.",
));
}
cli::doctor(
fix,
purge_sccache,
verify || checksums || repair,
checksums,
repair,
json,
)
}
Some(Commands::Sync {
manifest_path,
pull,
push,
dry_run,
all,
workspace,
allow_partial,
}) => cli::sync(
&config,
manifest_path.as_deref(),
pull,
push,
dry_run,
all,
workspace,
allow_partial,
),
Some(Commands::SaveManifest {
manifest_key,
namespace,
}) => cli::save_manifest(&config, manifest_key.as_deref(), namespace.as_deref()),
Some(Commands::Daemon { command: None }) => service::status(json),
Some(Commands::Daemon {
command: Some(DaemonCommands::Status),
}) => service::status(json),
Some(Commands::Daemon {
command: Some(DaemonCommands::Run),
}) => daemon::run_server(&config, &config_provenance, readiness),
Some(Commands::Daemon {
command: Some(DaemonCommands::Start),
}) => match daemon::start(&config) {
Ok(outcome) => {
eprintln!("{}", daemon_start_message(&outcome));
if !outcome.is_ready() {
std::process::exit(1);
}
Ok(())
}
Err(e) => {
eprintln!("failed to start daemon: {e}");
std::process::exit(1);
}
},
Some(Commands::Daemon {
command: Some(DaemonCommands::Stop),
}) => daemon::send_shutdown_request(&config),
Some(Commands::Daemon {
command: Some(DaemonCommands::Restart),
}) => {
let was_running = daemon::is_running(&config)?;
let ready = daemon::restart(&config)?;
eprintln!("{}", daemon_restart_message(was_running, ready));
if !ready {
std::process::exit(1);
}
Ok(())
}
Some(Commands::Daemon {
command: Some(DaemonCommands::Install),
}) => service::install(),
Some(Commands::Daemon {
command: Some(DaemonCommands::Uninstall),
}) => service::uninstall(),
Some(Commands::Daemon {
command: Some(DaemonCommands::Log),
}) => service::log(),
Some(Commands::Report {
format,
since,
last_build,
root,
output,
top,
record,
redact,
}) => {
let window = parse_since_window(&since)?;
cli::report(
&config,
report_format(Some(format), json)?,
window,
report::ReportFilter { root, last_build },
output,
top,
record,
redact,
)
}
Some(Commands::Stats {
since,
last_build,
root,
full,
format,
output,
top,
record,
redact,
}) => {
let full = stats_wants_full(
full,
format.is_some(),
output.is_some(),
top.is_some(),
record,
redact,
);
if full {
let format = report_format(format, json)?;
let window = parse_since_window(&since)?;
return cli::report(
&config,
format,
window,
report::ReportFilter { root, last_build },
output,
top.unwrap_or(DEFAULT_REPORT_TOP),
record,
redact,
);
}
if last_build {
return cli::stats_last_build(&config, root, json);
}
if root.is_some() {
return Err(machine::invalid_arguments(
"`--root` needs `--last-build` or `--full`",
));
}
let window = parse_since_window(&since)?;
cli::stats(&config, &config_provenance, window, json)
}
Some(Commands::Telemetry {
command:
TelemetryCommands::Write {
dir,
scenario,
phase,
},
}) => cli::telemetry_write(
&config,
&dir,
scenario.as_deref(),
phase.map(BenchPhase::as_str),
),
Some(Commands::Telemetry {
command:
TelemetryCommands::Push {
all,
session,
labels,
dry_run,
},
}) => {
let selection = match (all, session) {
(true, _) => cli::TimelineSelection::All,
(false, Some(session)) => cli::TimelineSelection::Session(session),
(false, None) => cli::TimelineSelection::Latest,
};
cli::telemetry_push(&config, &selection, &labels, dry_run)
}
Some(Commands::Explain {
crate_name: Some(crate_name),
..
}) => cli::why_miss(&config, &crate_name, json),
Some(Commands::Explain {
crate_name: None,
root,
}) => explain::run(&config, root.as_deref(), json),
Some(Commands::Diff { root }) => {
if run_diff::run(&config, root.as_deref(), json)? {
std::process::exit(1);
}
Ok(())
}
Some(Commands::Monitor { since }) => {
crate::machine::require_tty(
std::io::stdout().is_terminal(),
"monitor",
"`kache stats --json`",
)?;
let window = since.as_deref().map(parse_since_window).transpose()?;
tui::run_monitor(&config, window)
}
#[cfg(unix)]
Some(Commands::InstallShims {
dir,
force,
from_path,
canonical_only,
}) => install_shims(dir, force, from_path, canonical_only),
#[cfg(not(unix))]
Some(Commands::InstallShims { .. }) => Err(anyhow::anyhow!(UNIX_ONLY_SHIMS)),
Some(Commands::Cargo { .. }) => unreachable!(),
Some(Commands::Config) => unreachable!(),
Some(Commands::Login { .. } | Commands::Logout) => unreachable!(),
Some(Commands::Completions { .. }) => unreachable!(),
None => {
use clap::CommandFactory;
Cli::command().print_help()?;
println!();
Ok(())
}
}
}
const KACHE_ACTIVE_ENV: &str = "KACHE_ACTIVE";
const MAX_WRAPPERS_ABOVE: u32 = 8;
fn wrappers_above(active: Option<&std::ffi::OsStr>) -> Option<u32> {
let active = active?;
Some(active.to_str().and_then(|v| v.parse().ok()).unwrap_or(1))
}
fn next_wrapper_depth(above: u32) -> u32 {
above.saturating_add(1)
}
fn check_wrapper_depth(above: u32, compiler: Option<&str>) -> Result<()> {
if above < MAX_WRAPPERS_ABOVE {
return Ok(());
}
anyhow::bail!(
"kache is running inside {above} other kache wrappers to run `{}`, so two \
compiler shims are running each other in a loop. Another kache install's shim \
directory is on PATH and holds copies that kache cannot tell apart from a \
compiler. Remove that directory from PATH, or create an empty `.kache-shims` \
file in it so every kache skips it.",
compiler.unwrap_or("?")
)
}
fn run_compiler_directly(
config: &config::Config,
args: &[String],
preserve_incremental: bool,
) -> Result<i32> {
if is_cc_compiler_invocation(args) {
let parsed = compiler::cc::CcArgs::parse(args)?;
wrapper::refuse_legacy_cc_blob_outputs(config, &parsed)?;
}
run_compiler_process_directly(args, preserve_incremental)
}
fn is_cc_compiler_invocation(args: &[String]) -> bool {
if compiler::is_passthrough_compiler_invocation(args) {
return false;
}
if compiler::is_workspace_wrapper_chain(args) {
return false;
}
compiler::detect_compiler(args).is_some_and(|adapter| adapter.id() == compiler::cc::CC_ID)
}
fn preserve_workspace_wrapper_incremental(
preserve_requested: bool,
extra_inputs_active: bool,
) -> bool {
preserve_requested && !extra_inputs_active
}
fn run_workspace_wrapper_chain(config: &config::Config, args: &[String]) -> Result<i32> {
let parsed = args::RustcArgs::parse(args).context("parsing workspace-wrapper rustc chain")?;
let extra_inputs = wrapper::resolve_extra_inputs_for_passthrough(config, &parsed)?;
let preserve_incremental =
preserve_workspace_wrapper_incremental(config.preserve_incremental, extra_inputs.is_some());
let exit = run_compiler_directly(config, args, preserve_incremental)?;
if exit == 0 {
wrapper::complete_current_extra_inputs_after_success(
config,
&parsed,
extra_inputs.as_ref(),
)?;
}
Ok(exit)
}
fn run_compiler_process_directly(args: &[String], preserve_incremental: bool) -> Result<i32> {
let configured_rustc = args.first().is_some_and(|program| {
std::env::var_os("RUSTC")
.is_some_and(|rustc| rustc == std::ffi::OsStr::new(program.as_str()))
});
let is_nvcc = args
.first()
.and_then(|program| compiler::command_basename(program))
.map(compiler::strip_windows_exe_suffix)
.is_some_and(|name| name.eq_ignore_ascii_case("nvcc"));
let rustc_invocation =
!is_nvcc && (configured_rustc || rustc_args_for_direct_preclean(args).is_some());
if !rustc_invocation {
let program = compiler::resolve_program_on_path(&args[0])
.unwrap_or_else(|| std::path::PathBuf::from(&args[0]));
let status = std::process::Command::new(program)
.args(&args[1..])
.status()?;
return Ok(status.code().unwrap_or(1));
}
let parsed = args::RustcArgs::parse(args).ok();
if let Some(parsed) = &parsed {
compile::pre_clean_outputs(
parsed.output.as_deref(),
parsed.out_dir.as_deref(),
parsed.crate_name.as_deref(),
parsed.extra_filename.as_deref(),
&parsed.emit,
);
}
let effective_args = parsed
.as_ref()
.map_or(&args[1..], |parsed| parsed.all_args.as_slice());
let isolated_args = if preserve_incremental {
match compile::isolate_incremental_flags(effective_args) {
Some(isolated) => Some(isolated),
None => {
tracing::warn!(
"[kache] incremental directory has no safe sibling path; stripping incremental flags"
);
None
}
}
} else {
None
};
let incremental_preserved = isolated_args.is_some();
let compiler_args = if let Some(isolated_args) = isolated_args {
isolated_args
} else {
compile::strip_incremental_flags(effective_args)
.into_iter()
.cloned()
.collect()
};
let compiler_args_changed = compiler_args != effective_args;
let response_file = if parsed
.as_ref()
.is_some_and(args::RustcArgs::has_expanded_argfiles)
{
match compile::RustcResponseFile::new(compiler_args.iter().map(|arg| arg.as_str())) {
Ok(response) => Some(response),
Err(error) => {
if compiler_args_changed {
return Err(error).context(
"materializing rustc response file after rewriting incremental arguments",
);
}
tracing::warn!(
"failed to materialize expanded rustc response file; using unchanged original argv: {error:#}"
);
None
}
}
} else {
None
};
let program = compiler::resolve_program_on_path(&args[0])
.unwrap_or_else(|| std::path::PathBuf::from(&args[0]));
let mut command = std::process::Command::new(&program);
toolchain_dylib::apply(&mut command, &program);
if !incremental_preserved {
command.env("CARGO_INCREMENTAL", "0");
}
if let Some(inner) = parsed
.as_ref()
.and_then(|parsed| parsed.inner_rustc.as_ref())
{
command.arg(inner);
}
if let Some(response) = &response_file {
command.arg(response.argument());
} else if !compiler_args_changed
&& let Some(parsed) = parsed
.as_ref()
.filter(|parsed| parsed.has_expanded_argfiles())
{
command.args(parsed.raw_args());
} else {
command.args(&compiler_args);
}
let status = command.status()?;
Ok(status.code().unwrap_or(1))
}
fn rustc_args_for_direct_preclean(args: &[String]) -> Option<&[String]> {
match compiler::detect_compiler(args) {
Some(adapter) if adapter.id() == compiler::rustc::RUSTC_ID => Some(args),
Some(_) => None,
None if compiler::is_workspace_wrapper_chain(args) => Some(&args[1..]),
None => None,
}
}
fn wrapper_target(adapter: &compiler::CompilerAdapter) -> Option<WrapperTarget> {
if adapter.id() == compiler::rustc::RUSTC_ID {
Some(WrapperTarget::Rustc)
} else if adapter.id() == compiler::rustdoc::RUSTDOC_ID {
Some(WrapperTarget::Rustdoc)
} else if adapter.id() == compiler::cc::CC_ID {
Some(WrapperTarget::Cc)
} else if adapter.id() == compiler::nvcc::NVCC_ID {
Some(WrapperTarget::Nvcc)
} else {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WrapperTarget {
Rustc,
Rustdoc,
Cc,
Nvcc,
}
fn run_wrapper_mode(args: &[String]) -> Result<()> {
if let Some(above) = wrappers_above(std::env::var_os(KACHE_ACTIVE_ENV).as_deref()) {
check_wrapper_depth(above, args.first().map(String::as_str))?;
unsafe {
std::env::set_var(KACHE_ACTIVE_ENV, next_wrapper_depth(above).to_string());
}
let config = config::Config::load()?;
std::process::exit(run_compiler_directly(&config, args, false)?);
}
unsafe {
std::env::set_var(KACHE_ACTIVE_ENV, "1");
}
let config = config::Config::load()?;
scheduler::pressure::set_enabled(config.scheduler_memory_pressure);
if config.disabled {
std::process::exit(run_compiler_directly(
&config,
args,
config.preserve_incremental,
)?);
}
if compiler::cc::CcCompiler::recognizes_family_probe(args) {
std::process::exit(wrapper::run_cc_probe(args)?);
}
if compiler::is_workspace_wrapper_chain(args)
&& !compiler::is_cacheable_workspace_wrapper_chain(args)
{
tracing::debug!(
program = ?args.first(),
"workspace-wrapper chain is uncached; retaining extra-input Cargo freshness"
);
std::process::exit(run_workspace_wrapper_chain(&config, args)?);
}
let direct_passthrough = compiler::is_passthrough_compiler_invocation(args)
&& !compiler::rustc::RustcCompiler::recognizes(args);
if direct_passthrough {
tracing::debug!(
program = ?args.first(),
"compiler has no cache adapter; passing through uncached"
);
std::process::exit(run_compiler_directly(
&config,
args,
config.preserve_incremental,
)?);
}
let Some(adapter) = compiler::detect_compiler(args) else {
anyhow::bail!(
"wrapper-mode dispatched but no compiler adapter matched argv[0] = {:?}",
args.first()
);
};
let exit_code = match wrapper_target(adapter) {
Some(WrapperTarget::Rustc) => {
target_seed::before_probe(&config, args);
let trace = phase_trace::start("rustc", args);
let exit = match out_dir_alias::apply(&config, args) {
Some(exit) => exit,
None => wrapper::run(&config, args)?,
};
drop(trace);
exit
}
Some(WrapperTarget::Rustdoc) => compiler::rustdoc::run(&config, args)?,
Some(WrapperTarget::Cc) => wrapper::run_cc(&config, args)?,
Some(WrapperTarget::Nvcc) => wrapper::run_nvcc(&config, args)?,
None => anyhow::bail!(
"detected compiler adapter {} ({}) has no wrapper dispatch",
adapter.id(),
adapter.display_name()
),
};
std::process::exit(exit_code);
}
fn cache_clean_choice(stale_schema: bool, stale_hours: Option<u64>) -> cli::CacheClean {
match stale_hours {
_ if stale_schema => cli::CacheClean::StaleSchema,
Some(hours) => cli::CacheClean::OlderThan(hours),
None => cli::CacheClean::All,
}
}
fn clean_scope(path: Option<PathBuf>, orphans: bool, stale_hours: Option<u64>) -> cli::CleanScope {
match path {
Some(dir) => cli::CleanScope::Scan(dir),
None if orphans => cli::CleanScope::Tracked(cli::TrackedSelection::Orphaned),
None => cli::CleanScope::Tracked(cli::TrackedSelection::StaleOrOrphaned(
stale_hours.unwrap_or(cli::DEFAULT_TRACKED_STALE_HOURS),
)),
}
}
#[cfg(unix)]
fn install_shims(
dir: Option<PathBuf>,
force: bool,
from_path: bool,
canonical_only: bool,
) -> Result<()> {
let dir = dir
.or_else(compiler::shim::default_shim_dir)
.context("no home directory; pass the shim directory to use")?;
let extra = if canonical_only {
Vec::new()
} else {
let path = std::env::var_os("PATH").unwrap_or_default();
compiler::shim::names_on_path(&path, from_path)
};
cli::install_shims_named(&dir, force, &extra)
}
#[cfg(not(unix))]
const UNIX_ONLY_SHIMS: &str = "shim installation is Unix-only for now: it relies on symlinks, and \
the Windows `.exe` shim story differs (kunobi-ninja/kache#310). Use `CC`/`CXX` there.";
const DEFAULT_REPORT_TOP: usize = 10;
fn stats_wants_full(
full: bool,
format: bool,
output: bool,
top: bool,
record: bool,
redact: bool,
) -> bool {
full || format || output || top || record || redact
}
fn report_format(format: Option<cli::ReportFormat>, json: bool) -> Result<cli::ReportFormat> {
use cli::ReportFormat;
if json {
if !matches!(format, None | Some(ReportFormat::Json | ReportFormat::Text)) {
return Err(machine::invalid_arguments(
"`--json` cannot be combined with a different `--format`.",
));
}
return Ok(ReportFormat::Json);
}
Ok(format.unwrap_or(ReportFormat::Text))
}
fn parse_since_window(value: &str) -> Result<since::SinceWindow> {
let window = parse_duration("--since", value)?;
if window.secs() == 0 {
return Err(machine::invalid_arguments(
"--since must be longer than zero",
));
}
Ok(window)
}
fn parse_age_hours(flag: &str, value: &str) -> Result<u64> {
Ok(parse_duration(flag, value)?.secs().div_ceil(3600))
}
fn parse_duration(flag: &str, value: &str) -> Result<since::SinceWindow> {
since::SinceWindow::parse(value).ok_or_else(|| {
machine::invalid_arguments(format!(
"invalid {flag} {value:?}; expected a duration like 15m, 2h, or 7d (a bare number is hours)"
))
})
}
fn daemon_start_message(outcome: &daemon::Ensured) -> String {
match outcome {
daemon::Ensured::AlreadyRunning(Some(version)) => {
format!("daemon already running (version {version})")
}
daemon::Ensured::AlreadyRunning(None) => "daemon already running".to_string(),
daemon::Ensured::Started => "daemon started".to_string(),
daemon::Ensured::TimedOut => "daemon did not start within timeout".to_string(),
}
}
fn daemon_restart_message(was_running: bool, ready: bool) -> &'static str {
match (was_running, ready) {
(_, false) => "daemon did not start within timeout",
(true, true) => "daemon restarted",
(false, true) => "daemon was not running; daemon started",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
fn write_success_program(path: &std::path::Path) {
let shell = compiler::resolve_program_on_path("sh").expect("sh must be available on PATH");
kache_fs::testutil::write_executable(path, format!("#!{}\nexit 0\n", shell.display()));
}
#[cfg(unix)]
fn run_generated_compiler_process_directly(
args: &[String],
preserve_incremental: bool,
) -> Result<i32> {
match run_compiler_process_directly(args, preserve_incremental) {
Err(error)
if error
.downcast_ref::<std::io::Error>()
.is_some_and(|error| error.raw_os_error() == Some(libc::ETXTBSY)) =>
{
std::thread::sleep(std::time::Duration::from_millis(20));
run_compiler_process_directly(args, preserve_incremental)
}
result => result,
}
}
fn argv(args: &[&str]) -> Vec<String> {
args.iter().map(|arg| arg.to_string()).collect()
}
#[test]
fn a_self_spawned_gc_under_a_shim_path_routes_to_the_cli() {
let gc = Some(std::ffi::OsStr::new("gc"));
let args = argv(&["/x/shims/cc", "gc"]);
assert_eq!(entry_route(&args, gc), EntryRoute::SelfSpawn);
assert_eq!(
detect_log_mode_with_rustc(&args, None),
LogMode::Cli,
"the rest of argv must then parse as the gc subcommand"
);
let daemon = Some(std::ffi::OsStr::new("daemon"));
let args = argv(&["C:/shims/gcc.exe", "daemon", "run"]);
assert_eq!(entry_route(&args, daemon), EntryRoute::SelfSpawn);
assert_eq!(detect_log_mode_with_rustc(&args, None), LogMode::Cli);
}
#[test]
fn a_compile_through_a_shim_stays_a_compile() {
let gc = Some(std::ffi::OsStr::new("gc"));
let compile = argv(&["/x/shims/cc", "-c", "foo.c"]);
assert_eq!(entry_route(&compile, gc), EntryRoute::Shim);
assert_eq!(entry_route(&compile, None), EntryRoute::Shim);
assert_eq!(
entry_route(&argv(&["/x/shims/cc", "gc"]), None),
EntryRoute::Shim
);
}
#[test]
fn kache_under_its_own_name_routes_on_the_rest_of_argv() {
assert_eq!(entry_route(&argv(&["kache", "gc"]), None), EntryRoute::Argv);
assert_eq!(
entry_route(&argv(&["kache", "cc", "-c", "foo.c"]), None),
EntryRoute::Argv
);
assert_eq!(entry_route(&[], None), EntryRoute::Argv);
assert_eq!(
entry_route(
&argv(&["kache", "daemon", "run"]),
Some(std::ffi::OsStr::new("daemon"))
),
EntryRoute::SelfSpawn
);
}
#[test]
fn wrappers_above_reads_the_depth_and_treats_old_values_as_one() {
use std::ffi::OsStr;
assert_eq!(wrappers_above(None), None, "the outermost wrapper");
assert_eq!(wrappers_above(Some(OsStr::new("1"))), Some(1));
assert_eq!(wrappers_above(Some(OsStr::new("5"))), Some(5));
assert_eq!(wrappers_above(Some(OsStr::new(""))), Some(1));
assert_eq!(wrappers_above(Some(OsStr::new("yes"))), Some(1));
}
#[test]
fn a_nested_wrapper_hands_down_one_more_level() {
assert_eq!(next_wrapper_depth(1), 2);
assert_eq!(next_wrapper_depth(7), 8);
assert_eq!(next_wrapper_depth(u32::MAX), u32::MAX);
}
#[test]
fn wrapper_depth_is_refused_at_the_bound() {
assert!(check_wrapper_depth(1, Some("cc")).is_ok());
assert!(check_wrapper_depth(MAX_WRAPPERS_ABOVE - 1, Some("cc")).is_ok());
let err = check_wrapper_depth(MAX_WRAPPERS_ABOVE, Some("/copies/cc"))
.unwrap_err()
.to_string();
assert!(err.contains("/copies/cc"), "{err}");
assert!(err.contains("loop"), "{err}");
assert!(err.contains(".kache-shims"), "{err}");
assert!(check_wrapper_depth(MAX_WRAPPERS_ABOVE + 1, None).is_err());
}
#[test]
fn stats_wants_the_full_report_for_any_full_flag() {
assert!(!stats_wants_full(false, false, false, false, false, false));
assert!(stats_wants_full(true, false, false, false, false, false));
assert!(stats_wants_full(false, true, false, false, false, false));
assert!(stats_wants_full(false, false, true, false, false, false));
assert!(stats_wants_full(false, false, false, true, false, false));
assert!(stats_wants_full(false, false, false, false, true, false));
assert!(stats_wants_full(false, false, false, false, false, true));
}
fn is_invalid_arguments(error: &anyhow::Error) -> bool {
machine::failure_exit(error) == Some(("invalid_arguments", 2))
}
#[test]
fn report_format_prefers_an_explicit_name() {
use cli::ReportFormat;
assert_eq!(report_format(None, false).unwrap(), ReportFormat::Text);
assert_eq!(report_format(None, true).unwrap(), ReportFormat::Json);
assert_eq!(
report_format(Some(ReportFormat::Markdown), false).unwrap(),
ReportFormat::Markdown
);
assert_eq!(
report_format(Some(ReportFormat::Text), true).unwrap(),
ReportFormat::Json
);
assert_eq!(
report_format(Some(ReportFormat::Json), true).unwrap(),
ReportFormat::Json
);
for other in [
ReportFormat::Markdown,
ReportFormat::Github,
ReportFormat::Trace,
] {
let error = report_format(Some(other), true).unwrap_err();
assert!(is_invalid_arguments(&error), "{error:#}");
}
}
#[test]
fn report_formats_keep_every_old_spelling_and_refuse_typos() {
use cli::ReportFormat;
for (spelling, format) in [
("text", ReportFormat::Text),
("json", ReportFormat::Json),
("markdown", ReportFormat::Markdown),
("md", ReportFormat::Markdown),
("github", ReportFormat::Github),
("gh", ReportFormat::Github),
("trace", ReportFormat::Trace),
("perfetto", ReportFormat::Trace),
("chrome-trace", ReportFormat::Trace),
] {
let stats = Cli::try_parse_from(["kache", "stats", "--format", spelling]).unwrap();
assert!(
matches!(stats.command, Some(Commands::Stats { format: Some(f), .. }) if f == format),
"{spelling}"
);
let report = Cli::try_parse_from(["kache", "report", "--format", spelling]).unwrap();
assert!(
matches!(report.command, Some(Commands::Report { format: f, .. }) if f == format),
"{spelling}"
);
assert_eq!(
<ReportFormat as clap::ValueEnum>::from_str(format.name(), false),
Ok(format)
);
}
let report = Cli::try_parse_from(["kache", "report"]).unwrap();
assert!(matches!(
report.command,
Some(Commands::Report {
format: ReportFormat::Text,
..
})
));
for args in [
["kache", "stats", "--format", "jsno"],
["kache", "report", "--format", "html"],
] {
let error = Cli::try_parse_from(args).err().unwrap();
assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
assert!(error.to_string().contains("markdown"), "{error}");
}
}
#[test]
fn list_sort_and_bench_phase_take_only_known_values() {
for (spelling, sort) in [
("name", ListSort::Name),
("size", ListSort::Size),
("hits", ListSort::Hits),
("age", ListSort::Age),
] {
let list = Cli::try_parse_from(["kache", "list", "--sort", spelling]).unwrap();
assert!(matches!(list.command, Some(Commands::List { sort: s, .. }) if s == sort));
assert_eq!(sort.as_str(), spelling);
}
let list = Cli::try_parse_from(["kache", "list"]).unwrap();
assert!(matches!(
list.command,
Some(Commands::List {
sort: ListSort::Name,
..
})
));
let error = Cli::try_parse_from(["kache", "list", "--sort", "sise"])
.err()
.unwrap();
assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
for (spelling, phase) in [
("cold", BenchPhase::Cold),
("warm-same-tree", BenchPhase::WarmSameTree),
("warm", BenchPhase::Warm),
("pull", BenchPhase::Pull),
] {
let write =
Cli::try_parse_from(["kache", "telemetry", "write", "d", "--phase", spelling])
.unwrap();
assert!(matches!(
write.command,
Some(Commands::Telemetry {
command: TelemetryCommands::Write { phase: Some(p), .. }
}) if p == phase
));
assert_eq!(phase.as_str(), spelling);
}
let error = Cli::try_parse_from(["kache", "telemetry", "write", "d", "--phase", "hot"])
.err()
.unwrap();
assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
}
#[test]
fn sync_pull_and_push_conflict() {
let error = Cli::try_parse_from(["kache", "sync", "--pull", "--push"])
.err()
.unwrap();
assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict);
assert!(Cli::try_parse_from(["kache", "sync", "--pull"]).is_ok());
assert!(Cli::try_parse_from(["kache", "sync", "--push"]).is_ok());
}
#[test]
fn parse_since_window_accepts_minutes_and_rejects_garbage() {
assert_eq!(parse_since_window("15m").unwrap().secs(), 900);
assert_eq!(parse_since_window("2h").unwrap().secs(), 7200);
assert_eq!(parse_since_window("24").unwrap().secs(), 86_400);
assert_eq!(parse_since_window("1s").unwrap().secs(), 1);
let err = parse_since_window("soon").unwrap_err();
assert!(is_invalid_arguments(&err), "{err:#}");
let err = err.to_string();
assert!(err.contains("invalid --since \"soon\""), "{err}");
assert!(err.contains("15m"), "{err}");
for empty in ["0", "0h", "0s"] {
let err = parse_since_window(empty).unwrap_err();
assert!(is_invalid_arguments(&err), "{err:#}");
assert!(err.to_string().contains("longer than zero"), "{err}");
}
}
#[test]
fn age_flags_share_the_since_grammar_and_round_up_to_hours() {
assert_eq!(parse_age_hours("--stale", "7d").unwrap(), 168);
assert_eq!(parse_age_hours("--stale", "24h").unwrap(), 24);
assert_eq!(parse_age_hours("--stale", "48").unwrap(), 48);
assert_eq!(parse_age_hours("--stale", "30m").unwrap(), 1);
assert_eq!(parse_age_hours("--stale", "1s").unwrap(), 1);
assert_eq!(parse_age_hours("--stale", "60m").unwrap(), 1);
assert_eq!(parse_age_hours("--stale", "61m").unwrap(), 2);
assert_eq!(parse_age_hours("--max-age", "0h").unwrap(), 0);
assert_eq!(parse_age_hours("--stale", "+24h").unwrap(), 24);
assert_eq!(parse_age_hours("--stale", "+48").unwrap(), 48);
for huge in ["18446744073709551615h", "18446744073709551615d"] {
assert_eq!(
parse_age_hours("--max-age", huge).unwrap(),
u64::MAX.div_ceil(3600),
"{huge}"
);
}
assert_eq!(parse_since_window("+2h").unwrap().secs(), 7200);
assert!(is_invalid_arguments(&parse_since_window("+0").unwrap_err()));
for bad in ["invalid", "+", "1.5h"] {
let err = parse_age_hours("--max-age", bad).unwrap_err();
assert!(is_invalid_arguments(&err), "{err:#}");
assert!(err.to_string().contains("invalid --max-age"), "{err}");
}
}
#[test]
fn json_refusals_name_the_command_or_list_visible_ones() {
let general = json_refusal(&None).unwrap();
assert!(general.contains("is supported on"), "{general}");
for hidden in ["report", "gc", "diff", "targets"] {
assert!(!general.contains(hidden), "{hidden} is hidden: {general}");
}
assert_eq!(
json_refusal(&Some(Commands::Sync {
manifest_path: None,
pull: false,
push: false,
dry_run: false,
all: false,
workspace: false,
allow_partial: false,
})),
Some(general)
);
assert!(
json_refusal(&Some(Commands::Config))
.unwrap()
.contains("kache config")
);
assert!(
json_refusal(&Some(Commands::Login {
device: false,
retrust: false
}))
.unwrap()
.contains("kache login")
);
assert!(
json_refusal(&Some(Commands::Monitor { since: None }))
.unwrap()
.contains("kache stats --json")
);
assert!(
json_refusal(&Some(Commands::Purge { crate_name: None }))
.unwrap()
.contains("kache purge")
);
let shims = Cli::try_parse_from(["kache", "init", "--shims"]).unwrap();
assert!(json_refusal(&shims.command).unwrap().contains("--shims"));
let init = Cli::try_parse_from(["kache", "init", "--check"]).unwrap();
assert_eq!(json_refusal(&init.command), None);
assert_eq!(json_refusal(&Some(Commands::Diff { root: None })), None);
}
#[test]
fn daemon_lifecycle_messages_say_what_happened() {
assert_eq!(
daemon_start_message(&daemon::Ensured::AlreadyRunning(Some("1.2.3".into()))),
"daemon already running (version 1.2.3)"
);
assert_eq!(
daemon_start_message(&daemon::Ensured::AlreadyRunning(None)),
"daemon already running"
);
assert_eq!(
daemon_start_message(&daemon::Ensured::Started),
"daemon started"
);
assert_eq!(
daemon_start_message(&daemon::Ensured::TimedOut),
"daemon did not start within timeout"
);
assert!(daemon::Ensured::Started.is_ready());
assert!(daemon::Ensured::AlreadyRunning(None).is_ready());
assert!(!daemon::Ensured::TimedOut.is_ready());
assert_eq!(daemon_restart_message(true, true), "daemon restarted");
assert_eq!(
daemon_restart_message(false, true),
"daemon was not running; daemon started"
);
for was_running in [true, false] {
assert_eq!(
daemon_restart_message(was_running, false),
"daemon did not start within timeout"
);
}
}
#[test]
fn errors_reaching_main_exit_one_and_a_closed_stdout_exits_zero() {
assert_eq!(report_main_error(&anyhow::anyhow!("boom")), 1);
assert_eq!(
report_main_error(&machine::stdout_error(std::io::Error::from(
std::io::ErrorKind::BrokenPipe
))),
0
);
}
#[test]
fn status_runs_stats_and_daemon_status_stays_the_daemons() {
let status = Cli::try_parse_from(["kache", "status", "--since", "2h"]).unwrap();
assert!(matches!(
status.command,
Some(Commands::Stats { ref since, .. }) if since == "2h"
));
let daemon = Cli::try_parse_from(["kache", "daemon", "status"]).unwrap();
assert!(matches!(
daemon.command,
Some(Commands::Daemon {
command: Some(DaemonCommands::Status)
})
));
}
#[test]
fn targets_share_is_a_subcommand() {
let list = Cli::try_parse_from(["kache", "targets"]).unwrap();
assert!(matches!(
list.command,
Some(Commands::Targets { command: None })
));
let share =
Cli::try_parse_from(["kache", "targets", "share", "--apply", "target"]).unwrap();
assert!(matches!(
share.command,
Some(Commands::Targets {
command: Some(TargetCommands::Share { apply: true, ref paths })
}) if paths == &vec![PathBuf::from("target")]
));
assert!(Cli::try_parse_from(["kache", "targets-dedup"]).is_err());
}
#[test]
fn clean_flags_keep_targets_and_cache_apart() {
let parse = |args: &[&str]| Cli::try_parse_from(["kache", "clean"].iter().chain(args));
assert!(parse(&["--cache"]).is_ok());
assert!(
parse(&["--cache", "--all"]).is_ok(),
"older scripts still parse"
);
assert!(parse(&["--cache", "--stale", "7d"]).is_ok());
assert!(parse(&["--crate", "serde", "--yes"]).is_ok());
assert!(parse(&["some/dir", "--dry-run"]).is_ok());
assert!(parse(&["--tracked"]).is_ok(), "older scripts still parse");
assert!(parse(&["--all"]).is_err(), "--all needs --cache");
assert!(
parse(&["--stale-schema"]).is_err(),
"--stale-schema needs --cache"
);
assert!(parse(&["some/dir", "--cache"]).is_err());
for selection in [&["--stale", "30d"][..], &["--orphans"]] {
let parsed = parse(&[&["some/dir", "--dry-run"][..], selection].concat());
assert_eq!(
parsed.err().map(|error| error.kind()),
Some(clap::error::ErrorKind::ArgumentConflict)
);
}
assert!(parse(&["--orphans", "--stale", "7d"]).is_err());
assert!(parse(&["--crate", "serde", "--cache"]).is_err());
assert!(parse(&["--cache", "--all", "--stale", "7d"]).is_err());
for hidden in ["gc", "purge", "targets", "report"] {
assert!(Cli::try_parse_from(["kache", hidden]).is_ok(), "{hidden}");
}
}
#[test]
fn clean_units_accepts_preview_and_apply_but_rejects_other_scopes() {
for spelling in ["--units", "--artifacts"] {
let parsed =
Cli::try_parse_from(["kache", "clean", spelling, "target", "--dry-run", "--json"])
.unwrap();
assert!(
matches!(parsed.command, Some(Commands::Clean { units: true, dry_run: true, ref path, .. }) if path == &Some(PathBuf::from("target")))
);
assert!(Cli::try_parse_from(["kache", "clean", spelling, "--yes"]).is_ok());
for arguments in [
vec!["--cache"],
vec!["--crate", "serde"],
vec!["--stale", "1d"],
vec!["--orphans"],
vec!["--cache", "--stale-schema"],
] {
let args = ["kache", "clean", spelling].into_iter().chain(arguments);
assert!(Cli::try_parse_from(args).is_err());
}
}
}
#[test]
fn clean_flags_pick_what_to_remove() {
use cli::{CacheClean, CleanScope, TrackedSelection};
assert_eq!(cache_clean_choice(false, None), CacheClean::All);
assert_eq!(
cache_clean_choice(false, Some(24)),
CacheClean::OlderThan(24)
);
assert_eq!(cache_clean_choice(true, None), CacheClean::StaleSchema);
assert_eq!(
clean_scope(None, false, None),
CleanScope::Tracked(TrackedSelection::StaleOrOrphaned(
cli::DEFAULT_TRACKED_STALE_HOURS
))
);
assert_eq!(
clean_scope(None, false, Some(48)),
CleanScope::Tracked(TrackedSelection::StaleOrOrphaned(48))
);
assert_eq!(
clean_scope(None, true, None),
CleanScope::Tracked(TrackedSelection::Orphaned)
);
assert_eq!(
clean_scope(Some(PathBuf::from("d")), true, None),
CleanScope::Scan(PathBuf::from("d"))
);
}
#[test]
fn init_shims_and_why_miss_without_a_crate_parse() {
let shims =
Cli::try_parse_from(["kache", "init", "--shims", "--from-path", "--force"]).unwrap();
assert!(matches!(
shims.command,
Some(Commands::Init {
shims: Some(None),
from_path: true,
force: true,
..
})
));
let package =
Cli::try_parse_from(["kache", "init", "--shims", "/usr/lib/kache", "--force"]).unwrap();
assert!(matches!(
package.command,
Some(Commands::Init { shims: Some(Some(ref dir)), force: true, .. })
if dir == std::path::Path::new("/usr/lib/kache")
));
let plain = Cli::try_parse_from(["kache", "init"]).unwrap();
assert!(matches!(
plain.command,
Some(Commands::Init { shims: None, .. })
));
assert!(
Cli::try_parse_from(["kache", "init", "--from-path"]).is_err(),
"needs --shims"
);
assert!(Cli::try_parse_from(["kache", "init", "--shims", "--check"]).is_err());
let canonical =
Cli::try_parse_from(["kache", "init", "--shims", "--canonical-only"]).unwrap();
assert!(matches!(
canonical.command,
Some(Commands::Init {
canonical_only: true,
from_path: false,
..
})
));
assert!(Cli::try_parse_from(["kache", "init", "--canonical-only"]).is_err());
assert!(
Cli::try_parse_from([
"kache",
"init",
"--shims",
"--canonical-only",
"--from-path"
])
.is_err()
);
let build = Cli::try_parse_from(["kache", "why-miss", "--root", "x"]).unwrap();
assert!(matches!(
build.command,
Some(Commands::Explain {
crate_name: None,
root: Some(_)
})
));
assert!(Cli::try_parse_from(["kache", "why-miss", "serde", "--root", "x"]).is_err());
for hidden in ["install-shims", "diff"] {
assert!(Cli::try_parse_from(["kache", hidden]).is_ok(), "{hidden}");
}
}
#[test]
fn dry_run_spellings_reach_every_preview() {
for spelling in ["--check", "--dry-run", "-n"] {
let parsed = Cli::try_parse_from(["kache", "init", spelling, "--no-service"]).unwrap();
assert!(
matches!(
parsed.command,
Some(Commands::Init {
check: true,
no_service: true,
..
})
),
"{spelling}"
);
}
let parsed = Cli::try_parse_from(["kache", "init", "--no-service"]).unwrap();
assert!(matches!(
parsed.command,
Some(Commands::Init { check: false, .. })
));
for spelling in ["--dry-run", "-n"] {
let parsed = Cli::try_parse_from(["kache", "sync", spelling]).unwrap();
assert!(
matches!(parsed.command, Some(Commands::Sync { dry_run: true, .. })),
"{spelling}"
);
let parsed = Cli::try_parse_from(["kache", "telemetry", "push", spelling]).unwrap();
assert!(
matches!(
parsed.command,
Some(Commands::Telemetry {
command: TelemetryCommands::Push { dry_run: true, .. }
})
),
"{spelling}"
);
}
assert!(Cli::try_parse_from(["kache", "init", "--shims", "-n"]).is_err());
}
#[test]
fn completions_leave_out_hidden_commands_and_flags() {
let mut command = without_hidden(&Cli::command());
let mut script = Vec::new();
clap_complete::generate(
clap_complete::Shell::Bash,
&mut command,
"kache",
&mut script,
);
let script = String::from_utf8(script).unwrap();
for visible in ["clean", "init", "--dry-run", "--json", "--no-service"] {
assert!(script.contains(visible), "{visible}");
}
for hidden in [
"install-shims",
"save-manifest",
"--tracked",
"--stale-schema",
] {
assert!(!script.contains(hidden), "{hidden}");
}
assert!(command.find_subcommand("stats").is_some());
assert!(command.find_subcommand("diff").is_none());
}
#[test]
fn config_without_a_terminal_points_at_the_file_and_environment() {
let hint =
config_without_terminal(std::path::Path::new("/home/me/.config/kache/config.toml"));
assert!(
hint.contains("/home/me/.config/kache/config.toml"),
"{hint}"
);
assert!(hint.contains("KACHE_"), "{hint}");
assert!(hint.contains("needs a terminal"), "{hint}");
assert!(!hint.contains("`kache config` in a terminal"), "{hint}");
}
#[test]
fn doctor_fix_help_names_only_what_fix_does() {
use clap::CommandFactory;
let mut command = Cli::command();
let doctor = command.find_subcommand_mut("doctor").unwrap();
let fix = doctor
.get_arguments()
.find(|arg| arg.get_id() == "fix")
.and_then(|arg| arg.get_help())
.unwrap()
.to_string();
assert!(fix.contains("sccache") && fix.contains("daemon"), "{fix}");
assert!(!fix.contains("repair config"), "{fix}");
}
#[test]
fn help_lists_commands_by_task_and_leaves_out_plumbing() {
use clap::CommandFactory;
let help = Cli::command().render_help().to_string();
let listed: Vec<&str> = help
.lines()
.skip_while(|line| *line != "Commands:")
.skip(1)
.take_while(|line| !line.is_empty())
.filter_map(|line| line.split_whitespace().next())
.collect();
assert_eq!(
listed,
[
"init",
"doctor",
"config",
"stats",
"clean",
"monitor",
"explain",
"list",
"sync",
"login",
"logout",
"daemon",
"completions",
"help",
]
);
}
#[cfg(unix)]
#[test]
fn init_shims_creates_the_links_in_the_named_directory() {
let dir = tempfile::tempdir().unwrap();
let farm = dir.path().join("farm");
install_shims(Some(farm.clone()), false, false, true).unwrap();
for name in ["cc", "c++", "cargo"] {
assert!(
farm.join(name)
.symlink_metadata()
.is_ok_and(|meta| meta.file_type().is_symlink()),
"{name}"
);
}
}
#[test]
fn json_support_is_limited_to_machine_readable_commands() {
assert!(command_supports_json(&Some(Commands::Diff { root: None })));
assert!(command_supports_json(&Some(Commands::Targets {
command: None
})));
assert!(command_supports_json(&Some(Commands::Targets {
command: Some(TargetCommands::Share {
apply: false,
paths: Vec::new(),
}),
})));
assert!(command_supports_json(&Some(Commands::Stats {
since: "24h".to_string(),
last_build: false,
root: None,
full: false,
format: None,
output: None,
top: None,
record: false,
redact: false,
})));
assert!(command_supports_json(&Some(Commands::Daemon {
command: Some(DaemonCommands::Status),
})));
assert!(!command_supports_json(&Some(Commands::Config)));
assert!(!command_supports_json(&Some(Commands::Monitor {
since: None
})));
assert!(!command_supports_json(&Some(Commands::Telemetry {
command: TelemetryCommands::Write {
dir: PathBuf::from("/tmp"),
scenario: None,
phase: None,
},
})));
assert!(!command_supports_json(&None));
assert!(doctor_json_is_read_only(false, false, false, false, false));
for flags in [
(true, false, false, false, false),
(false, true, false, false, false),
(false, false, true, false, false),
(false, false, false, true, false),
(false, false, false, false, true),
] {
assert!(!doctor_json_is_read_only(
flags.0, flags.1, flags.2, flags.3, flags.4
));
}
}
#[test]
fn workspace_wrapper_incremental_preservation_requires_opt_in_and_no_extra_inputs() {
for (requested, extra_inputs_active, expected) in [
(false, false, false),
(false, true, false),
(true, false, true),
(true, true, false),
] {
assert_eq!(
preserve_workspace_wrapper_incremental(requested, extra_inputs_active),
expected
);
}
}
#[cfg(unix)]
#[test]
fn run_compiler_directly_propagates_exit_code() {
assert_eq!(
run_compiler_process_directly(&["true".to_string()], false).unwrap(),
0
);
assert_eq!(
run_compiler_process_directly(&["false".to_string()], false).unwrap(),
1
);
}
#[test]
fn rustc_args_for_direct_preclean_truth_table() {
let rustc = vec!["rustc".to_string()];
assert_eq!(
rustc_args_for_direct_preclean(&rustc),
Some(rustc.as_slice())
);
let cc = vec!["cc".to_string()];
assert_eq!(rustc_args_for_direct_preclean(&cc), None);
let chained = vec![
"/tmp/outer-wrapper".to_string(),
"rustc".to_string(),
"--crate-name".to_string(),
];
assert_eq!(
rustc_args_for_direct_preclean(&chained),
Some(&chained[1..])
);
let unknown = vec![
"/tmp/outer-wrapper".to_string(),
"other-tool".to_string(),
"--crate-name".to_string(),
];
assert_eq!(rustc_args_for_direct_preclean(&unknown), None);
}
#[test]
fn direct_cc_safety_check_applies_only_to_cc_invocations() {
assert!(is_cc_compiler_invocation(&["cc".to_string()]));
assert!(!is_cc_compiler_invocation(&["rustc".to_string()]));
assert!(!is_cc_compiler_invocation(&["other-tool".to_string()]));
assert!(!is_cc_compiler_invocation(&["nvcc".to_string()]));
}
#[test]
fn wrapper_target_routes_every_adapter() {
let adapter_for = |argv: &[&str]| {
compiler::detect_compiler(
&argv
.iter()
.map(|arg| (*arg).to_string())
.collect::<Vec<_>>(),
)
.expect("adapter must match")
};
assert_eq!(
wrapper_target(adapter_for(&["rustc", "--crate-name", "foo"])),
Some(WrapperTarget::Rustc)
);
assert_eq!(
wrapper_target(adapter_for(&["rustdoc", "--crate-name", "demo"])),
Some(WrapperTarget::Rustdoc)
);
assert_eq!(
wrapper_target(adapter_for(&["cc", "-c", "foo.c"])),
Some(WrapperTarget::Cc)
);
assert_eq!(
wrapper_target(adapter_for(&["nvcc", "-c", "kernel.cu"])),
Some(WrapperTarget::Nvcc)
);
let unknown =
compiler::CompilerAdapter::new(compiler::CompilerId::new("unwired"), "unwired", |_| {
false
});
assert_eq!(wrapper_target(&unknown), None);
}
#[cfg(unix)]
#[test]
fn run_compiler_directly_pre_cleans_readonly_restores() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let fake_rustc = dir.path().join("rustc");
write_success_program(&fake_rustc);
let restored = dir.path().join("libfoo.rlib");
std::fs::write(&restored, b"cached").unwrap();
std::fs::set_permissions(&restored, std::fs::Permissions::from_mode(0o444)).unwrap();
assert!(
std::fs::metadata(&restored)
.unwrap()
.permissions()
.readonly()
);
let code = run_generated_compiler_process_directly(
&[
fake_rustc.to_string_lossy().into_owned(),
"--crate-name".to_string(),
"foo".to_string(),
"--out-dir".to_string(),
dir.path().to_string_lossy().into_owned(),
],
false,
)
.unwrap();
assert_eq!(code, 0);
assert!(
!restored.exists(),
"read-only restore should have been unlinked before exec"
);
}
#[cfg(unix)]
#[test]
fn run_compiler_directly_pre_cleans_workspace_wrapper_chain_restores() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let outer_wrapper = dir.path().join("outer-wrapper");
write_success_program(&outer_wrapper);
let fake_rustc = dir.path().join("rustc");
write_success_program(&fake_rustc);
let restored = dir.path().join("libfoo.rlib");
std::fs::write(&restored, b"cached").unwrap();
std::fs::set_permissions(&restored, std::fs::Permissions::from_mode(0o444)).unwrap();
let code = run_generated_compiler_process_directly(
&[
outer_wrapper.to_string_lossy().into_owned(),
fake_rustc.to_string_lossy().into_owned(),
"--crate-name".to_string(),
"foo".to_string(),
"--out-dir".to_string(),
dir.path().to_string_lossy().into_owned(),
],
false,
)
.unwrap();
assert_eq!(code, 0);
assert!(
!restored.exists(),
"workspace-wrapper chains must pre-clean the inner rustc outputs"
);
}
#[cfg(unix)]
#[test]
fn run_compiler_directly_keeps_non_rustc_response_argv_verbatim() {
let dir = tempfile::tempdir().unwrap();
let compiler = dir.path().join("fake-cc");
let dump = dir.path().join("argv.txt");
let response = dir.path().join("cc.args");
kache_fs::testutil::write_executable(
&compiler,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"{}\"\n",
dump.display()
),
);
std::fs::write(&response, "-DNAME='two words'\n").unwrap();
let direct = "direct argument with spaces".to_string();
let response_arg = format!("@{}", response.display());
let code = run_generated_compiler_process_directly(
&[
compiler.to_string_lossy().into_owned(),
direct.clone(),
response_arg.clone(),
],
false,
)
.unwrap();
assert_eq!(code, 0);
assert_eq!(
std::fs::read_to_string(dump).unwrap(),
format!("{direct}\n{response_arg}\n")
);
}
#[cfg(unix)]
#[test]
fn run_compiler_directly_preserves_cc_readonly_output() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let dir = tempfile::tempdir().unwrap();
let fake_cc = dir.path().join("cc");
write_success_program(&fake_cc);
let output = dir.path().join("user-owned.o");
std::fs::write(&output, b"original").unwrap();
std::fs::set_permissions(&output, std::fs::Permissions::from_mode(0o444)).unwrap();
let before = std::fs::metadata(&output).unwrap();
let code = run_generated_compiler_process_directly(
&[
fake_cc.to_string_lossy().into_owned(),
"-c".to_string(),
"foo.c".to_string(),
"-o".to_string(),
output.to_string_lossy().into_owned(),
],
false,
)
.unwrap();
assert_eq!(code, 0);
let after = std::fs::metadata(&output).unwrap();
assert_eq!((after.dev(), after.ino()), (before.dev(), before.ino()));
assert_eq!(std::fs::read(&output).unwrap(), b"original");
}
#[cfg(unix)]
#[test]
fn run_compiler_directly_rewrites_double_wrapper_rustc_response_file() {
let dir = tempfile::tempdir().unwrap();
let outer = dir.path().join("workspace-wrapper");
let inner = dir.path().join("rustc");
let dump = dir.path().join("argv.txt");
let response = dir.path().join("rustc.args");
let incremental = dir.path().join("target/debug/incremental/unit");
kache_fs::testutil::write_executable(
&outer,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$1\" > \"{}\"\nshift\nfor arg in \"$@\"; do\n case \"$arg\" in\n @*) cat \"${{arg#@}}\" >> \"{}\";;\n *) printf '%s\\n' \"$arg\" >> \"{}\";;\n esac\ndone\n",
dump.display(),
dump.display(),
dump.display()
),
);
std::fs::write(
&response,
format!(
"--crate-name\nfixture\n-Cincremental={}\n",
incremental.display()
),
)
.unwrap();
let code = run_generated_compiler_process_directly(
&[
outer.to_string_lossy().into_owned(),
inner.to_string_lossy().into_owned(),
format!("@{}", response.display()),
],
true,
)
.unwrap();
assert_eq!(code, 0);
let argv = std::fs::read_to_string(dump).unwrap();
assert!(
argv.starts_with(&format!("{}\n", inner.display())),
"{argv}"
);
assert!(
argv.contains(&format!(
"-Cincremental={}.kache-preserved",
incremental.display()
)),
"{argv}"
);
assert!(!argv.contains(&format!("-Cincremental={}\n", incremental.display())));
}
#[test]
fn test_runner_args_take_everything_after_the_subcommand() {
let os = |args: &[&str]| -> Vec<std::ffi::OsString> {
args.iter().map(std::ffi::OsString::from).collect()
};
let argv = os(&[
"kache",
"test-runner",
"/t/probe-0123456789abcdef",
"--exact",
]);
assert_eq!(
test_runner_args(&argv),
Some(&argv[2..]),
"the binary and its arguments"
);
let bare = os(&["kache", "test-runner"]);
assert_eq!(test_runner_args(&bare), Some(&bare[2..]));
assert_eq!(
test_runner_args(&os(&["kache", "rustc", "test-runner"])),
None
);
assert_eq!(test_runner_args(&os(&["kache"])), None);
assert_eq!(
test_runner_args(&os(&["/x/shims/cc", "test-runner", "-o", "app"])),
None
);
}
#[test]
fn test_detect_log_mode() {
assert_eq!(detect_log_mode(&["kache".into()]), LogMode::Cli);
assert_eq!(
detect_log_mode(&["kache".into(), "monitor".into()]),
LogMode::TerminalUi
);
assert_eq!(
detect_log_mode(&["kache".into(), "config".into()]),
LogMode::TerminalUi
);
assert_eq!(
detect_log_mode(&["kache".into(), "stats".into()]),
LogMode::Cli
);
assert_eq!(
detect_log_mode(&["kache".into(), "rustc".into(), "--crate-name".into()]),
LogMode::Wrapper
);
assert_eq!(
detect_log_mode(&[
"kache".into(),
"C:/Users/dev/.mozbuild/clang/bin/clang.exe".into(),
"-E".into(),
"conftest.c".into()
]),
LogMode::Wrapper
);
assert_eq!(
detect_log_mode(&[
"kache".into(),
"arm-linux-gnueabihf-gcc".into(),
"--version".into(),
]),
LogMode::Wrapper
);
assert_eq!(
detect_log_mode(&[
"kache".into(),
"/opt/cross/bin/aarch64-linux-gnu-g++-13".into(),
"-c".into(),
"foo.cc".into(),
]),
LogMode::Wrapper
);
assert_eq!(
detect_log_mode(&[
"kache".into(),
r"G:\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\bin\clippy-driver.exe"
.into(),
"rustc".into(),
"-vV".into(),
]),
LogMode::Wrapper
);
assert_eq!(
detect_log_mode(&[
"kache".into(),
r"C:\Program Files\Rust\bin\rustc.exe".into(),
"--crate-name".into(),
]),
LogMode::Wrapper
);
assert_eq!(
detect_log_mode_with_rustc(
&[
"kache".into(),
"/home/user/.kani/kani-0.67.0/bin/kani-compiler".into(),
"-vV".into(),
],
Some(std::ffi::OsStr::new(
"/home/user/.kani/kani-0.67.0/bin/kani-compiler",
)),
),
LogMode::Wrapper
);
assert_eq!(
detect_log_mode_with_rustc(
&[
"kache".into(),
"/usr/local/cuda/bin/nvcc".into(),
"-c".into(),
"kernel.cu".into(),
],
None,
),
LogMode::Wrapper
);
assert_eq!(
detect_log_mode(&[
"kache".into(),
"/Users/dev/.dylint_drivers/nightly/dylint-driver".into(),
"rustc".into(),
"--crate-name".into(),
]),
LogMode::Wrapper
);
assert_eq!(
detect_log_mode(&["kache".into(), "init".into(), "rustc-project".into(),]),
LogMode::Cli
);
}
}