use super::TextJsonOutputFormat;
use super::cli::CacheScope;
use crate::cache::CacheStatus;
use crate::error::{RailError, RailResult};
use std::path::Path;
pub(crate) fn run_normalize(
remote_url: &str,
mode: Option<&str>,
environment: Vec<String>,
format: TextJsonOutputFormat,
) -> RailResult<()> {
let selection = crate::remote_cache::RemoteCacheSelection::parse(remote_url, mode, &environment)
.map_err(|error| RailError::message(format!("remote cache URL is invalid: {error}")))?;
let status = crate::remote_cache::RemoteCacheConfigurationStatus::from_selection(&selection);
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
"normalize",
"success",
0,
serde_json::json!({
"normalized_url": selection.normalized_url(),
"remote": status,
}),
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
println!("Normalized remote cache URL: {}", selection.normalized_url());
if crate::output::is_verbose() {
println!(
"Provider: {}; protocol: {}; mode: {}",
status.provider, status.protocol, status.mode
);
println!("Shared compiler environments: {}", status.shared_environment_names);
}
}
Ok(())
}
pub(crate) fn run_probe(current_dir: &Path, format: TextJsonOutputFormat) -> RailResult<()> {
match crate::remote_cache::probe(current_dir) {
Ok(probe) => {
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
"probe",
"ready",
0,
serde_json::json!({
"ready": true,
"remote": probe.remote,
"protocol_marker": probe.protocol_marker,
}),
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
println!("Remote cache ready.");
if crate::output::is_verbose() {
println!(
"Provider: {}; protocol: {}; mode: {}",
probe.remote.provider, probe.remote.protocol, probe.remote.mode
);
println!("Protocol marker: {}", probe.protocol_marker.as_str());
}
}
Ok(())
}
Err(error) => {
let failure = error.probe_failure();
let cause = error.probe_failure_cause();
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
"probe",
"probe_failed",
2,
serde_json::json!({
"ready": false,
"failure": {
"kind": failure,
"cause": cause,
"message": error.to_string(),
"retry": cause.map(crate::remote_cache::RemoteProbeFailureCause::retry_guidance),
},
}),
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
if let Some(cause) = cause {
println!("Remote cache probe failed ({failure}/{}): {error}", cause.as_str());
println!("Next: {}", cause.retry_guidance());
} else {
println!("Remote cache probe failed ({failure}): {error}");
}
}
Err(RailError::ExitWithCode { code: 2 })
}
}
}
pub(crate) fn run_setup(
current_dir: &Path,
request: crate::cache::installation::SetupRequest,
check: bool,
format: TextJsonOutputFormat,
) -> RailResult<()> {
if let Some(reason) = crate::compiler::native_cache::unsupported_native_cache_host_reason() {
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
if check { "setup_check" } else { "setup" },
"unsupported",
2,
serde_json::json!({
"capability": "transparent_compiler_cache",
"supported": false,
"reason": reason,
"host_os": std::env::consts::OS,
"host_arch": std::env::consts::ARCH,
"fallback": "cargo",
}),
);
println!("{}", serde_json::to_string_pretty(&output)?);
return Err(RailError::ExitWithCode { code: 2 });
}
return Err(crate::cache::installation::unsupported_native_cache_host_error(reason));
}
if check && request == crate::cache::installation::SetupRequest::default() {
let status = crate::cache::installation::status(current_dir)?;
if status.healthy && status.state == "installed" {
let details = serde_json::json!({
"changed": false,
"config_path": status.config_path,
"config_field": "build.rustc-wrapper",
"config_action": "unchanged",
"wrapper_path": status.wrapper_path,
"receipt_path": null,
"private_state_action": "verify",
"cache_base": status.cache_base,
});
return render_installation_operation("setup_check", false, &details, format);
}
}
let plan = crate::cache::installation::plan_setup(current_dir, &request)
.map_err(|error| cache_setup_source_error(current_dir, error))?;
let pending = plan.pending();
let receipt_path = plan.receipt_path()?;
let remote = plan
.remote_selection()?
.as_ref()
.map(crate::remote_cache::RemoteCacheConfigurationStatus::from_selection);
let details = serde_json::json!({
"changed": pending,
"config_path": plan.config_path(),
"config_field": "build.rustc-wrapper",
"config_action": plan.config_action(),
"wrapper_path": plan.wrapper_path(),
"receipt_path": receipt_path,
"private_state_action": if pending { "install_or_repair" } else { "verify" },
"profile_id": plan.profile_id(),
"cache_base": plan.cache_base(),
"max_bytes": plan.max_bytes(),
"remote": remote,
"root_portability": plan.root_portability(),
"distributed": plan.distributed_mode(),
"distributed_policy": plan.distributed_policy(),
});
if check {
render_installation_operation("setup_check", pending, &details, format)?;
return if pending {
Err(RailError::CheckHasPendingChanges)
} else {
Ok(())
};
}
crate::cache::installation::apply_setup(plan)?;
render_installation_operation("setup", false, &details, format)
}
fn cache_setup_source_error(current_dir: &Path, error: RailError) -> RailError {
if !error
.to_string()
.contains("compiler cache worker executable is unavailable")
{
return error;
}
let source_checkout = std::env::current_exe()
.ok()
.and_then(|path| path.canonicalize().ok())
.zip(current_dir.canonicalize().ok())
.is_some_and(|(executable, root)| executable.starts_with(root.join("target")));
if source_checkout {
error.context("run `just build`, then rerun `cargo rail cache setup --check`")
} else {
error.context("reinstall the complete cargo-rail component set, then rerun `cargo rail cache setup --check`")
}
}
pub(crate) fn run_uninstall(current_dir: &Path, check: bool, format: TextJsonOutputFormat) -> RailResult<()> {
let plan = crate::cache::installation::plan_removal(current_dir)?;
let pending = plan.pending();
let details = serde_json::json!({
"changed": pending,
"config_path": plan.config_path(),
"config_field": "build.rustc-wrapper",
"config_action": plan.config_action(),
"wrapper_path": plan.wrapper_path(),
"receipt_path": plan.receipt_path(),
"private_state_action": if pending { "remove_receipt_owned_installation" } else { "none" },
"cache_preserved": true,
});
if check {
render_installation_operation("uninstall_check", pending, &details, format)?;
return if pending {
Err(RailError::CheckHasPendingChanges)
} else {
Ok(())
};
}
crate::cache::installation::apply_removal(plan)?;
render_installation_operation("uninstall", false, &details, format)
}
fn render_installation_operation(
operation: &str,
pending: bool,
details: &serde_json::Value,
format: TextJsonOutputFormat,
) -> RailResult<()> {
let changed = details["changed"].as_bool().unwrap_or(false);
if format.is_json() {
let mut payload = details.clone();
payload["pending"] = serde_json::Value::Bool(pending);
let output = crate::output::machine_json_envelope(
"cache",
operation,
if pending { "pending_changes" } else { "success" },
if pending { 1 } else { 0 },
payload,
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
match (operation, pending, changed) {
("setup_check", true, _) => {
println!("Cache setup pending.");
println!("Next: cargo rail cache setup");
}
("setup_check", false, _) | ("setup", false, false) => println!("Cache already configured."),
("setup", false, true) => println!("Cache repaired."),
("uninstall_check", true, _) => {
println!("Global cache-wrapper uninstall pending.");
println!("Next: cargo rail cache uninstall");
}
("uninstall_check", false, _) | ("uninstall", false, false) => {
println!("Global cache wrapper already uninstalled.")
}
("uninstall", false, true) => println!("Global cache wrapper uninstalled; profiles preserved."),
_ => println!("Cache operation complete."),
}
if crate::output::is_verbose() {
println!("Cargo config: {}", details["config_path"].as_str().unwrap_or("unknown"));
if let Some(wrapper) = details["wrapper_path"].as_str() {
println!("Wrapper: {wrapper}");
}
if let Some(receipt) = details["receipt_path"].as_str() {
println!("Receipt: {receipt}");
}
}
}
Ok(())
}
pub(crate) fn run_status(workspace_root: &Path, scope: CacheScope, format: TextJsonOutputFormat) -> RailResult<()> {
let status = crate::cache::status(workspace_root, scope.includes_workspace(), scope.includes_local())?;
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
"status",
"success",
0,
serde_json::json!({ "scope": scope.as_str(), "status": status }),
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
render_status(&status);
}
Ok(())
}
pub(crate) fn run_report(start: Option<&Path>, finish: Option<&Path>, format: TextJsonOutputFormat) -> RailResult<()> {
let (mode, measurements) = match (start, finish) {
(Some(path), None) => {
crate::cache::report::start(path)?;
("start", None)
}
(None, Some(path)) => ("finish", Some(crate::cache::report::finish(path)?)),
_ => {
return Err(RailError::message(
"choose exactly one cache report operation: --start or --finish",
));
}
};
if format.is_json() {
println!(
"{}",
serde_json::to_string(&crate::output::machine_json_envelope(
"cache",
"report",
"success",
0,
serde_json::json!({"operation": mode, "measurements": measurements})
))?
);
} else if let Some(counts) = measurements {
println!(
"Cache: {} reused, {} misses, {} bypasses, {} failures",
counts.hits, counts.misses, counts.bypasses, counts.failures
);
if counts.incomplete {
println!("Measurements are incomplete.");
}
} else {
println!("Cache recording started. Set CARGO_RAIL_CACHE_REPORT to the recording path for subsequent commands.");
}
Ok(())
}
pub(crate) fn run_profiles(workspace_root: &Path, format: TextJsonOutputFormat) -> RailResult<()> {
let cargo_home = crate::cache::installation::selected_cargo_home(workspace_root)?;
let profiles = crate::cache::profile::list(&cargo_home)?;
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
"profiles",
"success",
0,
serde_json::json!({
"profiles": profiles,
}),
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
println!("Installed cache profiles: {}", profiles.len());
for profile in profiles {
println!(
" {}: {} root(s), {}, {}",
profile.profile_id,
profile.roots.len(),
profile.state,
profile.root_portability
);
}
}
Ok(())
}
pub(crate) fn run_detach(workspace_root: &Path, check: bool, format: TextJsonOutputFormat) -> RailResult<()> {
let cargo_home = crate::cache::installation::selected_cargo_home(workspace_root)?;
let plan = crate::cache::profile::plan_detach(&cargo_home, workspace_root)?;
let pending = plan.pending();
let profile_id = plan.profile_id().to_string();
if !check {
crate::cache::installation::stop_current_profile_coordinator(workspace_root)?;
crate::cache::profile::apply_detach(&plan)?;
}
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
if check { "detach_check" } else { "detach" },
if check && pending { "pending_changes" } else { "success" },
if check && pending { 1 } else { 0 },
serde_json::json!({
"pending": pending,
"profile_id": profile_id,
"cache_preserved": true,
}),
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else if pending {
println!(
"Workspace {} profile {}.",
if check { "would detach from" } else { "detached from" },
profile_id
);
} else {
println!("Workspace has no installed cache profile.");
}
if check && pending {
Err(RailError::CheckHasPendingChanges)
} else {
Ok(())
}
}
pub(crate) fn run_drop_profile(
workspace_root: &Path,
profile_id: &str,
check: bool,
format: TextJsonOutputFormat,
) -> RailResult<()> {
let cargo_home = crate::cache::installation::selected_cargo_home(workspace_root)?;
let plan = crate::cache::profile::plan_removal(&cargo_home, profile_id)?;
let pending = plan.pending();
let details = serde_json::json!({
"pending": pending,
"profile_id": plan.profile_id(),
"cache_root": plan.cache_root(),
"state_root": plan.state_root(),
"bytes": plan.bytes(),
});
if !check {
crate::cache::profile::apply_removal(&plan)?;
}
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
if check { "drop_profile_check" } else { "drop_profile" },
if check && pending { "pending_changes" } else { "success" },
if check && pending { 1 } else { 0 },
details,
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else if pending {
println!(
"Profile {} {} ({} reclaimed).",
plan.profile_id(),
if check { "would be removed" } else { "removed" },
human_bytes(plan.bytes())
);
} else {
println!("Profile {} is not installed.", plan.profile_id());
}
if check && pending {
Err(RailError::CheckHasPendingChanges)
} else {
Ok(())
}
}
pub(crate) fn run_recover(workspace_root: &Path, check: bool, format: TextJsonOutputFormat) -> RailResult<()> {
let plan = if check {
crate::cache::installation::plan_local_cache_recovery(workspace_root)?
} else {
crate::cache::installation::recover_local_cache(workspace_root)?
};
let pending = plan.is_some();
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
if check { "recover_check" } else { "recover" },
if check && pending { "pending_changes" } else { "success" },
if check && pending { 1 } else { 0 },
serde_json::json!({
"pending": pending,
"recovery": plan,
}),
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else if let Some(plan) = &plan {
if check {
println!("local CAS recovery pending");
} else {
println!("local CAS recovered");
}
println!(" selected root: {}", plan.selected_root);
println!(" quarantine: {}", plan.quarantine_root);
println!(" retained: {}", human_bytes(plan.bytes));
println!(" receipt: {}", plan.receipt_path);
} else {
println!("local CAS recovery not required");
}
if check && pending {
Err(RailError::CheckHasPendingChanges)
} else {
Ok(())
}
}
pub(crate) fn run_clean(
workspace_root: &Path,
scope: CacheScope,
check: bool,
format: TextJsonOutputFormat,
) -> RailResult<()> {
if check {
let status = crate::cache::status(workspace_root, scope.includes_workspace(), scope.includes_local())?;
let pending = has_state(&status);
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
"clean_check",
if pending { "pending_changes" } else { "success" },
if pending { 1 } else { 0 },
serde_json::json!({
"scope": scope.as_str(),
"would_reclaim_bytes": total_bytes(&status),
"status": status,
}),
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
render_status(&status);
if pending {
println!("\ncache state would be reclaimed; run without --check to apply");
} else {
println!("\nnothing to reclaim");
}
}
return if pending {
Err(RailError::CheckHasPendingChanges)
} else {
Ok(())
};
}
if scope.includes_workspace() && scope.includes_local() {
crate::cache::status(workspace_root, true, false)?;
}
let mut removal = crate::cache::CacheRemoval::default();
if scope.includes_local() {
removal.extend(crate::cache::remove_local(workspace_root)?)?;
}
if scope.includes_workspace() {
removal.extend(crate::cache::remove_workspace(workspace_root)?)?;
}
if format.is_json() {
let output = crate::output::machine_json_envelope(
"cache",
"clean",
"success",
0,
serde_json::json!({
"scope": scope.as_str(),
"reclaimed_bytes": removal.bytes,
"removed": removal.paths,
}),
);
println!("{}", serde_json::to_string_pretty(&output)?);
} else if removal.paths.is_empty() {
println!("nothing to reclaim");
} else {
println!(
"Reclaimed {} from {} cache path(s).",
human_bytes(removal.bytes),
removal.paths.len()
);
if crate::output::is_verbose() {
for path in &removal.paths {
println!(" {path}");
}
}
}
Ok(())
}
fn has_state(status: &CacheStatus) -> bool {
status.workspace.as_ref().is_some_and(|workspace| {
workspace
.artifacts
.iter()
.any(|artifact| artifact.kind != "workspace_cache_lock")
}) || status.local.as_ref().is_some_and(|local| local.present)
}
fn total_bytes(status: &CacheStatus) -> u64 {
status
.workspace
.as_ref()
.map_or(0, |workspace| workspace.bytes)
.saturating_add(
status
.local
.as_ref()
.and_then(|local| local.cache.as_ref())
.map_or(0, |local| local.bytes),
)
}
fn render_status(status: &CacheStatus) {
println!(
"Installation: {} ({})",
if status.installation.healthy {
"healthy"
} else {
"unhealthy"
},
status.installation.state
);
println!(
"Reuse: {} hits, {} misses, {} bypasses, {} failures",
status.installation.usage.hits,
status.installation.usage.misses,
status.installation.usage.bypasses,
status.installation.usage.failures
);
if status.installation.usage.failure_reason_counts_available {
for (reason, count) in &status.installation.usage.failure_reasons {
println!("Cache failure {reason}: {count}");
}
} else {
println!("Cache failure reasons: unavailable");
}
for issue in &status.installation.issues {
println!("Warning: {issue}");
}
if let Some(workspace) = &status.workspace {
println!(
"Workspace: {} in {} file(s)",
human_bytes(workspace.bytes),
workspace.files
);
}
if let Some(local) = &status.local {
if let Some(cache) = &local.cache {
println!(
"Local cache: {} / {} ({} results, {} objects)",
human_bytes(cache.bytes),
human_bytes(cache.max_bytes),
cache.results,
cache.objects
);
} else {
println!("Local cache: absent");
}
}
if crate::output::is_verbose() {
render_verbose_status(status);
}
}
fn render_verbose_status(status: &CacheStatus) {
println!("Cargo home: {}", status.installation.cargo_home);
println!("Cargo config: {}", status.installation.config_path);
if let Some(wrapper) = &status.installation.wrapper_path {
println!("Wrapper: {wrapper}");
}
println!(
"Usage ledger: {} event(s); full={}",
status.installation.usage.recorded_events, status.installation.usage.ledger_full
);
println!(
"Early bypass ledger: {} event(s); full={}; incomplete_tail={}",
status.installation.usage.early_bypasses,
status.installation.usage.early_bypass_ledger_full,
status.installation.usage.early_bypass_incomplete_tail
);
for (reason, count) in &status.installation.usage.early_bypass_reasons {
println!(" bypass {reason}: {count}");
}
println!(
"Failure reason counters: available={}",
status.installation.usage.failure_reason_counts_available
);
if let Some(workspace) = &status.workspace {
println!("Workspace root: {}", workspace.root);
for artifact in &workspace.artifacts {
println!(
" {}: {} at {}",
artifact.kind,
human_bytes(artifact.bytes),
artifact.path
);
}
}
if let Some(local) = &status.local
&& let Some(cache) = &local.cache
{
println!("Local root: {}", cache.root);
println!("Trust domain: {}", cache.trust_domain);
println!(
"Native actions: {} ({} unique, {} conflicted, {} quarantined)",
cache.native_actions, cache.native_unique, cache.native_conflicted, cache.native_quarantined
);
println!(
"Native restore synchronization: {} persistent shard(s); staging residue: {} entry(s)",
cache.native_restore_lock_files, cache.staging_entries
);
println!("Leases: {} active, {} stale", cache.active_leases, cache.stale_leases);
println!("Reclaimable: {}", human_bytes(cache.reclaimable_bytes));
}
if let Some(remote) = &status.remote {
println!(
"Remote: {} via {} ({}; {})",
remote.activation, remote.provider, remote.protocol, remote.mode
);
}
}
pub(crate) fn human_bytes(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB"];
let mut value = bytes as f64;
let mut unit = 0;
while value >= 1024.0 && unit + 1 < UNITS.len() {
value /= 1024.0;
unit += 1;
}
if unit == 0 {
format!("{bytes} B")
} else {
format!("{value:.1} {}", UNITS[unit])
}
}