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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
//! Which reinstall route, if any, the running binary's provenance permits.
//!
//! Why: `tm reinstall --binary` must not guess. [`crate::core::binary_provenance`]
//! already answers "where did this binary come from?" for `tm doctor`, but it
//! answers in prose — a [`CheckStatus`](crate::core::doctor::CheckStatus) and a
//! message, which is the right shape for a report and the wrong shape for a
//! decision. Re-deriving the answer at the call site would put a second,
//! drifting copy of the classification next to a command that REPLACES an
//! executable, and the #4033 incident is exactly what happens when provenance
//! is assumed: ten trusty-* binaries installed from worktrees, one from a
//! directory macOS had reaped, one binary served by two conflicting installs.
//!
//! What: [`reinstall_route`] runs the same guards `provenance_report` does and
//! returns a [`ReinstallRoute`] — a registry reinstall, a path reinstall from
//! the recorded source directory, or a refusal carrying the reason. Every
//! ambiguity refuses: no ledger, no ledger record, two installs providing the
//! same binary, a running executable that is not the file cargo installed, a
//! version disagreement, a reaped path source, a git or unrecognised source. A
//! refusal is a correct outcome, not a failure to classify.
//!
//! Test: the `tests` module below covers every branch.
use std::path::{Path, PathBuf};
use crate::core::binary_provenance::{CargoInstall, InstallSource, describe, same_file};
/// How `--binary` may reinstall this executable.
///
/// Why: the two viable routes need different machinery — the registry route
/// goes through `trusty_common::update`, the path route re-runs
/// `cargo install --path` — and everything else must stop.
/// What: `Registry` and `Path` carry what their route needs; `Refuse` carries
/// the operator-facing reason.
/// Test: every `route_*` test below.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReinstallRoute {
/// Installed from crates.io — upgrade through the shared update path.
Registry {
/// The crates.io package name to install.
package: String,
},
/// Installed with `cargo install --path`, and that directory still exists.
Path {
/// The crates.io package name, for the message.
package: String,
/// The source directory to reinstall from.
dir: PathBuf,
},
/// Provenance does not permit an automatic reinstall. Carries why.
Refuse(String),
}
/// Decide the reinstall route for the running binary.
///
/// Why: see the module doc. This is a pure function over injected inputs —
/// including the filesystem probe — so every branch is testable without
/// touching a real `~/.cargo`.
/// What: `bin_name` is the running executable's file name, `running_version`
/// its compiled-in `CARGO_PKG_VERSION`, `running_exe` its resolved path,
/// `cargo_bin_dir` the directory cargo installs into, `ledger` the parsed
/// installs (`None` = unreadable), and `source_exists` the probe for a path
/// install's source directory. Refuses, in order: unreadable ledger; no record
/// providing `bin_name`; more than one such record; a running executable that
/// is not the file cargo placed; a ledger version that disagrees with what is
/// running; a path install whose source is gone; a git or unrecognised source.
/// Test: `route_refuses_without_a_ledger`, `route_refuses_when_not_in_ledger`,
/// `route_refuses_on_duplicate_installs`,
/// `route_refuses_when_running_binary_is_not_the_cargo_install`,
/// `route_refuses_on_version_disagreement`,
/// `route_refuses_when_path_source_is_gone`, `route_refuses_for_a_git_install`,
/// `route_reinstalls_a_live_path_install`,
/// `route_upgrades_a_registry_install`.
pub fn reinstall_route(
bin_name: &str,
running_version: &str,
running_exe: &Path,
cargo_bin_dir: &Path,
ledger: Option<&[CargoInstall]>,
source_exists: &dyn Fn(&Path) -> bool,
) -> ReinstallRoute {
let Some(ledger) = ledger else {
return refuse(format!(
"cargo's install ledger (`$CARGO_HOME/.crates2.json`) is missing or unreadable, so \
how `{bin_name}` was installed cannot be determined. Reinstall it yourself with \
the command you originally used"
));
};
let providers: Vec<&CargoInstall> = ledger
.iter()
.filter(|i| i.bins.iter().any(|b| b == bin_name))
.collect();
if providers.is_empty() {
return refuse(format!(
"`{bin_name}` is not recorded in cargo's install ledger — it was placed by the \
prebuilt installer, a package manager, or built in place, so there is no install \
command to repeat (issue #4033)"
));
}
if providers.len() > 1 {
let sources: Vec<String> = providers
.iter()
.map(|i| format!("{} {} from {}", i.package, i.version, describe(&i.source)))
.collect();
return refuse(format!(
"`{bin_name}` is provided by {} separate installs ({}) — reinstalling one would not \
determine what runs. Remove the shadowing install first (issue #4033)",
providers.len(),
sources.join("; ")
));
}
let record = providers[0];
// #4622 review HIGH-2: the record was matched by BASENAME. Reinstalling
// cargo's copy would not replace a different file that shadows it on PATH.
let installed = cargo_bin_dir.join(bin_name);
if !same_file(running_exe, &installed) {
return refuse(format!(
"the running `{bin_name}` is `{}`, but cargo's ledger describes a DIFFERENT file \
(`{}`). Reinstalling the cargo install would not replace what is running — check \
`which -a {bin_name}` for a shadowing copy (issue #4033, ADR-0021)",
running_exe.display(),
installed.display(),
));
}
if record.version != running_version {
return refuse(format!(
"the running `{bin_name}` reports version {running_version}, but cargo's ledger \
records {} {} — the binary on disk is not the one cargo tracks, so a reinstall \
would not replace what is actually running (issue #4033)",
record.package, record.version,
));
}
match &record.source {
InstallSource::Registry => ReinstallRoute::Registry {
package: record.package.clone(),
},
InstallSource::Path(dir) if source_exists(dir) => ReinstallRoute::Path {
package: record.package.clone(),
dir: dir.clone(),
},
InstallSource::Path(dir) => refuse(format!(
"`{bin_name}` {running_version} was installed with `cargo install --path` from `{}`, \
and that directory NO LONGER EXISTS — there is nothing left to rebuild from. \
Install from crates.io instead: `cargo install {} --locked` (issue #4033)",
dir.display(),
record.package,
)),
InstallSource::Git(rev) => refuse(format!(
"`{bin_name}` {running_version} was installed from git ({rev}). Reinstalling would \
re-resolve that reference, which is a different binary than the pinned commit you \
are running — do it deliberately rather than through `--binary` (issue #4033)"
)),
InstallSource::Other(raw) => refuse(format!(
"`{bin_name}` {running_version} was installed from an unrecognised source (`{raw}`), \
so its reinstall command cannot be classified (issue #4033)"
)),
}
}
/// Wrap a reason as a refusal.
fn refuse(reason: String) -> ReinstallRoute {
ReinstallRoute::Refuse(reason)
}
#[cfg(test)]
#[path = "binary_reinstall_tests.rs"]
mod tests;