Compare commits
7 Commits
f90bcfbff7
...
1f63984b60
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f63984b60 | |||
| 85417f317c | |||
| 43a7d1f09d | |||
| 7c10ea2382 | |||
| f62cff49c6 | |||
| 815e5b1be2 | |||
| db1c9eb36d |
57
CHANGELOG.md
57
CHANGELOG.md
@@ -341,3 +341,60 @@ All notable changes to cargoxx will be documented in this file.
|
||||
window. `tests/linkdb_overlay.cpp` covers 7 cases (insert/persist,
|
||||
override-curated, version-range gating, components rejection,
|
||||
move semantics).
|
||||
- M7 generated flake.nix moves to `build/flake.nix`. The project root
|
||||
belongs to the user — any hand-written `flake.nix` there is never
|
||||
overwritten by cargoxx. `cargoxx build` always invokes `nix develop`
|
||||
against `path:./build`.
|
||||
- M7 lockfile pins top-level `nixpkgs_rev` and `flake_utils_rev`. The
|
||||
generated flake's `inputs.nixpkgs.url` / `inputs.flake-utils.url` now
|
||||
use the pinned revs (falling back to the branch tips during the
|
||||
transitional first build before the lock is written). Per-package
|
||||
schema gains the full recipe (`find_package`, `targets`,
|
||||
`pkg_config_module`, `brute_force_libs`, `brute_force_includes`,
|
||||
`linkdb_source`) so the lockfile is a complete dependency-pinning
|
||||
artifact and `cmd_build`'s `recipe_from_lock` can short-circuit the
|
||||
linkdb entirely. `tests/lockfile_round_trip.cpp` extended.
|
||||
- M7 `codegen::VendorIndex` + `parse_vendor_toml` — new pure parser
|
||||
(`src/codegen/vendor.cpp`) returns a struct of
|
||||
`nixpkgs_store_path`, `flake_utils_store_path`, and a per-dep
|
||||
`nixpkgs_attr → store_path` map. `GenerateInputs` gains an optional
|
||||
`vendor` field; when set, `emit_inputs_block` emits `path:` inputs
|
||||
and drops the per-dep `github:` pins.
|
||||
- M7 new helpers in `cargoxx.resolver`:
|
||||
`realize_path_at_rev(rev, attr)` realizes
|
||||
`github:NixOS/nixpkgs/<rev>#<attr>` to a `/nix/store/...` path
|
||||
(used by `cmd_vendor`); `realize_flake_source(flake_ref)` returns
|
||||
the source store path via `nix flake prefetch --json` (used to pin
|
||||
`nixpkgs` and `flake-utils` for offline mode).
|
||||
- M7 `cargoxx vendor [--output <path>]` — new CLI verb. Reads
|
||||
`Cargoxx.lock`, realizes each locked dep at its pinned
|
||||
`(nixpkgs_rev, nixpkgs_attr)` into `/nix/store`, and writes
|
||||
`vendor.toml` (schema = 1) recording the resolved store paths for
|
||||
every dep plus the `nixpkgs` and `flake-utils` flake sources. The
|
||||
output is the input to `cargoxx build --offline`.
|
||||
- M7 `cargoxx build --offline [--vendor <path>]` — skips every network
|
||||
probe (Conan/vcpkg fuzzy, devbox, nixpkgs_git, linkdb auto-resolve),
|
||||
reads `vendor.toml` (default `./vendor.toml`), and emits
|
||||
`build/flake.nix` with literal `path:/nix/store/...` inputs for
|
||||
`nixpkgs`, `flake-utils`, and every dep. Offline mode also runs cmake
|
||||
directly (no outer `nix develop` wrapper) since all paths are already
|
||||
realized in the local store.
|
||||
- M7 `cargoxx.lib.buildCppPackage` — hermetic, sandbox-safe nix builder
|
||||
for downstream flakes. Mirrors `rustPlatform.buildRustPackage`'s
|
||||
ergonomics: a consumer flake passes `src` and gets a derivation. Reads
|
||||
`Cargoxx.lock` at outer eval time, resolves each dep's
|
||||
`(nixpkgs_rev, nixpkgs_attr)` via `builtins.getFlake` into concrete
|
||||
`/nix/store/...` paths, and synthesizes a `vendor.toml` via
|
||||
`pkgs.writeText` — no network or nested `nix` invocations inside any
|
||||
build phase. The single derivation runs `cargoxx build --release
|
||||
--offline --vendor <store-path>/vendor.toml`, which emits a hermetic
|
||||
`build/flake.nix` with literal `path:/nix/store/...` inputs and drives
|
||||
cmake directly. Works under the host's default sandbox (sandbox=true,
|
||||
non-trusted user, no `__noChroot`, no daemon escape). New e2e fixture
|
||||
at `tests/e2e/buildCppPackage/` with a `run.sh` smoke test that
|
||||
scaffolds the fixture in a tmp dir and runs `nix build .#default`
|
||||
end-to-end. Live verified: `Hello from world!` from a binary built
|
||||
entirely inside the standard nix sandbox.
|
||||
- Fix: `-Wparentheses` warning in `looks_like_missing_attribute`
|
||||
(`src/resolver/nixpkgs_probe.cpp:34`) — explicitly parenthesize the
|
||||
`&&` clause inside `||`.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
flake_utils_rev = '11707dc2f618dd54ca8739b309ec4fc024de578b'
|
||||
nixpkgs_rev = 'da5ad661ba4e5ef59ba743f0d112cbc30e474f32'
|
||||
version = 1
|
||||
|
||||
[[package]]
|
||||
@@ -6,19 +8,25 @@ name = 'cargoxx'
|
||||
version = '0.1.0'
|
||||
|
||||
[[package]]
|
||||
find_package = 'reproc CONFIG REQUIRED'
|
||||
linkdb_source = 'nix-probe'
|
||||
name = 'reproc'
|
||||
nixpkgs_attr = 'reproc'
|
||||
targets = [ 'reproc' ]
|
||||
version = '*'
|
||||
|
||||
[[package]]
|
||||
find_package = 'SQLite3 REQUIRED'
|
||||
linkdb_source = 'cmake-findmodule'
|
||||
name = 'sqlite'
|
||||
nixpkgs_attr = 'sqlite'
|
||||
targets = [ 'SQLite::SQLite3' ]
|
||||
version = '*'
|
||||
|
||||
[[package]]
|
||||
find_package = 'Catch2 CONFIG REQUIRED'
|
||||
linkdb_source = 'nix-probe'
|
||||
name = 'catch2_3'
|
||||
nixpkgs_attr = 'catch2_3'
|
||||
targets = [ 'Catch2::Catch2', 'Catch2::Catch2WithMain' ]
|
||||
version = '*'
|
||||
|
||||
@@ -15,7 +15,7 @@ set(CMAKE_CXX_EXTENSIONS ON)
|
||||
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
add_compile_options(-Wall -Wextra -Wpedantic -Wconversion)
|
||||
add_compile_options(-Wall -Wextra -Wpedantic -Wconversion -Wno-missing-field-initializers)
|
||||
|
||||
# ----- dependencies -----
|
||||
find_package(reproc CONFIG REQUIRED)
|
||||
@@ -46,9 +46,11 @@ target_sources(cargoxx
|
||||
../src/cli/cmd_remove.cpp
|
||||
../src/cli/cmd_run.cpp
|
||||
../src/cli/cmd_test.cpp
|
||||
../src/cli/cmd_vendor.cpp
|
||||
../src/cli/run.cpp
|
||||
../src/codegen/cmake.cpp
|
||||
../src/codegen/flake.cpp
|
||||
../src/codegen/vendor.cpp
|
||||
../src/exec/subprocess.cpp
|
||||
../src/layout/layout.cpp
|
||||
../src/linkdb/database.cpp
|
||||
|
||||
116
flake.nix
116
flake.nix
@@ -10,22 +10,108 @@
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
|
||||
cargoxx-bin = pkgs.gcc15Stdenv.mkDerivation {
|
||||
pname = "cargoxx";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
nativeBuildInputs = [ pkgs.cmake pkgs.ninja ];
|
||||
buildInputs = [ pkgs.sqlite pkgs.reproc pkgs.catch2_3 ];
|
||||
configurePhase = ''
|
||||
cmake -S build -B build/release -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
'';
|
||||
buildPhase = ''
|
||||
cmake --build build/release
|
||||
'';
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp build/release/cargoxx $out/bin/
|
||||
'';
|
||||
hardeningDisable = [ "all" ];
|
||||
};
|
||||
|
||||
buildCppPackage = { src, name ? null, ... }@args:
|
||||
let
|
||||
lock = builtins.fromTOML (builtins.readFile (src + "/Cargoxx.lock"));
|
||||
isDep = p: p ? linkdb_source;
|
||||
isRoot = p: !(isDep p);
|
||||
root = builtins.head (builtins.filter isRoot lock.package);
|
||||
depPkgs = builtins.filter isDep lock.package;
|
||||
pname = if name != null then name else root.name;
|
||||
|
||||
pkgsAt = rev:
|
||||
(builtins.getFlake "github:NixOS/nixpkgs/${rev}")
|
||||
.legacyPackages.${system};
|
||||
|
||||
evalDep = p:
|
||||
let rev = if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
||||
then p.nixpkgs_rev
|
||||
else lock.nixpkgs_rev;
|
||||
in (pkgsAt rev).${p.nixpkgs_attr};
|
||||
|
||||
depInputs = map evalDep depPkgs;
|
||||
|
||||
usesPkgConfig = builtins.any
|
||||
(p: (p.linkdb_source or "") == "pkg-config") depPkgs;
|
||||
|
||||
nixpkgsSource = (builtins.getFlake
|
||||
"github:NixOS/nixpkgs/${lock.nixpkgs_rev}").outPath;
|
||||
flakeUtilsSource = (builtins.getFlake
|
||||
"github:numtide/flake-utils/${lock.flake_utils_rev}").outPath;
|
||||
|
||||
mkDepTomlEntry = p:
|
||||
let
|
||||
derivation = evalDep p;
|
||||
rev = if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
||||
then p.nixpkgs_rev else lock.nixpkgs_rev;
|
||||
in ''
|
||||
[[dep]]
|
||||
name = "${p.name}"
|
||||
nixpkgs_attr = "${p.nixpkgs_attr}"
|
||||
nixpkgs_rev = "${rev}"
|
||||
store_path = "${derivation}"
|
||||
'';
|
||||
|
||||
vendorToml = pkgs.writeText "vendor.toml" (''
|
||||
schema = 1
|
||||
|
||||
[nixpkgs]
|
||||
rev = "${lock.nixpkgs_rev}"
|
||||
store_path = "${nixpkgsSource}"
|
||||
|
||||
[flake_utils]
|
||||
rev = "${lock.flake_utils_rev}"
|
||||
store_path = "${flakeUtilsSource}"
|
||||
'' + builtins.concatStringsSep "\n" (map mkDepTomlEntry depPkgs));
|
||||
in pkgs.gcc15Stdenv.mkDerivation {
|
||||
inherit pname src;
|
||||
version = root.version;
|
||||
nativeBuildInputs =
|
||||
[ cargoxx-bin pkgs.cmake pkgs.ninja ]
|
||||
++ pkgs.lib.optional usesPkgConfig pkgs.pkg-config;
|
||||
buildInputs = depInputs;
|
||||
dontConfigure = true;
|
||||
buildPhase = ''
|
||||
export HOME=$(mktemp -d)
|
||||
cargoxx build --release --offline --vendor ${vendorToml}
|
||||
'';
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp build/release/${pname} $out/bin/ 2>/dev/null || \
|
||||
cp build/release/${pname}_bin $out/bin/${pname}
|
||||
'';
|
||||
hardeningDisable = [ "all" ];
|
||||
};
|
||||
in {
|
||||
devShell = pkgs.gcc15Stdenv.mkDerivation {
|
||||
name = "shell";
|
||||
version = "1.0";
|
||||
nativeBuildInputs = [
|
||||
pkgs.ninja
|
||||
pkgs.cmake
|
||||
];
|
||||
buildInputs = [
|
||||
pkgs.reproc
|
||||
pkgs.sqlite
|
||||
pkgs.catch2_3
|
||||
];
|
||||
hardeningDisable = [
|
||||
"all"
|
||||
];
|
||||
packages.default = cargoxx-bin;
|
||||
lib.buildCppPackage = buildCppPackage;
|
||||
devShells.default = pkgs.gcc15Stdenv.mkDerivation {
|
||||
name = "cargoxx-dev";
|
||||
version = "0.1.0";
|
||||
nativeBuildInputs = [ pkgs.ninja pkgs.cmake ];
|
||||
buildInputs = [ pkgs.reproc pkgs.sqlite pkgs.catch2_3 ];
|
||||
hardeningDisable = [ "all" ];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,13 @@ auto cmd_new(const std::string& name, bool lib_only,
|
||||
// `overlay_path` lets tests redirect the linkdb overlay away from ~/.cache.
|
||||
auto cmd_build(const std::filesystem::path& project_root, bool no_build, bool release,
|
||||
std::optional<std::string> target = std::nullopt,
|
||||
std::optional<std::filesystem::path> overlay_path = std::nullopt)
|
||||
std::optional<std::filesystem::path> overlay_path = std::nullopt,
|
||||
bool offline = false,
|
||||
std::optional<std::filesystem::path> vendor = std::nullopt)
|
||||
-> util::Result<void>;
|
||||
|
||||
auto cmd_vendor(const std::filesystem::path& project_root,
|
||||
const std::filesystem::path& output)
|
||||
-> util::Result<void>;
|
||||
|
||||
// Builds the project, picks a binary target, and execs it with `args`.
|
||||
|
||||
@@ -35,6 +35,34 @@ auto write_text(const fs::path& path, std::string_view content) -> util::Result<
|
||||
return {};
|
||||
}
|
||||
|
||||
auto query_flake_rev(std::string_view flake_ref) -> std::optional<std::string> {
|
||||
auto r = exec::run("nix",
|
||||
{"--extra-experimental-features",
|
||||
"nix-command flakes", "flake", "metadata", "--json",
|
||||
std::string{flake_ref}},
|
||||
exec::ExecOptions{
|
||||
.cwd = fs::current_path(),
|
||||
.env_overrides = {},
|
||||
.timeout = std::chrono::seconds{30},
|
||||
.inherit_stdio = false,
|
||||
});
|
||||
if (!r || r->exit_code != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string_view body = r->stdout_text;
|
||||
constexpr std::string_view key = "\"rev\":\"";
|
||||
auto pos = body.find(key);
|
||||
if (pos == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
pos += key.size();
|
||||
auto end = body.find('"', pos);
|
||||
if (end == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::string{body.substr(pos, end - pos)};
|
||||
}
|
||||
|
||||
// Builds the lockfile from the manifest + resolved recipes, **preserving**
|
||||
// `nixpkgs_rev` for any (name, version) entry that already exists in
|
||||
// `prior` with a matching key. This is what makes `cargoxx build`
|
||||
@@ -59,6 +87,13 @@ auto merge_lockfile(const manifest::Manifest& m,
|
||||
|
||||
lockfile::Lockfile lock;
|
||||
lock.version = 1;
|
||||
lock.nixpkgs_rev_pin = prior.nixpkgs_rev_pin.has_value()
|
||||
? prior.nixpkgs_rev_pin
|
||||
: query_flake_rev("github:NixOS/nixpkgs/nixos-unstable");
|
||||
lock.flake_utils_rev_pin =
|
||||
prior.flake_utils_rev_pin.has_value()
|
||||
? prior.flake_utils_rev_pin
|
||||
: query_flake_rev("github:numtide/flake-utils");
|
||||
|
||||
lockfile::LockfilePackage root{
|
||||
.name = m.package.name,
|
||||
@@ -92,6 +127,11 @@ auto merge_lockfile(const manifest::Manifest& m,
|
||||
.nixpkgs_attr = std::move(attr),
|
||||
.nixpkgs_rev = std::move(rev),
|
||||
.linkdb_source = rec.source,
|
||||
.find_package = rec.find_package,
|
||||
.targets = rec.targets,
|
||||
.pkg_config_module = rec.pkg_config_module,
|
||||
.brute_force_libs = rec.brute_force_libs,
|
||||
.brute_force_includes = rec.brute_force_includes,
|
||||
});
|
||||
};
|
||||
for (std::size_t i = 0; i < m.dependencies.size(); ++i) {
|
||||
@@ -109,7 +149,9 @@ namespace {
|
||||
|
||||
auto run_nix_cmake(const fs::path& project_root, const std::vector<std::string>& cmake_args,
|
||||
std::string_view phase) -> util::Result<void> {
|
||||
std::vector<std::string> args{"develop", "--command", "cmake"};
|
||||
std::vector<std::string> args{"--extra-experimental-features",
|
||||
"nix-command flakes", "develop",
|
||||
"path:./build", "--command", "cmake"};
|
||||
args.insert(args.end(), cmake_args.begin(), cmake_args.end());
|
||||
|
||||
auto r = exec::run("nix", args, exec::ExecOptions{
|
||||
@@ -135,7 +177,8 @@ auto run_nix_cmake(const fs::path& project_root, const std::vector<std::string>&
|
||||
|
||||
auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
||||
std::optional<std::string> target,
|
||||
std::optional<fs::path> overlay_path) -> util::Result<void> {
|
||||
std::optional<fs::path> overlay_path, bool offline,
|
||||
std::optional<fs::path> vendor) -> util::Result<void> {
|
||||
auto manifest_path = project_root / "Cargoxx.toml";
|
||||
auto m = manifest::parse(manifest_path);
|
||||
if (!m) {
|
||||
@@ -176,11 +219,45 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
||||
return {};
|
||||
};
|
||||
|
||||
lockfile::Lockfile prior;
|
||||
if (std::error_code ec; std::filesystem::exists(project_root / "Cargoxx.lock", ec)) {
|
||||
if (auto r = lockfile::parse(project_root / "Cargoxx.lock"); r) {
|
||||
prior = std::move(*r);
|
||||
}
|
||||
}
|
||||
auto recipe_from_lock = [&](const std::string& name, const std::string& version)
|
||||
-> std::optional<linkdb::Recipe> {
|
||||
for (const auto& p : prior.packages) {
|
||||
if (p.name != name || p.version != version) {
|
||||
continue;
|
||||
}
|
||||
if (!p.find_package || p.targets.empty()) {
|
||||
if (p.brute_force_libs.empty() && p.brute_force_includes.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return linkdb::Recipe{
|
||||
.nixpkgs_attr = p.nixpkgs_attr.value_or(""),
|
||||
.find_package = p.find_package.value_or(""),
|
||||
.targets = p.targets,
|
||||
.source = p.linkdb_source.value_or(""),
|
||||
.pkg_config_module = p.pkg_config_module,
|
||||
.brute_force_libs = p.brute_force_libs,
|
||||
.brute_force_includes = p.brute_force_includes,
|
||||
};
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
auto resolve_list = [&](const std::vector<manifest::Dependency>& deps)
|
||||
-> util::Result<std::vector<linkdb::Recipe>> {
|
||||
std::vector<linkdb::Recipe> out;
|
||||
out.reserve(deps.size());
|
||||
for (const auto& dep : deps) {
|
||||
if (auto cached = recipe_from_lock(dep.name, dep.version_spec); cached) {
|
||||
out.push_back(std::move(*cached));
|
||||
continue;
|
||||
}
|
||||
auto r = db->resolve(dep.name, dep.version_spec, dep.components);
|
||||
if (!r && r.error().code == util::ErrorCode::LinkdbUnknownPackage) {
|
||||
if (auto resolved = auto_resolve(dep.name, dep.version_spec,
|
||||
@@ -205,15 +282,26 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
||||
if (!dev_recipes) {
|
||||
return std::unexpected(dev_recipes.error());
|
||||
}
|
||||
|
||||
lockfile::Lockfile prior;
|
||||
if (std::error_code ec; std::filesystem::exists(project_root / "Cargoxx.lock", ec)) {
|
||||
if (auto r = lockfile::parse(project_root / "Cargoxx.lock"); r) {
|
||||
prior = std::move(*r);
|
||||
}
|
||||
}
|
||||
auto lock = merge_lockfile(*m, *recipes, *dev_recipes, prior);
|
||||
|
||||
std::optional<codegen::VendorIndex> vendor_index;
|
||||
if (offline) {
|
||||
auto vendor_path = vendor.value_or(project_root / "vendor.toml");
|
||||
if (std::error_code v_ec; !fs::exists(vendor_path, v_ec)) {
|
||||
return std::unexpected(io_error(
|
||||
std::format("--offline requires vendor.toml; expected at '{}'",
|
||||
vendor_path.string()),
|
||||
vendor_path));
|
||||
}
|
||||
std::ifstream in_file{vendor_path};
|
||||
std::string body{std::istreambuf_iterator<char>(in_file), {}};
|
||||
auto parsed = codegen::parse_vendor_toml(body);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
vendor_index = std::move(*parsed);
|
||||
}
|
||||
|
||||
codegen::GenerateInputs in{
|
||||
.manifest = *m,
|
||||
.layout = *layout_result,
|
||||
@@ -221,6 +309,7 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
||||
.recipes = *recipes,
|
||||
.dev_recipes = *dev_recipes,
|
||||
.project_root = project_root,
|
||||
.vendor = vendor_index,
|
||||
};
|
||||
auto flake_text = codegen::flake_nix(in);
|
||||
auto cmake_text = codegen::cmake_lists(in);
|
||||
@@ -233,7 +322,7 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
||||
project_root / "build"));
|
||||
}
|
||||
|
||||
if (auto r = write_text(project_root / "flake.nix", flake_text); !r) {
|
||||
if (auto r = write_text(project_root / "build" / "flake.nix", flake_text); !r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (auto r = write_text(project_root / "build" / "CMakeLists.txt", cmake_text); !r) {
|
||||
@@ -257,16 +346,41 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
||||
"-G", "Ninja",
|
||||
std::format("-DCMAKE_BUILD_TYPE={}", profile_cap),
|
||||
};
|
||||
if (auto r = run_nix_cmake(project_root, configure_args, "configure"); !r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
|
||||
std::vector<std::string> build_args{"--build", build_dir};
|
||||
if (target) {
|
||||
build_args.push_back("--target");
|
||||
build_args.push_back(*target);
|
||||
}
|
||||
if (auto r = run_nix_cmake(project_root, build_args, "build"); !r) {
|
||||
|
||||
auto run_cmake = [&](const std::vector<std::string>& args,
|
||||
std::string_view phase) -> util::Result<void> {
|
||||
if (offline) {
|
||||
auto r = exec::run("cmake", args,
|
||||
exec::ExecOptions{
|
||||
.cwd = project_root,
|
||||
.env_overrides = {},
|
||||
.timeout = std::nullopt,
|
||||
.inherit_stdio = true,
|
||||
});
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (r->exit_code != 0) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::BuildCmakeFailed,
|
||||
std::format("cmake {} failed (exit {})", phase, r->exit_code),
|
||||
"", std::nullopt, std::nullopt,
|
||||
});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
return run_nix_cmake(project_root, args, phase);
|
||||
};
|
||||
|
||||
if (auto r = run_cmake(configure_args, "configure"); !r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (auto r = run_cmake(build_args, "build"); !r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ auto cmd_test(const fs::path& project_root, bool release,
|
||||
const auto build_dir = std::format("build/{}", profile);
|
||||
|
||||
auto r = exec::run("nix",
|
||||
{"develop", "--command", "ctest", "--test-dir", build_dir,
|
||||
"--output-on-failure"},
|
||||
{"--extra-experimental-features", "nix-command flakes",
|
||||
"develop", "path:./build", "--command", "ctest",
|
||||
"--test-dir", build_dir, "--output-on-failure"},
|
||||
exec::ExecOptions{
|
||||
.cwd = project_root,
|
||||
.env_overrides = {},
|
||||
|
||||
128
src/cli/cmd_vendor.cpp
Normal file
128
src/cli/cmd_vendor.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
module cargoxx.cli;
|
||||
|
||||
import std;
|
||||
import cargoxx.lockfile;
|
||||
import cargoxx.resolver;
|
||||
import cargoxx.util;
|
||||
|
||||
namespace cargoxx::cli {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
auto error(util::ErrorCode code, std::string msg, fs::path path) -> util::Error {
|
||||
return util::Error{code, std::move(msg), "", std::move(path), std::nullopt};
|
||||
}
|
||||
|
||||
auto escape_toml(std::string_view s) -> std::string {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 2);
|
||||
out += '"';
|
||||
for (char c : s) {
|
||||
if (c == '"' || c == '\\') {
|
||||
out += '\\';
|
||||
}
|
||||
out += c;
|
||||
}
|
||||
out += '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto cmd_vendor(const fs::path& project_root, const fs::path& output)
|
||||
-> util::Result<void> {
|
||||
auto lock_path = project_root / "Cargoxx.lock";
|
||||
auto lock = lockfile::parse(lock_path);
|
||||
if (!lock) {
|
||||
return std::unexpected(lock.error());
|
||||
}
|
||||
if (!lock->nixpkgs_rev_pin || lock->nixpkgs_rev_pin->empty()) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ManifestInvalidField,
|
||||
"Cargoxx.lock has no top-level nixpkgs_rev — run cargoxx build "
|
||||
"online first to pin it",
|
||||
lock_path));
|
||||
}
|
||||
if (!lock->flake_utils_rev_pin || lock->flake_utils_rev_pin->empty()) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ManifestInvalidField,
|
||||
"Cargoxx.lock has no top-level flake_utils_rev",
|
||||
lock_path));
|
||||
}
|
||||
|
||||
auto nixpkgs_src = resolver::realize_flake_source(
|
||||
std::format("github:NixOS/nixpkgs/{}", *lock->nixpkgs_rev_pin));
|
||||
if (!nixpkgs_src) {
|
||||
return std::unexpected(nixpkgs_src.error());
|
||||
}
|
||||
auto flake_utils_src = resolver::realize_flake_source(
|
||||
std::format("github:numtide/flake-utils/{}", *lock->flake_utils_rev_pin));
|
||||
if (!flake_utils_src) {
|
||||
return std::unexpected(flake_utils_src.error());
|
||||
}
|
||||
|
||||
std::string body;
|
||||
body += "schema = 1\n\n";
|
||||
body += "[nixpkgs]\n";
|
||||
body += std::format("rev = {}\n", escape_toml(*lock->nixpkgs_rev_pin));
|
||||
body += std::format("store_path = {}\n\n", escape_toml(*nixpkgs_src));
|
||||
body += "[flake_utils]\n";
|
||||
body += std::format("rev = {}\n", escape_toml(*lock->flake_utils_rev_pin));
|
||||
body += std::format("store_path = {}\n", escape_toml(*flake_utils_src));
|
||||
|
||||
for (const auto& p : lock->packages) {
|
||||
if (!p.linkdb_source) {
|
||||
continue;
|
||||
}
|
||||
if (!p.nixpkgs_attr) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ManifestInvalidField,
|
||||
std::format("lockfile dep '{}' has no nixpkgs_attr", p.name),
|
||||
lock_path));
|
||||
}
|
||||
auto rev = p.nixpkgs_rev.has_value() && !p.nixpkgs_rev->empty()
|
||||
? *p.nixpkgs_rev
|
||||
: *lock->nixpkgs_rev_pin;
|
||||
auto store = resolver::realize_path_at_rev(rev, *p.nixpkgs_attr);
|
||||
if (!store) {
|
||||
return std::unexpected(store.error());
|
||||
}
|
||||
std::optional<std::string> dev_path;
|
||||
if (auto d = resolver::realize_path_at_rev(
|
||||
rev, std::format("{}.dev", *p.nixpkgs_attr));
|
||||
d) {
|
||||
dev_path = *d;
|
||||
}
|
||||
|
||||
body += "\n[[dep]]\n";
|
||||
body += std::format("name = {}\n", escape_toml(p.name));
|
||||
body += std::format("nixpkgs_attr = {}\n", escape_toml(*p.nixpkgs_attr));
|
||||
body += std::format("nixpkgs_rev = {}\n", escape_toml(rev));
|
||||
body += std::format("store_path = {}\n", escape_toml(*store));
|
||||
if (dev_path) {
|
||||
body += std::format("dev_path = {}\n", escape_toml(*dev_path));
|
||||
}
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
fs::create_directories(output.parent_path(), ec);
|
||||
std::ofstream out{output};
|
||||
if (!out) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::Internal,
|
||||
std::format("cannot open vendor file for writing: {}",
|
||||
output.string()),
|
||||
output));
|
||||
}
|
||||
out << body;
|
||||
if (!out) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::Internal,
|
||||
std::format("write failed: {}", output.string()), output));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace cargoxx::cli
|
||||
@@ -23,13 +23,26 @@ auto run(int argc, char** argv) -> int {
|
||||
"build", "Generate flake.nix and build/CMakeLists.txt; build with nix+cmake");
|
||||
bool build_no_build = false;
|
||||
bool build_release = false;
|
||||
bool build_offline = false;
|
||||
std::string build_target;
|
||||
std::string build_vendor;
|
||||
build_cmd->add_flag("--no-build", build_no_build,
|
||||
"Generate files only; do not invoke nix/cmake");
|
||||
build_cmd->add_flag("--release", build_release, "Build the release profile");
|
||||
build_cmd->add_flag("--offline", build_offline,
|
||||
"Skip network probes and nix develop wrappers. "
|
||||
"Reads vendor.toml for store-path inputs.");
|
||||
build_cmd->add_option("--vendor", build_vendor,
|
||||
"Path to vendor.toml (used with --offline; default ./vendor.toml)");
|
||||
build_cmd->add_option("--target", build_target,
|
||||
"Build a specific target (passed to cmake --build)");
|
||||
|
||||
auto* vendor_cmd = app.add_subcommand(
|
||||
"vendor", "Resolve every locked dependency into /nix/store and write vendor.toml");
|
||||
std::string vendor_output;
|
||||
vendor_cmd->add_option("--output", vendor_output,
|
||||
"Path to write vendor.toml (default ./vendor.toml)");
|
||||
|
||||
auto* run_cmd = app.add_subcommand("run", "Build and run a binary target");
|
||||
bool run_release = false;
|
||||
std::string run_bin;
|
||||
@@ -109,19 +122,36 @@ auto run(int argc, char** argv) -> int {
|
||||
if (!build_target.empty()) {
|
||||
target = build_target;
|
||||
}
|
||||
auto r = cmd_build(cwd, build_no_build, build_release, target);
|
||||
std::optional<std::filesystem::path> vendor_path;
|
||||
if (!build_vendor.empty()) {
|
||||
vendor_path = build_vendor;
|
||||
}
|
||||
auto r = cmd_build(cwd, build_no_build, build_release, target,
|
||||
std::nullopt, build_offline, vendor_path);
|
||||
if (!r) {
|
||||
std::cerr << util::format(r.error());
|
||||
return 1;
|
||||
}
|
||||
if (build_no_build) {
|
||||
std::cout << " Generated flake.nix, build/CMakeLists.txt, Cargoxx.lock\n";
|
||||
std::cout << " Generated build/flake.nix, build/CMakeLists.txt, Cargoxx.lock\n";
|
||||
} else {
|
||||
std::cout << " Built\n";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (*vendor_cmd) {
|
||||
auto out = vendor_output.empty() ? cwd / "vendor.toml"
|
||||
: std::filesystem::path{vendor_output};
|
||||
auto r = cmd_vendor(cwd, out);
|
||||
if (!r) {
|
||||
std::cerr << util::format(r.error());
|
||||
return 1;
|
||||
}
|
||||
std::cout << std::format(" Wrote {}\n", out.string());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (*run_cmd) {
|
||||
std::optional<std::string> bin;
|
||||
if (!run_bin.empty()) {
|
||||
|
||||
@@ -75,7 +75,8 @@ auto emit_header(const manifest::Manifest& m) -> std::string {
|
||||
"set(CMAKE_CXX_SCAN_FOR_MODULES ON)\n"
|
||||
"set(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n"
|
||||
"\n"
|
||||
"add_compile_options(-Wall -Wextra -Wpedantic -Wconversion)\n",
|
||||
"add_compile_options(-Wall -Wextra -Wpedantic -Wconversion "
|
||||
"-Wno-missing-field-initializers)\n",
|
||||
m.package.name, edition_to_int(m.package.edition));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,16 @@ import cargoxx.lockfile;
|
||||
|
||||
export namespace cargoxx::codegen {
|
||||
|
||||
// When set, codegen emits the generated flake's nixpkgs / flake-utils
|
||||
// inputs as `path:/nix/store/...` references (already-realized source
|
||||
// paths) instead of `github:NixOS/nixpkgs/<rev>` URLs. This makes the
|
||||
// inner build hermetic — no network and no nix daemon access required.
|
||||
struct VendorIndex {
|
||||
std::string nixpkgs_store_path;
|
||||
std::string flake_utils_store_path;
|
||||
std::unordered_map<std::string, std::string> dep_store_paths; // by attr
|
||||
};
|
||||
|
||||
// All inputs the generators need. Held by const reference; the caller owns
|
||||
// the underlying objects. Not copyable.
|
||||
struct GenerateInputs {
|
||||
@@ -18,9 +28,14 @@ struct GenerateInputs {
|
||||
std::vector<linkdb::Recipe> recipes; // one per manifest dep, same order
|
||||
std::vector<linkdb::Recipe> dev_recipes; // one per dev_dependency, same order
|
||||
std::filesystem::path project_root;
|
||||
std::optional<VendorIndex> vendor;
|
||||
};
|
||||
|
||||
auto flake_nix(const GenerateInputs& in) -> std::string;
|
||||
auto cmake_lists(const GenerateInputs& in) -> std::string;
|
||||
|
||||
// Pure: parses a vendor.toml (see cmd_vendor) into a VendorIndex.
|
||||
auto parse_vendor_toml(std::string_view body)
|
||||
-> util::Result<VendorIndex>;
|
||||
|
||||
} // namespace cargoxx::codegen
|
||||
|
||||
@@ -99,20 +99,40 @@ auto pinned_inputs_dedup(const std::vector<DepBinding>& bindings)
|
||||
return out;
|
||||
}
|
||||
|
||||
auto emit_inputs_block(const std::vector<const DepBinding*>& pinned)
|
||||
auto emit_inputs_block(const std::vector<const DepBinding*>& pinned,
|
||||
const lockfile::Lockfile& lock,
|
||||
const std::optional<VendorIndex>& vendor)
|
||||
-> std::string {
|
||||
// Always emit the shared toolchain `nixpkgs` and `flake-utils`
|
||||
// inputs. Per-pinned-dep inputs land between them so the output
|
||||
// diff stays stable across reruns.
|
||||
auto nixpkgs_url = [&]() -> std::string {
|
||||
if (vendor && !vendor->nixpkgs_store_path.empty()) {
|
||||
return std::format("path:{}", vendor->nixpkgs_store_path);
|
||||
}
|
||||
if (lock.nixpkgs_rev_pin && !lock.nixpkgs_rev_pin->empty()) {
|
||||
return std::format("github:NixOS/nixpkgs/{}", *lock.nixpkgs_rev_pin);
|
||||
}
|
||||
return "github:NixOS/nixpkgs/nixos-unstable";
|
||||
}();
|
||||
auto flake_utils_url = [&]() -> std::string {
|
||||
if (vendor && !vendor->flake_utils_store_path.empty()) {
|
||||
return std::format("path:{}", vendor->flake_utils_store_path);
|
||||
}
|
||||
if (lock.flake_utils_rev_pin && !lock.flake_utils_rev_pin->empty()) {
|
||||
return std::format("github:numtide/flake-utils/{}",
|
||||
*lock.flake_utils_rev_pin);
|
||||
}
|
||||
return "github:numtide/flake-utils";
|
||||
}();
|
||||
std::string out =
|
||||
" inputs = {\n"
|
||||
" nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n";
|
||||
for (const auto* b : pinned) {
|
||||
out += std::format(" {}.url = \"github:NixOS/nixpkgs/{}\";\n",
|
||||
b->sanitized, *b->rev);
|
||||
+ std::format(" nixpkgs.url = \"{}\";\n", nixpkgs_url);
|
||||
if (!vendor) {
|
||||
for (const auto* b : pinned) {
|
||||
out += std::format(" {}.url = \"github:NixOS/nixpkgs/{}\";\n",
|
||||
b->sanitized, *b->rev);
|
||||
}
|
||||
}
|
||||
out += " flake-utils.url = \"github:numtide/flake-utils\";\n"
|
||||
" };\n";
|
||||
out += std::format(" flake-utils.url = \"{}\";\n", flake_utils_url);
|
||||
out += " };\n";
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -165,7 +185,7 @@ auto flake_nix(const GenerateInputs& in) -> std::string {
|
||||
out += "{\n";
|
||||
out += std::format(" description = \"{}\";\n\n", in.manifest.package.name);
|
||||
|
||||
out += emit_inputs_block(pinned);
|
||||
out += emit_inputs_block(pinned, in.lock, in.vendor);
|
||||
|
||||
const bool any_pkg_config =
|
||||
std::ranges::any_of(in.recipes,
|
||||
|
||||
69
src/codegen/vendor.cpp
Normal file
69
src/codegen/vendor.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
module;
|
||||
|
||||
#include <toml.hpp>
|
||||
|
||||
module cargoxx.codegen;
|
||||
|
||||
import std;
|
||||
import cargoxx.util;
|
||||
|
||||
namespace cargoxx::codegen {
|
||||
|
||||
auto parse_vendor_toml(std::string_view body) -> util::Result<VendorIndex> {
|
||||
toml::table root;
|
||||
try {
|
||||
root = toml::parse(body);
|
||||
} catch (const toml::parse_error& e) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::ManifestParseError,
|
||||
std::format("vendor.toml is not valid TOML: {}", e.description()),
|
||||
"", std::nullopt, std::nullopt,
|
||||
});
|
||||
}
|
||||
|
||||
auto missing = [](std::string msg) {
|
||||
return util::Error{
|
||||
util::ErrorCode::ManifestInvalidField, std::move(msg), "",
|
||||
std::nullopt, std::nullopt,
|
||||
};
|
||||
};
|
||||
|
||||
VendorIndex out;
|
||||
if (const auto* tbl = root["nixpkgs"].as_table()) {
|
||||
if (auto v = (*tbl)["store_path"].value<std::string>()) {
|
||||
out.nixpkgs_store_path = *v;
|
||||
} else {
|
||||
return std::unexpected(missing("vendor.toml: [nixpkgs].store_path is required"));
|
||||
}
|
||||
} else {
|
||||
return std::unexpected(missing("vendor.toml: [nixpkgs] table is required"));
|
||||
}
|
||||
if (const auto* tbl = root["flake_utils"].as_table()) {
|
||||
if (auto v = (*tbl)["store_path"].value<std::string>()) {
|
||||
out.flake_utils_store_path = *v;
|
||||
} else {
|
||||
return std::unexpected(missing("vendor.toml: [flake_utils].store_path is required"));
|
||||
}
|
||||
} else {
|
||||
return std::unexpected(missing("vendor.toml: [flake_utils] table is required"));
|
||||
}
|
||||
|
||||
if (const auto* arr = root["dep"].as_array()) {
|
||||
for (const auto& el : *arr) {
|
||||
const auto* tbl = el.as_table();
|
||||
if (!tbl) {
|
||||
return std::unexpected(missing("vendor.toml: [[dep]] entries must be tables"));
|
||||
}
|
||||
auto attr = (*tbl)["nixpkgs_attr"].value<std::string>();
|
||||
auto path = (*tbl)["store_path"].value<std::string>();
|
||||
if (!attr || !path) {
|
||||
return std::unexpected(missing(
|
||||
"vendor.toml: each [[dep]] needs nixpkgs_attr and store_path"));
|
||||
}
|
||||
out.dep_store_paths.emplace(*attr, *path);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace cargoxx::codegen
|
||||
@@ -79,6 +79,33 @@ auto parse_package(const toml::table& tbl, const std::filesystem::path& path)
|
||||
if (auto v = tbl["linkdb_source"].value<std::string>()) {
|
||||
pkg.linkdb_source = *v;
|
||||
}
|
||||
if (auto v = tbl["find_package"].value<std::string>()) {
|
||||
pkg.find_package = *v;
|
||||
}
|
||||
if (const auto* arr = tbl["targets"].as_array()) {
|
||||
auto r = extract_string_array(*arr, "targets", path);
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
pkg.targets = std::move(*r);
|
||||
}
|
||||
if (auto v = tbl["pkg_config_module"].value<std::string>()) {
|
||||
pkg.pkg_config_module = *v;
|
||||
}
|
||||
if (const auto* arr = tbl["brute_force_libs"].as_array()) {
|
||||
auto r = extract_string_array(*arr, "brute_force_libs", path);
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
pkg.brute_force_libs = std::move(*r);
|
||||
}
|
||||
if (const auto* arr = tbl["brute_force_includes"].as_array()) {
|
||||
auto r = extract_string_array(*arr, "brute_force_includes", path);
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
pkg.brute_force_includes = std::move(*r);
|
||||
}
|
||||
|
||||
return pkg;
|
||||
}
|
||||
@@ -104,6 +131,12 @@ auto parse(const std::filesystem::path& path) -> util::Result<Lockfile> {
|
||||
if (auto v = root["version"].value<int>()) {
|
||||
lock.version = *v;
|
||||
}
|
||||
if (auto v = root["nixpkgs_rev"].value<std::string>()) {
|
||||
lock.nixpkgs_rev_pin = *v;
|
||||
}
|
||||
if (auto v = root["flake_utils_rev"].value<std::string>()) {
|
||||
lock.flake_utils_rev_pin = *v;
|
||||
}
|
||||
|
||||
if (const auto* arr = root["package"].as_array()) {
|
||||
lock.packages.reserve(arr->size());
|
||||
@@ -127,6 +160,12 @@ auto parse(const std::filesystem::path& path) -> util::Result<Lockfile> {
|
||||
auto write(const Lockfile& lock, const std::filesystem::path& path) -> util::Result<void> {
|
||||
toml::table root;
|
||||
root.insert_or_assign("version", lock.version);
|
||||
if (lock.nixpkgs_rev_pin) {
|
||||
root.insert_or_assign("nixpkgs_rev", *lock.nixpkgs_rev_pin);
|
||||
}
|
||||
if (lock.flake_utils_rev_pin) {
|
||||
root.insert_or_assign("flake_utils_rev", *lock.flake_utils_rev_pin);
|
||||
}
|
||||
|
||||
toml::array packages;
|
||||
for (const auto& p : lock.packages) {
|
||||
@@ -149,6 +188,33 @@ auto write(const Lockfile& lock, const std::filesystem::path& path) -> util::Res
|
||||
if (p.linkdb_source) {
|
||||
tbl.insert_or_assign("linkdb_source", *p.linkdb_source);
|
||||
}
|
||||
if (p.find_package) {
|
||||
tbl.insert_or_assign("find_package", *p.find_package);
|
||||
}
|
||||
if (!p.targets.empty()) {
|
||||
toml::array arr;
|
||||
for (const auto& t : p.targets) {
|
||||
arr.push_back(t);
|
||||
}
|
||||
tbl.insert_or_assign("targets", std::move(arr));
|
||||
}
|
||||
if (p.pkg_config_module) {
|
||||
tbl.insert_or_assign("pkg_config_module", *p.pkg_config_module);
|
||||
}
|
||||
if (!p.brute_force_libs.empty()) {
|
||||
toml::array arr;
|
||||
for (const auto& l : p.brute_force_libs) {
|
||||
arr.push_back(l);
|
||||
}
|
||||
tbl.insert_or_assign("brute_force_libs", std::move(arr));
|
||||
}
|
||||
if (!p.brute_force_includes.empty()) {
|
||||
toml::array arr;
|
||||
for (const auto& i : p.brute_force_includes) {
|
||||
arr.push_back(i);
|
||||
}
|
||||
tbl.insert_or_assign("brute_force_includes", std::move(arr));
|
||||
}
|
||||
packages.push_back(std::move(tbl));
|
||||
}
|
||||
root.insert_or_assign("package", std::move(packages));
|
||||
|
||||
@@ -9,22 +9,27 @@ export namespace cargoxx::lockfile {
|
||||
struct LockfilePackage {
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::vector<std::string> dependencies; // "<name> <version>" entries; non-empty for the root
|
||||
std::vector<std::string> dependencies;
|
||||
std::optional<std::string> nixpkgs_attr;
|
||||
std::optional<std::string> nixpkgs_rev;
|
||||
std::optional<std::string> linkdb_source;
|
||||
std::optional<std::string> find_package;
|
||||
std::vector<std::string> targets;
|
||||
std::optional<std::string> pkg_config_module;
|
||||
std::vector<std::string> brute_force_libs;
|
||||
std::vector<std::string> brute_force_includes;
|
||||
|
||||
bool operator==(const LockfilePackage&) const = default;
|
||||
};
|
||||
|
||||
struct Lockfile {
|
||||
int version = 1;
|
||||
std::optional<std::string> nixpkgs_rev_pin;
|
||||
std::optional<std::string> flake_utils_rev_pin;
|
||||
std::vector<LockfilePackage> packages;
|
||||
|
||||
bool operator==(const Lockfile&) const = default;
|
||||
|
||||
// The nixpkgs revision is shared across every dep package per SPEC §5.
|
||||
// Returns the first non-empty rev seen, or nullopt if no deps are pinned.
|
||||
[[nodiscard]] auto nixpkgs_rev() const -> std::optional<std::string>;
|
||||
};
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ auto make_error(util::ErrorCode code, std::string msg) -> util::Error {
|
||||
// nix eval emits these markers when an attribute is missing on the flake.
|
||||
auto looks_like_missing_attribute(std::string_view stderr_text) -> bool {
|
||||
return stderr_text.find("does not provide attribute") != std::string_view::npos ||
|
||||
stderr_text.find("attribute '") != std::string_view::npos &&
|
||||
stderr_text.find("missing") != std::string_view::npos;
|
||||
(stderr_text.find("attribute '") != std::string_view::npos &&
|
||||
stderr_text.find("missing") != std::string_view::npos);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -162,4 +162,95 @@ auto realize_path(const std::string& flake_attr) -> util::Result<std::string> {
|
||||
return path;
|
||||
}
|
||||
|
||||
auto realize_path_at_rev(const std::string& rev, const std::string& attr)
|
||||
-> util::Result<std::string> {
|
||||
if (rev.empty() || attr.empty()) {
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionUnknownPackage,
|
||||
"realize_path_at_rev: rev and attr must be non-empty"));
|
||||
}
|
||||
std::vector<std::string> args{
|
||||
"--extra-experimental-features", "nix-command flakes",
|
||||
"build", std::format("github:NixOS/nixpkgs/{}#{}", rev, attr),
|
||||
"--no-link", "--print-out-paths",
|
||||
};
|
||||
auto r = exec::run("nix", args,
|
||||
exec::ExecOptions{
|
||||
.cwd = {},
|
||||
.env_overrides = {},
|
||||
.timeout = std::chrono::seconds{600},
|
||||
.inherit_stdio = false,
|
||||
});
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (r->exit_code != 0) {
|
||||
if (looks_like_missing_attribute(r->stderr_text)) {
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionUnknownPackage,
|
||||
std::format("nixpkgs/{} has no attribute '{}'", rev, attr)));
|
||||
}
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("nix build failed (exit {}): {}", r->exit_code,
|
||||
r->stderr_text)));
|
||||
}
|
||||
auto path = r->stdout_text;
|
||||
while (!path.empty() && (path.back() == '\n' || path.back() == ' ')) {
|
||||
path.pop_back();
|
||||
}
|
||||
if (path.empty()) {
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("nix build emitted no path for '{}#{}'", rev, attr)));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
auto realize_flake_source(const std::string& flake_ref)
|
||||
-> util::Result<std::string> {
|
||||
if (flake_ref.empty()) {
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionUnknownPackage,
|
||||
"realize_flake_source: flake_ref is empty"));
|
||||
}
|
||||
std::vector<std::string> args{
|
||||
"--extra-experimental-features", "nix-command flakes",
|
||||
"flake", "prefetch", flake_ref, "--json",
|
||||
};
|
||||
auto r = exec::run("nix", args,
|
||||
exec::ExecOptions{
|
||||
.cwd = {},
|
||||
.env_overrides = {},
|
||||
.timeout = std::chrono::seconds{300},
|
||||
.inherit_stdio = false,
|
||||
});
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (r->exit_code != 0) {
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("nix flake prefetch failed (exit {}): {}",
|
||||
r->exit_code, r->stderr_text)));
|
||||
}
|
||||
std::string_view body = r->stdout_text;
|
||||
constexpr std::string_view key = "\"storePath\":\"";
|
||||
auto pos = body.find(key);
|
||||
if (pos == std::string_view::npos) {
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("nix flake prefetch emitted no storePath for '{}'",
|
||||
flake_ref)));
|
||||
}
|
||||
pos += key.size();
|
||||
auto end = body.find('"', pos);
|
||||
if (end == std::string_view::npos) {
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
"nix flake prefetch JSON malformed"));
|
||||
}
|
||||
return std::string{body.substr(pos, end - pos)};
|
||||
}
|
||||
|
||||
} // namespace cargoxx::resolver
|
||||
|
||||
@@ -45,6 +45,20 @@ auto nixpkgs_probe(const std::string& attr) -> util::Result<NixpkgsInfo>;
|
||||
// for build / network errors.
|
||||
auto realize_path(const std::string& flake_attr) -> util::Result<std::string>;
|
||||
|
||||
// Like `realize_path`, but pins the nixpkgs revision instead of using the
|
||||
// registry alias. Builds `github:NixOS/nixpkgs/<rev>#<attr>` and returns
|
||||
// the resulting `/nix/store/...` path. Used by the vendor subcommand to
|
||||
// materialize each lockfile-pinned dep without depending on the user's
|
||||
// flake registry.
|
||||
auto realize_path_at_rev(const std::string& rev, const std::string& attr)
|
||||
-> util::Result<std::string>;
|
||||
|
||||
// Returns the source store path for `github:NixOS/nixpkgs/<rev>` (the
|
||||
// path Nix would set as `nixpkgs.outPath` when imported). Used by the
|
||||
// vendor subcommand to record nixpkgs/flake-utils source locations.
|
||||
auto realize_flake_source(const std::string& flake_ref)
|
||||
-> util::Result<std::string>;
|
||||
|
||||
// One CMake config-file's IMPORTED targets together with the find_package
|
||||
// expression derived from its filename stem.
|
||||
struct NixCmakeCandidate {
|
||||
|
||||
@@ -64,7 +64,7 @@ TEST_CASE("cmd_build generates files for a no-deps binary project",
|
||||
auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent));
|
||||
REQUIRE(r.has_value());
|
||||
|
||||
REQUIRE(std::filesystem::exists(root / "flake.nix"));
|
||||
REQUIRE(std::filesystem::exists(root / "build" / "flake.nix"));
|
||||
REQUIRE(std::filesystem::exists(root / "build" / "CMakeLists.txt"));
|
||||
REQUIRE(std::filesystem::exists(root / "Cargoxx.lock"));
|
||||
|
||||
@@ -73,9 +73,9 @@ TEST_CASE("cmd_build generates files for a no-deps binary project",
|
||||
REQUIRE(cmake_text.find("add_executable(hello_bin ../src/main.cpp)") !=
|
||||
std::string::npos);
|
||||
|
||||
auto flake_text = read_file(root / "flake.nix");
|
||||
auto flake_text = read_file(root / "build" / "flake.nix");
|
||||
REQUIRE(flake_text.find("description = \"hello\";") != std::string::npos);
|
||||
REQUIRE(flake_text.find("github:NixOS/nixpkgs/nixos-unstable") != std::string::npos);
|
||||
REQUIRE(flake_text.find("github:NixOS/nixpkgs/") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("cmd_build generates files for a library project", "[cli][build]") {
|
||||
@@ -113,7 +113,7 @@ TEST_CASE("cmd_build resolves a manually-seeded dep into find_package + targets"
|
||||
REQUIRE(cmake_text.find("find_package(fmt CONFIG REQUIRED)") != std::string::npos);
|
||||
REQUIRE(cmake_text.find("fmt::fmt") != std::string::npos);
|
||||
|
||||
auto flake_text = read_file(root / "flake.nix");
|
||||
auto flake_text = read_file(root / "build" / "flake.nix");
|
||||
REQUIRE(flake_text.find("pkgs.fmt_10") != std::string::npos);
|
||||
}
|
||||
|
||||
@@ -197,11 +197,11 @@ TEST_CASE("cmd_build is idempotent — second run produces identical files",
|
||||
|
||||
REQUIRE(cmd_build(root, true, false, std::nullopt, overlay_path(parent)).has_value());
|
||||
auto first_cmake = read_file(root / "build" / "CMakeLists.txt");
|
||||
auto first_flake = read_file(root / "flake.nix");
|
||||
auto first_flake = read_file(root / "build" / "flake.nix");
|
||||
auto first_lock = read_file(root / "Cargoxx.lock");
|
||||
|
||||
REQUIRE(cmd_build(root, true, false, std::nullopt, overlay_path(parent)).has_value());
|
||||
REQUIRE(read_file(root / "build" / "CMakeLists.txt") == first_cmake);
|
||||
REQUIRE(read_file(root / "flake.nix") == first_flake);
|
||||
REQUIRE(read_file(root / "build" / "flake.nix") == first_flake);
|
||||
REQUIRE(read_file(root / "Cargoxx.lock") == first_lock);
|
||||
}
|
||||
|
||||
@@ -290,7 +290,8 @@ TEST_CASE("cmake_lists emits baseline warnings", "[codegen][cmake]") {
|
||||
GenerateInputs in{m, layout, lock, {}, {}, ROOT};
|
||||
|
||||
auto out = cmake_lists(in);
|
||||
REQUIRE(out.find("add_compile_options(-Wall -Wextra -Wpedantic -Wconversion)") !=
|
||||
REQUIRE(out.find("add_compile_options(-Wall -Wextra -Wpedantic -Wconversion "
|
||||
"-Wno-missing-field-initializers)") !=
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ TEST_CASE("flake_nix adds pkgs.pkg-config to nativeBuildInputs only when needed"
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {dep("sqlite", "*")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("app", "0.1.0")}};
|
||||
Lockfile lock{.version = 1, .packages = {root_pkg("app", "0.1.0")}};
|
||||
std::vector<Recipe> recipes = {Recipe{
|
||||
.nixpkgs_attr = "sqlite",
|
||||
.find_package = "PkgConfig REQUIRED",
|
||||
@@ -96,7 +96,7 @@ TEST_CASE("flake_nix omits pkgs.pkg-config when no recipe needs it",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("hello"), {dep("fmt", "*")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("hello", "0.1.0")}};
|
||||
Lockfile lock{.version = 1, .packages = {root_pkg("hello", "0.1.0")}};
|
||||
std::vector<Recipe> recipes = {recipe("fmt_10")};
|
||||
GenerateInputs in{m, layout, lock, recipes, {}, "/tmp/hello"};
|
||||
|
||||
@@ -108,7 +108,7 @@ TEST_CASE("flake_nix always emits the shared nixos-unstable nixpkgs input",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("hello"), {}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("hello", "0.1.0")}};
|
||||
Lockfile lock{.version = 1, .packages = {root_pkg("hello", "0.1.0")}};
|
||||
GenerateInputs in{m, layout, lock, {}, {}, "/tmp/hello"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
@@ -122,7 +122,7 @@ TEST_CASE("flake_nix always emits the shared nixos-unstable nixpkgs input",
|
||||
TEST_CASE("flake_nix emits a per-pinned-dep nixpkgs input", "[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {dep("fmt", "10.2.1")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {
|
||||
Lockfile lock{.version = 1, .packages = {
|
||||
root_pkg("app", "0.1.0"),
|
||||
dep_pkg("fmt", "10.2.1", "abc123def456"),
|
||||
}};
|
||||
@@ -146,7 +146,7 @@ TEST_CASE("flake_nix uses shared `pkgs` for unpinned deps",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {dep("fmt", "*")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("app", "0.1.0"), dep_pkg("fmt", "*", std::nullopt)}};
|
||||
Lockfile lock{.version = 1, .packages = {root_pkg("app", "0.1.0"), dep_pkg("fmt", "*", std::nullopt)}};
|
||||
std::vector<Recipe> recipes = {recipe("fmt_10")};
|
||||
GenerateInputs in{m, layout, lock, recipes, {}, "/tmp/app"};
|
||||
|
||||
@@ -158,7 +158,7 @@ TEST_CASE("flake_nix uses shared `pkgs` for unpinned deps",
|
||||
TEST_CASE("flake_nix mixes pinned and unpinned deps", "[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {dep("fmt", "10.2.1"), dep("zlib", "*")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {
|
||||
Lockfile lock{.version = 1, .packages = {
|
||||
root_pkg("app", "0.1.0"),
|
||||
dep_pkg("fmt", "10.2.1", "abc"),
|
||||
dep_pkg("zlib", "*", std::nullopt),
|
||||
@@ -175,7 +175,7 @@ TEST_CASE("flake_nix emits an empty buildInputs list when there are no deps",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("hello"), {}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("hello", "0.1.0")}};
|
||||
Lockfile lock{.version = 1, .packages = {root_pkg("hello", "0.1.0")}};
|
||||
GenerateInputs in{m, layout, lock, {}, {}, "/tmp/hello"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
@@ -188,7 +188,7 @@ TEST_CASE("flake_nix dedupes deps that share input + attr",
|
||||
{dep("boost", "1.84.0"), dep("boost", "1.84.0")},
|
||||
{}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {
|
||||
Lockfile lock{.version = 1, .packages = {
|
||||
root_pkg("app", "0.1.0"),
|
||||
dep_pkg("boost", "1.84.0", "rev42"),
|
||||
}};
|
||||
@@ -205,7 +205,7 @@ TEST_CASE("flake_nix dedupes deps that share input + attr",
|
||||
TEST_CASE("flake_nix produces deterministic output", "[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {dep("fmt", "10.2.1"), dep("spdlog", "*")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {
|
||||
Lockfile lock{.version = 1, .packages = {
|
||||
root_pkg("app", "0.1.0"),
|
||||
dep_pkg("fmt", "10.2.1", "abc"),
|
||||
dep_pkg("spdlog", "*", std::nullopt),
|
||||
@@ -219,7 +219,7 @@ TEST_CASE("flake_nix produces deterministic output", "[codegen][flake]") {
|
||||
TEST_CASE("flake_nix output ends with a newline", "[codegen][flake]") {
|
||||
Manifest m{pkg("hello"), {}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("hello", "0.1.0")}};
|
||||
Lockfile lock{.version = 1, .packages = {root_pkg("hello", "0.1.0")}};
|
||||
GenerateInputs in{m, layout, lock, {}, {}, "/tmp/hello"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
@@ -231,7 +231,7 @@ TEST_CASE("flake_nix sanitizes hyphens and dots in dep names",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {dep("range-v3", "0.12.0")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {
|
||||
Lockfile lock{.version = 1, .packages = {
|
||||
root_pkg("app", "0.1.0"),
|
||||
dep_pkg("range-v3", "0.12.0", "rev123"),
|
||||
}};
|
||||
|
||||
7
tests/e2e/buildCppPackage/Cargoxx.toml
Normal file
7
tests/e2e/buildCppPackage/Cargoxx.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "e2e_demo"
|
||||
version = "0.1.0"
|
||||
edition = "cpp23"
|
||||
|
||||
[dependencies]
|
||||
nlohmann_json = "*"
|
||||
10
tests/e2e/buildCppPackage/flake.nix
Normal file
10
tests/e2e/buildCppPackage/flake.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
description = "e2e buildCppPackage smoke";
|
||||
|
||||
inputs.cargoxx.url = "path:../../..";
|
||||
|
||||
outputs = { self, cargoxx }: {
|
||||
packages.x86_64-linux.default =
|
||||
cargoxx.lib.x86_64-linux.buildCppPackage { src = ./.; };
|
||||
};
|
||||
}
|
||||
38
tests/e2e/buildCppPackage/run.sh
Executable file
38
tests/e2e/buildCppPackage/run.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo="$(cd "${here}/../../.." && pwd)"
|
||||
cargoxx_bin="${CARGOXX_BIN:-${repo}/build/debug/cargoxx}"
|
||||
|
||||
if [[ ! -x "${cargoxx_bin}" ]]; then
|
||||
echo "error: cargoxx binary not found at ${cargoxx_bin}" >&2
|
||||
echo "build it first: nix develop --command cmake --build build/debug" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
work="$(mktemp -d -t cargoxx-e2e-XXXXXX)"
|
||||
trap 'rm -rf "${work}"' EXIT
|
||||
|
||||
cp -r "${here}/." "${work}/"
|
||||
sed -i "s|path:\\.\\./\\.\\./\\.\\.|path:${repo}|" "${work}/flake.nix"
|
||||
|
||||
cd "${work}"
|
||||
|
||||
echo "=== cargoxx build --no-build"
|
||||
"${cargoxx_bin}" build --no-build
|
||||
|
||||
[[ -f Cargoxx.lock ]] || { echo "Cargoxx.lock missing"; exit 1; }
|
||||
[[ -f build/flake.nix ]] || { echo "build/flake.nix missing"; exit 1; }
|
||||
|
||||
echo "=== nix build .#default"
|
||||
out="$(nix build .#default --no-link --print-out-paths \
|
||||
--extra-experimental-features 'nix-command flakes')"
|
||||
|
||||
[[ -n "${out}" ]] || { echo "nix build produced no output path"; exit 1; }
|
||||
[[ -x "${out}/bin/e2e_demo" ]] || { echo "missing ${out}/bin/e2e_demo"; exit 1; }
|
||||
|
||||
echo "=== execute"
|
||||
"${out}/bin/e2e_demo"
|
||||
|
||||
echo "ok"
|
||||
9
tests/e2e/buildCppPackage/src/main.cpp
Normal file
9
tests/e2e/buildCppPackage/src/main.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
import std;
|
||||
|
||||
int main() {
|
||||
nlohmann::json j;
|
||||
j["hello"] = "world";
|
||||
std::println("Hello from {}!", j["hello"].get<std::string>());
|
||||
return 0;
|
||||
}
|
||||
@@ -54,14 +54,14 @@ auto round_trip(const Lockfile& l) -> Lockfile {
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("write round-trips a minimal lockfile", "[lockfile]") {
|
||||
Lockfile l{1, {root_pkg("my-project", "0.1.0")}};
|
||||
Lockfile l{.version = 1, .packages = {root_pkg("my-project", "0.1.0")}};
|
||||
REQUIRE(round_trip(l) == l);
|
||||
}
|
||||
|
||||
TEST_CASE("write round-trips a lockfile with deps", "[lockfile]") {
|
||||
Lockfile l{
|
||||
1,
|
||||
{
|
||||
.version = 1,
|
||||
.packages = {
|
||||
root_pkg("my-project", "0.1.0", {"fmt 10.2.1", "spdlog 1.13.0"}),
|
||||
dep_pkg("fmt", "10.2.1", "fmt_10", "8a3f...c2d1"),
|
||||
dep_pkg("spdlog", "1.13.0", "spdlog", "8a3f...c2d1"),
|
||||
@@ -70,10 +70,58 @@ TEST_CASE("write round-trips a lockfile with deps", "[lockfile]") {
|
||||
REQUIRE(round_trip(l) == l);
|
||||
}
|
||||
|
||||
TEST_CASE("write round-trips lockfile recipe fields", "[lockfile]") {
|
||||
Lockfile l{
|
||||
.version = 1,
|
||||
.packages = {
|
||||
LockfilePackage{
|
||||
.name = "fmt",
|
||||
.version = "10.2.1",
|
||||
.dependencies = {},
|
||||
.nixpkgs_attr = "fmt_10",
|
||||
.nixpkgs_rev = std::nullopt,
|
||||
.linkdb_source = "conan",
|
||||
.find_package = "fmt CONFIG REQUIRED",
|
||||
.targets = {"fmt::fmt"},
|
||||
.pkg_config_module = std::nullopt,
|
||||
.brute_force_libs = {},
|
||||
.brute_force_includes = {},
|
||||
},
|
||||
LockfilePackage{
|
||||
.name = "sqlite",
|
||||
.version = "*",
|
||||
.dependencies = {},
|
||||
.nixpkgs_attr = "sqlite",
|
||||
.nixpkgs_rev = std::nullopt,
|
||||
.linkdb_source = "pkg-config",
|
||||
.find_package = "PkgConfig REQUIRED",
|
||||
.targets = {"PkgConfig::SQLITE3"},
|
||||
.pkg_config_module = "sqlite3",
|
||||
.brute_force_libs = {},
|
||||
.brute_force_includes = {},
|
||||
},
|
||||
LockfilePackage{
|
||||
.name = "obscure",
|
||||
.version = "*",
|
||||
.dependencies = {},
|
||||
.nixpkgs_attr = "obscure",
|
||||
.nixpkgs_rev = std::nullopt,
|
||||
.linkdb_source = "brute-force",
|
||||
.find_package = "",
|
||||
.targets = {"obscure::obscure"},
|
||||
.pkg_config_module = std::nullopt,
|
||||
.brute_force_libs = {"/nix/store/abc/lib/libobscure.a"},
|
||||
.brute_force_includes = {"/nix/store/abc/include"},
|
||||
},
|
||||
},
|
||||
};
|
||||
REQUIRE(round_trip(l) == l);
|
||||
}
|
||||
|
||||
TEST_CASE("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") {
|
||||
Lockfile l{
|
||||
1,
|
||||
{
|
||||
.version = 1,
|
||||
.packages = {
|
||||
root_pkg("p", "0.1.0", {"fmt 10.2.1"}),
|
||||
dep_pkg("fmt", "10.2.1", "fmt_10", "abc123"),
|
||||
},
|
||||
@@ -81,8 +129,19 @@ TEST_CASE("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") {
|
||||
REQUIRE(l.nixpkgs_rev() == "abc123");
|
||||
}
|
||||
|
||||
TEST_CASE("write round-trips top-level nixpkgs_rev + flake_utils_rev pins",
|
||||
"[lockfile]") {
|
||||
Lockfile l{
|
||||
.version = 1,
|
||||
.nixpkgs_rev_pin = "549bd84d6279f9852cae6225e372cc67fb91a4c1",
|
||||
.flake_utils_rev_pin = "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
.packages = {root_pkg("p", "0.1.0")},
|
||||
};
|
||||
REQUIRE(round_trip(l) == l);
|
||||
}
|
||||
|
||||
TEST_CASE("Lockfile::nixpkgs_rev is nullopt when no deps", "[lockfile]") {
|
||||
Lockfile l{1, {root_pkg("p", "0.1.0")}};
|
||||
Lockfile l{.version = 1, .packages = {root_pkg("p", "0.1.0")}};
|
||||
REQUIRE_FALSE(l.nixpkgs_rev().has_value());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user