Initial commit

This commit is contained in:
2026-05-07 23:32:46 +00:00
commit 6e922b7249
20 changed files with 1616 additions and 0 deletions

16
.clang-format Normal file
View File

@@ -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

20
.gitignore vendored Normal file
View File

@@ -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

14
CHANGELOG.md Normal file
View File

@@ -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`.

52
CMakeLists.txt Normal file
View File

@@ -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)

23
README.md Normal file
View File

@@ -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`.

641
SPEC.md Normal file
View File

@@ -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 | `<package-name>` |
| `src/main.cpp` | executable | `<package-name>` |
| `src/bin/<name>.cpp` | executable | `<name>` |
| `src/<dir>/*.cppm` | module units belonging to `src/lib.cppm` | (none — added to library) |
| `src/<dir>/*.cpp` | implementation units belonging to `src/lib.cppm` | (none — added to library) |
| `tests/<name>.cpp` | executable + `add_test` | `test_<name>` |
| `examples/<name>.cpp` | executable | `example_<name>` |
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 <package-name>;` — 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 <email>"] # 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 <name>` and `cargoxx new --lib <name>`
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 <pkg>[@<version>]` and `cargoxx add <pkg> --components <a,b>`
Adds a dependency.
Algorithm:
1. Parse the package spec.
2. If `@<version>` 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 <pkg>`
Inverse of `add`. Removes from manifest, lockfile, and regenerates `flake.nix`.
### `cargoxx build [--release] [--target <name>]`
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/<profile> -S build -G Ninja -DCMAKE_BUILD_TYPE=<Profile>` if the binary dir doesn't exist or is stale.
6. Run `nix develop --command cmake --build build/<profile> [--target <name>]`.
`<profile>` is `debug` (default) or `release` (with `--release`). Capitalized `<Profile>` is `Debug` or `Release`.
### `cargoxx run [--release] [--bin <name>] [-- <args>...]`
`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/<profile>`.
### `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 = "<<package.name>>";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/<<resolved-rev>>";
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"
];
};
});
}
```
`<<resolved-rev>>` 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(<<package.name>> LANGUAGES CXX)
# ----- toolchain configuration -----
set(CMAKE_CXX_STANDARD <<edition-numeric>>)
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 -----
<<one find_package line per dependency, from link recipe>>
# ----- library target (if present) -----
<<if src/lib.cppm exists>>
add_library(<<package.name>> STATIC)
target_sources(<<package.name>>
PUBLIC
FILE_SET CXX_MODULES FILES
../src/lib.cppm
<<each additional .cppm under src/, recursive>>
PRIVATE
<<each .cpp under src/ except src/main.cpp and src/bin/*, recursive>>
)
target_link_libraries(<<package.name>> PUBLIC
<<each dep's CMake target name from link recipe>>
)
<<endif>>
# ----- binary target (if src/main.cpp exists) -----
<<if src/main.cpp exists>>
add_executable(<<package.name>>_bin ../src/main.cpp)
set_target_properties(<<package.name>>_bin PROPERTIES OUTPUT_NAME <<package.name>>)
target_link_libraries(<<package.name>>_bin PRIVATE
<<package.name>> # only if library target also exists
<<each dep's CMake target name>>
)
<<endif>>
# ----- additional binaries (src/bin/*.cpp) -----
<<for each src/bin/<name>.cpp>>
add_executable(<<name>> ../src/bin/<<name>>.cpp)
target_link_libraries(<<name>> PRIVATE
<<package.name>> # only if library target also exists
<<each dep's CMake target name>>
)
<<endfor>>
# ----- tests -----
<<if any tests/*.cpp>>
enable_testing()
<<for each tests/<name>.cpp>>
add_executable(test_<<name>> ../tests/<<name>>.cpp)
target_link_libraries(test_<<name>> PRIVATE
<<package.name>> # only if library target also exists
<<each dep's CMake target name>>
)
add_test(NAME <<name>> COMMAND test_<<name>>)
<<endfor>>
<<endif>>
# ----- examples -----
<<for each examples/<name>.cpp>>
add_executable(example_<<name>> ../examples/<<name>>.cpp)
target_link_libraries(example_<<name>> PRIVATE
<<package.name>> # only if library target also exists
<<each dep's CMake target name>>
)
<<endfor>>
# ----- warnings and sanitizers from [build] section -----
<<if build.warnings_as_errors>>
foreach(target_name IN ITEMS <<all_targets>>)
target_compile_options(${target_name} PRIVATE -Wall -Wextra -Wpedantic -Werror)
endforeach()
<<endif>>
<<if build.sanitizers non-empty>>
<<emit -fsanitize=... flags via target_compile_options and target_link_options>>
<<endif>>
```
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/<package>/all/conanfile.py`. Parse `cmake_target_name`, `cmake_file_name`. Convert to a recipe.
5. Try `microsoft/vcpkg/ports/<package>/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/<package>?_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=<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 = "<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 <name>` 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 <repo>/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.

696
TECH_SPEC.md Normal file
View File

@@ -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<std::string> components; // empty if not a componentized package
};
struct BuildSettings {
bool warnings_as_errors = false;
std::vector<std::string> sanitizers;
};
enum class Edition { Cpp20, Cpp23, Cpp26 };
struct Package {
std::string name;
std::string version;
Edition edition = Edition::Cpp23;
std::vector<std::string> authors;
std::optional<std::string> license;
};
struct Manifest {
Package package;
std::vector<Dependency> dependencies;
BuildSettings build;
};
auto parse(const std::filesystem::path& path) -> util::Result<Manifest>;
auto write(const Manifest& m, const std::filesystem::path& path) -> util::Result<void>;
}
```
```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<std::filesystem::path> additional_sources;
std::vector<std::filesystem::path> module_units; // .cppm files
};
struct DiscoveredLayout {
std::optional<Target> library; // exactly 0 or 1
std::vector<Target> binaries; // 0..N
std::vector<Target> tests; // 0..N
std::vector<Target> examples; // 0..N
};
auto discover(const std::filesystem::path& project_root,
const std::string& package_name)
-> util::Result<DiscoveredLayout>;
}
```
```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<std::string> targets;// post-substitution
std::string source; // 'curated' | 'manual' | etc
};
struct Database {
static auto open() -> util::Result<Database>;
auto resolve(const std::string& package,
const std::string& version,
const std::vector<std::string>& components)
-> util::Result<Recipe>;
auto add_manual(const std::string& package,
const std::string& version_range,
const Recipe& r) -> util::Result<void>;
// 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<linkdb::Recipe> 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<std::filesystem::path> location;
std::optional<std::pair<int, int>> line_col;
};
template <typename T>
class Result {
// std::expected<T, Error> 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<T>` 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<std::pair<std::string, std::string>> env_overrides;
std::optional<std::chrono::seconds> timeout;
bool inherit_stdio = false; // for `cargoxx run`
};
auto run(const std::string& program,
const std::vector<std::string>& args,
const ExecOptions& opts = {}) -> Result<ExecResult>;
```
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/<name>/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(<<recipe.find_package>>)
```
For a recipe with components and the dep specifies them:
```
find_package(<<find_package with {{components}} replaced by COMPONENTS list>>)
```
`{{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<Dependency>) -> Result<ResolutionPlan>:
# 1. For each dep, query candidate versions + revisions from nixhub
candidates: map<string, vector<(version, rev)>>
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/<feature>.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:
```
<project>/
├── 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:
```
[<level>] <component>: <message>
```
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.)

61
flake.lock generated Normal file
View File

@@ -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
}

38
flake.nix Normal file
View File

@@ -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" ];
};
});
}

10
src/cli/cli.cppm Normal file
View File

@@ -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;

7
src/codegen/codegen.cppm Normal file
View File

@@ -0,0 +1,7 @@
export module cargoxx.codegen;
import cargoxx.util;
import cargoxx.manifest;
import cargoxx.lockfile;
import cargoxx.layout;
import cargoxx.linkdb;

3
src/exec/exec.cppm Normal file
View File

@@ -0,0 +1,3 @@
export module cargoxx.exec;
import cargoxx.util;

3
src/layout/layout.cppm Normal file
View File

@@ -0,0 +1,3 @@
export module cargoxx.layout;
import cargoxx.util;

11
src/lib.cppm Normal file
View File

@@ -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;

3
src/linkdb/linkdb.cppm Normal file
View File

@@ -0,0 +1,3 @@
export module cargoxx.linkdb;
import cargoxx.util;

View File

@@ -0,0 +1,4 @@
export module cargoxx.lockfile;
import cargoxx.util;
import cargoxx.manifest;

5
src/main.cpp Normal file
View File

@@ -0,0 +1,5 @@
import cargoxx;
int main(int /*argc*/, char** /*argv*/) {
return 0;
}

View File

@@ -0,0 +1,3 @@
export module cargoxx.manifest;
import cargoxx.util;

View File

@@ -0,0 +1,5 @@
export module cargoxx.resolver;
import cargoxx.util;
import cargoxx.exec;
import cargoxx.linkdb;

1
src/util/util.cppm Normal file
View File

@@ -0,0 +1 @@
export module cargoxx.util;