From 6e922b72490d09480906b6dfdc45f6fcd0e0e38c Mon Sep 17 00:00:00 2001 From: Amadey Vorontsov Date: Thu, 7 May 2026 23:32:46 +0000 Subject: [PATCH] Initial commit --- .clang-format | 16 + .gitignore | 20 ++ CHANGELOG.md | 14 + CMakeLists.txt | 52 +++ README.md | 23 ++ SPEC.md | 641 ++++++++++++++++++++++++++++++++++ TECH_SPEC.md | 696 +++++++++++++++++++++++++++++++++++++ flake.lock | 61 ++++ flake.nix | 38 ++ src/cli/cli.cppm | 10 + src/codegen/codegen.cppm | 7 + src/exec/exec.cppm | 3 + src/layout/layout.cppm | 3 + src/lib.cppm | 11 + src/linkdb/linkdb.cppm | 3 + src/lockfile/lockfile.cppm | 4 + src/main.cpp | 5 + src/manifest/manifest.cppm | 3 + src/resolver/resolver.cppm | 5 + src/util/util.cppm | 1 + 20 files changed, 1616 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 SPEC.md create mode 100644 TECH_SPEC.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/cli/cli.cppm create mode 100644 src/codegen/codegen.cppm create mode 100644 src/exec/exec.cppm create mode 100644 src/layout/layout.cppm create mode 100644 src/lib.cppm create mode 100644 src/linkdb/linkdb.cppm create mode 100644 src/lockfile/lockfile.cppm create mode 100644 src/main.cpp create mode 100644 src/manifest/manifest.cppm create mode 100644 src/resolver/resolver.cppm create mode 100644 src/util/util.cppm diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..c73f9be --- /dev/null +++ b/.clang-format @@ -0,0 +1,16 @@ +--- +BasedOnStyle: LLVM +ColumnLimit: 100 +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +PointerAlignment: Left +AlwaysBreakTemplateDeclarations: Yes +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +SortIncludes: CaseSensitive +IncludeBlocks: Regroup +SpaceAfterTemplateKeyword: true +NamespaceIndentation: None +FixNamespaceComments: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf6e331 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Build outputs +/build/ +/result +/result-* + +# CMake +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +compile_commands.json + +# Editors / IDEs +.vscode/ +.idea/ +*.swp +*.swo + +# direnv / Nix +.direnv/ +.envrc.local diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7337029 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to cargoxx will be documented in this file. + +## [Unreleased] + +### Added +- M0 repo skeleton: hand-written `CMakeLists.txt`, bootstrap `flake.nix`, + empty C++23 module units for every component listed in `TECH_SPEC.md` §1 + (`util`, `exec`, `manifest`, `lockfile`, `layout`, `linkdb`, `resolver`, + `codegen`, `cli`), root module `cargoxx`, and a stub `main.cpp` that + builds an empty `cargoxx` binary. +- `.clang-format` (LLVM, 100-column) and `.gitignore`. +- `SPEC.md`, `TECH_SPEC.md`. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9724378 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.30) + +# Opt into experimental C++ modules dyndep + `import std;` support. +# Required until CMake declares these stable; see CMake docs for the current UUID. +set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1) +set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444") +set(CMAKE_CXX_MODULE_STD ON) + +project(cargoxx LANGUAGES CXX VERSION 0.1.0) + +# Phase 0: hand-written CMakeLists.txt. Replaced by generated build/CMakeLists.txt +# at milestone M3 once cargoxx can build itself. See TECH_SPEC.md §15. + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_SCAN_FOR_MODULES ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE) +endif() + +# TECH_SPEC.md §17: -Wall -Wextra -Wpedantic -Wconversion, -Werror in CI. +add_compile_options(-Wall -Wextra -Wpedantic -Wconversion) + +option(CARGOXX_WERROR "Treat warnings as errors" OFF) +if(CARGOXX_WERROR) + add_compile_options(-Werror) +endif() + +# ----- cargoxx library: all module units ----- +add_library(cargoxx STATIC) +target_sources(cargoxx + PUBLIC + FILE_SET CXX_MODULES FILES + src/lib.cppm + src/util/util.cppm + src/exec/exec.cppm + src/manifest/manifest.cppm + src/lockfile/lockfile.cppm + src/layout/layout.cppm + src/linkdb/linkdb.cppm + src/resolver/resolver.cppm + src/codegen/codegen.cppm + src/cli/cli.cppm +) + +# ----- cargoxx binary ----- +add_executable(cargoxx_bin src/main.cpp) +set_target_properties(cargoxx_bin PROPERTIES OUTPUT_NAME cargoxx) +target_link_libraries(cargoxx_bin PRIVATE cargoxx) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d119304 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# cargoxx + +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. + +> Status: pre-v0.1, milestone M0 (repo skeleton). Not yet usable. + +See [`SPEC.md`](./SPEC.md) for the user-facing contract and +[`TECH_SPEC.md`](./TECH_SPEC.md) for the implementation plan. + +## Building from source + +cargoxx builds inside a Nix-managed dev shell that pins the toolchain +(Clang with C++23 modules + `import std;`, CMake ≥ 3.30, Ninja). + +```sh +nix develop +cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug +cmake --build build +``` + +The resulting binary is at `build/cargoxx`. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..6d2b084 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,641 @@ +# 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. diff --git a/TECH_SPEC.md b/TECH_SPEC.md new file mode 100644 index 0000000..1978d6a --- /dev/null +++ b/TECH_SPEC.md @@ -0,0 +1,696 @@ +# cargoxx — technical specification + +Companion to `SPEC.md`. Where `SPEC.md` defines what cargoxx does, this document defines how it is built. It is the contract between the project and the implementation. + +This is the v0.1 design. It is intentionally conservative and rigid — the goal is a working, debuggable tool, not an extensible platform. + +--- + +## 1. Source tree + +The cargoxx repository itself follows the layout cargoxx will eventually generate. This is deliberate: the moment cargoxx can build a non-trivial C++ project, we switch its own build to use itself (see §15, bootstrap). + +``` +cargoxx/ +├── Cargoxx.toml # populated once we self-host (§15) +├── CMakeLists.txt # hand-written until self-hosted; lives at root for now +├── flake.nix # hand-written until self-hosted +├── flake.lock +├── README.md +├── SPEC.md +├── TECH_SPEC.md +├── AGENTS.md +├── LICENSE +├── data/ +│ └── linkdb.json # curated link database (§9 in SPEC.md) +├── src/ +│ ├── main.cpp # CLI entry point +│ ├── lib.cppm # primary module, re-exports submodules +│ ├── manifest/ +│ │ ├── manifest.cppm +│ │ ├── parser.cpp +│ │ └── writer.cpp +│ ├── lockfile/ +│ │ ├── lockfile.cppm +│ │ └── lockfile.cpp +│ ├── layout/ +│ │ ├── layout.cppm # source-tree discovery, target inference +│ │ └── layout.cpp +│ ├── codegen/ +│ │ ├── codegen.cppm +│ │ ├── flake.cpp # flake.nix generator +│ │ └── cmake.cpp # CMakeLists.txt generator +│ ├── linkdb/ +│ │ ├── linkdb.cppm +│ │ ├── curated.cpp # loads embedded JSON +│ │ ├── overlay.cpp # user SQLite cache +│ │ └── recipe.cpp +│ ├── resolver/ +│ │ ├── resolver.cppm +│ │ ├── nixhub.cpp +│ │ ├── lazamar.cpp +│ │ └── nixpkgs_git.cpp # local git fallback +│ ├── exec/ +│ │ ├── exec.cppm +│ │ └── subprocess.cpp # wrapping reproc +│ ├── cli/ +│ │ ├── cli.cppm +│ │ ├── cmd_new.cpp +│ │ ├── cmd_add.cpp +│ │ ├── cmd_remove.cpp +│ │ ├── cmd_build.cpp +│ │ ├── cmd_run.cpp +│ │ ├── cmd_test.cpp +│ │ └── cmd_clean.cpp +│ └── util/ +│ ├── util.cppm +│ ├── error.cpp # error type and formatting +│ ├── log.cpp # spdlog wrapper +│ └── semver.cpp # version range matching +├── tests/ +│ ├── manifest_parse.cpp +│ ├── layout_discovery.cpp +│ ├── linkdb_lookup.cpp +│ ├── codegen_flake.cpp +│ ├── codegen_cmake.cpp +│ ├── semver.cpp +│ └── e2e/ # golden-file tests, see §13 +│ ├── projects/ +│ │ ├── hello/ +│ │ ├── lib_only/ +│ │ ├── multi_bin/ +│ │ └── with_fmt/ +│ └── runner.cpp +├── third_party/ # vendored single-header libs +│ ├── toml.hpp # toml++ +│ ├── json.hpp # nlohmann/json +│ ├── httplib.h # cpp-httplib +│ ├── CLI11.hpp +│ └── spdlog/ # spdlog (header-only build) +└── scripts/ + ├── bootstrap-build.sh # one-shot build from a clean tree + └── verify-curated-db.sh # checks every entry in data/linkdb.json +``` + +`third_party/` is vendored on purpose. In Phase 1 and more we avoid Nix for cargoxx's own dependencies because cargoxx is the thing being bootstrapped and we want a short, debuggable path from clean clone to working binary. + +`reproc` and `sqlite3` are NOT vendored — they come from Nix in the bootstrap `flake.nix`. They have C sources or build systems and aren't drop-in headers. + +--- + +## 2. Module layout + +All cargoxx C++ sources are modules. The dependency graph between modules is: + +``` +cargoxx (lib.cppm, root module) +├── cargoxx.util +├── cargoxx.exec depends on: util +├── cargoxx.manifest depends on: util +├── cargoxx.lockfile depends on: util, manifest +├── cargoxx.linkdb depends on: util +├── cargoxx.resolver depends on: util, exec, linkdb +├── cargoxx.layout depends on: util +├── cargoxx.codegen depends on: util, manifest, linkdb, layout, lockfile +└── cargoxx.cli depends on: everything above +``` + +`main.cpp` imports `cargoxx.cli` and dispatches on argv. No business logic in `main.cpp`. + +Each `.cppm` declares one module: `export module cargoxx.manifest;` etc. The root `lib.cppm` is `export module cargoxx;` and re-exports submodules selectively. + +--- + +## 3. Core types + +Definitions below are normative for the public interface. Implementation details (constructors, helpers) are at the agent's discretion. + +```cpp +// in cargoxx.manifest +export module cargoxx.manifest; + +import std; +import cargoxx.util; + +export namespace cargoxx::manifest { + +struct Dependency { + std::string name; + std::string version_spec; // e.g. "10.2", "^1.0", "*" + std::vector components; // empty if not a componentized package +}; + +struct BuildSettings { + bool warnings_as_errors = false; + std::vector sanitizers; +}; + +enum class Edition { Cpp20, Cpp23, Cpp26 }; + +struct Package { + std::string name; + std::string version; + Edition edition = Edition::Cpp23; + std::vector authors; + std::optional license; +}; + +struct Manifest { + Package package; + std::vector dependencies; + BuildSettings build; +}; + +auto parse(const std::filesystem::path& path) -> util::Result; +auto write(const Manifest& m, const std::filesystem::path& path) -> util::Result; + +} +``` + +```cpp +// in cargoxx.layout +export module cargoxx.layout; + +import std; + +export namespace cargoxx::layout { + +enum class TargetKind { Library, Binary, Test, Example }; + +struct Target { + TargetKind kind; + std::string name; + std::filesystem::path entry; // primary source file + std::vector additional_sources; + std::vector module_units; // .cppm files +}; + +struct DiscoveredLayout { + std::optional library; // exactly 0 or 1 + std::vector binaries; // 0..N + std::vector tests; // 0..N + std::vector examples; // 0..N +}; + +auto discover(const std::filesystem::path& project_root, + const std::string& package_name) + -> util::Result; + +} +``` + +```cpp +// in cargoxx.linkdb +export module cargoxx.linkdb; + +import std; +import cargoxx.util; + +export namespace cargoxx::linkdb { + +struct Recipe { + std::string nixpkgs_attr; + std::string find_package; // raw CMake snippet, post-substitution + std::vector targets;// post-substitution + std::string source; // 'curated' | 'manual' | etc +}; + +struct Database { + static auto open() -> util::Result; + + auto resolve(const std::string& package, + const std::string& version, + const std::vector& components) + -> util::Result; + + auto add_manual(const std::string& package, + const std::string& version_range, + const Recipe& r) -> util::Result; + + // private: holds sqlite handle + parsed curated JSON +}; + +} +``` + +```cpp +// in cargoxx.codegen +export module cargoxx.codegen; + +import std; +import cargoxx.manifest; +import cargoxx.layout; +import cargoxx.linkdb; +import cargoxx.lockfile; + +export namespace cargoxx::codegen { + +struct GenerateInputs { + const manifest::Manifest& manifest; + const layout::DiscoveredLayout& layout; + const lockfile::Lockfile& lock; + std::vector recipes; // one per dependency, same order + std::filesystem::path project_root; +}; + +auto flake_nix(const GenerateInputs& in) -> std::string; +auto cmake_lists(const GenerateInputs& in) -> std::string; + +} +``` + +The two generator functions are pure: input → string. They do no I/O. The caller writes the result. + +--- + +## 4. Error model — implementation + +```cpp +// in cargoxx.util +export namespace cargoxx::util { + +enum class ErrorCode { + // Manifest (E0001-E0019) + ManifestNotFound = 1, + ManifestParseError, + ManifestInvalidField, + ManifestUnknownField, // strict-parse mode only + ManifestVersionInvalid, + + // Layout (E0020-E0039) + LayoutNoTarget = 20, + LayoutAmbiguousLib, + LayoutInvalidName, + + // Resolution (E0040-E0059) + ResolutionUnknownPackage = 40, + ResolutionNetworkError, + ResolutionUnsatisfiable, + ResolutionVersionNotFound, + + // Linkdb (E0060-E0079) + LinkdbUnknownPackage = 60, + LinkdbCorrupt, + LinkdbComponentNotSupported, + + // Build / exec (E0080-E0099) + ExecCommandFailed = 80, + ExecToolNotFound, + BuildCmakeFailed, + BuildNixFailed, + + // Internal (E0100+) + Internal = 100, + NotImplemented, +}; + +struct Error { + ErrorCode code; + std::string message; + std::string hint; + std::optional location; + std::optional> line_col; +}; + +template +class Result { + // std::expected when available; otherwise tl::expected. + // Public surface: has_value(), value(), error(). +}; + +auto format(const Error& e) -> std::string; // produces SPEC.md §12 output + +} +``` + +We do not throw exceptions across module boundaries. `Result` is the only way to propagate failure. `throw` is permitted only inside a single `.cpp` file when the catch site is in the same file. + +--- + +## 5. Subprocess discipline + +All external commands go through `cargoxx::exec::run`: + +```cpp +struct ExecResult { + int exit_code; + std::string stdout_text; + std::string stderr_text; +}; + +struct ExecOptions { + std::filesystem::path cwd; + std::vector> env_overrides; + std::optional timeout; + bool inherit_stdio = false; // for `cargoxx run` +}; + +auto run(const std::string& program, + const std::vector& args, + const ExecOptions& opts = {}) -> Result; +``` + +Backed by `reproc`. Never use `system()`, `popen()`, or shell strings — argv only, no shell expansion. Every external invocation is logged at `debug` level with the full argv and the cwd. + +--- + +## 6. Generators — testability + +Generators are pure functions over POD inputs. Tests assert exact string equality against golden files in `tests/e2e/projects//expected/`. + +To regenerate goldens during development: +``` +CARGOXX_TEST_REGENERATE=1 ctest -R codegen +``` +The test runner detects the env var, writes new goldens, and reports as a notice (not a pass). CI never sets this var. + +Whitespace and trailing newline are part of the contract. Generators emit `\n` line endings unconditionally. + +--- + +## 7. Manifest parser — edge cases + +- Comments: `toml++` preserves them on round-trip if we round-trip via `toml::table`. `cargoxx add` MUST NOT strip the user's comments. Implementation: parse to `toml::table`, mutate, serialize. +- Unknown top-level keys: warn but accept. Forward-compat (see SPEC.md §4 reserved fields). +- Unknown keys inside `[package]`, `[build]`: error. +- Dependency value is neither string nor table: error E0003. +- Empty `[dependencies]`: valid. +- `name` containing characters outside `[a-zA-Z0-9_-]`: error E0022. +- `name` starting with digit: error. + +--- + +## 8. Layout discovery — algorithm + +``` +discover(project_root, package_name): + let lib = project_root / "src" / "lib.cppm" + let main = project_root / "src" / "main.cpp" + let bin_dir = project_root / "src" / "bin" + let tests_dir = project_root / "tests" + let examples_dir = project_root / "examples" + + # Collect library sources if lib.cppm exists + library = None + if exists(lib): + all_cppm = [lib] + all_cpp = [] + for entry in walk(project_root / "src"): + if entry == lib: continue + if entry.parent == bin_dir: continue + if entry == main: continue + if entry.ext == ".cppm": all_cppm.push(entry) + elif entry.ext == ".cpp": all_cpp.push(entry) + library = Target { + kind: Library, + name: package_name, + entry: lib, + module_units: all_cppm, + additional_sources: all_cpp, + } + + binaries = [] + if exists(main): + binaries.push(Target { + kind: Binary, + name: package_name, + entry: main, + }) + if exists(bin_dir): + for f in list_dir(bin_dir): + if f.ext == ".cpp": + binaries.push(Target { + kind: Binary, + name: f.stem, + entry: f, + }) + + tests = [Target { kind: Test, name: f.stem, entry: f } + for f in list_dir(tests_dir) if f.ext == ".cpp"] + examples = [Target { kind: Example, name: f.stem, entry: f } + for f in list_dir(examples_dir) if f.ext == ".cpp"] + + if library is None and binaries.empty(): + return Err(LayoutNoTarget) + + return Ok(DiscoveredLayout { library, binaries, tests, examples }) +``` + +`walk` is non-recursive into `bin/`, `tests/`, `examples/` — those are flat folders. It IS recursive into other subdirectories of `src/` (e.g. `src/internal/foo.cppm` is part of the library). + +--- + +## 9. CMake generator — algorithm + +Pseudocode: + +``` +cmake_lists(in): + out = StringBuilder() + + out += header(in.manifest) # cmake_minimum_required, project, CXX flags + + # find_package per dependency, in manifest order + for dep, recipe in zip(in.manifest.dependencies, in.recipes): + out += emit_find_package(dep, recipe) + + # Library target if discovered + if in.layout.library: + out += emit_library(in.layout.library, in.recipes) + + # Primary binary (src/main.cpp) — links library if present + primary_bin = first(in.layout.binaries, .entry endsWith "src/main.cpp") + if primary_bin: + out += emit_primary_binary(in.layout, primary_bin, in.recipes) + + # Additional binaries from src/bin/ + for b in in.layout.binaries where b is not primary_bin: + out += emit_extra_binary(b, in.layout, in.recipes) + + # Tests + if in.layout.tests: + out += "enable_testing()\n" + for t in in.layout.tests: + out += emit_test(t, in.layout, in.recipes) + + # Examples + for e in in.layout.examples: + out += emit_example(e, in.layout, in.recipes) + + # Build flags + out += emit_build_flags(in.manifest.build, all_target_names(in.layout)) + + return out.str() +``` + +Each `emit_*` returns a string with a trailing blank line. The output is deterministic given identical inputs — no timestamps, no nondeterministic ordering, no machine-dependent paths. + +### find_package emission + +For a recipe with no components: +``` +find_package(<>) +``` + +For a recipe with components and the dep specifies them: +``` +find_package(<>) +``` + +`{{components}}` expands to a space-separated list. `{{component}}` inside `targets` expands to one entry per requested component. Example for boost with `["filesystem", "system"]`: + +``` +find_package(Boost REQUIRED COMPONENTS filesystem system) +``` +And targets become `Boost::filesystem` and `Boost::system`. + +--- + +## 10. flake.nix generator — algorithm + +``` +flake_nix(in): + nixpkgs_rev = in.lock.nixpkgs_rev # all deps share one rev + deps_attrs = [recipe.nixpkgs_attr for recipe in in.recipes] + deduped = stable_dedup(deps_attrs) + + return template_substitute(FLAKE_TEMPLATE, { + description: in.manifest.package.name, + nixpkgs_rev: nixpkgs_rev, + dep_attrs: deduped, + }) +``` + +`FLAKE_TEMPLATE` is a string constant. Substitution is plain text replacement of `<<...>>` markers, not a Nix-aware transform. + +--- + +## 11. Version resolution — implementation + +``` +class Resolver { + auto resolve(deps: vector) -> Result: + # 1. For each dep, query candidate versions + revisions from nixhub + candidates: map> + for dep in deps: + candidates[dep.name] = query(dep) + + # 2. Aggregate revisions + all_revs = union of revs across candidates values + sorted_revs = sort_descending_by_commit_date(all_revs) + + # 3. Try each rev (newest first), find one where every dep has a matching version + for rev in sorted_revs[:50]: # cap + plan = [] + for dep in deps: + m = candidates[dep.name].filter(_.rev == rev) + .filter(satisfies(_.version, dep.version_spec)) + .max_by(.version) + if m is None: break + plan.push((dep.name, m.version, rev)) + if plan.size == deps.size: + return Ok(ResolutionPlan { rev, entries: plan }) + + return Err(ResolutionUnsatisfiable) +}; +``` + +Network calls are wrapped in a 10-second timeout each. nixhub.io failures fall through to lazamar; both failing falls through to `nixpkgs_git`. The local nixpkgs git clone is created lazily on first use. + +--- + +## 12. Lockfile semantics + +The lockfile is rewritten in full on every successful `add` / `remove` / `build`. We do not attempt incremental edits. This simplifies the writer and avoids drift between manifest and lock. + +`Cargoxx.lock` is read at the start of `build`: +- If absent → run resolution, then write. +- If present → check that every manifest dep has a satisfying entry. If yes → use as-is. If no → run resolution. + +`cargoxx update` (deferred to v0.2) will force re-resolution. + +--- + +## 13. Testing strategy + +Three layers. + +### Unit tests +One `tests/.cpp` per module. Test pure functions: parser, semver, codegen helpers. Catch2 with `TEST_CASE`. No I/O outside of `tmp_path`. + +### Golden-file tests +Each subdirectory in `tests/e2e/projects/` contains: +``` +/ +├── input/ +│ ├── Cargoxx.toml +│ └── src/... +├── expected/ +│ ├── flake.nix +│ ├── CMakeLists.txt +│ └── Cargoxx.lock +└── meta.toml # describes the test (e.g. "fixed_rev = abc123" to make output deterministic) +``` + +The runner copies `input/` into a temp dir, runs cargoxx `build --no-build` with a stubbed resolver (returning `meta.toml`'s pinned rev), and diffs every generated file against `expected/`. + +### End-to-end build tests +A small set of projects that actually compile via Nix. Marked `slow`, skipped on dev machines without `nix` in PATH. Run in CI with cached `/nix/store`. + +### Curated DB verification +`scripts/verify-curated-db.sh` constructs a tiny project per package, runs `cargoxx build`, and verifies the binary links. Run on every PR that touches `data/linkdb.json`. This is how we catch upstream Nixpkgs changes that rename attrs. + +--- + +## 14. Logging + +`spdlog` initialized at `info` level by default. `--verbose` raises to `debug`, `--quiet` to `warn`. + +Format: +``` +[] : +``` + +Components are module names (`manifest`, `resolver`, `codegen`, …). Every external command (subprocess, HTTP) is logged at `debug` with the full argv / URL. + +User-facing errors are formatted via `util::format(Error)` and printed to stderr without log decoration. Diagnostic logs go through spdlog. + +--- + +## 15. Bootstrap and self-hosting + +Three phases. + +**Phase 0 — hand-written CMake (commits before milestone M3).** +`CMakeLists.txt` and `flake.nix` at the repo root are written by humans. `cargoxx` builds `cargoxx`. + +**Phase 1 — generated CMake, hand-written flake.** +At milestone M3 (codegen complete), commit a populated `Cargoxx.toml`. Generate `build/CMakeLists.txt` from it. Delete the root `CMakeLists.txt`. The root `flake.nix` stays hand-written because cargoxx doesn't know about its own host-language deps yet. + +**Phase 2 — fully self-hosted.** +At milestone M5, vendor all third-party headers into `third_party/` and have cargoxx generate the flake too. The bootstrap path becomes: pre-built cargoxx binary → run `cargoxx build` → produce next cargoxx. + +For continuity, ship a known-good cargoxx binary as a release artifact. Anyone bootstrapping from source clones the repo, downloads the latest release binary, and runs `./bootstrap-cargoxx build`. If we ever want to bootstrap from absolute zero, `scripts/bootstrap-build.sh` does it with a hand-written CMake invocation. + +--- + +## 16. Milestones + +Each milestone is one mergeable PR series. No milestone is "done" until its tests pass in CI. + +**M0 — repo skeleton.** Empty modules, CMakeLists.txt that builds an empty `cargoxx` binary, flake.nix with toolchain. CI green. + +**M1 — manifest + layout.** `manifest::parse`, `manifest::write`, `layout::discover`. Unit tests. `cargoxx new` works (writes `Cargoxx.toml` and source skeleton, no codegen yet). + +**M2 — linkdb + curated.** `linkdb.json` with all 25 packages. `linkdb::Database::resolve` works for curated entries. SQLite overlay schema created on first run. + +**M3 — codegen.** `codegen::flake_nix` and `codegen::cmake_lists`. Golden tests for 4-6 representative projects. `cargoxx build --no-build` produces correct files. + +**M4 — exec + build.** `exec::run`. `cargoxx build` invokes nix and cmake end-to-end. `cargoxx run`, `cargoxx test`, `cargoxx clean`. + +**M5 — resolver + add/remove.** `resolver::Resolver` against nixhub.io. `cargoxx add fmt` works. Lockfile updates correctly. + +**M6 — polish.** Error message overhaul to match SPEC.md §12. `--verbose` / `--quiet`. Self-hosting (Phase 1). + +Post-v0.1 (out of this spec): automatic linkdb resolution, workspaces, `cargoxx publish`, Windows. + +--- + +## 17. Coding conventions + +- C++23 modules, no headers in `src/` (third_party/ excepted). +- Names: `snake_case` for functions and variables, `PascalCase` for types, `SCREAMING_SNAKE_CASE` for constants and enum values (`ErrorCode::ManifestNotFound`). +- One module per directory. `foo/foo.cppm` exports `cargoxx.foo`. +- No raw `new`/`delete`. Smart pointers or value types. +- `std::filesystem::path` for paths everywhere. Strings only at the very edge (CLI parsing, JSON). +- No global mutable state. Configuration is passed explicitly. +- Format with `clang-format` using the config in `.clang-format` (LLVM style, 100-column). +- Lints: `-Wall -Wextra -Wpedantic -Wconversion`. CI fails on warnings. + +--- + +## 18. Performance budget + +cargoxx is interactive. Targets: + +| Operation | Budget | +| --- | --- | +| `cargoxx new` | < 100 ms | +| `cargoxx add fmt` (cached resolution) | < 200 ms | +| `cargoxx add fmt` (network resolution) | < 5 s | +| `cargoxx build` (no codegen change) | < 50 ms before invoking cmake | +| Codegen for a 50-source project | < 100 ms | + +Profile if budgets are exceeded. SQLite I/O is by far the most likely culprit — open the connection once per process, reuse prepared statements. + +--- + +## 19. Open questions for the implementation phase + +These are decisions that should be made by the implementer with the user's input. Don't silently pick one — surface the choice when you reach the relevant milestone. + +1. Should `cargoxx run` preserve the user's PATH or only inject Nix's? (Recommend: only Nix's, for reproducibility.) +2. Should generated `flake.nix` pin `flake-utils` or inline the function? (Recommend: pin, smaller diffs on regeneration.) +3. When the layout has both `lib.cppm` and `main.cpp`, should the binary always link the library even if it doesn't `import` it? (Recommend: yes, it's harmless and matches Cargo.) +4. Should `Cargoxx.lock` include a hash of `Cargoxx.toml` to detect tampering? (Recommend: no for v0.1, complicates the format.) +5. macOS uses `clang_18` from Nix; Linux too. Is there any reason to prefer GCC on Linux? (Recommend: no, Clang has the most mature module support.) diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ae76b78 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1777954456, + "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4b46002 --- /dev/null +++ b/flake.nix @@ -0,0 +1,38 @@ +{ + description = "cargoxx — Cargo-style frontend for modern C++"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + 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 { + devShells.default = llvmPkgs.libcxxStdenv.mkDerivation { + name = "cargoxx-dev"; + version = "0.1.0"; + nativeBuildInputs = [ + pkgs.cmake + pkgs.ninja + pkgs.clang-tools + pkgs.git + pkgs.pkg-config + ]; + buildInputs = [ + pkgs.sqlite + pkgs.reproc + ]; + 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" ]; + }; + }); +} diff --git a/src/cli/cli.cppm b/src/cli/cli.cppm new file mode 100644 index 0000000..70e0835 --- /dev/null +++ b/src/cli/cli.cppm @@ -0,0 +1,10 @@ +export module cargoxx.cli; + +import cargoxx.util; +import cargoxx.exec; +import cargoxx.manifest; +import cargoxx.lockfile; +import cargoxx.layout; +import cargoxx.linkdb; +import cargoxx.resolver; +import cargoxx.codegen; diff --git a/src/codegen/codegen.cppm b/src/codegen/codegen.cppm new file mode 100644 index 0000000..6329337 --- /dev/null +++ b/src/codegen/codegen.cppm @@ -0,0 +1,7 @@ +export module cargoxx.codegen; + +import cargoxx.util; +import cargoxx.manifest; +import cargoxx.lockfile; +import cargoxx.layout; +import cargoxx.linkdb; diff --git a/src/exec/exec.cppm b/src/exec/exec.cppm new file mode 100644 index 0000000..70c99a2 --- /dev/null +++ b/src/exec/exec.cppm @@ -0,0 +1,3 @@ +export module cargoxx.exec; + +import cargoxx.util; diff --git a/src/layout/layout.cppm b/src/layout/layout.cppm new file mode 100644 index 0000000..49ab611 --- /dev/null +++ b/src/layout/layout.cppm @@ -0,0 +1,3 @@ +export module cargoxx.layout; + +import cargoxx.util; diff --git a/src/lib.cppm b/src/lib.cppm new file mode 100644 index 0000000..151d8ab --- /dev/null +++ b/src/lib.cppm @@ -0,0 +1,11 @@ +export module cargoxx; + +export import cargoxx.util; +export import cargoxx.exec; +export import cargoxx.manifest; +export import cargoxx.lockfile; +export import cargoxx.layout; +export import cargoxx.linkdb; +export import cargoxx.resolver; +export import cargoxx.codegen; +export import cargoxx.cli; diff --git a/src/linkdb/linkdb.cppm b/src/linkdb/linkdb.cppm new file mode 100644 index 0000000..fd71f27 --- /dev/null +++ b/src/linkdb/linkdb.cppm @@ -0,0 +1,3 @@ +export module cargoxx.linkdb; + +import cargoxx.util; diff --git a/src/lockfile/lockfile.cppm b/src/lockfile/lockfile.cppm new file mode 100644 index 0000000..aade254 --- /dev/null +++ b/src/lockfile/lockfile.cppm @@ -0,0 +1,4 @@ +export module cargoxx.lockfile; + +import cargoxx.util; +import cargoxx.manifest; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..bddc361 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,5 @@ +import cargoxx; + +int main(int /*argc*/, char** /*argv*/) { + return 0; +} diff --git a/src/manifest/manifest.cppm b/src/manifest/manifest.cppm new file mode 100644 index 0000000..c4807f8 --- /dev/null +++ b/src/manifest/manifest.cppm @@ -0,0 +1,3 @@ +export module cargoxx.manifest; + +import cargoxx.util; diff --git a/src/resolver/resolver.cppm b/src/resolver/resolver.cppm new file mode 100644 index 0000000..fc5f046 --- /dev/null +++ b/src/resolver/resolver.cppm @@ -0,0 +1,5 @@ +export module cargoxx.resolver; + +import cargoxx.util; +import cargoxx.exec; +import cargoxx.linkdb; diff --git a/src/util/util.cppm b/src/util/util.cppm new file mode 100644 index 0000000..4f82f7a --- /dev/null +++ b/src/util/util.cppm @@ -0,0 +1 @@ +export module cargoxx.util;