676 lines
26 KiB
Markdown
676 lines
26 KiB
Markdown
# 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`
|
||
|
||
The shared `nixpkgs` input always tracks `nixos-unstable` and provides the
|
||
toolchain (clang, cmake, ninja, libc++). Each *pinned* manifest dep
|
||
(`<pkg>@<concrete-version>`) gets its own additional `nixpkgs_<...>`
|
||
input pointing at a specific commit; the dep is built from that
|
||
flake's `pkgs` set instead of the shared one. Wildcard deps stay on
|
||
the shared `nixpkgs`.
|
||
|
||
```nix
|
||
{
|
||
description = "<<package.name>>";
|
||
|
||
inputs = {
|
||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||
# one line per pinned dep, in manifest order:
|
||
nixpkgs_<<sanitized_dep_a>>.url = "github:NixOS/nixpkgs/<<rev_a>>";
|
||
nixpkgs_<<sanitized_dep_b>>.url = "github:NixOS/nixpkgs/<<rev_b>>";
|
||
flake-utils.url = "github:numtide/flake-utils";
|
||
};
|
||
|
||
outputs = { self, nixpkgs, nixpkgs_<<sanitized_dep_a>>,
|
||
nixpkgs_<<sanitized_dep_b>>, flake-utils }:
|
||
flake-utils.lib.eachDefaultSystem (system:
|
||
let
|
||
pkgs = import nixpkgs { inherit system; };
|
||
pkgs_nixpkgs_<<sanitized_dep_a>> =
|
||
import nixpkgs_<<sanitized_dep_a>> { inherit system; };
|
||
pkgs_nixpkgs_<<sanitized_dep_b>> =
|
||
import nixpkgs_<<sanitized_dep_b>> { inherit system; };
|
||
llvmPkgs = pkgs.llvmPackages;
|
||
in {
|
||
devShell = llvmPkgs.libcxxStdenv.mkDerivation {
|
||
name = "shell";
|
||
version = "1.0";
|
||
nativeBuildInputs = [
|
||
pkgs.ninja
|
||
pkgs.cmake
|
||
pkgs.clang-tools
|
||
];
|
||
buildInputs = [
|
||
# pinned dep: from the per-dep nixpkgs set
|
||
pkgs_nixpkgs_<<sanitized_dep_a>>.<<dep_a_attr>>
|
||
# wildcard / unpinned dep: from the shared `pkgs`
|
||
pkgs.<<dep_c_attr>>
|
||
];
|
||
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"
|
||
];
|
||
};
|
||
});
|
||
}
|
||
```
|
||
|
||
`<<sanitized_dep>>` is `<dep-name>_<dep-version>` with every char
|
||
outside `[a-zA-Z0-9_]` replaced by `_`. The same identifier is reused
|
||
in three places: the input attribute, the outputs lambda parameter,
|
||
and the `pkgs_...` `let` binding — keeping it a valid Nix identifier
|
||
in all three contexts.
|
||
|
||
`<<rev_a>>` etc. come from `Cargoxx.lock`'s per-dep `nixpkgs_rev`
|
||
field. Wildcard pins (`pkg = "*"`) leave the field null and emit no
|
||
extra input.
|
||
|
||
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
|
||
|
||
cargoxx pins **per dep**: each `<pkg>@<concrete-version>` resolves to
|
||
its own nixpkgs revision and lands as a separate flake input. The
|
||
shared `nixpkgs` input always tracks `nixos-unstable` and provides
|
||
the toolchain.
|
||
|
||
`resolve_version(package, version) -> nixpkgs_rev`:
|
||
|
||
1. **Primary — devbox API.** `GET https://search.devbox.sh/v1/resolve?name=<package>&version=<version>`. The JSON response contains `commit_hash`. Same Jetify backend that powers nixhub.io, but the API is documented and returns the commit directly. (See `devbox/internal/searcher/client.go`.) 10-second timeout.
|
||
2. **Fallback — local nixpkgs git clone.** Lazy clone of `https://github.com/NixOS/nixpkgs.git` at `~/.cache/cargoxx/nixpkgs/` (or `$XDG_CACHE_HOME/cargoxx/nixpkgs/`). On a miss, run `git -C <repo> log --all -S 'version = "<version>"' --pretty='%H %ct' -- pkgs/` and pick the youngest matching commit.
|
||
3. If both fail, return `ResolutionVersionNotFound`.
|
||
|
||
When `cargoxx add <pkg>@<ver>` succeeds, the rev is written to `Cargoxx.lock` next to the dep entry (`nixpkgs_rev`). `cargoxx build` then merges the lockfile rather than overwriting it — pins survive arbitrary rebuilds. Wildcard pins (`<pkg>` with no `@<ver>`, or `<pkg>@*`) skip resolution entirely; the dep tracks the shared `nixos-unstable` input.
|
||
|
||
There is no whole-project rev solver. Two pinned deps may pull from different nixpkgs commits, with the ABI-mismatch risk that implies (different glibc / libc++ majors). cargoxx accepts that trade-off in exchange for fine-grained control; a future `cargoxx update` may surface compatibility warnings.
|
||
|
||
---
|
||
|
||
## 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.
|