[M5+] add resolver::vcpkg_probe
This commit is contained in:
@@ -71,4 +71,22 @@ auto parse_conanfile(std::string_view conanfile_text, const std::string& fallbac
|
||||
// ResolutionNetworkError.
|
||||
auto conan_probe(const std::string& name) -> util::Result<ConanRecipe>;
|
||||
|
||||
// Output of a microsoft/vcpkg port usage-file scrape.
|
||||
struct VcpkgRecipe {
|
||||
std::string find_package; // e.g. "fmt CONFIG REQUIRED"
|
||||
std::vector<std::string> targets; // e.g. ["fmt::fmt"]
|
||||
};
|
||||
|
||||
// Pure: scrape a vcpkg port `usage` file (plain CMake) for the first
|
||||
// find_package(...) arguments and the targets named in the corresponding
|
||||
// target_link_libraries(...) call. Returns ResolutionUnknownPackage when
|
||||
// no find_package directive appears.
|
||||
auto parse_vcpkg_usage(std::string_view usage_text)
|
||||
-> util::Result<VcpkgRecipe>;
|
||||
|
||||
// Fetches https://raw.githubusercontent.com/microsoft/vcpkg/master/ports/<name>/usage
|
||||
// via `curl` and feeds it through parse_vcpkg_usage. 404 →
|
||||
// ResolutionUnknownPackage; transport errors → ResolutionNetworkError.
|
||||
auto vcpkg_probe(const std::string& name) -> util::Result<VcpkgRecipe>;
|
||||
|
||||
} // namespace cargoxx::resolver
|
||||
|
||||
173
src/resolver/vcpkg_probe.cpp
Normal file
173
src/resolver/vcpkg_probe.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
module cargoxx.resolver;
|
||||
|
||||
import std;
|
||||
import cargoxx.util;
|
||||
import cargoxx.exec;
|
||||
|
||||
namespace cargoxx::resolver {
|
||||
|
||||
namespace {
|
||||
|
||||
auto error(util::ErrorCode code, std::string msg) -> util::Error {
|
||||
return util::Error{code, std::move(msg), "", std::nullopt, std::nullopt};
|
||||
}
|
||||
|
||||
// Find the first balanced `(...)` block following `keyword` (with no
|
||||
// alphanumeric / underscore character immediately preceding `keyword` to
|
||||
// avoid matching e.g. `_find_package`).
|
||||
auto extract_call_args(std::string_view text, std::string_view keyword)
|
||||
-> std::optional<std::string_view> {
|
||||
std::size_t pos = 0;
|
||||
while (pos < text.size()) {
|
||||
auto next = text.find(keyword, pos);
|
||||
if (next == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (next > 0) {
|
||||
char prev = text[next - 1];
|
||||
if (std::isalnum(static_cast<unsigned char>(prev)) || prev == '_') {
|
||||
pos = next + keyword.size();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
auto open = text.find('(', next + keyword.size());
|
||||
if (open == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto close = text.find(')', open + 1);
|
||||
if (close == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return text.substr(open + 1, close - open - 1);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto tokenize(std::string_view s) -> std::vector<std::string> {
|
||||
std::vector<std::string> out;
|
||||
std::size_t i = 0;
|
||||
while (i < s.size()) {
|
||||
while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) {
|
||||
++i;
|
||||
}
|
||||
if (i >= s.size()) {
|
||||
break;
|
||||
}
|
||||
std::size_t start = i;
|
||||
while (i < s.size() && !std::isspace(static_cast<unsigned char>(s[i]))) {
|
||||
++i;
|
||||
}
|
||||
out.emplace_back(s.substr(start, i - start));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// In `target_link_libraries(<target> [PRIVATE|PUBLIC|INTERFACE] dep dep ...)`,
|
||||
// every token after a visibility keyword that contains "::" is treated as
|
||||
// an external link target. Tokens that look like CMake variables
|
||||
// (${...}) or contain commas (e.g. "$<...>" generator-expressions) are
|
||||
// dropped.
|
||||
auto extract_link_targets(std::string_view args) -> std::vector<std::string> {
|
||||
auto toks = tokenize(args);
|
||||
std::vector<std::string> out;
|
||||
bool past_visibility = false;
|
||||
for (auto& t : toks) {
|
||||
if (t == "PRIVATE" || t == "PUBLIC" || t == "INTERFACE") {
|
||||
past_visibility = true;
|
||||
continue;
|
||||
}
|
||||
if (!past_visibility) {
|
||||
continue;
|
||||
}
|
||||
if (t.find("::") == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
if (t.find('$') != std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
out.push_back(std::move(t));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto parse_vcpkg_usage(std::string_view text) -> util::Result<VcpkgRecipe> {
|
||||
auto fp_args = extract_call_args(text, "find_package");
|
||||
if (!fp_args) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionUnknownPackage,
|
||||
"vcpkg usage file has no find_package(...) directive"));
|
||||
}
|
||||
|
||||
// The args block is "fmt CONFIG REQUIRED" or similar. Strip leading/
|
||||
// trailing whitespace + collapse runs to single spaces.
|
||||
std::string find_package_args;
|
||||
for (auto& tok : tokenize(*fp_args)) {
|
||||
if (!find_package_args.empty()) {
|
||||
find_package_args += ' ';
|
||||
}
|
||||
find_package_args += tok;
|
||||
}
|
||||
if (find_package_args.empty()) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionUnknownPackage,
|
||||
"vcpkg usage file has empty find_package() args"));
|
||||
}
|
||||
|
||||
// If REQUIRED isn't present, add it — generated CMake always wants it.
|
||||
if (find_package_args.find("REQUIRED") == std::string::npos) {
|
||||
find_package_args += " REQUIRED";
|
||||
}
|
||||
|
||||
// Targets: scrape the first target_link_libraries(...) call.
|
||||
std::vector<std::string> targets;
|
||||
if (auto tll_args = extract_call_args(text, "target_link_libraries")) {
|
||||
targets = extract_link_targets(*tll_args);
|
||||
}
|
||||
|
||||
// Stable-dedup.
|
||||
std::vector<std::string> deduped;
|
||||
for (auto& t : targets) {
|
||||
if (std::ranges::find(deduped, t) == deduped.end()) {
|
||||
deduped.push_back(std::move(t));
|
||||
}
|
||||
}
|
||||
|
||||
return VcpkgRecipe{
|
||||
.find_package = std::move(find_package_args),
|
||||
.targets = std::move(deduped),
|
||||
};
|
||||
}
|
||||
|
||||
auto vcpkg_probe(const std::string& name) -> util::Result<VcpkgRecipe> {
|
||||
if (name.empty()) {
|
||||
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
|
||||
"package name is empty"));
|
||||
}
|
||||
auto url = std::format(
|
||||
"https://raw.githubusercontent.com/microsoft/vcpkg/master/ports/{}/usage",
|
||||
name);
|
||||
auto r = exec::run("curl", {"-fsSL", "--max-time", "30", url},
|
||||
exec::ExecOptions{
|
||||
.cwd = {},
|
||||
.env_overrides = {},
|
||||
.timeout = std::chrono::seconds{40},
|
||||
.inherit_stdio = false,
|
||||
});
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (r->exit_code == 22) {
|
||||
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
|
||||
std::format("vcpkg has no port '{}'", name)));
|
||||
}
|
||||
if (r->exit_code != 0) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("curl failed (exit {}): {}", r->exit_code, r->stderr_text)));
|
||||
}
|
||||
return parse_vcpkg_usage(r->stdout_text);
|
||||
}
|
||||
|
||||
} // namespace cargoxx::resolver
|
||||
Reference in New Issue
Block a user