[M4] cargoxx build invokes nix+cmake
This commit is contained in:
14
CHANGELOG.md
14
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
|
and `[build]` honoring `warnings_as_errors` and `sanitizers`. Source
|
||||||
paths emitted relative to `build/` (i.e. prefixed with `../`).
|
paths emitted relative to `build/` (i.e. prefixed with `../`).
|
||||||
Output is deterministic. `tests/codegen_cmake.cpp` covers 11 cases.
|
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/<profile> -S build -G Ninja
|
||||||
|
-DCMAKE_BUILD_TYPE=<Profile>` 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
|
- `cargoxx.exec`: `ExecResult`, `ExecOptions`, and
|
||||||
`run(program, args, opts)` — argv-only subprocess wrapper around
|
`run(program, args, opts)` — argv-only subprocess wrapper around
|
||||||
reproc 14.2.4. Captures stdout/stderr (or inherits stdio when
|
reproc 14.2.4. Captures stdout/stderr (or inherits stdio when
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ auto cmd_new(const std::string& name, bool lib_only,
|
|||||||
const std::filesystem::path& parent_dir) -> util::Result<void>;
|
const std::filesystem::path& parent_dir) -> util::Result<void>;
|
||||||
|
|
||||||
// Generates flake.nix, build/CMakeLists.txt, and Cargoxx.lock for the project
|
// Generates flake.nix, build/CMakeLists.txt, and Cargoxx.lock for the project
|
||||||
// rooted at `project_root`. With `no_build = true`, only generates files; the
|
// rooted at `project_root`. With `no_build = true`, only generates files;
|
||||||
// nix/cmake invocation lands at M4. `overlay_path` lets tests redirect the
|
// otherwise also runs `nix develop -c cmake ...` to configure and build.
|
||||||
// linkdb overlay away from ~/.cache.
|
// `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,
|
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)
|
||||||
-> util::Result<void>;
|
-> util::Result<void>;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import cargoxx.layout;
|
|||||||
import cargoxx.linkdb;
|
import cargoxx.linkdb;
|
||||||
import cargoxx.lockfile;
|
import cargoxx.lockfile;
|
||||||
import cargoxx.codegen;
|
import cargoxx.codegen;
|
||||||
|
import cargoxx.exec;
|
||||||
|
|
||||||
namespace cargoxx::cli {
|
namespace cargoxx::cli {
|
||||||
|
|
||||||
@@ -72,7 +73,36 @@ auto synthesize_lockfile(const manifest::Manifest& m,
|
|||||||
|
|
||||||
} // namespace
|
} // 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<std::string>& cmake_args,
|
||||||
|
std::string_view phase) -> util::Result<void> {
|
||||||
|
std::vector<std::string> 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<std::string> target,
|
||||||
std::optional<fs::path> overlay_path) -> util::Result<void> {
|
std::optional<fs::path> overlay_path) -> util::Result<void> {
|
||||||
auto manifest_path = project_root / "Cargoxx.toml";
|
auto manifest_path = project_root / "Cargoxx.toml";
|
||||||
auto m = manifest::parse(manifest_path);
|
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());
|
return std::unexpected(r.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!no_build) {
|
if (no_build) {
|
||||||
return std::unexpected(util::Error{
|
return {};
|
||||||
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)",
|
const std::string profile = release ? "release" : "debug";
|
||||||
std::nullopt, std::nullopt,
|
const std::string profile_cap = release ? "Release" : "Debug";
|
||||||
});
|
const auto build_dir = std::format("build/{}", profile);
|
||||||
|
|
||||||
|
std::vector<std::string> 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<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) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -23,9 +23,12 @@ auto run(int argc, char** argv) -> int {
|
|||||||
"build", "Generate flake.nix and build/CMakeLists.txt; build with nix+cmake");
|
"build", "Generate flake.nix and build/CMakeLists.txt; build with nix+cmake");
|
||||||
bool build_no_build = false;
|
bool build_no_build = false;
|
||||||
bool build_release = false;
|
bool build_release = false;
|
||||||
|
std::string build_target;
|
||||||
build_cmd->add_flag("--no-build", build_no_build,
|
build_cmd->add_flag("--no-build", build_no_build,
|
||||||
"Generate files only; do not invoke nix/cmake");
|
"Generate files only; do not invoke nix/cmake");
|
||||||
build_cmd->add_flag("--release", build_release, "Build the release profile");
|
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 {
|
try {
|
||||||
app.parse(argc, argv);
|
app.parse(argc, argv);
|
||||||
@@ -52,12 +55,20 @@ auto run(int argc, char** argv) -> int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (*build_cmd) {
|
if (*build_cmd) {
|
||||||
auto r = cmd_build(cwd, build_no_build, build_release);
|
std::optional<std::string> target;
|
||||||
|
if (!build_target.empty()) {
|
||||||
|
target = build_target;
|
||||||
|
}
|
||||||
|
auto r = cmd_build(cwd, build_no_build, build_release, target);
|
||||||
if (!r) {
|
if (!r) {
|
||||||
std::cerr << util::format(r.error());
|
std::cerr << util::format(r.error());
|
||||||
return 1;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
auto emit_header(const manifest::Manifest& m) -> std::string {
|
||||||
return std::format(
|
return std::format(
|
||||||
"cmake_minimum_required(VERSION 3.30)\n"
|
"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"
|
"project({} LANGUAGES CXX)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"# Generated by cargoxx — do not edit.\n"
|
"# Generated by cargoxx — do not edit.\n"
|
||||||
@@ -67,7 +74,8 @@ auto emit_header(const manifest::Manifest& m) -> std::string {
|
|||||||
"# ----- toolchain configuration -----\n"
|
"# ----- toolchain configuration -----\n"
|
||||||
"set(CMAKE_CXX_STANDARD {})\n"
|
"set(CMAKE_CXX_STANDARD {})\n"
|
||||||
"set(CMAKE_CXX_STANDARD_REQUIRED ON)\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_CXX_SCAN_FOR_MODULES ON)\n"
|
||||||
"set(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n",
|
"set(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n",
|
||||||
m.package.name, edition_to_int(m.package.edition));
|
m.package.name, edition_to_int(m.package.edition));
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ TEST_CASE("cmd_build generates files for a no-deps binary project",
|
|||||||
REQUIRE(cmd_new("hello", false, parent).has_value());
|
REQUIRE(cmd_new("hello", false, parent).has_value());
|
||||||
auto root = parent / "hello";
|
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(r.has_value());
|
||||||
|
|
||||||
REQUIRE(std::filesystem::exists(root / "flake.nix"));
|
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());
|
REQUIRE(cmd_new("widget", true, parent).has_value());
|
||||||
auto root = parent / "widget";
|
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());
|
REQUIRE(r.has_value());
|
||||||
|
|
||||||
auto cmake_text = read_file(root / "build" / "CMakeLists.txt");
|
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";
|
auto root = parent / "app";
|
||||||
add_dep(root, "fmt", "10.2.0");
|
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());
|
REQUIRE(r.has_value());
|
||||||
|
|
||||||
auto cmake_text = read_file(root / "build" / "CMakeLists.txt");
|
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";
|
auto root = parent / "app";
|
||||||
add_dep(root, "boost", "1.84.0", {"filesystem", "system"});
|
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());
|
REQUIRE(r.has_value());
|
||||||
|
|
||||||
auto cmake_text = read_file(root / "build" / "CMakeLists.txt");
|
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, "fmt", "10.2.0");
|
||||||
add_dep(root, "spdlog", "1.13.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());
|
REQUIRE(r.has_value());
|
||||||
|
|
||||||
auto lock = lockfile::parse(root / "Cargoxx.lock");
|
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";
|
auto root = parent / "app";
|
||||||
add_dep(root, "obscurelib", "0.0.1");
|
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_FALSE(r.has_value());
|
||||||
REQUIRE(r.error().code == ErrorCode::LinkdbUnknownPackage);
|
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();
|
auto parent = fresh_dir();
|
||||||
REQUIRE(cmd_new("app", false, parent).has_value());
|
REQUIRE(cmd_new("app", false, parent).has_value());
|
||||||
auto root = parent / "app";
|
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));
|
TEST_CASE("cmd_build accepts a --target argument under --no-build",
|
||||||
REQUIRE_FALSE(r.has_value());
|
"[cli][build]") {
|
||||||
REQUIRE(r.error().code == ErrorCode::NotImplemented);
|
auto parent = fresh_dir();
|
||||||
// Files are still generated before the build step would run
|
REQUIRE(cmd_new("app", false, parent).has_value());
|
||||||
REQUIRE(std::filesystem::exists(root / "flake.nix"));
|
auto root = parent / "app";
|
||||||
REQUIRE(std::filesystem::exists(root / "build" / "CMakeLists.txt"));
|
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",
|
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";
|
auto root = parent / "app";
|
||||||
add_dep(root, "fmt", "10.2.0");
|
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_cmake = read_file(root / "build" / "CMakeLists.txt");
|
||||||
auto first_flake = read_file(root / "flake.nix");
|
auto first_flake = read_file(root / "flake.nix");
|
||||||
auto first_lock = read_file(root / "Cargoxx.lock");
|
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 / "build" / "CMakeLists.txt") == first_cmake);
|
||||||
REQUIRE(read_file(root / "flake.nix") == first_flake);
|
REQUIRE(read_file(root / "flake.nix") == first_flake);
|
||||||
REQUIRE(read_file(root / "Cargoxx.lock") == first_lock);
|
REQUIRE(read_file(root / "Cargoxx.lock") == first_lock);
|
||||||
|
|||||||
Reference in New Issue
Block a user