use crate::diagnostic_log::debug;
use crate::package_repair_request::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> = match std::fs::canonicalize(destination) {
Ok(real) => Some(real.join(LOCK_FILE)),
Err(_) => None,
};
let mut stale: Vec<PathBuf> = Vec::new();
for item in listing.flatten() {
let path: PathBuf = item.path();
let is_record: bool = match path.extension() {
Some(extension) => extension == RECORD_EXTENSION,
None => false,
};
if !is_record {
continue;
}
let named: Option<PathBuf> = lock_named_by_record(&path);
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 lock_named_by_record(path: &Path) -> Option<PathBuf> {
let text: String = std::fs::read_to_string(path).ok()?;
let record: serde_json::Value = serde_json::from_str::<serde_json::Value>(text.as_str()).ok()?;
let lock: &str = record.get(LOCK_KEY).and_then(serde_json::Value::as_str)?;
return Some(PathBuf::from(lock));
}
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();
for line in text.lines().rev() {
if let Some(rest) = line.strip_prefix(PREFIX)
&& rest.contains("installed")
{
return Some(rest.to_owned());
}
}
return None;
}
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 found_program: Option<PathBuf> = match find_program(PROGRAM, environment.path()) {
Some(program) => Some(program),
None => {
if own_link.is_file() { Some(own_link.clone()) } else { None }
}
};
let Some(program) = found_program 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;