[M5+] add resolver::conan_probe
This commit is contained in:
192
src/resolver/conan_probe.cpp
Normal file
192
src/resolver/conan_probe.cpp
Normal file
@@ -0,0 +1,192 @@
|
||||
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 *value* (in quotes) of a key in patterns like
|
||||
// set_property("cmake_target_name", "fmt::fmt")
|
||||
// names["cmake_find_package"] = "fmt"
|
||||
//
|
||||
// `key_pattern` is a substring guaranteed to appear right before the value
|
||||
// (with quotes/punctuation between). We then look for the next quoted
|
||||
// string after `key_pattern`'s position.
|
||||
auto extract_quoted_after(std::string_view text, std::string_view key_pattern)
|
||||
-> std::optional<std::string> {
|
||||
auto pos = text.find(key_pattern);
|
||||
if (pos == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
pos += key_pattern.size();
|
||||
// Scan forward looking for the *next* quoted string.
|
||||
while (pos < text.size()) {
|
||||
char c = text[pos];
|
||||
if (c == '"' || c == '\'') {
|
||||
char quote = c;
|
||||
++pos;
|
||||
std::string out;
|
||||
while (pos < text.size() && text[pos] != quote) {
|
||||
out += text[pos++];
|
||||
}
|
||||
if (pos < text.size()) {
|
||||
return out;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
// newline → bail; the value is on this line per Conan convention
|
||||
if (c == '\n') {
|
||||
return std::nullopt;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto extract_set_property(std::string_view text, std::string_view property_name)
|
||||
-> std::optional<std::string> {
|
||||
auto needle = std::format("set_property(\"{}\"", property_name);
|
||||
auto v = extract_quoted_after(text, needle);
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
needle = std::format("set_property('{}'", property_name);
|
||||
return extract_quoted_after(text, needle);
|
||||
}
|
||||
|
||||
auto extract_names_legacy(std::string_view text, std::string_view legacy_key)
|
||||
-> std::optional<std::string> {
|
||||
auto needle = std::format("names[\"{}\"]", legacy_key);
|
||||
auto v = extract_quoted_after(text, needle);
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
needle = std::format("names['{}']", legacy_key);
|
||||
return extract_quoted_after(text, needle);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto parse_conanfile(std::string_view text, const std::string& fallback_name)
|
||||
-> util::Result<ConanRecipe> {
|
||||
auto target = extract_set_property(text, "cmake_target_name");
|
||||
auto file = extract_set_property(text, "cmake_file_name");
|
||||
|
||||
if (!target) {
|
||||
target = extract_names_legacy(text, "cmake_find_package");
|
||||
}
|
||||
if (!file) {
|
||||
file = extract_names_legacy(text, "cmake_find_package");
|
||||
}
|
||||
|
||||
// If only one of the two surfaces, derive the other heuristically:
|
||||
// cmake_target_name "fmt::fmt" → cmake_file_name "fmt"
|
||||
// cmake_file_name "fmt" → cmake_target_name "fmt::fmt"
|
||||
if (target && !file) {
|
||||
auto t = *target;
|
||||
if (auto cc = t.find("::"); cc != std::string::npos) {
|
||||
file = t.substr(0, cc);
|
||||
} else {
|
||||
file = t;
|
||||
}
|
||||
}
|
||||
if (file && !target) {
|
||||
target = std::format("{}::{}", *file, *file);
|
||||
}
|
||||
|
||||
if (!target || !file) {
|
||||
// Last resort: fall back to the package name itself.
|
||||
if (!fallback_name.empty()) {
|
||||
file = fallback_name;
|
||||
target = std::format("{}::{}", fallback_name, fallback_name);
|
||||
} else {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionUnknownPackage,
|
||||
"conanfile.py contained no cmake_target_name or cmake_file_name"));
|
||||
}
|
||||
}
|
||||
|
||||
// Conan recipes often parameterize the target via Python f-strings:
|
||||
// f"fmt::{target}" or f"{name}::{component}"
|
||||
// We can't evaluate Python — substitute every `{...}` placeholder with
|
||||
// the cmake_file_name (or fallback) which is the canonical target stem
|
||||
// for the vast majority of recipes.
|
||||
auto substitute_braces = [&](std::string s) -> std::string {
|
||||
const auto& sub = !file->empty() ? *file : fallback_name;
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
std::size_t i = 0;
|
||||
while (i < s.size()) {
|
||||
if (s[i] == '{') {
|
||||
auto close = s.find('}', i + 1);
|
||||
if (close == std::string::npos) {
|
||||
out += s.substr(i);
|
||||
break;
|
||||
}
|
||||
out += sub;
|
||||
i = close + 1;
|
||||
} else {
|
||||
out += s[i++];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
*target = substitute_braces(*target);
|
||||
*file = substitute_braces(*file);
|
||||
|
||||
// Normalize bare names to the conventional `<file>::<target>` form so
|
||||
// `target_link_libraries` actually resolves. Legacy Conan recipes set
|
||||
// `names["cmake_find_package"] = "spdlog"` for both fields, leaving us
|
||||
// with a bare "spdlog" target that CMake won't accept as an imported
|
||||
// target reference.
|
||||
if (target->find("::") == std::string::npos) {
|
||||
target = std::format("{}::{}", *file, *target);
|
||||
}
|
||||
|
||||
return ConanRecipe{
|
||||
.find_package = std::format("{} CONFIG REQUIRED", *file),
|
||||
.targets = {*target},
|
||||
};
|
||||
}
|
||||
|
||||
auto conan_probe(const std::string& name) -> util::Result<ConanRecipe> {
|
||||
if (name.empty()) {
|
||||
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
|
||||
"package name is empty"));
|
||||
}
|
||||
auto url = std::format(
|
||||
"https://raw.githubusercontent.com/conan-io/conan-center-index/"
|
||||
"master/recipes/{}/all/conanfile.py",
|
||||
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) {
|
||||
// curl --fail returns 22 on HTTP 4xx — treat 404s as "not in conan".
|
||||
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
|
||||
std::format("conan-center has no recipe for '{}'",
|
||||
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_conanfile(r->stdout_text, name);
|
||||
}
|
||||
|
||||
} // namespace cargoxx::resolver
|
||||
@@ -51,4 +51,24 @@ auto nix_cmake_scan(const std::filesystem::path& store_path,
|
||||
const std::string& package_name)
|
||||
-> util::Result<NixCmakeCandidate>;
|
||||
|
||||
// Output of a conan-center-index recipe scrape.
|
||||
struct ConanRecipe {
|
||||
std::string find_package; // e.g. "fmt CONFIG REQUIRED"
|
||||
std::vector<std::string> targets; // e.g. ["fmt::fmt"]
|
||||
};
|
||||
|
||||
// Pure: scrapes a conanfile.py text for `cmake_target_name` and
|
||||
// `cmake_file_name`. Handles both the modern
|
||||
// `cpp_info.set_property("cmake_target_name", "...")` form and the
|
||||
// legacy `cpp_info.names["cmake_find_package"] = "..."` form. Returns
|
||||
// ResolutionUnknownPackage when no recognizable recipe is found.
|
||||
auto parse_conanfile(std::string_view conanfile_text, const std::string& fallback_name)
|
||||
-> util::Result<ConanRecipe>;
|
||||
|
||||
// Fetches https://raw.githubusercontent.com/conan-io/conan-center-index/
|
||||
// master/recipes/<name>/all/conanfile.py via `curl` and feeds it through
|
||||
// parse_conanfile. 404 → ResolutionUnknownPackage; transport errors →
|
||||
// ResolutionNetworkError.
|
||||
auto conan_probe(const std::string& name) -> util::Result<ConanRecipe>;
|
||||
|
||||
} // namespace cargoxx::resolver
|
||||
|
||||
Reference in New Issue
Block a user