240 lines
11 KiB
C++
240 lines
11 KiB
C++
export module cargoxx.resolver;
|
|
|
|
import std;
|
|
import cargoxx.util;
|
|
import cargoxx.exec;
|
|
import cargoxx.linkdb;
|
|
import cargoxx.manifest;
|
|
|
|
export namespace cargoxx::resolver {
|
|
|
|
// What `nix eval nixpkgs#<pkg>` reports for a package: a confirmation that
|
|
// the attribute exists, a best-effort version string, and the realized
|
|
// nix-store path(s) so later probes can scan its installed CMake configs.
|
|
//
|
|
// Multi-output packages (boost, openssl, llvm, ...) expose CMake configs
|
|
// in their `dev` output, not in the default `out`. When the package has
|
|
// a separate dev output its store path is captured here so callers can
|
|
// scan it preferentially.
|
|
struct NixpkgsInfo {
|
|
std::string attr; // the queried name, e.g. "simdjson"
|
|
std::string version; // empty when the derivation has no version
|
|
std::string out_path; // default output's absolute /nix/store/... path
|
|
std::string dev_path; // dev output's path; empty when no dev output
|
|
};
|
|
|
|
// Pure parser exposed for unit testing. Accepts the raw JSON returned by
|
|
// `nix eval --json --apply 'p: { ... }'` and extracts NixpkgsInfo.
|
|
auto parse_nix_eval_json(std::string_view attr, std::string_view json)
|
|
-> util::Result<NixpkgsInfo>;
|
|
|
|
// Runs `nix eval nixpkgs#<attr> --json --apply ...` via exec::run. Returns
|
|
// `ResolutionUnknownPackage` when the attribute is missing,
|
|
// `ResolutionNetworkError` on timeout or evaluator errors.
|
|
auto nixpkgs_probe(const std::string& attr) -> util::Result<NixpkgsInfo>;
|
|
|
|
// Materializes a flake attribute on disk by shelling out to
|
|
// nix build --no-link --print-out-paths nixpkgs#<flake_attr>
|
|
// and returns the absolute store path. The eval-only `nixpkgs_probe`
|
|
// computes the path the output *would* have, but it isn't actually
|
|
// present on disk until it's been built / fetched from the binary
|
|
// cache; nix_cmake_scan needs it on disk to walk lib/cmake. Use
|
|
// `realize_path("libclang.dev")` to scan a dev output, etc.
|
|
//
|
|
// `ResolutionUnknownPackage` for unknown attrs, `ResolutionNetworkError`
|
|
// for build / network errors.
|
|
auto realize_path(const std::string& flake_attr) -> util::Result<std::string>;
|
|
|
|
// One CMake config-file's IMPORTED targets together with the find_package
|
|
// expression derived from its filename stem.
|
|
struct NixCmakeCandidate {
|
|
std::string find_package; // e.g. "fmt CONFIG REQUIRED"
|
|
std::vector<std::string> targets; // e.g. ["fmt::fmt"]
|
|
std::filesystem::path config_file; // the *Config.cmake we scraped
|
|
};
|
|
|
|
// Pure: scan a single CMake config text for `add_library(... IMPORTED)`
|
|
// target names. ALIAS targets are also collected so canonical
|
|
// `<alias>::<member>` forms get picked up.
|
|
auto scan_imported_targets(std::string_view config_text) -> std::vector<std::string>;
|
|
|
|
// Pure: pick the public-API subset out of a freshly scanned target
|
|
// list. See nix_cmake_scan.cpp for the rule (namespaced > stem-named
|
|
// umbrella > pass-through). Exported so unit tests can drive it
|
|
// without scaffolding an on-disk store.
|
|
auto filter_public_targets(std::vector<std::string> targets,
|
|
std::string_view stem) -> std::vector<std::string>;
|
|
|
|
// Pure: turn a CMake config filename into the find_package name.
|
|
// `fmtConfig.cmake` / `fmt-config.cmake` -> `fmt`.
|
|
auto config_stem_to_package(std::string_view filename) -> std::string;
|
|
|
|
// Walks <store_path>/lib/cmake/** for *Config.cmake / *-config.cmake files.
|
|
// Picks the candidate whose derived package name best matches `package_name`
|
|
// (exact case-insensitive equality > prefix > first non-empty target list).
|
|
// Returns ResolutionUnknownPackage when nothing usable is found.
|
|
auto nix_cmake_scan(const std::filesystem::path& store_path,
|
|
const std::string& package_name)
|
|
-> util::Result<NixCmakeCandidate>;
|
|
|
|
// A pkg-config-shaped recipe: the package ships a `.pc` file rather
|
|
// than a CMake config. Consumed via `find_package(PkgConfig REQUIRED)`
|
|
// + `pkg_check_modules(<NAME> REQUIRED IMPORTED_TARGET <pc_module>)`,
|
|
// linking against the generated `PkgConfig::<NAME>` target.
|
|
struct PcCandidate {
|
|
std::string pc_module; // file stem, e.g. "sqlite3"
|
|
std::filesystem::path pc_file; // path to the .pc on disk
|
|
};
|
|
|
|
// Walks <store_path>/lib/pkgconfig/*.pc and picks the best match for
|
|
// `package_name`. Returns ResolutionUnknownPackage when no `.pc` file
|
|
// is present or none scores acceptably.
|
|
auto pc_scan(const std::filesystem::path& store_path,
|
|
const std::string& package_name)
|
|
-> util::Result<PcCandidate>;
|
|
|
|
// 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>;
|
|
|
|
// 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>;
|
|
|
|
// Caller-supplied closure that runs `cargoxx build` (or any equivalent
|
|
// build) on a project rooted at the given path. Injected so the resolver
|
|
// stays decoupled from `cargoxx.cli`.
|
|
using BuildFn =
|
|
std::function<util::Result<void>(const std::filesystem::path& project_root)>;
|
|
|
|
struct VerifyLinkRequest {
|
|
linkdb::Recipe candidate; // recipe under test
|
|
std::string source; // "conan" | "vcpkg" | "nix-probe"
|
|
std::string package_name;
|
|
std::string version_spec; // user-supplied spec (e.g. "*", "1.2")
|
|
std::vector<std::string> components;
|
|
std::filesystem::path overlay_path; // sqlite file we read/write
|
|
std::filesystem::path scratch_root; // parent dir for the tmp project
|
|
};
|
|
|
|
// Scaffolds a tiny Cargoxx project under `req.scratch_root`, writes a
|
|
// provisional overlay row pointing at `req.candidate`, runs `build_fn` on
|
|
// the project (typically `cli::cmd_build`), and depending on the build
|
|
// result either confirms the provisional row (verified_at = now) or
|
|
// deletes it. Cleans the tmp project regardless. Returns success only
|
|
// when the build itself succeeded.
|
|
auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
|
|
-> util::Result<void>;
|
|
|
|
// What discover() returns: the verified Recipe and a tag identifying
|
|
// which probe yielded it ("conan", "vcpkg", or "nix-probe").
|
|
struct Discovered {
|
|
linkdb::Recipe recipe;
|
|
std::string source;
|
|
};
|
|
|
|
// Walks the full auto-resolution chain for a package not present in the
|
|
// curated linkdb or the user's overlay:
|
|
// 1. nixpkgs_probe(name) — confirms the attribute exists, captures
|
|
// version + out_path
|
|
// 2. for each of conan_probe, vcpkg_probe, nix_cmake_scan(out_path,…):
|
|
// build a candidate linkdb::Recipe, run verify_link on it, return
|
|
// on first success
|
|
// 3. all candidates failed → ResolutionUnsatisfiable
|
|
auto discover(const std::string& name, const std::string& version_spec,
|
|
const std::vector<std::string>& components,
|
|
const std::filesystem::path& overlay_path,
|
|
const std::filesystem::path& scratch_root, const BuildFn& build_fn)
|
|
-> util::Result<Discovered>;
|
|
|
|
// Output of devbox's /v1/resolve API. We capture only the fields cargoxx
|
|
// uses; the response carries far more metadata (license, summary, per-
|
|
// system store hashes) that we deliberately ignore.
|
|
struct DevboxResolution {
|
|
std::string name;
|
|
std::string version;
|
|
std::string commit_hash;
|
|
std::vector<std::string> attr_paths;
|
|
};
|
|
|
|
// Pure: parse the JSON body of GET https://search.devbox.sh/v1/resolve.
|
|
// `commit_hash` must be present and non-empty; other fields tolerate
|
|
// absence by leaving themselves blank.
|
|
auto parse_devbox_resolve(std::string_view json)
|
|
-> util::Result<DevboxResolution>;
|
|
|
|
// Calls GET https://search.devbox.sh/v1/resolve?name=<n>&version=<v>
|
|
// via curl. 404 → ResolutionUnknownPackage; transport / parse errors →
|
|
// ResolutionNetworkError.
|
|
auto devbox_resolve(const std::string& name, const std::string& version)
|
|
-> util::Result<DevboxResolution>;
|
|
|
|
// Pure: parse the output of
|
|
// git log --all -S 'version = "<v>"' --pretty='%H %ct' -- pkgs/
|
|
// (one commit per line: "<40-char-sha> <unix-epoch-seconds>") and
|
|
// return the youngest matching SHA, or nullopt on empty input.
|
|
auto pick_youngest_commit(std::string_view git_log_output)
|
|
-> std::optional<std::string>;
|
|
|
|
// Searches a local nixpkgs clone for the youngest commit that
|
|
// introduced `version = "<version>"` under pkgs/. `repo_path`
|
|
// defaults to ~/.cache/cargoxx/nixpkgs/ — when the directory doesn't
|
|
// exist the repo is cloned lazily (multi-GB, only on first miss).
|
|
// 404-equivalent: ResolutionVersionNotFound.
|
|
auto nixpkgs_git_resolve(const std::string& name, const std::string& version,
|
|
std::optional<std::filesystem::path> repo_path = std::nullopt)
|
|
-> util::Result<std::string>;
|
|
|
|
// What `resolve_version` returns: a nixpkgs commit and the attribute
|
|
// path under which the package lives at that commit. The attr is
|
|
// authoritative *for that rev* — the cargoxx-curated linkdb attrs
|
|
// (e.g. `fmt_10`) only apply to the unpinned `nixos-unstable` set.
|
|
// When devbox supplies multiple attr paths the first is canonical.
|
|
struct ResolvedVersion {
|
|
std::string nixpkgs_rev;
|
|
std::string nixpkgs_attr;
|
|
};
|
|
|
|
// Top-level orchestrator: try `devbox_resolve` first, then
|
|
// `nixpkgs_git_resolve` as fallback. Returns the rev + attr or
|
|
// `ResolutionVersionNotFound` when both probes come back empty.
|
|
//
|
|
// Use this from `cargoxx add <pkg>@<concrete-ver>` to capture the pin
|
|
// that lockfile/codegen will use. Wildcards (`*`, empty) should NOT
|
|
// be passed — they are not concrete versions and the resolver
|
|
// returns `ResolutionVersionNotFound` for them by design.
|
|
auto resolve_version(const std::string& name, const std::string& version)
|
|
-> util::Result<ResolvedVersion>;
|
|
|
|
} // namespace cargoxx::resolver
|