[M8] cargoxx-git dependencies: { git = ..., rev = ... } deps
This commit is contained in:
30
CHANGELOG.md
30
CHANGELOG.md
@@ -498,3 +498,33 @@ All notable changes to cargoxx will be documented in this file.
|
|||||||
validation + binary-cache substitution. Phase 1a (install rules)
|
validation + binary-cache substitution. Phase 1a (install rules)
|
||||||
and Phase 1b (path deps) shipped in this commit; phases 1c, 1d,
|
and Phase 1b (path deps) shipped in this commit; phases 1c, 1d,
|
||||||
and 2 remain to be built.
|
and 2 remain to be built.
|
||||||
|
- M8 cargoxx-git dependencies (`{ git = "<url>", rev = "<40-char>" }`).
|
||||||
|
The manifest accepts a third dep-table form:
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
mylib = { git = "https://gitea/me/mylib", rev = "abc…" }
|
||||||
|
```
|
||||||
|
Branch/tag refs are not yet supported — the rev must be a literal
|
||||||
|
40-char commit. `manifest::Dependency` carries `git_url` + `git_rev`;
|
||||||
|
parser branches on `git`, rejects git deps that omit `rev`.
|
||||||
|
Lockfile schema adds `source_git_url` + `source_git_commit` +
|
||||||
|
`source_git_sha256` (SRI form, e.g. `sha256-<base64>`). The sha256
|
||||||
|
pins the fetched source as a fixed-output derivation: `pkgs.fetchgit`
|
||||||
|
in the consumer's `buildCppPackage` substitutes from cache when the
|
||||||
|
same `(url, rev, sha256)` triple appears elsewhere.
|
||||||
|
`cmd_build::resolve_git_dep` reuses the lockfile's prior sha256 on
|
||||||
|
re-builds; on a fresh dep it calls
|
||||||
|
`resolver::prefetch_flake_source(git+<url>?rev=<rev>)` which now
|
||||||
|
returns `{store_path, hash}` (extended via a new
|
||||||
|
`PrefetchedSource` struct, with `realize_flake_source` kept as a
|
||||||
|
thin compat wrapper). Verifies the dep's name by reading the
|
||||||
|
fetched tree's `Cargoxx.toml`. In `--offline` mode, a git dep
|
||||||
|
without a cached hash errors with a clear "run online first"
|
||||||
|
message.
|
||||||
|
`flake.nix`'s `evalDep` branches on `source_kind == "cargoxx-git"`
|
||||||
|
and feeds `pkgs.fetchgit { url, rev, hash }` into a recursive
|
||||||
|
`buildCppPackage` call. The fetched source is content-addressed,
|
||||||
|
so the entire `(fetch → install)` closure is cacheable end-to-end.
|
||||||
|
Codegen unchanged — the synthesized recipe (find_package +
|
||||||
|
`<name>::<name>` target) is identical to the path-dep case, just
|
||||||
|
with a different `linkdb_source` discriminator.
|
||||||
|
|||||||
19
flake.nix
19
flake.nix
@@ -69,12 +69,19 @@
|
|||||||
(builtins.getFlake "github:NixOS/nixpkgs/${rev}")
|
(builtins.getFlake "github:NixOS/nixpkgs/${rev}")
|
||||||
.legacyPackages.${system};
|
.legacyPackages.${system};
|
||||||
|
|
||||||
# cargoxx-path deps recurse into buildCppPackage on the sibling
|
# cargoxx-source deps recurse into buildCppPackage; the result
|
||||||
# source tree; the result joins buildInputs so the consumer's
|
# joins buildInputs so the consumer's find_package(<dep> CONFIG
|
||||||
# find_package(<dep> CONFIG REQUIRED) resolves via CMAKE_PREFIX_PATH.
|
# REQUIRED) resolves via CMAKE_PREFIX_PATH.
|
||||||
evalDep = p:
|
evalDep = p:
|
||||||
if (p.source_kind or "") == "cargoxx-path" then
|
if (p.source_kind or "") == "cargoxx-path" then
|
||||||
buildCppPackage { src = src + ("/" + p.source_path); }
|
buildCppPackage { src = src + ("/" + p.source_path); }
|
||||||
|
else if (p.source_kind or "") == "cargoxx-git" then
|
||||||
|
let depSrc = pkgs.fetchgit {
|
||||||
|
url = p.source_git_url;
|
||||||
|
rev = p.source_git_commit;
|
||||||
|
hash = p.source_git_sha256;
|
||||||
|
};
|
||||||
|
in buildCppPackage { src = depSrc; }
|
||||||
else
|
else
|
||||||
let rev = if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
let rev = if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
||||||
then p.nixpkgs_rev
|
then p.nixpkgs_rev
|
||||||
@@ -97,9 +104,9 @@
|
|||||||
# For cargoxx-source deps we don't have a nixpkgs rev/attr — the
|
# For cargoxx-source deps we don't have a nixpkgs rev/attr — the
|
||||||
# vendor.toml entry just needs a name + store_path so cargoxx's
|
# vendor.toml entry just needs a name + store_path so cargoxx's
|
||||||
# offline pathway can find the dep's installed prefix.
|
# offline pathway can find the dep's installed prefix.
|
||||||
isPath = (p.source_kind or "") == "cargoxx-path";
|
isCargoxx = (p.source_kind or "") != "";
|
||||||
attr = if isPath then "" else p.nixpkgs_attr;
|
attr = if isCargoxx then "" else p.nixpkgs_attr;
|
||||||
rev = if isPath then ""
|
rev = if isCargoxx then ""
|
||||||
else if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
else if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
||||||
then p.nixpkgs_rev else lock.nixpkgs_rev;
|
then p.nixpkgs_rev else lock.nixpkgs_rev;
|
||||||
in ''
|
in ''
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ auto query_flake_rev(std::string_view flake_ref) -> std::optional<std::string> {
|
|||||||
auto merge_lockfile(const manifest::Manifest& m,
|
auto merge_lockfile(const manifest::Manifest& m,
|
||||||
const std::vector<linkdb::Recipe>& recipes,
|
const std::vector<linkdb::Recipe>& recipes,
|
||||||
const std::vector<linkdb::Recipe>& dev_recipes,
|
const std::vector<linkdb::Recipe>& dev_recipes,
|
||||||
const lockfile::Lockfile& prior) -> lockfile::Lockfile {
|
const lockfile::Lockfile& prior,
|
||||||
|
const std::map<std::string, std::string>& git_sha256s)
|
||||||
|
-> lockfile::Lockfile {
|
||||||
auto find_prior = [&](const std::string& name, const std::string& version)
|
auto find_prior = [&](const std::string& name, const std::string& version)
|
||||||
-> std::optional<lockfile::LockfilePackage> {
|
-> std::optional<lockfile::LockfilePackage> {
|
||||||
for (const auto& p : prior.packages) {
|
for (const auto& p : prior.packages) {
|
||||||
@@ -122,9 +124,19 @@ auto merge_lockfile(const manifest::Manifest& m,
|
|||||||
}
|
}
|
||||||
std::optional<std::string> source_kind;
|
std::optional<std::string> source_kind;
|
||||||
std::optional<std::string> source_path;
|
std::optional<std::string> source_path;
|
||||||
|
std::optional<std::string> source_git_url;
|
||||||
|
std::optional<std::string> source_git_commit;
|
||||||
|
std::optional<std::string> source_git_sha256;
|
||||||
if (dep.source == manifest::DepSource::CargoxxPath) {
|
if (dep.source == manifest::DepSource::CargoxxPath) {
|
||||||
source_kind = "cargoxx-path";
|
source_kind = "cargoxx-path";
|
||||||
source_path = dep.path;
|
source_path = dep.path;
|
||||||
|
} else if (dep.source == manifest::DepSource::CargoxxGit) {
|
||||||
|
source_kind = "cargoxx-git";
|
||||||
|
source_git_url = dep.git_url;
|
||||||
|
source_git_commit = dep.git_rev;
|
||||||
|
if (auto it = git_sha256s.find(dep.name); it != git_sha256s.end()) {
|
||||||
|
source_git_sha256 = it->second;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lock.packages.push_back(lockfile::LockfilePackage{
|
lock.packages.push_back(lockfile::LockfilePackage{
|
||||||
.name = dep.name,
|
.name = dep.name,
|
||||||
@@ -140,6 +152,9 @@ auto merge_lockfile(const manifest::Manifest& m,
|
|||||||
.brute_force_includes = rec.brute_force_includes,
|
.brute_force_includes = rec.brute_force_includes,
|
||||||
.source_kind = std::move(source_kind),
|
.source_kind = std::move(source_kind),
|
||||||
.source_path = std::move(source_path),
|
.source_path = std::move(source_path),
|
||||||
|
.source_git_url = std::move(source_git_url),
|
||||||
|
.source_git_commit = std::move(source_git_commit),
|
||||||
|
.source_git_sha256 = std::move(source_git_sha256),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
for (std::size_t i = 0; i < m.dependencies.size(); ++i) {
|
for (std::size_t i = 0; i < m.dependencies.size(); ++i) {
|
||||||
@@ -288,6 +303,70 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Side channel: (dep name) → SRI hash captured during git resolution,
|
||||||
|
// consumed by merge_lockfile to persist source_git_sha256.
|
||||||
|
std::map<std::string, std::string> git_sha256s;
|
||||||
|
|
||||||
|
// For { git = "...", rev = "..." } deps: if the prior lockfile already
|
||||||
|
// records an SRI hash for this (url, commit), reuse it. Otherwise run
|
||||||
|
// `nix flake prefetch` to fetch + hash the source tree (FOD-compatible),
|
||||||
|
// then verify the dep's Cargoxx.toml name matches.
|
||||||
|
auto resolve_git_dep = [&](const manifest::Dependency& dep)
|
||||||
|
-> util::Result<linkdb::Recipe> {
|
||||||
|
std::optional<std::string> cached_sha;
|
||||||
|
for (const auto& p : prior.packages) {
|
||||||
|
if (p.name == dep.name && p.source_kind == "cargoxx-git" &&
|
||||||
|
p.source_git_url == dep.git_url &&
|
||||||
|
p.source_git_commit == dep.git_rev && p.source_git_sha256) {
|
||||||
|
cached_sha = p.source_git_sha256;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cached_sha) {
|
||||||
|
if (offline) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::BuildCmakeFailed,
|
||||||
|
std::format("git dep '{}' has no cached hash and --offline "
|
||||||
|
"forbids network fetch", dep.name),
|
||||||
|
"run `cargoxx build` once online to populate Cargoxx.lock",
|
||||||
|
std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
auto flake_ref = std::format("git+{}?rev={}", *dep.git_url, *dep.git_rev);
|
||||||
|
auto prefetched = resolver::prefetch_flake_source(flake_ref);
|
||||||
|
if (!prefetched) {
|
||||||
|
return std::unexpected(prefetched.error());
|
||||||
|
}
|
||||||
|
// Verify name matches by reading the fetched tree's Cargoxx.toml.
|
||||||
|
auto dep_manifest = manifest::parse(
|
||||||
|
std::filesystem::path{prefetched->store_path} / "Cargoxx.toml");
|
||||||
|
if (!dep_manifest) {
|
||||||
|
return std::unexpected(dep_manifest.error());
|
||||||
|
}
|
||||||
|
if (dep_manifest->package.name != dep.name) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ManifestInvalidField,
|
||||||
|
std::format("git dep '{}' points to a project named '{}'",
|
||||||
|
dep.name, dep_manifest->package.name),
|
||||||
|
"rename the dep or use a repo whose [package].name matches",
|
||||||
|
std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cached_sha = prefetched->hash;
|
||||||
|
}
|
||||||
|
git_sha256s[dep.name] = *cached_sha;
|
||||||
|
return linkdb::Recipe{
|
||||||
|
.nixpkgs_attr = "",
|
||||||
|
.find_package = std::format("{} CONFIG REQUIRED", dep.name),
|
||||||
|
.targets = {std::format("{}::{}", dep.name, dep.name)},
|
||||||
|
.source = "cargoxx-git",
|
||||||
|
.pkg_config_module = std::nullopt,
|
||||||
|
.brute_force_libs = {},
|
||||||
|
.brute_force_includes = {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
auto resolve_list = [&](const std::vector<manifest::Dependency>& deps)
|
auto resolve_list = [&](const std::vector<manifest::Dependency>& deps)
|
||||||
-> util::Result<std::vector<linkdb::Recipe>> {
|
-> util::Result<std::vector<linkdb::Recipe>> {
|
||||||
std::vector<linkdb::Recipe> out;
|
std::vector<linkdb::Recipe> out;
|
||||||
@@ -301,6 +380,14 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
|||||||
out.push_back(std::move(*r));
|
out.push_back(std::move(*r));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (dep.source == manifest::DepSource::CargoxxGit) {
|
||||||
|
auto r = resolve_git_dep(dep);
|
||||||
|
if (!r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
out.push_back(std::move(*r));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (auto cached = recipe_from_lock(dep.name, dep.version_spec); cached) {
|
if (auto cached = recipe_from_lock(dep.name, dep.version_spec); cached) {
|
||||||
out.push_back(std::move(*cached));
|
out.push_back(std::move(*cached));
|
||||||
continue;
|
continue;
|
||||||
@@ -329,7 +416,7 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
|||||||
if (!dev_recipes) {
|
if (!dev_recipes) {
|
||||||
return std::unexpected(dev_recipes.error());
|
return std::unexpected(dev_recipes.error());
|
||||||
}
|
}
|
||||||
auto lock = merge_lockfile(*m, *recipes, *dev_recipes, prior);
|
auto lock = merge_lockfile(*m, *recipes, *dev_recipes, prior, git_sha256s);
|
||||||
|
|
||||||
std::optional<codegen::VendorIndex> vendor_index;
|
std::optional<codegen::VendorIndex> vendor_index;
|
||||||
if (offline) {
|
if (offline) {
|
||||||
|
|||||||
@@ -112,6 +112,15 @@ auto parse_package(const toml::table& tbl, const std::filesystem::path& path)
|
|||||||
if (auto v = tbl["source_path"].value<std::string>()) {
|
if (auto v = tbl["source_path"].value<std::string>()) {
|
||||||
pkg.source_path = *v;
|
pkg.source_path = *v;
|
||||||
}
|
}
|
||||||
|
if (auto v = tbl["source_git_url"].value<std::string>()) {
|
||||||
|
pkg.source_git_url = *v;
|
||||||
|
}
|
||||||
|
if (auto v = tbl["source_git_commit"].value<std::string>()) {
|
||||||
|
pkg.source_git_commit = *v;
|
||||||
|
}
|
||||||
|
if (auto v = tbl["source_git_sha256"].value<std::string>()) {
|
||||||
|
pkg.source_git_sha256 = *v;
|
||||||
|
}
|
||||||
|
|
||||||
return pkg;
|
return pkg;
|
||||||
}
|
}
|
||||||
@@ -227,6 +236,15 @@ auto write(const Lockfile& lock, const std::filesystem::path& path) -> util::Res
|
|||||||
if (p.source_path) {
|
if (p.source_path) {
|
||||||
tbl.insert_or_assign("source_path", *p.source_path);
|
tbl.insert_or_assign("source_path", *p.source_path);
|
||||||
}
|
}
|
||||||
|
if (p.source_git_url) {
|
||||||
|
tbl.insert_or_assign("source_git_url", *p.source_git_url);
|
||||||
|
}
|
||||||
|
if (p.source_git_commit) {
|
||||||
|
tbl.insert_or_assign("source_git_commit", *p.source_git_commit);
|
||||||
|
}
|
||||||
|
if (p.source_git_sha256) {
|
||||||
|
tbl.insert_or_assign("source_git_sha256", *p.source_git_sha256);
|
||||||
|
}
|
||||||
packages.push_back(std::move(tbl));
|
packages.push_back(std::move(tbl));
|
||||||
}
|
}
|
||||||
root.insert_or_assign("package", std::move(packages));
|
root.insert_or_assign("package", std::move(packages));
|
||||||
|
|||||||
@@ -18,10 +18,15 @@ struct LockfilePackage {
|
|||||||
std::optional<std::string> pkg_config_module;
|
std::optional<std::string> pkg_config_module;
|
||||||
std::vector<std::string> brute_force_libs;
|
std::vector<std::string> brute_force_libs;
|
||||||
std::vector<std::string> brute_force_includes;
|
std::vector<std::string> brute_force_includes;
|
||||||
// For cargoxx-source deps (not nixpkgs/linkdb-resolved). v1 supports
|
// For cargoxx-source deps (not nixpkgs/linkdb-resolved).
|
||||||
// "cargoxx-path"; "cargoxx-git" / "cargoxx-registry" land in 1c/1d.
|
// "cargoxx-path" → source_path only
|
||||||
|
// "cargoxx-git" → source_git_url + source_git_commit + source_git_sha256
|
||||||
|
// "cargoxx-registry" → (1d)
|
||||||
std::optional<std::string> source_kind;
|
std::optional<std::string> source_kind;
|
||||||
std::optional<std::string> source_path;
|
std::optional<std::string> source_path;
|
||||||
|
std::optional<std::string> source_git_url;
|
||||||
|
std::optional<std::string> source_git_commit;
|
||||||
|
std::optional<std::string> source_git_sha256;
|
||||||
|
|
||||||
bool operator==(const LockfilePackage&) const = default;
|
bool operator==(const LockfilePackage&) const = default;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export namespace cargoxx::manifest {
|
|||||||
enum class DepSource {
|
enum class DepSource {
|
||||||
Auto, // string form or { version = ... } only → existing resolver chain
|
Auto, // string form or { version = ... } only → existing resolver chain
|
||||||
CargoxxPath, // { path = "../foo" } → recursive cargoxx build
|
CargoxxPath, // { path = "../foo" } → recursive cargoxx build
|
||||||
|
CargoxxGit, // { git = "...", rev = "..." } → fetch + recursive cargoxx build
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Dependency {
|
struct Dependency {
|
||||||
@@ -16,6 +17,8 @@ struct Dependency {
|
|||||||
std::vector<std::string> components;
|
std::vector<std::string> components;
|
||||||
DepSource source = DepSource::Auto;
|
DepSource source = DepSource::Auto;
|
||||||
std::optional<std::string> path; // when source == CargoxxPath
|
std::optional<std::string> path; // when source == CargoxxPath
|
||||||
|
std::optional<std::string> git_url; // when source == CargoxxGit
|
||||||
|
std::optional<std::string> git_rev; // when source == CargoxxGit (40-char)
|
||||||
|
|
||||||
bool operator==(const Dependency&) const = default;
|
bool operator==(const Dependency&) const = default;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -153,9 +153,7 @@ auto parse_dependency(std::string name, const toml::node& value,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (const auto* tbl = value.as_table()) {
|
if (const auto* tbl = value.as_table()) {
|
||||||
// Path form takes precedence: { path = "../foo" } → cargoxx-source dep.
|
// Path form: { path = "../foo" } → cargoxx-source dep.
|
||||||
// Version is optional in the path form (defaulting to "*"); the dep's
|
|
||||||
// own Cargoxx.toml supplies the real version at resolve time.
|
|
||||||
if (auto path_str = (*tbl)["path"].value<std::string>()) {
|
if (auto path_str = (*tbl)["path"].value<std::string>()) {
|
||||||
dep.source = DepSource::CargoxxPath;
|
dep.source = DepSource::CargoxxPath;
|
||||||
dep.path = *path_str;
|
dep.path = *path_str;
|
||||||
@@ -167,12 +165,36 @@ auto parse_dependency(std::string name, const toml::node& value,
|
|||||||
return dep;
|
return dep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Git form: { git = "...", rev = "<40-char>" } → cargoxx-source dep
|
||||||
|
// fetched at the pinned commit. Branch/tag resolution is not yet
|
||||||
|
// supported; require a literal commit.
|
||||||
|
if (auto git_url = (*tbl)["git"].value<std::string>()) {
|
||||||
|
auto rev = (*tbl)["rev"].value<std::string>();
|
||||||
|
if (!rev) {
|
||||||
|
return std::unexpected(err(
|
||||||
|
ErrorCode::ManifestInvalidField,
|
||||||
|
std::format("git dependency '{}' must specify a 'rev' (40-char commit)",
|
||||||
|
dep.name),
|
||||||
|
path, source_pos(value)));
|
||||||
|
}
|
||||||
|
dep.source = DepSource::CargoxxGit;
|
||||||
|
dep.git_url = *git_url;
|
||||||
|
dep.git_rev = *rev;
|
||||||
|
if (auto v = (*tbl)["version"].value<std::string>()) {
|
||||||
|
dep.version_spec = *v;
|
||||||
|
} else {
|
||||||
|
dep.version_spec = "*";
|
||||||
|
}
|
||||||
|
return dep;
|
||||||
|
}
|
||||||
|
|
||||||
if (auto v = (*tbl)["version"].value<std::string>()) {
|
if (auto v = (*tbl)["version"].value<std::string>()) {
|
||||||
dep.version_spec = *v;
|
dep.version_spec = *v;
|
||||||
} else {
|
} else {
|
||||||
return std::unexpected(err(
|
return std::unexpected(err(
|
||||||
ErrorCode::ManifestInvalidField,
|
ErrorCode::ManifestInvalidField,
|
||||||
std::format("dependency '{}' table must have a 'version' or 'path' string",
|
std::format(
|
||||||
|
"dependency '{}' table must have one of: 'version', 'path', 'git'",
|
||||||
dep.name),
|
dep.name),
|
||||||
path, source_pos(value)));
|
path, source_pos(value)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ auto build_table(const Manifest& m) -> toml::table {
|
|||||||
dep_tbl.insert_or_assign("path", *dep.path);
|
dep_tbl.insert_or_assign("path", *dep.path);
|
||||||
dep_tbl.is_inline(true);
|
dep_tbl.is_inline(true);
|
||||||
out.insert_or_assign(dep.name, std::move(dep_tbl));
|
out.insert_or_assign(dep.name, std::move(dep_tbl));
|
||||||
|
} else if (dep.source == DepSource::CargoxxGit) {
|
||||||
|
toml::table dep_tbl;
|
||||||
|
dep_tbl.insert_or_assign("git", *dep.git_url);
|
||||||
|
dep_tbl.insert_or_assign("rev", *dep.git_rev);
|
||||||
|
dep_tbl.is_inline(true);
|
||||||
|
out.insert_or_assign(dep.name, std::move(dep_tbl));
|
||||||
} else if (dep.components.empty()) {
|
} else if (dep.components.empty()) {
|
||||||
out.insert_or_assign(dep.name, dep.version_spec);
|
out.insert_or_assign(dep.name, dep.version_spec);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -207,12 +207,31 @@ auto realize_path_at_rev(const std::string& rev, const std::string& attr)
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto realize_flake_source(const std::string& flake_ref)
|
namespace {
|
||||||
-> util::Result<std::string> {
|
|
||||||
|
auto extract_json_string(std::string_view body, std::string_view key)
|
||||||
|
-> std::optional<std::string> {
|
||||||
|
auto needle = std::format("\"{}\":\"", key);
|
||||||
|
auto pos = body.find(needle);
|
||||||
|
if (pos == std::string_view::npos) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
pos += needle.size();
|
||||||
|
auto end = body.find('"', pos);
|
||||||
|
if (end == std::string_view::npos) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return std::string{body.substr(pos, end - pos)};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto prefetch_flake_source(const std::string& flake_ref)
|
||||||
|
-> util::Result<PrefetchedSource> {
|
||||||
if (flake_ref.empty()) {
|
if (flake_ref.empty()) {
|
||||||
return std::unexpected(make_error(
|
return std::unexpected(make_error(
|
||||||
util::ErrorCode::ResolutionUnknownPackage,
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
"realize_flake_source: flake_ref is empty"));
|
"prefetch_flake_source: flake_ref is empty"));
|
||||||
}
|
}
|
||||||
std::vector<std::string> args{
|
std::vector<std::string> args{
|
||||||
"--extra-experimental-features", "nix-command flakes",
|
"--extra-experimental-features", "nix-command flakes",
|
||||||
@@ -234,23 +253,33 @@ auto realize_flake_source(const std::string& flake_ref)
|
|||||||
std::format("nix flake prefetch failed (exit {}): {}",
|
std::format("nix flake prefetch failed (exit {}): {}",
|
||||||
r->exit_code, r->stderr_text)));
|
r->exit_code, r->stderr_text)));
|
||||||
}
|
}
|
||||||
std::string_view body = r->stdout_text;
|
auto store_path = extract_json_string(r->stdout_text, "storePath");
|
||||||
constexpr std::string_view key = "\"storePath\":\"";
|
auto hash = extract_json_string(r->stdout_text, "hash");
|
||||||
auto pos = body.find(key);
|
if (!store_path) {
|
||||||
if (pos == std::string_view::npos) {
|
|
||||||
return std::unexpected(make_error(
|
return std::unexpected(make_error(
|
||||||
util::ErrorCode::ResolutionNetworkError,
|
util::ErrorCode::ResolutionNetworkError,
|
||||||
std::format("nix flake prefetch emitted no storePath for '{}'",
|
std::format("nix flake prefetch emitted no storePath for '{}'",
|
||||||
flake_ref)));
|
flake_ref)));
|
||||||
}
|
}
|
||||||
pos += key.size();
|
if (!hash) {
|
||||||
auto end = body.find('"', pos);
|
|
||||||
if (end == std::string_view::npos) {
|
|
||||||
return std::unexpected(make_error(
|
return std::unexpected(make_error(
|
||||||
util::ErrorCode::ResolutionNetworkError,
|
util::ErrorCode::ResolutionNetworkError,
|
||||||
"nix flake prefetch JSON malformed"));
|
std::format("nix flake prefetch emitted no hash for '{}'",
|
||||||
|
flake_ref)));
|
||||||
}
|
}
|
||||||
return std::string{body.substr(pos, end - pos)};
|
return PrefetchedSource{
|
||||||
|
.store_path = std::move(*store_path),
|
||||||
|
.hash = std::move(*hash),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto realize_flake_source(const std::string& flake_ref)
|
||||||
|
-> util::Result<std::string> {
|
||||||
|
auto r = prefetch_flake_source(flake_ref);
|
||||||
|
if (!r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
return std::move(r->store_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace cargoxx::resolver
|
} // namespace cargoxx::resolver
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ auto realize_path_at_rev(const std::string& rev, const std::string& attr)
|
|||||||
auto realize_flake_source(const std::string& flake_ref)
|
auto realize_flake_source(const std::string& flake_ref)
|
||||||
-> util::Result<std::string>;
|
-> util::Result<std::string>;
|
||||||
|
|
||||||
|
struct PrefetchedSource {
|
||||||
|
std::string store_path;
|
||||||
|
std::string hash; // SRI form, e.g. "sha256-<base64>"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Same as realize_flake_source but also returns the SRI hash so the
|
||||||
|
// caller can persist it in a lockfile and feed it to `pkgs.fetchgit`
|
||||||
|
// as a fixed-output derivation pin. Used by cargoxx-git deps.
|
||||||
|
auto prefetch_flake_source(const std::string& flake_ref)
|
||||||
|
-> util::Result<PrefetchedSource>;
|
||||||
|
|
||||||
// One CMake config-file's IMPORTED targets together with the find_package
|
// One CMake config-file's IMPORTED targets together with the find_package
|
||||||
// expression derived from its filename stem.
|
// expression derived from its filename stem.
|
||||||
struct NixCmakeCandidate {
|
struct NixCmakeCandidate {
|
||||||
|
|||||||
@@ -142,6 +142,33 @@ TEST_CASE("write round-trips cargoxx-path source fields", "[lockfile]") {
|
|||||||
REQUIRE(round_trip(l) == l);
|
REQUIRE(round_trip(l) == l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips cargoxx-git source fields", "[lockfile]") {
|
||||||
|
Lockfile l{
|
||||||
|
.version = 1,
|
||||||
|
.packages = {
|
||||||
|
LockfilePackage{
|
||||||
|
.name = "mylib",
|
||||||
|
.version = "*",
|
||||||
|
.dependencies = {},
|
||||||
|
.nixpkgs_attr = std::nullopt,
|
||||||
|
.nixpkgs_rev = std::nullopt,
|
||||||
|
.linkdb_source = "cargoxx-git",
|
||||||
|
.find_package = "mylib CONFIG REQUIRED",
|
||||||
|
.targets = {"mylib::mylib"},
|
||||||
|
.pkg_config_module = std::nullopt,
|
||||||
|
.brute_force_libs = {},
|
||||||
|
.brute_force_includes = {},
|
||||||
|
.source_kind = "cargoxx-git",
|
||||||
|
.source_path = std::nullopt,
|
||||||
|
.source_git_url = "https://gitea.example/me/mylib",
|
||||||
|
.source_git_commit = "0123456789012345678901234567890123456789",
|
||||||
|
.source_git_sha256 = "sha256-abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123=",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
REQUIRE(round_trip(l) == l);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") {
|
TEST_CASE("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") {
|
||||||
Lockfile l{
|
Lockfile l{
|
||||||
.version = 1,
|
.version = 1,
|
||||||
|
|||||||
@@ -336,3 +336,40 @@ mylib = { components = ["a"] }
|
|||||||
REQUIRE_FALSE(r.has_value());
|
REQUIRE_FALSE(r.has_value());
|
||||||
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parse recognizes { git = \"...\", rev = \"...\" } as a cargoxx git dep",
|
||||||
|
"[manifest][parse]") {
|
||||||
|
auto p = write_manifest(R"(
|
||||||
|
[package]
|
||||||
|
name = "consumer"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "cpp23"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mylib = { git = "https://gitea.example/me/mylib", rev = "0123456789012345678901234567890123456789" }
|
||||||
|
)");
|
||||||
|
auto r = parse(p);
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->dependencies.size() == 1);
|
||||||
|
const auto& dep = r->dependencies[0];
|
||||||
|
REQUIRE(dep.name == "mylib");
|
||||||
|
REQUIRE(dep.source == cargoxx::manifest::DepSource::CargoxxGit);
|
||||||
|
REQUIRE(dep.git_url == "https://gitea.example/me/mylib");
|
||||||
|
REQUIRE(dep.git_rev == "0123456789012345678901234567890123456789");
|
||||||
|
REQUIRE(dep.version_spec == "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parse rejects git dep without rev", "[manifest][parse]") {
|
||||||
|
auto p = write_manifest(R"(
|
||||||
|
[package]
|
||||||
|
name = "consumer"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "cpp23"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mylib = { git = "https://gitea.example/me/mylib" }
|
||||||
|
)");
|
||||||
|
auto r = parse(p);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user