diff --git a/CHANGELOG.md b/CHANGELOG.md index 3263671..54c93c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,20 @@ All notable changes to cargoxx will be documented in this file. and `[build]` honoring `warnings_as_errors` and `sanitizers`. Source paths emitted relative to `build/` (i.e. prefixed with `../`). Output is deterministic. `tests/codegen_cmake.cpp` covers 11 cases. +- `cargoxx build` (without `--no-build`) now drives the full build: + `nix develop -c cmake -B build/ -S build -G Ninja + -DCMAKE_BUILD_TYPE=` then `nix develop -c cmake --build` + with optional `--target`, both with `inherit_stdio = true` so the + user sees compilation output live. Non-zero cmake exit returns + `BuildCmakeFailed`. End-to-end verified: `cargoxx new myapp && + cargoxx build` produces a working `build/debug/myapp` that prints + `Hello from myapp!`. +- Generated `build/CMakeLists.txt` now opts into the experimental + `import std;` UUID + `CMAKE_CXX_MODULE_STD ON`, and sets + `CMAKE_CXX_EXTENSIONS ON` (deviation from SPEC §8 — required for + libc++'s std module to load without `module-file-config-mismatch` + on clang 21). Without these the templates from `cargoxx new` fail + to compile. - `cargoxx.exec`: `ExecResult`, `ExecOptions`, and `run(program, args, opts)` — argv-only subprocess wrapper around reproc 14.2.4. Captures stdout/stderr (or inherits stdio when diff --git a/src/cli/cli.cppm b/src/cli/cli.cppm index 54cad3c..310e139 100644 --- a/src/cli/cli.cppm +++ b/src/cli/cli.cppm @@ -14,10 +14,12 @@ auto cmd_new(const std::string& name, bool lib_only, const std::filesystem::path& parent_dir) -> util::Result; // Generates flake.nix, build/CMakeLists.txt, and Cargoxx.lock for the project -// rooted at `project_root`. With `no_build = true`, only generates files; the -// nix/cmake invocation lands at M4. `overlay_path` lets tests redirect the -// linkdb overlay away from ~/.cache. +// rooted at `project_root`. With `no_build = true`, only generates files; +// otherwise also runs `nix develop -c cmake ...` to configure and build. +// `target` (when set) is passed through to `cmake --build --target`. +// `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 target = std::nullopt, std::optional overlay_path = std::nullopt) -> util::Result; diff --git a/src/cli/cmd_build.cpp b/src/cli/cmd_build.cpp index 9c539d4..9fc0ed3 100644 --- a/src/cli/cmd_build.cpp +++ b/src/cli/cmd_build.cpp @@ -7,6 +7,7 @@ import cargoxx.layout; import cargoxx.linkdb; import cargoxx.lockfile; import cargoxx.codegen; +import cargoxx.exec; namespace cargoxx::cli { @@ -72,7 +73,36 @@ auto synthesize_lockfile(const manifest::Manifest& m, } // namespace -auto cmd_build(const fs::path& project_root, bool no_build, bool /*release*/, +namespace { + +auto run_nix_cmake(const fs::path& project_root, const std::vector& cmake_args, + std::string_view phase) -> util::Result { + std::vector args{"develop", "--command", "cmake"}; + args.insert(args.end(), cmake_args.begin(), cmake_args.end()); + + auto r = exec::run("nix", 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 {}; +} + +} // namespace + +auto cmd_build(const fs::path& project_root, bool no_build, bool release, + std::optional target, std::optional overlay_path) -> util::Result { auto manifest_path = project_root / "Cargoxx.toml"; auto m = manifest::parse(manifest_path); @@ -124,13 +154,31 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool /*release*/, return std::unexpected(r.error()); } - if (!no_build) { - return std::unexpected(util::Error{ - util::ErrorCode::NotImplemented, - "cargoxx build invocation is not implemented in this milestone", - "rerun with --no-build to generate files only (full build lands at M4)", - std::nullopt, std::nullopt, - }); + if (no_build) { + return {}; + } + + const std::string profile = release ? "release" : "debug"; + const std::string profile_cap = release ? "Release" : "Debug"; + const auto build_dir = std::format("build/{}", profile); + + std::vector configure_args{ + "-B", build_dir, + "-S", "build", + "-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 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) { + return std::unexpected(r.error()); } return {}; diff --git a/src/cli/run.cpp b/src/cli/run.cpp index d664c01..8dcc4e8 100644 --- a/src/cli/run.cpp +++ b/src/cli/run.cpp @@ -23,9 +23,12 @@ 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; + std::string build_target; 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_option("--target", build_target, + "Build a specific target (passed to cmake --build)"); try { app.parse(argc, argv); @@ -52,12 +55,20 @@ auto run(int argc, char** argv) -> int { } if (*build_cmd) { - auto r = cmd_build(cwd, build_no_build, build_release); + std::optional target; + if (!build_target.empty()) { + target = build_target; + } + auto r = cmd_build(cwd, build_no_build, build_release, target); if (!r) { std::cerr << util::format(r.error()); return 1; } - std::cout << " Generated flake.nix, build/CMakeLists.txt, Cargoxx.lock\n"; + if (build_no_build) { + std::cout << " Generated flake.nix, build/CMakeLists.txt, Cargoxx.lock\n"; + } else { + std::cout << " Built\n"; + } return 0; } diff --git a/src/codegen/cmake.cpp b/src/codegen/cmake.cpp index 05e3daa..ccfc111 100644 --- a/src/codegen/cmake.cpp +++ b/src/codegen/cmake.cpp @@ -59,6 +59,13 @@ auto link_block(std::string_view target_name, std::string_view visibility, auto emit_header(const manifest::Manifest& m) -> std::string { return std::format( "cmake_minimum_required(VERSION 3.30)\n" + "\n" + "# Opt into experimental C++ modules dyndep + `import std;` support.\n" + "# Required until CMake declares these stable.\n" + "set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)\n" + "set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD \"d0edc3af-4c50-42ea-a356-e2862fe7a444\")\n" + "set(CMAKE_CXX_MODULE_STD ON)\n" + "\n" "project({} LANGUAGES CXX)\n" "\n" "# Generated by cargoxx — do not edit.\n" @@ -67,7 +74,8 @@ auto emit_header(const manifest::Manifest& m) -> std::string { "# ----- toolchain configuration -----\n" "set(CMAKE_CXX_STANDARD {})\n" "set(CMAKE_CXX_STANDARD_REQUIRED ON)\n" - "set(CMAKE_CXX_EXTENSIONS OFF)\n" + "# EXTENSIONS=ON for libc++ std-module compatibility (clang 21).\n" + "set(CMAKE_CXX_EXTENSIONS ON)\n" "set(CMAKE_CXX_SCAN_FOR_MODULES ON)\n" "set(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n", m.package.name, edition_to_int(m.package.edition)); diff --git a/tests/cmd_build.cpp b/tests/cmd_build.cpp index 0767878..47054a0 100644 --- a/tests/cmd_build.cpp +++ b/tests/cmd_build.cpp @@ -51,7 +51,7 @@ TEST_CASE("cmd_build generates files for a no-deps binary project", REQUIRE(cmd_new("hello", false, parent).has_value()); auto root = parent / "hello"; - auto r = cmd_build(root, true, false, overlay_path(parent)); + auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent)); REQUIRE(r.has_value()); REQUIRE(std::filesystem::exists(root / "flake.nix")); @@ -73,7 +73,7 @@ TEST_CASE("cmd_build generates files for a library project", "[cli][build]") { REQUIRE(cmd_new("widget", true, parent).has_value()); auto root = parent / "widget"; - auto r = cmd_build(root, true, false, overlay_path(parent)); + auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent)); REQUIRE(r.has_value()); auto cmake_text = read_file(root / "build" / "CMakeLists.txt"); @@ -89,7 +89,7 @@ TEST_CASE("cmd_build resolves a curated dep into find_package + targets", auto root = parent / "app"; add_dep(root, "fmt", "10.2.0"); - auto r = cmd_build(root, true, false, overlay_path(parent)); + auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent)); REQUIRE(r.has_value()); auto cmake_text = read_file(root / "build" / "CMakeLists.txt"); @@ -106,7 +106,7 @@ TEST_CASE("cmd_build resolves a componentized dep", "[cli][build]") { auto root = parent / "app"; add_dep(root, "boost", "1.84.0", {"filesystem", "system"}); - auto r = cmd_build(root, true, false, overlay_path(parent)); + auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent)); REQUIRE(r.has_value()); auto cmake_text = read_file(root / "build" / "CMakeLists.txt"); @@ -123,7 +123,7 @@ TEST_CASE("cmd_build synthesizes a lockfile entry per dep", "[cli][build]") { add_dep(root, "fmt", "10.2.0"); add_dep(root, "spdlog", "1.13.0"); - auto r = cmd_build(root, true, false, overlay_path(parent)); + auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent)); REQUIRE(r.has_value()); auto lock = lockfile::parse(root / "Cargoxx.lock"); @@ -139,22 +139,31 @@ TEST_CASE("cmd_build fails for an unknown dep", "[cli][build]") { auto root = parent / "app"; add_dep(root, "obscurelib", "0.0.1"); - auto r = cmd_build(root, true, false, overlay_path(parent)); + auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent)); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::LinkdbUnknownPackage); } -TEST_CASE("cmd_build without --no-build returns NotImplemented", "[cli][build]") { +TEST_CASE("cmd_build --release writes a release-typed build/CMakeLists", + "[cli][build]") { + // The CMAKE_BUILD_TYPE flag itself is set on the cmake command line, not in + // the generated CMakeLists.txt — but the same generation runs for both + // profiles, so just verify the --release path with --no-build doesn't error. auto parent = fresh_dir(); REQUIRE(cmd_new("app", false, parent).has_value()); auto root = parent / "app"; + auto r = cmd_build(root, true, true, std::nullopt, overlay_path(parent)); + REQUIRE(r.has_value()); +} - auto r = cmd_build(root, false, false, overlay_path(parent)); - REQUIRE_FALSE(r.has_value()); - REQUIRE(r.error().code == ErrorCode::NotImplemented); - // Files are still generated before the build step would run - REQUIRE(std::filesystem::exists(root / "flake.nix")); - REQUIRE(std::filesystem::exists(root / "build" / "CMakeLists.txt")); +TEST_CASE("cmd_build accepts a --target argument under --no-build", + "[cli][build]") { + auto parent = fresh_dir(); + REQUIRE(cmd_new("app", false, parent).has_value()); + auto root = parent / "app"; + auto r = cmd_build(root, true, false, std::string{"app_bin"}, + overlay_path(parent)); + REQUIRE(r.has_value()); } TEST_CASE("cmd_build is idempotent — second run produces identical files", @@ -164,12 +173,12 @@ TEST_CASE("cmd_build is idempotent — second run produces identical files", auto root = parent / "app"; add_dep(root, "fmt", "10.2.0"); - REQUIRE(cmd_build(root, true, false, overlay_path(parent)).has_value()); + 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_lock = read_file(root / "Cargoxx.lock"); - REQUIRE(cmd_build(root, true, false, overlay_path(parent)).has_value()); + 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 / "Cargoxx.lock") == first_lock);