# cargoxx — project specification A Cargo-style frontend for modern C++ that uses Nix as the source of truth for dependencies and generates CMake for the build. Users author projects with C++23 modules and never touch CMake or `flake.nix` by hand. This document defines the user-facing contract: project layout, CLI commands, manifest schema, generated files, and the algorithms that drive code generation. It is the source of truth for what cargoxx must do. Implementation details are in `TECH_SPEC.md`. --- ## 1. Goals and non-goals ### Goals (v0.1) - Single binary CLI `cargoxx` with `new`, `build`, `add`, `run`, `clean` commands. - Cargo-compatible project layout. A user who knows Cargo recognizes everything. - Module-first authoring: every C++ source unit is a module interface (`.cppm`) or a module implementation (`.cpp`). Headers are consumed via the global module fragment, not authored. - Reproducible builds via `flake.nix` + `flake.lock`. The Nix store is the dependency cache. - Generated CMake is hidden in `build/` and treated as a build artifact. Users never edit it. - Curated link database covering ~25 popular libraries, shipped with the tool. ### Non-goals (v0.1) - Not a workspace tool. No multi-package repos in v0.1. - Not a publisher. No `cargoxx publish`, no central registry. - No support for non-Nix package managers as primary (Conan / vcpkg recipes are read-only data sources for the link database). - No Windows support in v0.1. Linux + macOS via Nix. - No automatic resolution of arbitrary packages from Conan / vcpkg recipes. v0.1 ships a hand-curated database; v0.2 adds automatic resolution. ### Explicit scope cuts that may be requested but are out - Cross-compilation - Custom build scripts (`build.rs` equivalent) - Features / conditional compilation flags - Dev-dependencies separate from regular dependencies - Profiles other than `debug` and `release` --- ## 2. Glossary - **Manifest** — the `Cargoxx.toml` file at the project root. - **Link recipe** — the CMake snippet (`find_package(...)` + target names) needed to consume a package. Stored per `(package, version)` in the link database. - **Link database** — SQLite database at `~/.cache/cargoxx/linkdb.sqlite` plus a curated JSON file shipped with cargoxx. - **Module unit** — a `.cppm` file containing `export module foo;`. - **Implementation unit** — a `.cpp` file containing `module foo;` (no `export`). - **Crate root** — Cargo terminology for the entry source file. We use the same convention: `src/main.cpp` is the binary crate root, `src/lib.cppm` is the library crate root. --- ## 3. User-facing project layout The layout is fixed. cargoxx infers targets from file paths; it does not read globs from the manifest. ``` my-project/ ├── Cargoxx.toml # manifest (committed) ├── Cargoxx.lock # resolved dep versions (committed) ├── flake.nix # generated from manifest (committed) ├── flake.lock # nix lockfile (committed) ├── .gitignore # cargoxx-managed entries ├── src/ │ ├── main.cpp # → binary target named after [package].name │ ├── lib.cppm # → library target named after [package].name │ └── bin/ │ └── tool.cpp # → additional binary target "tool" ├── tests/ │ └── basic.cpp # → test target "basic" ├── examples/ │ └── demo.cpp # → example target "demo" └── build/ # gitignored ├── CMakeLists.txt # generated ├── debug/ # cmake binary dir for debug profile └── release/ # cmake binary dir for release profile ``` ### Target inference rules Applied in order, each rule produces zero or more CMake targets. | Path | Target kind | Target name | | --- | --- | --- | | `src/lib.cppm` | static library | `` | | `src/main.cpp` | executable | `` | | `src/bin/.cpp` | executable | `` | | `src//*.cppm` | module units belonging to `src/lib.cppm` | (none — added to library) | | `src//*.cpp` | implementation units belonging to `src/lib.cppm` | (none — added to library) | | `tests/.cpp` | executable + `add_test` | `test_` | | `examples/.cpp` | executable | `example_` | Rules are deliberately rigid. If a user wants exotic layouts, they can drop down to raw CMake — but cargoxx will refuse to generate from a layout it doesn't recognize. ### Conflict and validation - If `src/lib.cppm` exists, `src/main.cpp` (when present) must `import ;` — cargoxx does not enforce this with a parser, but documents it. - If neither `src/main.cpp` nor `src/lib.cppm` exists, `cargoxx build` errors with: `no target found: expected src/main.cpp or src/lib.cppm`. - File names other than the listed ones in `src/`, `src/bin/`, `tests/`, `examples/` are ignored (not an error). This allows users to keep auxiliary scripts in those directories. - Subdirectories under `src/bin/`, `tests/`, `examples/` are not walked. Each top-level file is one target. --- ## 4. Manifest format — `Cargoxx.toml` ```toml [package] name = "my-project" version = "0.1.0" edition = "cpp23" # one of: cpp20, cpp23, cpp26 authors = ["Name "] # optional license = "MIT" # optional, SPDX expression [dependencies] fmt = "10.2" spdlog = "1.13" boost = { version = "1.84", components = ["filesystem", "system"] } nlohmann_json = "3.11" [build] warnings_as_errors = true # optional, default false sanitizers = ["address"] # optional, list of: address, undefined, thread, leak ``` ### Dependency syntax Two forms allowed: ```toml fmt = "10.2" # version string only boost = { version = "1.84", components = ["filesystem"] } # table form ``` Version strings follow Cargo semver semantics (`"10.2"` means `>=10.2.0, <11.0.0`). cargoxx resolves to the highest matching version available in the configured Nixpkgs revision. `components` is meaningful only for packages whose link recipe declares component support (Boost, Qt, abseil). Specified components map directly to CMake targets. ### Reserved fields The following keys are parsed and stored but not yet acted on. They MUST be accepted without error so manifests written for v0.1 stay valid in later versions: - `[dev-dependencies]` - `[features]` - `[workspace]` - `[package].repository` - `[package].description` --- ## 5. Lockfile — `Cargoxx.lock` Generated and updated by cargoxx. Format is TOML, committed to version control. ```toml version = 1 [[package]] name = "my-project" version = "0.1.0" dependencies = [ "fmt 10.2.1", "spdlog 1.13.0", ] [[package]] name = "fmt" version = "10.2.1" nixpkgs_attr = "fmt_10" nixpkgs_rev = "8a3f...c2d1" linkdb_source = "curated" [[package]] name = "spdlog" version = "1.13.0" nixpkgs_attr = "spdlog" nixpkgs_rev = "8a3f...c2d1" linkdb_source = "curated" ``` `nixpkgs_rev` is the same for all entries in a single resolution — cargoxx pins one Nixpkgs revision per project, picked to satisfy all `[dependencies]` simultaneously. This is the simplest model and matches what Nix flakes do natively. --- ## 6. CLI commands ### `cargoxx new ` and `cargoxx new --lib ` Creates a new project directory. Default (`cargoxx new foo`) creates a binary project: ``` foo/ ├── Cargoxx.toml ├── flake.nix ├── flake.lock ├── .gitignore └── src/ └── main.cpp ``` With `--lib`: ``` foo/ ├── Cargoxx.toml ├── flake.nix ├── flake.lock ├── .gitignore └── src/ └── lib.cppm ``` `src/main.cpp` template: ```cpp import std; int main() { std::println("Hello from {}!", "foo"); return 0; } ``` `src/lib.cppm` template: ```cpp export module foo; import std; export namespace foo { auto greeting() -> std::string_view { return "Hello from foo!"; } } ``` After scaffolding, runs `cargoxx build --no-build` (generate flake.nix and CMakeLists.txt, do not invoke nix/cmake) so the project opens correctly in IDEs immediately. ### `cargoxx add [@]` and `cargoxx add --components ` Adds a dependency. Algorithm: 1. Parse the package spec. 2. If `@` is omitted, query the resolver for the latest version available in Nixpkgs and use that. 3. Resolve the link recipe (see §9). 4. Update `Cargoxx.toml` `[dependencies]` table, preserving formatting where possible. 5. Update `Cargoxx.lock`. 6. Regenerate `flake.nix`. 7. Print the chosen version and link recipe source for transparency: `Added fmt 10.2.1 (linkdb: curated)`. If the link recipe cannot be resolved, the command fails with instructions for the user to file an issue or supply a manual recipe via `Cargoxx.toml`. ### `cargoxx remove ` Inverse of `add`. Removes from manifest, lockfile, and regenerates `flake.nix`. ### `cargoxx build [--release] [--target ]` 1. Validate manifest and project layout. 2. Resolve dependencies if `Cargoxx.lock` is missing or stale. 3. Generate `build/CMakeLists.txt` from manifest and source tree. 4. Generate / update `flake.nix` if stale. 5. Run `nix develop --command cmake -B build/ -S build -G Ninja -DCMAKE_BUILD_TYPE=` if the binary dir doesn't exist or is stale. 6. Run `nix develop --command cmake --build build/ [--target ]`. `` is `debug` (default) or `release` (with `--release`). Capitalized `` is `Debug` or `Release`. ### `cargoxx run [--release] [--bin ] [-- ...]` `build` then exec the chosen binary. If the project has multiple binary targets and `--bin` is omitted, fail with a list. If exactly one binary exists, run it. ### `cargoxx test [--release]` Build all `tests/*.cpp` targets, then run them via `ctest --output-on-failure` inside `build/`. ### `cargoxx clean` Removes `build/`. Does not touch `Cargoxx.lock` or `flake.lock`. ### `cargoxx fmt` and `cargoxx check` Stubs in v0.1 — accepted but only print a deprecation-style message: `cargoxx fmt: not implemented in v0.1, run clang-format directly`. This reserves the command names. --- ## 7. Generated `flake.nix` cargoxx generates exactly this template. Fields in `<<...>>` are substituted from the manifest. ```nix { description = "<>"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/<>"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; llvmPkgs = pkgs.llvmPackages; in { devShell = llvmPkgs.libcxxStdenv.mkDerivation { name = "shell"; version = "1.0"; nativeBuildInputs = [ pkgs.ninja pkgs.cmake pkgs.clang-tools ]; env.NIX_CFLAGS_COMPILE = toString [ "-stdlib=libc++" "-Wno-unused-command-line-argument" "-B${pkgs.lib.getLib pkgs.libcxx}/lib" "-isystem ${pkgs.lib.getDev pkgs.libcxx}/include/c++/v1" ]; hardeningDisable = [ "all" ]; }; }); } ``` `<>` is the Nixpkgs commit hash from `Cargoxx.lock`. The toolchain (`clang_21`, `cmake`, `ninja`) is fixed in v0.1 — Clang because module support is most mature there. Regeneration is idempotent: cargoxx writes the file only if its content would change. This avoids spurious `flake.lock` updates. --- ## 8. Generated `build/CMakeLists.txt` ```cmake cmake_minimum_required(VERSION 3.30) project(<> LANGUAGES CXX) # ----- toolchain configuration ----- set(CMAKE_CXX_STANDARD <>) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_SCAN_FOR_MODULES ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Generated by cargoxx — do not edit. # Source of truth: ../Cargoxx.toml # ----- dependencies ----- <> # ----- library target (if present) ----- <> add_library(<> STATIC) target_sources(<> PUBLIC FILE_SET CXX_MODULES FILES ../src/lib.cppm <> PRIVATE <> ) target_link_libraries(<> PUBLIC <> ) <> # ----- binary target (if src/main.cpp exists) ----- <> add_executable(<>_bin ../src/main.cpp) set_target_properties(<>_bin PROPERTIES OUTPUT_NAME <>) target_link_libraries(<>_bin PRIVATE <> # only if library target also exists <> ) <> # ----- additional binaries (src/bin/*.cpp) ----- <.cpp>> add_executable(<> ../src/bin/<>.cpp) target_link_libraries(<> PRIVATE <> # only if library target also exists <> ) <> # ----- tests ----- <> enable_testing() <.cpp>> add_executable(test_<> ../tests/<>.cpp) target_link_libraries(test_<> PRIVATE <> # only if library target also exists <> ) add_test(NAME <> COMMAND test_<>) <> <> # ----- examples ----- <.cpp>> add_executable(example_<> ../examples/<>.cpp) target_link_libraries(example_<> PRIVATE <> # only if library target also exists <> ) <> # ----- warnings and sanitizers from [build] section ----- <> foreach(target_name IN ITEMS <>) target_compile_options(${target_name} PRIVATE -Wall -Wextra -Wpedantic -Werror) endforeach() <> <> <> <> ``` Edition mapping: - `cpp20` → `set(CMAKE_CXX_STANDARD 20)` - `cpp23` → `set(CMAKE_CXX_STANDARD 23)` - `cpp26` → `set(CMAKE_CXX_STANDARD 26)` (CMake 3.30 supports the value as experimental) Source paths are written relative to `build/` (i.e. prefixed with `../`) because the file lives in `build/CMakeLists.txt` but sources live in `../src/`. --- ## 9. Link database — schema and resolution ### On-disk format Curated database shipped with cargoxx, embedded as a resource (compiled in or read from `${prefix}/share/cargoxx/linkdb.json`): ```json { "version": 1, "packages": { "fmt": [ { "version": ">=10.0.0", "nixpkgs_attr": "fmt_10", "find_package": "fmt CONFIG REQUIRED", "targets": ["fmt::fmt"] }, { "version": ">=8.0.0,<10.0.0", "nixpkgs_attr": "fmt_8", "find_package": "fmt CONFIG REQUIRED", "targets": ["fmt::fmt"] } ], "boost": [ { "version": ">=1.70.0", "nixpkgs_attr": "boost", "find_package": "Boost REQUIRED COMPONENTS {{components}}", "targets": ["Boost::{{component}}"], "components": "supported" } ], "openssl": [ { "version": "*", "nixpkgs_attr": "openssl", "find_package": "OpenSSL REQUIRED", "targets": ["OpenSSL::SSL", "OpenSSL::Crypto"] } ] } } ``` The `version` field is a Cargo-style range. cargoxx picks the first entry whose range matches the resolved version. `{{components}}` and `{{component}}` are Mustache-style placeholders substituted at codegen time. ### User-overlay database `~/.cache/cargoxx/linkdb.sqlite`: ```sql CREATE TABLE recipes ( package TEXT NOT NULL, version_range TEXT NOT NULL, nixpkgs_attr TEXT NOT NULL, find_package TEXT NOT NULL, targets TEXT NOT NULL, -- JSON array components TEXT, -- 'supported' | NULL source TEXT NOT NULL, -- 'curated' | 'conan' | 'vcpkg' | 'nix-probe' | 'manual' verified_at INTEGER NOT NULL, -- unix epoch PRIMARY KEY (package, version_range, source) ); CREATE TABLE resolution_failures ( package TEXT NOT NULL, version TEXT NOT NULL, last_attempt INTEGER NOT NULL, error TEXT NOT NULL, PRIMARY KEY (package, version) ); ``` Lookup precedence: user overlay (any source) → curated (shipped JSON) → automatic resolution (v0.2+) → fail. ### Resolution algorithm (v0.1) `resolve_link_recipe(package, version) -> Recipe | Error`: 1. Query user overlay SQLite. If a row matches and `verified_at` is recent (<30 days for `nix-probe`, never expires for `curated` / `manual`), return it. 2. Query the embedded curated JSON. If a range matches, return it. 3. Return `Error::Unknown { package }`. In v0.2, after step 2 the algorithm continues: 4. Try `conan-center-index` GitHub raw lookup for `recipes//all/conanfile.py`. Parse `cmake_target_name`, `cmake_file_name`. Convert to a recipe. 5. Try `microsoft/vcpkg/ports//usage`. Parse the literal CMake snippet. 6. Run `nix-build` for the package, then scan the output for `lib/cmake/*/​*Config.cmake` and grep `add_library(... IMPORTED)` lines. Each successful step writes the result to the user overlay before returning. --- ## 10. Version resolution algorithm `resolve_version(package, version_spec) -> (version, nixpkgs_rev)`: 1. If `Cargoxx.lock` already pins this package and the spec is satisfied, return the lockfile entry. 2. Query nixhub.io: `https://www.nixhub.io/packages/?_data=routes%2F_nixhub.packages.%24pkg._index`. Parse JSON for available versions and their commits. 3. If nixhub.io is unreachable, fall back to lazamar: `https://lazamar.co.uk/nix-versions/?package=&channel=nixpkgs-unstable`. Parse HTML (well-formed table). 4. If both fail, fall back to a local Nixpkgs git clone at `~/.cache/cargoxx/nixpkgs/`. Run `git log --all -S 'version = ""' -- pkgs/`. 5. Filter the candidate list by the version spec, choose the highest match, return `(version, rev)`. For the *whole-project* resolution (multiple deps), cargoxx picks one revision: the latest revision that contains acceptable versions of every dependency. This is brute-force in v0.1: for each candidate revision (newest first, capped at 50 attempts), check whether all deps resolve. Take the first hit. If no revision satisfies all constraints simultaneously, fail with a list of conflicting deps. The user resolves manually by relaxing version specs. --- ## 11. Curated package list (v0.1) cargoxx ships with link recipes for these 25 packages. They cover the common cases. The list is fixed for v0.1; any package outside it requires a v0.2 automatic resolver or a manual user-overlay entry. | Package | Nixpkgs attr | CMake `find_package` | CMake targets | | --- | --- | --- | --- | | fmt | `fmt` | `fmt CONFIG REQUIRED` | `fmt::fmt` | | spdlog | `spdlog` | `spdlog CONFIG REQUIRED` | `spdlog::spdlog` | | nlohmann_json | `nlohmann_json` | `nlohmann_json CONFIG REQUIRED` | `nlohmann_json::nlohmann_json` | | boost | `boost` | `Boost REQUIRED COMPONENTS {{c}}` | `Boost::{{c}}` | | openssl | `openssl` | `OpenSSL REQUIRED` | `OpenSSL::SSL`, `OpenSSL::Crypto` | | zlib | `zlib` | `ZLIB REQUIRED` | `ZLIB::ZLIB` | | sqlite3 | `sqlite` | `SQLite3 REQUIRED` | `SQLite::SQLite3` | | curl | `curl` | `CURL REQUIRED` | `CURL::libcurl` | | protobuf | `protobuf` | `Protobuf REQUIRED` | `protobuf::libprotobuf` | | grpc | `grpc` | `gRPC CONFIG REQUIRED` | `gRPC::grpc++` | | abseil-cpp | `abseil-cpp` | `absl CONFIG REQUIRED` | `absl::{{c}}` | | gtest | `gtest` | `GTest CONFIG REQUIRED` | `GTest::gtest`, `GTest::gtest_main` | | catch2 | `catch2_3` | `Catch2 CONFIG REQUIRED` | `Catch2::Catch2WithMain` | | eigen | `eigen` | `Eigen3 CONFIG REQUIRED` | `Eigen3::Eigen` | | tbb | `tbb` | `TBB CONFIG REQUIRED` | `TBB::tbb` | | libpng | `libpng` | `PNG REQUIRED` | `PNG::PNG` | | libjpeg | `libjpeg` | `JPEG REQUIRED` | `JPEG::JPEG` | | freetype | `freetype` | `Freetype REQUIRED` | `Freetype::Freetype` | | glfw | `glfw` | `glfw3 CONFIG REQUIRED` | `glfw` | | glm | `glm` | `glm CONFIG REQUIRED` | `glm::glm` | | sdl2 | `SDL2` | `SDL2 CONFIG REQUIRED` | `SDL2::SDL2` | | cli11 | `cli11` | `CLI11 CONFIG REQUIRED` | `CLI11::CLI11` | | cxxopts | `cxxopts` | `cxxopts CONFIG REQUIRED` | `cxxopts::cxxopts` | | range-v3 | `range-v3` | `range-v3 CONFIG REQUIRED` | `range-v3::range-v3` | | magic_enum | `magic-enum` | `magic_enum CONFIG REQUIRED` | `magic_enum::magic_enum` | Verification of these entries is part of acceptance for v0.1 — see §13 of `TECH_SPEC.md`. --- ## 12. Error model User-facing errors must be actionable. Each error has a code, a one-line message, and a "what to do" hint. Examples: ``` error[E0001]: no target found --> ./ expected one of: src/main.cpp, src/lib.cppm hint: run `cargoxx new --lib ` to create a library project ``` ``` error[E0042]: package not in link database --> Cargoxx.toml:7:1 package "obscurelib" has no known CMake link recipe hint: file an issue at /issues/new, or add a manual recipe via: cargoxx linkdb add obscurelib --find-package "obscurelib CONFIG REQUIRED" --targets "obscurelib::obscurelib" ``` ``` error[E0010]: unsatisfiable version constraint fmt = "11.0" is not available in any Nixpkgs revision that also has spdlog = "1.13" available revisions for fmt 11.0: 8a3f...c2d1, 7e21...b09a available revisions for spdlog 1.13: 4d22...e1f8 hint: relax one constraint and re-run `cargoxx add` ``` The full list of error codes is in `TECH_SPEC.md` §6. --- ## 13. Tools and dependencies (host project) cargoxx itself is a C++23 application. It is a single static binary at install time. | Concern | Library | Why | | --- | --- | --- | | TOML parsing | `toml++` (header-only) | Best C++ TOML library, supports comment-preserving writes via toml::table | | JSON parsing | `nlohmann/json` | Familiar, header-only, fine for our scale | | HTTP client | `cpp-httplib` (header-only) | No OpenSSL hard dep at build time, bundled TLS via system OpenSSL | | SQLite | `sqlite3` C API directly | One translation unit, no abstraction tax | | Git operations | shell out to `git` | libgit2 is a heavy dep for what we need | | Subprocess | `reproc` (or `boost::process`) | Cross-platform, capture stdout/stderr cleanly | | CLI parsing | `CLI11` | Subcommand support, good UX | | Logging | `spdlog` | Everyone has it | | Filesystem | `std::filesystem` | Built in | | Testing | `Catch2 v3` | Module support, modern | Bootstrap toolchain: `clang_21`, `cmake_3_30+`, `ninja`. Provided by a bootstrap `flake.nix` shipped in cargoxx's own repo. --- ## 14. Security considerations - cargoxx never runs commands from network responses. Conan recipes are Python files; v0.2's automatic resolver MUST parse them as text (regex / minimal AST), never execute them. - vcpkg `usage` files are plain text and safe to read. - nixhub.io and lazamar are trusted only as version directories. The actual fetch happens through Nix, which verifies hashes via `flake.lock`. - The user overlay SQLite is per-user with default permissions (0600). - cargoxx never writes outside the project directory and `~/.cache/cargoxx/`. --- ## 15. Versioning and stability v0.1 is explicitly experimental. Manifest format may break at v0.2. After v1.0: - Bumping `[package].edition` is the only way to opt into breaking C++ language changes. - The manifest format is stable across minor versions. - The link database format is independent of the manifest version. - Generated `flake.nix` and `CMakeLists.txt` are not part of the stability contract — they're build artifacts. `Cargoxx.lock` format version is bumped on incompatible changes; cargoxx refuses to read a newer lockfile than it understands.