use crate::diagnostic_log::debug;
use crate::package_repair::RepairEnvironment;
use crate::package_repair_command::{ToolCommand, ToolRun, find_program, run_tool};
use crate::package_repair_report::{Finding, Outcome, Tool, finding_from, missing_tool};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
const TAG: &str = "packageRepairToolchain";
const LAUNCHERS: &str = ".cache/toolchain-launchers";
const RECORD_EXTENSION: &str = "launch";
const LOCK_FILE: &str = "toolchain.lock.json";
const LOCK_KEY: &str = "lockFile";
const PROGRAM: &str = "mpm-shadow";
const OWN_LINK: &str = ".cache/toolchain-bin/mpm-shadow";
const ROOT_VARIABLE: &str = "MONOCHROMATIC_WORKTREE_ROOT";
pub const MANUAL_COMMAND: &str = "mise run toolchain:install";
pub fn stale_launch_records(destination: &Path) -> Vec<PathBuf> {
let Ok(listing) = std::fs::read_dir(destination.join(LAUNCHERS)) else { return Vec::new() };
let own: PathBuf = destination.join(LOCK_FILE);
let own_real: Option<PathBuf> = std::fs::canonicalize(destination).ok().map(|real: PathBuf| { return real.join(LOCK_FILE); });
let mut stale: Vec<PathBuf> = Vec::new();
for item in listing.flatten() {
let path: PathBuf = item.path();
if path.extension().is_none_or(|extension| { return extension != RECORD_EXTENSION; }) {
continue;
}
let named: Option<PathBuf> = std::fs::read_to_string(&path)
.ok()
.and_then(|text: String| { return serde_json::from_str::<serde_json::Value>(text.as_str()).ok(); })
.and_then(|record: serde_json::Value| { return record.get(LOCK_KEY).and_then(serde_json::Value::as_str).map(PathBuf::from); });
let Some(lock) = named else {
debug(TAG, format!("{} names no lock file and is left alone", path.display()).as_str());
continue;
};
if lock != own && Some(&lock) != own_real.as_ref() {
stale.push(path);
}
}
stale.sort();
return stale;
}
fn mpm_command(destination: &Path, arguments: &[&str]) -> ToolCommand {
return ToolCommand {
arguments: arguments.iter().map(OsString::from).collect(),
directory: PathBuf::from(destination),
set: vec![(OsString::from(ROOT_VARIABLE), destination.as_os_str().to_os_string())],
};
}
fn install_summary(stdout: &[u8]) -> Option<String> {
const PREFIX: &str = "toolchain install: ";
let text: String = String::from_utf8_lossy(stdout).into_owned();
return text.lines().rev().find_map(|line: &str| { return line.strip_prefix(PREFIX).filter(|rest: &&str| { return rest.contains("installed"); }).map(str::to_owned); });
}
pub fn repair_toolchain(destination: &Path, environment: &RepairEnvironment) -> Option<Finding> {
let stale: Vec<PathBuf> = stale_launch_records(destination);
if stale.is_empty() {
debug(TAG, format!("{}: every launcher record names this worktree", destination.display()).as_str());
return None;
}
let own_link: PathBuf = destination.join(OWN_LINK);
let Some(program) = find_program(PROGRAM, environment.path()).or_else(|| { return own_link.is_file().then_some(own_link.clone()); }) else {
return Some(missing_tool(Tool::Toolchain, PROGRAM, MANUAL_COMMAND));
};
let installed: ToolRun = run_tool(program.as_path(), &mpm_command(destination, &["install", "--frozen-lockfile"]), environment.limits());
debug(TAG, format!("{}: install ended {:?} after {} ms", destination.display(), installed.end, installed.millis).as_str());
let detail: String = match install_summary(installed.stdout.as_slice()) {
Some(summary) => format!("mpm-shadow rebuilt the toolchain links ({summary})"),
None => String::from("mpm-shadow rebuilt the toolchain links"),
};
let finding: Finding = finding_from(Tool::Toolchain, &installed, detail.as_str(), MANUAL_COMMAND);
let remaining: usize = stale_launch_records(destination).len();
if matches!(finding.outcome, Outcome::Repaired { .. }) && remaining > 0 {
return Some(Finding { tool: Tool::Toolchain, outcome: Outcome::Failed { reason: format!("{remaining} launcher records still name another lock file"), run_instead: String::from(MANUAL_COMMAND) } });
}
return Some(finding);
}
#[cfg(test)]
#[path = "package_repair_toolchain_tests.rs"]
mod tests;