use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::core::build_env::{BuildEnvError, ResolvedBuildEnv, paste_line, resolve_build_env};
use crate::core::doctor::{CheckStatus, DoctorCheck};
use crate::disk::human_bytes;
pub(super) const CHECK_NAME: &str = "rust_build_env";
const SIZE_BUDGET: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum TargetDirState {
Missing,
Writable {
bytes: u64,
partial: bool,
},
NotWritable(String),
Unknown(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum WrapperState {
Wired(String),
NotWired,
Unknown(String),
}
#[derive(Debug, Clone)]
pub(super) enum BuildEnvFacts {
NotApplicable(String),
Undetermined(String),
Probed {
env: ResolvedBuildEnv,
target_dir: TargetDirState,
sccache_on_path: Option<PathBuf>,
wrapper: WrapperState,
},
}
pub(super) fn build_check(facts: &BuildEnvFacts) -> DoctorCheck {
let (env, target_dir, sccache_on_path, wrapper) = match facts {
BuildEnvFacts::NotApplicable(why) => {
return DoctorCheck::new(
CHECK_NAME,
CheckStatus::Ok,
format!("{why} — shared cargo target directory not applicable (#6868)"),
);
}
BuildEnvFacts::Undetermined(why) => {
return DoctorCheck::new(
CHECK_NAME,
CheckStatus::Unknown,
format!("this machine's Rust build settings are UNDETERMINED: {why} (#6868)"),
);
}
BuildEnvFacts::Probed {
env,
target_dir,
sccache_on_path,
wrapper,
} => (env, target_dir, sccache_on_path, wrapper),
};
let source = if env.target_dir_configured {
"from `build.cargo_target_dir`"
} else {
"derived from the git remote"
};
let (status, dir_detail) = match target_dir {
TargetDirState::Writable { bytes, partial } => (
CheckStatus::Ok,
format!(
"exists and is writable, holding {}{}",
human_bytes(*bytes),
if *partial {
" (at least — the measurement was cut short)"
} else {
""
}
),
),
TargetDirState::Missing => (
CheckStatus::Warn,
"DOES NOT EXIST yet — every worktree still builds cold; `tm doctor --fix --yes` \
creates it"
.to_string(),
),
TargetDirState::NotWritable(why) => (
CheckStatus::Fail,
format!("exists and is NOT writable: {why}"),
),
TargetDirState::Unknown(why) => (
CheckStatus::Unknown,
format!("could not be measured: {why}"),
),
};
let (sccache_status, sccache_detail) = sccache_detail(env.sccache, sccache_on_path, wrapper);
let status = status.worst(sccache_status);
DoctorCheck::new(
CHECK_NAME,
status,
format!(
"cargo target dir {} ({source}) {dir_detail}; build_jobs {}; {sccache_detail}. \
Paste into an engineer brief: {}",
env.cargo_target_dir.display(),
env.build_jobs,
paste_line(env)
),
)
}
fn sccache_detail(
requested: bool,
on_path: &Option<PathBuf>,
wrapper: &WrapperState,
) -> (CheckStatus, String) {
let path_clause = match on_path {
Some(p) => format!("sccache on PATH at {}", p.display()),
None => "sccache NOT on PATH".to_string(),
};
let wired_clause = match wrapper {
WrapperState::Wired(cmd) => format!("`build.rustc-wrapper` = `{cmd}`"),
WrapperState::NotWired => "`build.rustc-wrapper` NOT wired".to_string(),
WrapperState::Unknown(why) => format!("`build.rustc-wrapper` UNREADABLE: {why}"),
};
let policy = "`~/.cargo/config.toml` is read, never written — it is machine-global for \
every Rust project on this host";
let status = match (requested, wrapper) {
(false, _) | (true, WrapperState::Wired(_)) => CheckStatus::Ok,
(true, WrapperState::Unknown(_)) => CheckStatus::Unknown,
(true, WrapperState::NotWired) => CheckStatus::Warn,
};
let asked = if requested {
"config asks for sccache"
} else {
"config does not ask for sccache"
};
let remedy = if status == CheckStatus::Warn {
" — set `build.rustc-wrapper = \"sccache\"` there yourself, or set `build.sccache: \
false`"
} else {
""
};
(
status,
format!("{asked}, {path_clause}, {wired_clause}{remedy} ({policy})"),
)
}
pub(super) fn check_rust_build_env(project_dir: Option<&Path>, home: &Path) -> DoctorCheck {
build_check(&gather(project_dir, home))
}
pub(super) fn gather(project_dir: Option<&Path>, home: &Path) -> BuildEnvFacts {
let Some(project_dir) = project_dir else {
return BuildEnvFacts::NotApplicable("no project directory supplied".to_string());
};
match crate::core::build_env::project_is_rust(project_dir) {
Some(true) => {}
Some(false) => {
return BuildEnvFacts::NotApplicable(
"this project's detected stack does not include Rust".to_string(),
);
}
None => {
return BuildEnvFacts::Undetermined(
"stack detection was cut short before it could say whether this project is Rust, \
so the build settings were never resolved"
.to_string(),
);
}
}
let config = trusty_common::crate_config::load_at::<
crate::core::trusty_tools_config::TrustyToolsConfig,
>(&trusty_common::crate_config::crate_config_path_at(
home,
crate::core::trusty_tools_config::CRATE_NAME,
))
.ok()
.flatten();
let identity = trusty_common::github_path::derive_github_path(project_dir);
let env = match resolve_build_env(
config.as_ref().and_then(|c| c.build.as_ref()),
home,
identity.as_ref(),
crate::core::build_env::host_cores(),
) {
Ok(env) => env,
Err(e @ BuildEnvError::NoRepoIdentity) => {
return BuildEnvFacts::Undetermined(format!(
"{e}. Set `build.cargo_target_dir` in \
`~/.trusty-tools/trusty-mpm/config.yaml`"
));
}
};
let target_dir = probe_target_dir(&env.cargo_target_dir);
BuildEnvFacts::Probed {
target_dir,
sccache_on_path: trusty_common::bin_resolve::resolve_binary("sccache"),
wrapper: read_rustc_wrapper(&home.join(".cargo").join("config.toml")),
env,
}
}
fn probe_target_dir(dir: &Path) -> TargetDirState {
match std::fs::symlink_metadata(dir) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return TargetDirState::Missing,
Err(e) => return TargetDirState::Unknown(e.to_string()),
Ok(_) if !dir.is_dir() => {
return TargetDirState::NotWritable("the path is not a directory".to_string());
}
Ok(_) => {}
}
let probe = dir.join(".tm-doctor-write-probe");
if let Err(e) = std::fs::write(&probe, b"") {
return TargetDirState::NotWritable(e.to_string());
}
let _ = std::fs::remove_file(&probe);
let mut index = crate::disk::size_index::DirSizeIndex::new();
match index.measure_within(dir, Some(SIZE_BUDGET)) {
Ok(size) => TargetDirState::Writable {
bytes: size.bytes,
partial: size.truncated || !size.unreadable.is_empty(),
},
Err(e) => TargetDirState::Unknown(e.to_string()),
}
}
fn read_rustc_wrapper(cargo_config: &Path) -> WrapperState {
let raw = match std::fs::read_to_string(cargo_config) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return WrapperState::NotWired,
Err(e) => return WrapperState::Unknown(e.to_string()),
};
let doc = match raw.parse::<toml::Value>() {
Ok(doc) => doc,
Err(e) => return WrapperState::Unknown(e.to_string()),
};
match doc
.get("build")
.and_then(|b| b.get("rustc-wrapper"))
.and_then(toml::Value::as_str)
{
Some(cmd) => WrapperState::Wired(cmd.to_string()),
None => WrapperState::NotWired,
}
}
#[cfg(test)]
#[path = "doctor_rust_build_env_tests.rs"]
mod tests;