1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! What:
//! The toolchain repair: when the launchers copied into a new worktree's `.cache/toolchain-launchers` still name another
//! checkout's `toolchain.lock.json`, rebuild the worktree's own links and launchers with `mpm-shadow install
//! --frozen-lockfile`.
//! Why:
//! mpm-shadow owns `toolchain.plan.jsonc`, `toolchain.lock.json` and the pinned tools. Its exposed links and launcher records
//! hold absolute paths, so a copy would run the source checkout's tool versions and guard, and a branch that changes the
//! lock would silently use the source's. The install is lock-constrained: it relinks what is installed and, when this
//! worktree's lock names tools that are not installed, downloads them. The owner chose that on 2026-11-03 ("Install online
//! during the add"), knowing an add can then download and can retire tool copies another checkout still links. The summary
//! line mpm-shadow prints says what was installed. A failed install never fails the add: it prints the reason and
//! `mise run toolchain:install`.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const finding = await repairToolchain(destination, environment); // undefined when the launchers already name this worktree
//! ```
use super::diagnostic_log::debug;
use super::package_repair::RepairEnvironment;
use super::package_repair_command::{ToolCommand, ToolRun, find_program, run_tool};
use super::package_repair_report::{Finding, Outcome, Tool, finding_from, missing_tool};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
/// Tag used for diagnostics from this module.
const TAG: &str = "packageRepairToolchain";
/// Where mpm-shadow keeps the launchers and their records, relative to a worktree.
const LAUNCHERS: &str = ".cache/toolchain-launchers";
/// The record that sits beside a launcher, named after it.
const RECORD_EXTENSION: &str = "launch";
/// The lock file a launcher record names, relative to a worktree.
const LOCK_FILE: &str = "toolchain.lock.json";
/// The key of a launcher record that names the lock file.
const LOCK_KEY: &str = "lockFile";
/// The program that owns the toolchain, and where a worktree keeps a link to it.
const PROGRAM: &str = "mpm-shadow";
/// Where the worktree's own link to the program sits when the search path does not provide one.
const OWN_LINK: &str = ".cache/toolchain-bin/mpm-shadow";
/// The variable that names the worktree whose toolchain a command acts on.
const ROOT_VARIABLE: &str = "MONOCHROMATIC_WORKTREE_ROOT";
/// The command a person runs to finish the repair: the repository's own task, which runs the same installer.
pub const MANUAL_COMMAND: &str = "mise run toolchain:install";
/// What:
/// The launcher records of a worktree that name a lock file other than the worktree's own.
/// Why:
/// They are what a copy leaves behind; a record naming this worktree's lock is already right and needs no repair.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function staleLaunchRecords(destination: string): string[];
/// ```
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;
}
/// What:
/// The command that starts mpm-shadow with the worktree as the toolchain root.
/// Why:
/// An inherited `MONOCHROMATIC_WORKTREE_ROOT` names the checkout the person works in, which is not the new worktree.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function mpmCommand(destination: string, verb: string[]): ToolCommand;
/// ```
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())],
};
}
/// What:
/// The part of mpm-shadow's last summary line that says what was installed, such as `3 installed, 62 unchanged, 0 failed`.
/// Why:
/// The person is told what an add installed, in mpm-shadow's own words.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function installSummary(stdout: string): string | undefined;
/// ```
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); });
}
/// What:
/// Rebuild the worktree's toolchain links and launchers, when its launcher records are stale.
/// Why:
/// See the module comment: one frozen install, with the network when the lock names tools that are not installed yet.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function repairToolchain(destination: string, environment: RepairEnvironment): Promise<Finding | undefined>;
/// ```
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.tool_limit);
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);
}
/// Toolchain repair controls stay out of the release executable.
#[cfg(test)]
#[path = "package_repair_toolchain_tests.rs"]
mod tests;