-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathrunner_lookup.cppm
More file actions
162 lines (147 loc) · 6.99 KB
/
Copy pathrunner_lookup.cppm
File metadata and controls
162 lines (147 loc) · 6.99 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
// mcpp.build.runner_lookup — where the runner's program is, and what to say
// when it is not.
//
// The lookup is mcpp's own rather than posix_spawnp's for one measured reason:
// a bare name on PATH resolves to an xvm shim, and the shim answers for the
// current subos rather than for the package (e2e 130 in CI: `mcpp run` exec'ing
// the bare `qemu-system-riscv64` answered "not installed" while
// `qemu-system-riscv64 --version` in the same job succeeded; `python3` on the
// development host answers "not installed in this subos (_) — installed
// elsewhere"). The payload's bin/ is the binary itself, so it is searched
// first. That is what lets a runner name a program the project declared under
// `[xlings] deps` without writing the payload's home-and-version path into the
// manifest — the thing BuildConfig::runner's comment says a static manifest
// cannot do.
//
// Doing the lookup here has a second effect: "not found anywhere" is decided
// before any spawn, so a spawn-time ENOENT can only mean the program was found
// and its interpreter or loader was not. The two messages differ, and neither
// guesses.
//
// Design: .agents/docs/2026-09-02-runner-beyond-baremetal-design.md §4.3-4.4.
module;
#include <cerrno>
export module mcpp.build.runner_lookup;
import std;
import mcpp.platform;
export namespace mcpp::build::runner_lookup {
struct Lookup {
std::optional<std::filesystem::path> program; // absolute, executable
std::vector<std::filesystem::path> searched; // in order, for the message
};
namespace detail {
inline bool executable_file(const std::filesystem::path& p) {
std::error_code ec;
if (!std::filesystem::is_regular_file(p, ec)) return false;
if constexpr (mcpp::platform::is_windows) return true;
auto perms = std::filesystem::status(p, ec).permissions();
using P = std::filesystem::perms;
return (perms & (P::owner_exec | P::group_exec | P::others_exec)) != P::none;
}
} // namespace detail
// The directories one installed payload contributes to `locate`, in order:
// `<root>/bin`, then `<root>` itself.
//
// `bin/` is the convention and stays first. It is not universal: `xim:7zip`
// installs `7zz` straight into its payload directory, and so did the first
// version of `xim:apple-simulator-tools` -- which is how the second entry was
// measured, on a macOS runner with that package correctly installed:
//
// error: runner 'simctl-run' for 'aarch64-ios-sim' was not found on any
// search path.
// Searched: .../xim-x-apple-simulator-tools/0.1.0/bin
//
// The directory searched was right and the program was one level up. Two
// directories per payload is cheaper than a layout rule every recipe has to
// know, and a recipe that uses `bin/` is unaffected because that entry is
// still tried first. A function rather than two `push_back`s at the call site
// so that the order is a stated rule a unit test can hold.
inline std::vector<std::filesystem::path>
payload_search_dirs(const std::filesystem::path& payloadRoot) {
return { payloadRoot / "bin", payloadRoot };
}
// `argv0` absolute, or containing a directory separator: taken as-is when it
// is an executable file. Otherwise `<each depBinDir>/argv0`, then each `PATH`
// entry (`pathEnv` split on the platform's list separator); the first
// executable regular file wins. Every directory looked in is recorded so the
// not-found message can list them.
inline Lookup locate(std::string_view argv0,
std::span<const std::filesystem::path> depBinDirs,
std::string_view pathEnv)
{
Lookup out;
std::filesystem::path a0(argv0);
const bool hasDir = a0.is_absolute()
|| argv0.find('/') != std::string_view::npos
|| argv0.find('\\') != std::string_view::npos;
if (hasDir) {
std::error_code ec;
if (detail::executable_file(a0)) out.program = std::filesystem::absolute(a0, ec);
out.searched.push_back(a0.parent_path());
return out;
}
for (auto const& d : depBinDirs) {
out.searched.push_back(d);
if (auto c = d / a0; detail::executable_file(c)) { out.program = c; return out; }
}
constexpr char sep = mcpp::platform::is_windows ? ';' : ':';
for (auto part : std::views::split(pathEnv, sep)) {
std::string_view sv(part.begin(), part.end());
if (sv.empty()) continue;
std::filesystem::path d(sv);
out.searched.push_back(d);
if (auto c = d / a0; detail::executable_file(c)) { out.program = c; return out; }
}
return out;
}
// What the kernel's refusal means. Only ENOEXEC (and EBADARCH where the
// platform defines it) says "this host cannot load the artifact"; everything
// else — EACCES, ENOENT on a found program, E2BIG — is reported verbatim and
// never turned into advice about runners.
enum class SpawnClass { Unloadable, Other };
inline SpawnClass classify(int e) {
if (e == ENOEXEC) return SpawnClass::Unloadable;
#if defined(EBADARCH)
if (e == EBADARCH) return SpawnClass::Unloadable;
#endif
return SpawnClass::Other;
}
inline std::string errno_text(int e) {
return std::generic_category().message(e);
}
inline std::string not_found_message(std::string_view triple, std::string_view argv0,
std::span<const std::filesystem::path> searched) {
// The first line stands on its own: `mcpp test` repeats it in its summary.
std::string dirs;
for (auto const& d : searched) dirs += "\n " + d.string();
return std::format(
"runner '{}' for '{}' was not found on any search path.\n"
" Searched:{}\n"
" Declare the package that provides it under [xlings.workspace], or "
"install it on PATH.\n"
" Pass --no-runner to execute the artifact directly on this host.",
argv0, triple, dirs);
}
inline std::string spawn_failed_message(std::string_view program, int e) {
return std::format("'{}' could not be started: {} (error {})",
program, errno_text(e), e);
}
// The hosted sibling of mcpp::freestanding::no_runner_message. It reports what
// the kernel answered rather than asserting why: on a hosted triple mcpp does
// not know whether the refusal is a foreign ISA or a file that is not an
// executable at all, and the example it prints is a user-mode emulator.
inline std::string unrunnable_message(std::string_view triple,
const std::filesystem::path& artifact, int e) {
return std::format(
"this host cannot execute '{}': {} (error {}).\n"
" The artifact was built for '{}'. Declare how to run it here:\n"
"\n"
" [target.{}]\n"
" runner = [\"qemu-aarch64-static\"]\n"
"\n"
" The artifact path is appended, or substituted for `{{}}` if the "
"template contains it.\n"
" A host that can execute it directly may pass --no-runner.",
artifact.string(), errno_text(e), e, triple, triple);
}
} // namespace mcpp::build::runner_lookup