-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathtest_post_install.cpp
More file actions
282 lines (250 loc) · 12.1 KB
/
Copy pathtest_post_install.cpp
File metadata and controls
282 lines (250 loc) · 12.1 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
277
278
279
280
281
282
#include <gtest/gtest.h>
#include <fstream>
import std;
import mcpp.toolchain.post_install;
import mcpp.xlings;
import mcpp.config;
import mcpp.toolchain.registry;
import mcpp.platform;
// detect_baked_loader parses gcc SPECS GRAMMAR, not plain text. The baked
// loader path is embedded inside %-spec conditionals, e.g.
// %{mmusl:/lib/ld-musl-x86_64.so.1;:/baked/dir/ld-linux-x86-64.so.2}
// A scanner that treats "whitespace or :;" as the boundary swallows the
// closing braces; replacing that string then corrupts the spec grammar and
// EVERY subsequent g++ invocation dies with "braced spec body ... is
// invalid" (observed on CI). These tests pin the exact grammar shape.
namespace {
using mcpp::toolchain::detect_baked_loader;
// Realistic *link_spec fragment as xim bakes it (64-bit branch rewritten to
// the installing user's home; other multilib branches pristine).
const std::string kBakedSpecs =
"*link:\n"
"%{m16|m32|mx32:;:-m elf_x86_64} %{shared:-shared} %{!shared: %{!static: "
"%{m16|m32:-dynamic-linker %{muclibc:/lib/ld-uClibc.so.0;:%{mbionic:/system/bin/linker;:"
"%{mmusl:/lib/ld-musl-i386.so.1;:/lib/ld-linux.so.2}}}} "
"%{m16|m32|mx32:;:-dynamic-linker %{muclibc:/lib/ld64-uClibc.so.0;:%{mbionic:/system/bin/linker64;:"
"%{mmusl:/lib/ld-musl-x86_64.so.1;:/opt/other-home/.xlings/data/xpkgs/xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2}}}}} "
"%{static:-static}}\n";
TEST(DetectBakedLoader, ExtractsExactPathWithoutSpecBraces) {
auto got = detect_baked_loader(kBakedSpecs);
EXPECT_EQ(got,
"/opt/other-home/.xlings/data/xpkgs/xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2");
// The regression: any brace in the result corrupts the specs on rewrite.
EXPECT_EQ(got.find('}'), std::string::npos);
EXPECT_EQ(got.find('{'), std::string::npos);
}
TEST(DetectBakedLoader, IgnoresPristineMultilibDefaults) {
// An unbaked spec (all-standard /lib*/ paths) must not be rewritten.
const std::string pristine =
"%{mmusl:/lib/ld-musl-x86_64.so.1;:/lib64/ld-linux-x86-64.so.2} "
"%{m16|m32:-dynamic-linker /lib/ld-linux.so.2} "
"%{mx32:-dynamic-linker /libx32/ld-linux-x32.so.2}";
EXPECT_EQ(detect_baked_loader(pristine), "");
}
TEST(DetectBakedLoader, Aarch64LoaderNameDetected) {
const std::string specs =
"-dynamic-linker %{mmusl:/lib/ld-musl-aarch64.so.1;:"
"/srv/build/.xlings/xpkgs/xim-x-glibc/2.39/lib/ld-linux-aarch64.so.1}";
EXPECT_EQ(detect_baked_loader(specs),
"/srv/build/.xlings/xpkgs/xim-x-glibc/2.39/lib/ld-linux-aarch64.so.1");
}
TEST(DetectBakedLoader, EmptyWhenNoGnuLoaderPresent) {
EXPECT_EQ(detect_baked_loader("no loaders here"), "");
EXPECT_EQ(detect_baked_loader("%{mmusl:/lib/ld-musl-x86_64.so.1}"), "");
}
} // namespace
// ── the post-install fixup gate (mcpp#427) ───────────────────────────────
//
// A unit test rather than an e2e ON PURPOSE, and the reason is the defect
// itself. `ensure_post_install_fixup` returns early — before any of the logic
// under test — for a payload that resolves OUTSIDE the caller's registry
// ("inherited payload, owner is responsible for its fixup"). Every affordable
// e2e inherits its toolchain by symlink to avoid a multi-gigabyte download, so
// no e2e can reach this code. That is precisely the shape of mcpp#221: a test
// that creates the missing thing somewhere the code under test never looks,
// and passes.
//
// Here the payload is a real directory inside the test's own registry, so the
// containment guard passes and the gate actually runs.
namespace fixup_gate {
struct Sandbox {
std::filesystem::path root;
mcpp::config::GlobalConfig cfg;
Sandbox() {
root = std::filesystem::temp_directory_path()
/ std::format("mcpp-fixup-{}", std::random_device{}());
std::filesystem::create_directories(root / "registry" / "data" / "xpkgs");
cfg.registryDir = root / "registry";
}
~Sandbox() {
std::error_code ec;
std::filesystem::remove_all(root, ec);
}
// A payload physically inside the registry, so the #273 containment guard
// treats it as ours to patch and the gate is actually reached.
std::filesystem::path payload(std::string_view name) {
auto p = root / "registry" / "data" / "xpkgs" / name / "1.0.0";
std::filesystem::create_directories(p / "bin");
return p;
}
};
mcpp::toolchain::XimToolchainPackage gcc_pkg() {
mcpp::toolchain::XimToolchainPackage pkg;
pkg.ximName = "gcc";
pkg.ximVersion = "16.1.0";
pkg.needsGccPostInstallFixup = true;
return pkg;
}
} // namespace fixup_gate
// THE REGRESSION. An empty runtime identity used to be
// std::unexpected("… default SubOS has no RuntimeBinding identity …")
// and `prepare.cppm` turned that into `error: toolchain post-install fixup: …`,
// so `mcpp build` AND `mcpp toolchain install` both died on any Linux machine
// whose default SubOS predated xlings' `subos_info` block. Absence is not a
// contradiction: it degrades.
TEST(PostInstallFixup, AnAbsentRuntimeIdentityDegradesInsteadOfFailing) {
if constexpr (!mcpp::platform::is_linux) {
GTEST_SKIP() << "the runtime binding gate is Linux-only";
} else {
fixup_gate::Sandbox sb;
auto result = mcpp::toolchain::ensure_post_install_fixup(
sb.cfg, sb.payload("xim-x-gcc"), fixup_gate::gcc_pkg(),
/*runtimeId=*/"", /*selectedRuntimeLibDir=*/{});
ASSERT_TRUE(result.has_value())
<< "an undescribed SubOS became a hard error again: "
<< result.error();
EXPECT_FALSE(result->applied);
// ...and it must SAY why. A degradation nobody can report is
// indistinguishable from a fixup that silently stopped working.
EXPECT_FALSE(result->skippedReason.empty())
<< "degraded silently — the caller has nothing to print";
}
}
// The other direction, so relaxing absence cannot quietly relax everything. A
// runtime that is DECLARED but cannot be produced is wrong information, not
// missing information: guessing a different glibc would make one mcpp.toml mean
// different ABIs on different machines.
TEST(PostInstallFixup, ADeclaredRuntimeThatCannotBeHonouredStillFails) {
if constexpr (!mcpp::platform::is_linux) {
GTEST_SKIP() << "the runtime binding gate is Linux-only";
} else {
fixup_gate::Sandbox sb;
auto result = mcpp::toolchain::ensure_post_install_fixup(
sb.cfg, sb.payload("xim-x-gcc"), fixup_gate::gcc_pkg(),
/*runtimeId=*/"glibc@0.0.0-does-not-exist",
/*selectedRuntimeLibDir=*/{});
EXPECT_FALSE(result.has_value())
<< "a runtime identity that names nothing installed was accepted";
}
}
// No marker may be written for a fixup that did not happen. The marker is a
// content fingerprint whose whole job is to answer "were these inputs ever
// applied"; one written for "we did nothing" reads back as "already applied",
// and the fixup would then never run again — including on the day the user
// moves to a SubOS that does describe itself and the identity becomes knowable.
TEST(PostInstallFixup, ASkippedFixupLeavesNoMarkerBehind) {
if constexpr (!mcpp::platform::is_linux) {
GTEST_SKIP() << "the runtime binding gate is Linux-only";
} else {
fixup_gate::Sandbox sb;
auto payload = sb.payload("xim-x-gcc");
auto result = mcpp::toolchain::ensure_post_install_fixup(
sb.cfg, payload, fixup_gate::gcc_pkg(), /*runtimeId=*/"", {});
ASSERT_TRUE(result.has_value()) << result.error();
EXPECT_FALSE(std::filesystem::exists(payload / ".mcpp-fixup.json"))
<< "'we did nothing' was recorded as 'already applied'";
}
}
// A package that needs no fixup at all is not a degradation and must not report
// one, or the caller warns about every msvc/system install.
TEST(PostInstallFixup, APackageWithNoFixupReportsNothingToReport) {
fixup_gate::Sandbox sb;
mcpp::toolchain::XimToolchainPackage none;
none.ximName = "msvc";
none.ximVersion = "system";
auto result = mcpp::toolchain::ensure_post_install_fixup(
sb.cfg, sb.payload("xim-x-msvc"), none, "", {});
ASSERT_TRUE(result.has_value()) << result.error();
EXPECT_TRUE(result->skippedReason.empty())
<< "a toolchain with no fixup reported a degradation: "
<< result->skippedReason;
}
// THE DECLARED VERSION NAMES ITS PAYLOAD DIRECTORY EXACTLY (mcpp#660).
//
// The lookup used to accept a unique `2.44.x` for a binding of `2.44` and to
// refuse two of them. That tolerance existed because the declared payload was
// never installed on purpose; it arrived as a side effect of another install,
// under whatever version that install resolved. It concealed the missing
// install, and it failed as soon as a cache held one revision and an install
// added the next:
//
// error: toolchain post-install fixup: selected RuntimeBinding glibc@2.44
// requires payload '.../xim-x-glibc/2.44', but no installed payload
// is its resolution; mcpp will not fall back to another directory
// entry
// $ ls .../xim-x-glibc/ -> 2.44.2 2.44.3
//
// `ensure_declared_runtime` now installs the declared version, so the lookup
// is exact and a neighbouring version is never a substitute.
namespace {
// A payload is "installed" for this purpose when it has a lib dir with a
// loader in it — that is what select_glibc_payload_lib returns.
std::filesystem::path make_glibc_payload(const std::filesystem::path& root,
std::string_view version) {
auto lib = root / std::string(version) / "lib64";
std::filesystem::create_directories(lib);
std::ofstream(lib / "ld-linux-x86-64.so.2");
return lib;
}
struct GlibcRootFixture {
std::filesystem::path root;
explicit GlibcRootFixture(std::string_view name)
: root(std::filesystem::temp_directory_path() / name) {
std::error_code ec;
std::filesystem::remove_all(root, ec);
std::filesystem::create_directories(root);
}
~GlibcRootFixture() {
std::error_code ec;
std::filesystem::remove_all(root, ec);
}
};
} // namespace
TEST(GlibcPayload, TheExactVersionIsSelected) {
GlibcRootFixture fx{"mcpp_glibc_exact"};
auto want = make_glibc_payload(fx.root, "2.44.3");
make_glibc_payload(fx.root, "2.44.2");
auto got = mcpp::toolchain::select_glibc_payload_lib(fx.root, "glibc@2.44.3");
ASSERT_TRUE(got.has_value()) << got.error();
EXPECT_EQ(*got, want);
}
// THE CASE OF mcpp#660. Two revisions of the line are present and the declared
// version is neither; the answer is "not installed", naming the coordinate
// that provides it, and never one of the two.
TEST(GlibcPayload, ANeighbouringRevisionIsNotASubstitute) {
GlibcRootFixture fx{"mcpp_glibc_neighbour"};
make_glibc_payload(fx.root, "2.44.2");
make_glibc_payload(fx.root, "2.44.3");
auto got = mcpp::toolchain::select_glibc_payload_lib(fx.root, "glibc@2.44");
ASSERT_FALSE(got.has_value());
EXPECT_NE(got.error().find("xim:glibc@2.44"), std::string::npos) << got.error();
EXPECT_NE(got.error().find("not installed"), std::string::npos) << got.error();
// One neighbour is not a substitute either: the old tolerance accepted it.
GlibcRootFixture one{"mcpp_glibc_one_neighbour"};
make_glibc_payload(one.root, "2.44.2");
EXPECT_FALSE(mcpp::toolchain::select_glibc_payload_lib(one.root, "glibc@2.44"));
}
TEST(GlibcPayload, NothingInstalledIsStillRefused) {
GlibcRootFixture fx{"mcpp_glibc_empty"};
auto got = mcpp::toolchain::select_glibc_payload_lib(fx.root, "glibc@2.44");
EXPECT_FALSE(got.has_value());
}
// A DIRECTORY WITHOUT A LOADER IS REPORTED AS INCOMPLETE, NOT AS ABSENT. The
// two call for different repairs, so the messages must not coincide.
TEST(GlibcPayload, APayloadWithoutALoaderIsIncomplete) {
GlibcRootFixture fx{"mcpp_glibc_husk"};
std::filesystem::create_directories(fx.root / "2.44.3");
auto got = mcpp::toolchain::select_glibc_payload_lib(fx.root, "glibc@2.44.3");
ASSERT_FALSE(got.has_value());
EXPECT_NE(got.error().find("stale/incomplete"), std::string::npos) << got.error();
}