Skip to content

Commit c1e9360

Browse files
authored
fix(subos): prepend 丢掉了调用方的值 (#383)
解析出的键值对**替换**子进程里的变量(作为 extraEnv 进去),而 `prepend` 只发出声明值,把调用方原有内容整个丢掉 —— 对 PATH 形状的变量,丢掉的是用户的整条搜索路径。 现在它接在调用方的值前面,且对已存在元素幂等(嵌套运行不会让列表增长)。空值不算「用户设过」。 `set` 的部分**已撤回**:`set` 与「默认值」是两种意图,而 op 词汇表是 xlings 的契约。#382 想要的逃生通道应由 xlings 新增一个 op,mcpp 认它 —— 且因为 mcpp 会丢弃未知 op,那个 op 必须两侧同时到位。已加断言把「`set` 就是要赢」钉住。
1 parent 6e6baa4 commit c1e9360

3 files changed

Lines changed: 129 additions & 4 deletions

File tree

‎src/build/execute.cppm‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,15 @@ compute_subos_env(const mcpp::build::BuildPlan& plan) {
361361
// diagnostic path in doctor.
362362
if (!info.note.empty())
363363
mcpp::log::verbose("subos", info.note);
364-
return mcpp::xlings::subos::resolve_env(info, dir);
364+
// Resolved AGAINST the caller's environment, not in a vacuum: these
365+
// entries replace the variable in the child, so a `set` that ignores an
366+
// exported value overwrites it and a `prepend` that ignores it drops it.
367+
return mcpp::xlings::subos::resolve_env(
368+
info, dir, [](std::string_view v) -> std::optional<std::string> {
369+
if (const char* e = std::getenv(std::string(v).c_str()))
370+
return std::string(e);
371+
return std::nullopt;
372+
});
365373
}
366374

367375
// Compile a prepared BuildContext. Shared between `mcpp build` and `mcpp run`
@@ -877,7 +885,13 @@ std::optional<int> try_fast_run(const std::filesystem::path& projectRoot,
877885
auto subosDir = subos_dir_for_run(std::filesystem::path(match->subosDir));
878886
if (!subosDir.empty()) {
879887
auto info = mcpp::xlings::subos::read(subosDir);
880-
for (auto& kv : mcpp::xlings::subos::resolve_env(info, subosDir))
888+
for (auto& kv : mcpp::xlings::subos::resolve_env(
889+
info, subosDir,
890+
[](std::string_view v) -> std::optional<std::string> {
891+
if (const char* e = std::getenv(std::string(v).c_str()))
892+
return std::string(e);
893+
return std::nullopt;
894+
}))
881895
childEnv.push_back(std::move(kv));
882896
}
883897
}

‎src/xlings/subos_info.cppm‎

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,21 @@ Info read(const std::filesystem::path& subosDir) {
201201
// never matches and the joined value is a corrupt list. Caught by CI on
202202
// Windows, not by any amount of reading.
203203
std::vector<std::pair<std::string, std::string>>
204-
resolve_env(const Info& info, const std::filesystem::path& subosDir) {
204+
resolve_env(const Info& info, const std::filesystem::path& subosDir,
205+
const std::function<std::optional<std::string>(std::string_view)>&
206+
ambient = {}) {
205207
std::vector<std::pair<std::string, std::string>> out;
206208

209+
// What the caller's environment already says about a variable.
210+
//
211+
// The declarations are merged against this, not in a vacuum, because the
212+
// result REPLACES the variable in the child (it goes in as extraEnv). A
213+
// resolution that ignores the ambient value silently discards it.
214+
auto ambient_of = [&](std::string_view var) -> std::optional<std::string> {
215+
if (!ambient) return std::nullopt;
216+
return ambient(var);
217+
};
218+
207219
const std::string subos = subosDir.string();
208220
auto expand = [&](std::string v) {
209221
constexpr std::string_view kPh = "${subosdir}";
@@ -241,7 +253,45 @@ resolve_env(const Info& info, const std::filesystem::path& subosDir) {
241253
std::pair<std::string, std::string>* hit = nullptr;
242254
for (auto& kv : out)
243255
if (kv.first == d.var) { hit = &kv; break; }
244-
if (!hit) { out.emplace_back(d.var, value); continue; }
256+
if (!hit) {
257+
auto amb = ambient_of(d.var);
258+
if (d.op == "set") {
259+
// `set` wins, ambient or not.
260+
//
261+
// Deliberately NOT "yield to an exported value". That
262+
// reading was written here first and withdrawn: `set` and
263+
// "default" are two different intentions, and a subos has
264+
// real need of the first -- a variable naming its own
265+
// loader configuration must not be overridable by a stale
266+
// value in the caller's shell. Collapsing them here would
267+
// remove the ability to express it.
268+
//
269+
// It is also not mcpp's vocabulary to redefine. `envs` is
270+
// xlings' wire format; a consumer that quietly gives an op
271+
// a second meaning makes the same subos behave differently
272+
// depending on which tool launched the program.
273+
//
274+
// The escape hatch mcpp#382 asks for (a recipe declaring a
275+
// DEFAULT the user can override) therefore wants a new op
276+
// from xlings, not a reinterpretation of this one. When it
277+
// exists, it is honoured here -- unknown ops are dropped
278+
// today, which is why it has to arrive on both sides.
279+
out.emplace_back(d.var, value);
280+
continue;
281+
}
282+
// `prepend` against the ambient value, not instead of it.
283+
// These entries replace the variable in the child, so
284+
// emitting the declared value alone DROPS whatever the caller
285+
// had -- for a PATH-shaped variable that is the user's whole
286+
// search path.
287+
if (amb && !amb->empty() && !contains_element(*amb, value))
288+
out.emplace_back(d.var, value + sep + *amb);
289+
else if (amb && !amb->empty())
290+
out.emplace_back(d.var, *amb);
291+
else
292+
out.emplace_back(d.var, value);
293+
continue;
294+
}
245295
if (d.op == "set") { hit->second = value; continue; }
246296
if (!contains_element(hit->second, value))
247297
hit->second = value + sep + hit->second;

‎tests/unit/test_subos_info.cpp‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,65 @@ TEST(SubosInfo, UnknownOpIsDroppedLikeXlingsDrops) {
249249
EXPECT_EQ(env[0].second, "/yes");
250250
}
251251

252+
253+
254+
255+
// `prepend` prepends TO the caller's value rather than replacing it. Same
256+
// reason: the pair replaces the variable, so emitting the declared value alone
257+
// discards whatever search path the user had.
258+
TEST(SubosResolveEnv, PrependKeepsTheExportedValue) {
259+
Tmp t;
260+
t.write(R"({"workspace":{},"subos_info":{"schema_version":1,"runtime":"glibc@2.39",
261+
"envs":{"glibc@2.39":[{"var":"LD_LIBRARY_PATH","op":"prepend","value":"/sub/lib"}]}}})");
262+
auto info = su::read(t.dir);
263+
auto out = su::resolve_env(
264+
info, t.dir, [](std::string_view v) -> std::optional<std::string> {
265+
if (v == "LD_LIBRARY_PATH") return std::string("/user/lib");
266+
return std::nullopt;
267+
});
268+
ASSERT_EQ(out.size(), 1u);
269+
const auto sep = mcpp::platform::env::path_list_separator();
270+
EXPECT_EQ(out[0].second, std::string("/sub/lib") + sep + "/user/lib");
271+
}
272+
273+
// Already there: prepending again would grow the list on every nested run.
274+
TEST(SubosResolveEnv, PrependIsIdempotentAgainstTheExportedValue) {
275+
Tmp t;
276+
t.write(R"({"workspace":{},"subos_info":{"schema_version":1,"runtime":"glibc@2.39",
277+
"envs":{"glibc@2.39":[{"var":"LD_LIBRARY_PATH","op":"prepend","value":"/sub/lib"}]}}})");
278+
auto info = su::read(t.dir);
279+
const auto sep = mcpp::platform::env::path_list_separator();
280+
const auto existing = std::string("/sub/lib") + sep + "/user/lib";
281+
auto out = su::resolve_env(
282+
info, t.dir, [&](std::string_view v) -> std::optional<std::string> {
283+
if (v == "LD_LIBRARY_PATH") return existing;
284+
return std::nullopt;
285+
});
286+
ASSERT_EQ(out.size(), 1u);
287+
EXPECT_EQ(out[0].second, existing);
288+
}
289+
290+
// `set` wins over an exported value, and that is deliberate.
291+
//
292+
// This assertion exists to stop the opposite reading from being reintroduced
293+
// -- it was, once, as a fix for mcpp#382, and withdrawn: `set` and "a default
294+
// the user may override" are two intentions, and a subos needs the first for
295+
// variables naming its own configuration. The escape hatch that issue wants is
296+
// a NEW op from xlings, whose wire format this is, not a second meaning for
297+
// this one applied by one consumer.
298+
TEST(SubosResolveEnv, SetWinsOverAnExportedValue) {
299+
Tmp t;
300+
t.write(R"({"workspace":{},"subos_info":{"schema_version":1,
301+
"runtime":"glibc@2.39",
302+
"envs":{"glibc@2.39":[{"var":"GALLIUM_DRIVER","op":"set","value":"d3d12"}]}}})");
303+
auto info = su::read(t.dir);
304+
auto out = su::resolve_env(
305+
info, t.dir, [](std::string_view v) -> std::optional<std::string> {
306+
if (v == "GALLIUM_DRIVER") return std::string("llvmpipe");
307+
return std::nullopt;
308+
});
309+
ASSERT_EQ(out.size(), 1u);
310+
EXPECT_EQ(out[0].second, "d3d12");
311+
}
312+
252313
} // namespace

0 commit comments

Comments
 (0)