Files
cargoxx/src/resolver/resolver.cppm

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