-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathxlings_binary.cppm
More file actions
276 lines (253 loc) · 12.3 KB
/
Copy pathxlings_binary.cppm
File metadata and controls
276 lines (253 loc) · 12.3 KB
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// mcpp.fallback.xlings_binary — xlings binary acquisition chain.
//
// Tries multiple strategies to obtain the xlings binary:
// 1. MCPP_VENDORED_XLINGS env var (explicit override)
// 2. the xlings released with this mcpp, `<prefix>/registry/bin/xlings`
// 3. system `which xlings`
// 4. Fail with user-facing instructions
module;
#include <cstdio>
#include <cstdlib>
#include <cctype>
export module mcpp.fallback.xlings_binary;
import std;
import mcpp.platform;
export namespace mcpp::fallback {
// Try to acquire (copy) the xlings binary to destBin.
// Returns destBin on success or an error string.
// The version already vendored at destBin, or empty when it cannot be read.
std::string vendored_xlings_version(const std::filesystem::path& bin);
// The xlings that was released together with the running mcpp, when this mcpp
// runs from its release layout `<prefix>/bin/mcpp` and the file exists. Empty
// otherwise, and empty when it is `destBin` itself (a self-contained home is
// that layout, so its vendored binary is already the released one).
//
// WHY IT IS A SOURCE (mcpp#660). A release pins an xlings version and ships
// that binary beside the mcpp executable. A home that is not the release
// directory (an mcpp installed as an xlings package uses `~/.mcpp`) consulted
// only the override variable and the PATH, so on a machine whose system
// xlings was older than the pin it kept the older one and printed a note. The
// released binary is the one that satisfies the pin by construction.
std::filesystem::path released_xlings_source(const std::filesystem::path& destBin);
// The version the acquisition chain WOULD install, without installing it.
// Empty when nothing is available. Replacing a vendored binary is only an
// improvement when this is newer than what is already there.
std::string candidate_source_version(const std::filesystem::path& destBin = {});
// True when `have` is strictly older than `want`, comparing dot-separated
// numeric components. Anything unparseable answers false -- a version this
// code does not understand is not evidence of being behind.
bool version_is_older(std::string_view have, std::string_view want);
// Try to acquire (copy) the xlings binary to destBin.
//
// `pinnedVersion` is the version this mcpp expects. A vendored binary OLDER
// than the pin is replaced; one that is newer or equal is left alone.
//
// This used to return early on mere existence, with no version comparison at
// all, and nothing else ever revisited the file. A home created once kept
// whatever xlings it first acquired forever -- measured at 2026.8.2.1 against
// a pin of 2026.8.6.3, with `mcpp self env` printing both numbers side by side
// and saying nothing about the gap. The cost is not cosmetic: features mcpp
// reads from xlings simply do not appear. The subos_info block arrived in
// 2026.8.5.1, so on that machine the graphics packages' declarations were
// discarded by a client too old to have the API, and mcpp#352's fix could
// never take effect.
//
// Strictly-older, not not-equal: a user who put a newer xlings there on
// purpose must not be downgraded by an mcpp that happens to pin an older one.
std::expected<std::filesystem::path, std::string>
acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet = false,
std::string_view pinnedVersion = {}) {
if (std::filesystem::exists(destBin)) {
auto have = vendored_xlings_version(destBin);
if (pinnedVersion.empty() || have.empty()
|| !version_is_older(have, pinnedVersion))
return destBin;
// Behind the pin -- but replacing is only an improvement if what we
// would put there is actually newer. The acquisition chain below ends
// at whatever `which xlings` finds, and on a machine whose system
// xlings is ANCIENT that is a downgrade dressed up as an update.
//
// Learned by doing it: this code first deleted the vendored binary and
// re-acquired, which replaced 2026.8.2.1 with the system's 0.4.51 --
// older still, and equally missing the feature the check exists to
// restore. Look before leaping.
auto candidate = candidate_source_version(destBin);
if (candidate.empty() || !version_is_older(have, candidate)) {
// stderr, not stdout. This is a remark about the environment,
// not output of the command that happens to be running -- and
// `mcpp test --json` promises every stdout line is NDJSON, a
// promise this line broke the moment a machine fell behind the
// pin (e2e 155).
if (!quiet)
std::println(stderr,
"{:>12} vendored xlings {} is older than the "
"pinned {}, but no newer source is available "
"(keeping it; run `xlings self update`)",
"Note", have, pinnedVersion);
return destBin;
}
if (!quiet)
std::println(stderr,
"{:>12} vendored xlings {} -> {} (pinned {})",
"Updating", have, candidate, pinnedVersion);
std::error_code rec;
std::filesystem::remove(destBin, rec);
// fall through and re-acquire
}
std::error_code ec;
std::filesystem::create_directories(destBin.parent_path(), ec);
// Right-pad verb to 12 columns (matches mcpp::ui::verb_padded layout).
auto print_status = [](std::string_view verb, std::string_view msg) {
constexpr std::size_t W = 12;
if (verb.size() >= W)
std::println("{} {}", verb, msg);
else
std::println("{}{} {}", std::string(W - verb.size(), ' '), verb, msg);
};
// 1. Explicit override
if (auto* e = std::getenv("MCPP_VENDORED_XLINGS"); e && *e) {
std::filesystem::path src{e};
if (std::filesystem::exists(src)) {
std::filesystem::copy_file(src, destBin,
std::filesystem::copy_options::overwrite_existing, ec);
if (!ec) {
std::filesystem::permissions(destBin,
std::filesystem::perms::owner_exec
| std::filesystem::perms::group_exec
| std::filesystem::perms::others_exec,
std::filesystem::perm_options::add, ec);
if (!quiet) print_status("Bundled",
std::format("xlings (from MCPP_VENDORED_XLINGS)"));
return destBin;
}
}
}
// 2. The xlings released with this mcpp. Ahead of the system copy, which
// may be any version (see released_xlings_source).
if (auto released = released_xlings_source(destBin); !released.empty()) {
std::filesystem::copy_file(released, destBin,
std::filesystem::copy_options::overwrite_existing, ec);
if (!ec) {
std::filesystem::permissions(destBin,
std::filesystem::perms::owner_exec
| std::filesystem::perms::group_exec
| std::filesystem::perms::others_exec,
std::filesystem::perm_options::add, ec);
if (!quiet) print_status("Bundled",
std::format("xlings (released with this mcpp: {})", released.string()));
return destBin;
}
ec.clear();
}
// 3. Copy from system (`which xlings`)
auto xlings_name = std::string("xlings") + std::string(mcpp::platform::exe_suffix);
auto sysXlings = mcpp::platform::fs::which(xlings_name);
if (sysXlings) {
std::string p = sysXlings->string();
if (!p.empty() && std::filesystem::exists(p)) {
std::filesystem::copy_file(p, destBin,
std::filesystem::copy_options::overwrite_existing, ec);
if (!ec) {
std::filesystem::permissions(destBin,
std::filesystem::perms::owner_exec
| std::filesystem::perms::group_exec
| std::filesystem::perms::others_exec,
std::filesystem::perm_options::add, ec);
if (!quiet) print_status("Bundled",
std::format("xlings (copied from system: {})", p));
return destBin;
}
}
}
// 3. Fail with instructions
return std::unexpected(std::format(
"xlings binary not found. Either:\n"
" - install via: curl -fsSL https://raw.githubusercontent.com/d2learn/xlings/refs/heads/main/tools/other/quick_install.sh | bash\n"
" - export MCPP_VENDORED_XLINGS=/abs/path/to/xlings\n"
" - set [xlings].binary = \"system\" in {}",
(destBin.parent_path().parent_path() / "config.toml").string()));
}
std::string vendored_xlings_version(const std::filesystem::path& bin) {
std::error_code ec;
if (!std::filesystem::exists(bin, ec)) return {};
// An argument vector, not a command string. The string form carried
// `2>/dev/null`, which cmd.exe cannot open: on Windows it printed "The
// system cannot find the path specified." in every command after the first,
// did not run xlings, and so returned an empty version, which made the
// pin comparison in acquire_xlings_binary return early forever.
auto r = mcpp::platform::process::capture_stdout({bin.string(), "--version"});
if (r.exit_code != 0) return {};
// Output carries ANSI colour; take the first dotted-numeric run.
std::string out;
for (std::size_t i = 0; i < r.output.size(); ++i) {
if (r.output[i] == '\x1b') { // skip CSI
while (i < r.output.size() && r.output[i] != 'm') ++i;
continue;
}
out += r.output[i];
}
std::size_t i = 0;
while (i < out.size()) {
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
auto j = i;
while (j < out.size()
&& (std::isdigit(static_cast<unsigned char>(out[j]))
|| out[j] == '.')) ++j;
auto cand = out.substr(i, j - i);
if (cand.find('.') != std::string::npos) return cand;
i = j;
} else ++i;
}
return {};
}
bool version_is_older(std::string_view have, std::string_view want) {
auto parts = [](std::string_view v) {
std::vector<long long> out;
std::size_t i = 0;
while (i <= v.size()) {
auto dot = v.find('.', i);
auto seg = v.substr(i, dot == std::string_view::npos
? std::string_view::npos : dot - i);
long long n = 0;
auto [p, e] = std::from_chars(seg.data(), seg.data() + seg.size(), n);
if (e != std::errc{}) return std::vector<long long>{};
out.push_back(n);
if (dot == std::string_view::npos) break;
i = dot + 1;
}
return out;
};
auto a = parts(have), b = parts(want);
if (a.empty() || b.empty()) return false; // unparseable is not "behind"
for (std::size_t i = 0; i < std::max(a.size(), b.size()); ++i) {
long long x = i < a.size() ? a[i] : 0;
long long y = i < b.size() ? b[i] : 0;
if (x != y) return x < y;
}
return false;
}
std::filesystem::path released_xlings_source(const std::filesystem::path& destBin) {
auto exe = mcpp::platform::fs::self_exe_path();
if (exe.empty() || exe.parent_path().filename() != "bin") return {};
auto released = exe.parent_path().parent_path() / "registry" / "bin"
/ (std::string("xlings") + std::string(mcpp::platform::exe_suffix));
std::error_code ec;
if (!std::filesystem::is_regular_file(released, ec)) return {};
if (!destBin.empty() && std::filesystem::equivalent(released, destBin, ec))
return {};
return released;
}
std::string candidate_source_version(const std::filesystem::path& destBin) {
if (const char* e = std::getenv("MCPP_VENDORED_XLINGS"); e && *e) {
std::error_code ec;
if (std::filesystem::exists(std::filesystem::path(e), ec))
return vendored_xlings_version(std::filesystem::path(e));
}
if (auto released = released_xlings_source(destBin); !released.empty())
return vendored_xlings_version(released);
if (auto sys = mcpp::platform::fs::which(
std::string("xlings") + std::string(mcpp::platform::exe_suffix)))
return vendored_xlings_version(*sys);
return {};
}
} // namespace mcpp::fallback