[M7] buildCppPackage: hermetic single-derivation, sandbox-safe
Resolve dep store paths and synthesize vendor.toml at outer eval time. Add tests/e2e/buildCppPackage smoke fixture with a run.sh Update CHANGELOG.md with the M7 changes.
This commit is contained in:
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,
|
window. `tests/linkdb_overlay.cpp` covers 7 cases (insert/persist,
|
||||||
override-curated, version-range gating, components rejection,
|
override-curated, version-range gating, components rejection,
|
||||||
move semantics).
|
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 `||`.
|
||||||
|
|||||||
83
flake.nix
83
flake.nix
@@ -10,19 +10,13 @@
|
|||||||
flake-utils.lib.eachDefaultSystem (system:
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
|
||||||
cargoxx-bin = pkgs.gcc15Stdenv.mkDerivation {
|
cargoxx-bin = pkgs.gcc15Stdenv.mkDerivation {
|
||||||
pname = "cargoxx";
|
pname = "cargoxx";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [ pkgs.cmake pkgs.ninja ];
|
||||||
pkgs.cmake
|
buildInputs = [ pkgs.sqlite pkgs.reproc pkgs.catch2_3 ];
|
||||||
pkgs.ninja
|
|
||||||
];
|
|
||||||
buildInputs = [
|
|
||||||
pkgs.sqlite
|
|
||||||
pkgs.reproc
|
|
||||||
pkgs.catch2_3
|
|
||||||
];
|
|
||||||
configurePhase = ''
|
configurePhase = ''
|
||||||
cmake -S build -B build/release -G Ninja \
|
cmake -S build -B build/release -G Ninja \
|
||||||
-DCMAKE_BUILD_TYPE=Release
|
-DCMAKE_BUILD_TYPE=Release
|
||||||
@@ -40,16 +34,67 @@
|
|||||||
buildCppPackage = { src, name ? null, ... }@args:
|
buildCppPackage = { src, name ? null, ... }@args:
|
||||||
let
|
let
|
||||||
lock = builtins.fromTOML (builtins.readFile (src + "/Cargoxx.lock"));
|
lock = builtins.fromTOML (builtins.readFile (src + "/Cargoxx.lock"));
|
||||||
root = builtins.head
|
isDep = p: p ? linkdb_source;
|
||||||
(builtins.filter (p: !(p ? linkdb_source)) lock.package);
|
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;
|
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 {
|
in pkgs.gcc15Stdenv.mkDerivation {
|
||||||
inherit pname src;
|
inherit pname src;
|
||||||
version = root.version;
|
version = root.version;
|
||||||
nativeBuildInputs = [ cargoxx-bin pkgs.nix ];
|
nativeBuildInputs =
|
||||||
|
[ cargoxx-bin pkgs.cmake pkgs.ninja ]
|
||||||
|
++ pkgs.lib.optional usesPkgConfig pkgs.pkg-config;
|
||||||
|
buildInputs = depInputs;
|
||||||
|
dontConfigure = true;
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
export HOME=$(mktemp -d)
|
export HOME=$(mktemp -d)
|
||||||
cargoxx build --release
|
cargoxx build --release --offline --vendor ${vendorToml}
|
||||||
'';
|
'';
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
@@ -57,7 +102,6 @@
|
|||||||
cp build/release/${pname}_bin $out/bin/${pname}
|
cp build/release/${pname}_bin $out/bin/${pname}
|
||||||
'';
|
'';
|
||||||
hardeningDisable = [ "all" ];
|
hardeningDisable = [ "all" ];
|
||||||
__noChroot = false;
|
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
packages.default = cargoxx-bin;
|
packages.default = cargoxx-bin;
|
||||||
@@ -65,15 +109,8 @@
|
|||||||
devShells.default = pkgs.gcc15Stdenv.mkDerivation {
|
devShells.default = pkgs.gcc15Stdenv.mkDerivation {
|
||||||
name = "cargoxx-dev";
|
name = "cargoxx-dev";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [ pkgs.ninja pkgs.cmake ];
|
||||||
pkgs.ninja
|
buildInputs = [ pkgs.reproc pkgs.sqlite pkgs.catch2_3 ];
|
||||||
pkgs.cmake
|
|
||||||
];
|
|
||||||
buildInputs = [
|
|
||||||
pkgs.reproc
|
|
||||||
pkgs.sqlite
|
|
||||||
pkgs.catch2_3
|
|
||||||
];
|
|
||||||
hardeningDisable = [ "all" ];
|
hardeningDisable = [ "all" ];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
// nix eval emits these markers when an attribute is missing on the flake.
|
||||||
auto looks_like_missing_attribute(std::string_view stderr_text) -> bool {
|
auto looks_like_missing_attribute(std::string_view stderr_text) -> bool {
|
||||||
return stderr_text.find("does not provide attribute") != std::string_view::npos ||
|
return stderr_text.find("does not provide attribute") != std::string_view::npos ||
|
||||||
stderr_text.find("attribute '") != std::string_view::npos &&
|
(stderr_text.find("attribute '") != std::string_view::npos &&
|
||||||
stderr_text.find("missing") != std::string_view::npos;
|
stderr_text.find("missing") != std::string_view::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user