diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a975f5..a6b5e07 100644 --- a/CHANGELOG.md +++ b/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/#` 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 ]` — 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 ]` — 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 /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 `||`. diff --git a/flake.nix b/flake.nix index 7fc072d..2551fe1 100644 --- a/flake.nix +++ b/flake.nix @@ -10,19 +10,13 @@ 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 - ]; + 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 @@ -40,16 +34,67 @@ buildCppPackage = { src, name ? null, ... }@args: let lock = builtins.fromTOML (builtins.readFile (src + "/Cargoxx.lock")); - root = builtins.head - (builtins.filter (p: !(p ? linkdb_source)) lock.package); + 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.nix ]; + 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 + cargoxx build --release --offline --vendor ${vendorToml} ''; installPhase = '' mkdir -p $out/bin @@ -57,7 +102,6 @@ cp build/release/${pname}_bin $out/bin/${pname} ''; hardeningDisable = [ "all" ]; - __noChroot = false; }; in { packages.default = cargoxx-bin; @@ -65,15 +109,8 @@ 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 - ]; + nativeBuildInputs = [ pkgs.ninja pkgs.cmake ]; + buildInputs = [ pkgs.reproc pkgs.sqlite pkgs.catch2_3 ]; hardeningDisable = [ "all" ]; }; }); diff --git a/src/resolver/nixpkgs_probe.cpp b/src/resolver/nixpkgs_probe.cpp index 7b05762..adfae19 100644 --- a/src/resolver/nixpkgs_probe.cpp +++ b/src/resolver/nixpkgs_probe.cpp @@ -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 diff --git a/tests/e2e/buildCppPackage/Cargoxx.toml b/tests/e2e/buildCppPackage/Cargoxx.toml new file mode 100644 index 0000000..1105668 --- /dev/null +++ b/tests/e2e/buildCppPackage/Cargoxx.toml @@ -0,0 +1,7 @@ +[package] +name = "e2e_demo" +version = "0.1.0" +edition = "cpp23" + +[dependencies] +nlohmann_json = "*" diff --git a/tests/e2e/buildCppPackage/flake.nix b/tests/e2e/buildCppPackage/flake.nix new file mode 100644 index 0000000..cac972d --- /dev/null +++ b/tests/e2e/buildCppPackage/flake.nix @@ -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 = ./.; }; + }; +} diff --git a/tests/e2e/buildCppPackage/run.sh b/tests/e2e/buildCppPackage/run.sh new file mode 100755 index 0000000..4db5630 --- /dev/null +++ b/tests/e2e/buildCppPackage/run.sh @@ -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" diff --git a/tests/e2e/buildCppPackage/src/main.cpp b/tests/e2e/buildCppPackage/src/main.cpp new file mode 100644 index 0000000..1bd969a --- /dev/null +++ b/tests/e2e/buildCppPackage/src/main.cpp @@ -0,0 +1,9 @@ +#include +import std; + +int main() { + nlohmann::json j; + j["hello"] = "world"; + std::println("Hello from {}!", j["hello"].get()); + return 0; +}