[M5+] add resolver::vcpkg_probe

This commit is contained in:
2026-05-10 10:23:57 +00:00
parent e5c173b466
commit 941d5b3284
8 changed files with 319 additions and 2 deletions

View File

@@ -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

View 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