Initial commit
This commit is contained in:
16
.clang-format
Normal file
16
.clang-format
Normal 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
20
.gitignore
vendored
Normal 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
14
CHANGELOG.md
Normal 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
52
CMakeLists.txt
Normal 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
23
README.md
Normal 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
641
SPEC.md
Normal 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
696
TECH_SPEC.md
Normal 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
61
flake.lock
generated
Normal 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
38
flake.nix
Normal 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
10
src/cli/cli.cppm
Normal 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
7
src/codegen/codegen.cppm
Normal 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
3
src/exec/exec.cppm
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export module cargoxx.exec;
|
||||||
|
|
||||||
|
import cargoxx.util;
|
||||||
3
src/layout/layout.cppm
Normal file
3
src/layout/layout.cppm
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export module cargoxx.layout;
|
||||||
|
|
||||||
|
import cargoxx.util;
|
||||||
11
src/lib.cppm
Normal file
11
src/lib.cppm
Normal 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
3
src/linkdb/linkdb.cppm
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export module cargoxx.linkdb;
|
||||||
|
|
||||||
|
import cargoxx.util;
|
||||||
4
src/lockfile/lockfile.cppm
Normal file
4
src/lockfile/lockfile.cppm
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export module cargoxx.lockfile;
|
||||||
|
|
||||||
|
import cargoxx.util;
|
||||||
|
import cargoxx.manifest;
|
||||||
5
src/main.cpp
Normal file
5
src/main.cpp
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import cargoxx;
|
||||||
|
|
||||||
|
int main(int /*argc*/, char** /*argv*/) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
3
src/manifest/manifest.cppm
Normal file
3
src/manifest/manifest.cppm
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export module cargoxx.manifest;
|
||||||
|
|
||||||
|
import cargoxx.util;
|
||||||
5
src/resolver/resolver.cppm
Normal file
5
src/resolver/resolver.cppm
Normal 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
1
src/util/util.cppm
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export module cargoxx.util;
|
||||||
Reference in New Issue
Block a user