Compare commits
7 Commits
1f63984b60
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bb39a64c1 | |||
| 9d33379f94 | |||
| f9932a3ad9 | |||
| 3138e78f47 | |||
| 09f151ad82 | |||
| e6c39914b3 | |||
| fdf97861a4 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,6 +3,13 @@
|
||||
!/build/CMakeLists.txt
|
||||
/result
|
||||
/result-*
|
||||
/cargoxx.AppImage
|
||||
/cargoxx-arx
|
||||
|
||||
# e2e fixtures regenerate these on every run.sh invocation.
|
||||
tests/e2e/*/build/
|
||||
tests/e2e/**/flake.lock
|
||||
tests/e2e/**/Cargoxx.lock
|
||||
|
||||
# CMake
|
||||
CMakeCache.txt
|
||||
|
||||
165
CHANGELOG.md
165
CHANGELOG.md
@@ -398,3 +398,168 @@ All notable changes to cargoxx will be documented in this file.
|
||||
- Fix: `-Wparentheses` warning in `looks_like_missing_attribute`
|
||||
(`src/resolver/nixpkgs_probe.cpp:34`) — explicitly parenthesize the
|
||||
`&&` clause inside `||`.
|
||||
- M7 `cargoxx-bin` wraps the binary with `makeWrapper` to lock its
|
||||
runtime CLI dependencies — `nix`, `cmake`, `ninja`, `curl`, `git`
|
||||
— onto `PATH`. Previously cargoxx silently relied on the user's
|
||||
ambient PATH, so it broke whenever invoked outside a `nix develop`
|
||||
shell. The wrapped binary works under `env -i` with a minimal PATH.
|
||||
- M7 the wrapper also `--set-default`s `NIX_CONFIG` to
|
||||
`experimental-features = nix-command flakes` and
|
||||
`build-users-group =`. Without this, the bundled `pkgs.nix`
|
||||
defaults to the multi-user daemon model and fails on non-NixOS
|
||||
hosts (`error: the group 'nixbld' specified in 'build-users-group'
|
||||
does not exist`). `--set-default` lets a user with a properly-
|
||||
configured nix daemon override by exporting `NIX_CONFIG=...`
|
||||
before invoking cargoxx. Verified end-to-end in `archlinux:latest`
|
||||
via `docker run`: install the `.pkg.tar.zst`, `cargoxx new demo &&
|
||||
cargoxx add fmt` runs the resolver chain through verify-link
|
||||
cmake/ninja and writes the dep without error.
|
||||
- M7 packaging functions exposed as a stable `to*` library and as
|
||||
ready-to-build flake packages, mirroring the
|
||||
`github:NixOS/bundlers` naming. Available as both
|
||||
`packages.<format>` (for `nix build .#<format>`) and
|
||||
`lib.to<Format>` (for downstream flakes to package their own
|
||||
derivations):
|
||||
- `toAppImage` / `packages.appimage` — Linux AppImage (~207 MB)
|
||||
- `toDockerImage` / `packages.dockerImage` — `docker load`-able
|
||||
tar.gz (~213 MB)
|
||||
- `toDEB` / `packages.deb` — Debian `.deb` (~211 MB)
|
||||
- `toRPM` / `packages.rpm` — Red Hat `.rpm` (~212 MB)
|
||||
- `toArchPkg` / `packages.archpkg` — Arch `.pkg.tar.zst`
|
||||
(~196 MB), implemented locally because no NixOS bundler exists.
|
||||
Builds a closure via `pkgs.closureInfo`, lays it under
|
||||
`/nix/store`, drops a `/usr/bin/<mainProgram>` symlink, writes
|
||||
`.PKGINFO` from `drv.pname`/`drv.version`/`drv.meta`, and packs
|
||||
with `bsdtar --zstd`. Generic — works for any derivation with
|
||||
a `bin/<mainProgram>` and the usual `pname`/`version` attrs.
|
||||
Added `inputs.bundlers.url = "github:NixOS/bundlers"` (with
|
||||
`inputs.nixpkgs.follows = "nixpkgs"`) to keep the closure aligned
|
||||
with the project's pinned nixpkgs.
|
||||
- M8 multiple binary build artifacts (Cargo `src/bin/` parity).
|
||||
Codegen now routes every executable (`add_executable`) to
|
||||
`${CMAKE_BINARY_DIR}/bin` via `set_target_properties(... RUNTIME_OUTPUT_DIRECTORY ...)`.
|
||||
`buildCppPackage` and `cargoxx run --bin <name>` follow suit, and
|
||||
layout discovery picks up Cargo's `src/bin/<sub>/main.cpp` form
|
||||
(subdir name becomes the binary name). End-to-end fixture
|
||||
`tests/e2e/buildCppPackage/src/bin/extra.cpp` proves a second
|
||||
binary lands in `$out/bin/` alongside the primary.
|
||||
- M8 reusable library install layout. `cargoxx`-built libraries now
|
||||
ship a CMake-idiomatic `$out` tree: `lib/lib<name>.a`,
|
||||
`lib/cmake/<name>/{<name>Config.cmake,<name>ConfigVersion.cmake,
|
||||
<name>Targets.cmake}`, `lib/pkgconfig/<name>.pc`, and modules
|
||||
under `include/<name>/` (via `install(TARGETS ... FILE_SET CXX_MODULES ...)`).
|
||||
Codegen emits the install rules + a `Config.cmake.in` template
|
||||
(inline `file(WRITE …)`) consumed by
|
||||
`configure_package_config_file` and `write_basic_package_version_file`,
|
||||
plus a basic `.pc.in` template. The exported library target
|
||||
carries `target_compile_features(... PUBLIC cxx_std_<NN>)` so
|
||||
consumers `find_package`-ing it get the right standard for
|
||||
module BMI regeneration.
|
||||
`buildCppPackage.installPhase` switched from `cp -a build/release/bin/.`
|
||||
to `cmake --install build/release --prefix $out` — bins, libs,
|
||||
headers, config, and pc files all land via one invocation.
|
||||
`project(... VERSION <pkg.version> ...)` is now part of the
|
||||
generated header so `<name>ConfigVersion.cmake` reflects the
|
||||
manifest's version.
|
||||
- M8 cargoxx-path dependencies (Cargo's `{ path = "..." }`). The
|
||||
manifest gains a discriminated dep table form:
|
||||
```toml
|
||||
[dependencies]
|
||||
greeter = { path = "./greeter" }
|
||||
```
|
||||
`manifest::Dependency` carries `DepSource source` +
|
||||
`optional<string> path`. The parser branches on `path`; unknown
|
||||
dep-table keys still rejected for tables that have neither
|
||||
`version` nor `path`. Round-tripped by the writer.
|
||||
Lockfile schema adds per-package `source_kind` + `source_path`
|
||||
so the consumer's `Cargoxx.lock` records "this dep is built from
|
||||
a cargoxx source tree at `./greeter`" (not nixpkgs).
|
||||
`cmd_build::resolve_path_dep` reads `<path>/Cargoxx.toml` to
|
||||
verify the dep's name matches, then synthesizes a Recipe
|
||||
(`find_package = "<name> CONFIG REQUIRED"`,
|
||||
`targets = ["<name>::<name>"]`, `source = "cargoxx-path"`).
|
||||
Codegen needs no special case — the synthesized recipe flows
|
||||
through the existing `find_package` emission path.
|
||||
`buildCppPackage`'s recursion: `evalDep` branches on
|
||||
`source_kind == "cargoxx-path"` and recurses with
|
||||
`src = src + "/" + source_path`. The resulting derivation joins
|
||||
`buildInputs`; CMake's `CMAKE_PREFIX_PATH` picks it up so the
|
||||
consumer's `find_package(<dep> CONFIG REQUIRED)` resolves
|
||||
against the producer's installed Config.cmake. Path deps must be
|
||||
subdirectories of the consumer's source tree (no sibling form in
|
||||
v1 — sibling deps will land with git/registry sources in 1c/1d).
|
||||
New e2e fixture `tests/e2e/pathDep/`: a `consumer` project with
|
||||
`[dependencies] greeter = { path = "./greeter" }`. `run.sh`
|
||||
generates locks in both, then `nix build .#default` produces a
|
||||
binary that prints "Hello from greeter, world!".
|
||||
- M8 design doc at `docs/library-reuse-and-publish.md` covers the
|
||||
full two-part roadmap: library reuse (path → git → registry deps)
|
||||
and Gitea-hosted public registry with `cargoxx publish` + bot
|
||||
validation + binary-cache substitution. Phase 1a (install rules)
|
||||
and Phase 1b (path deps) shipped in this commit; phases 1c, 1d,
|
||||
and 2 remain to be built.
|
||||
- M8 cargoxx-git dependencies (`{ git = "<url>", rev = "<40-char>" }`).
|
||||
The manifest accepts a third dep-table form:
|
||||
```toml
|
||||
[dependencies]
|
||||
mylib = { git = "https://gitea/me/mylib", rev = "abc…" }
|
||||
```
|
||||
Branch/tag refs are not yet supported — the rev must be a literal
|
||||
40-char commit. `manifest::Dependency` carries `git_url` + `git_rev`;
|
||||
parser branches on `git`, rejects git deps that omit `rev`.
|
||||
Lockfile schema adds `source_git_url` + `source_git_commit` +
|
||||
`source_git_sha256` (SRI form, e.g. `sha256-<base64>`). The sha256
|
||||
pins the fetched source as a fixed-output derivation: `pkgs.fetchgit`
|
||||
in the consumer's `buildCppPackage` substitutes from cache when the
|
||||
same `(url, rev, sha256)` triple appears elsewhere.
|
||||
`cmd_build::resolve_git_dep` reuses the lockfile's prior sha256 on
|
||||
re-builds; on a fresh dep it calls
|
||||
`resolver::prefetch_flake_source(git+<url>?rev=<rev>)` which now
|
||||
returns `{store_path, hash}` (extended via a new
|
||||
`PrefetchedSource` struct, with `realize_flake_source` kept as a
|
||||
thin compat wrapper). Verifies the dep's name by reading the
|
||||
fetched tree's `Cargoxx.toml`. In `--offline` mode, a git dep
|
||||
without a cached hash errors with a clear "run online first"
|
||||
message.
|
||||
`flake.nix`'s `evalDep` branches on `source_kind == "cargoxx-git"`
|
||||
and feeds `pkgs.fetchgit { url, rev, hash }` into a recursive
|
||||
`buildCppPackage` call. The fetched source is content-addressed,
|
||||
so the entire `(fetch → install)` closure is cacheable end-to-end.
|
||||
Codegen unchanged — the synthesized recipe (find_package +
|
||||
`<name>::<name>` target) is identical to the path-dep case, just
|
||||
with a different `linkdb_source` discriminator.
|
||||
- M8 `cargoxx publish` (Phase 2c). Publishes the project's HEAD as a
|
||||
new version recipe in a Gitea-hosted cargoxx registry. Steps:
|
||||
1. Validate `Cargoxx.toml` has `name`, `version`, `license`.
|
||||
2. Verify the working tree is clean and `Cargoxx.lock` exists.
|
||||
3. Reject path-dep deps (registry can't reference local trees).
|
||||
4. Read `git remote get-url origin` + `git rev-parse HEAD`,
|
||||
normalize SSH URLs to https.
|
||||
5. Compute the source SRI hash via `nix flake prefetch
|
||||
git+<url>?rev=<commit> --json`.
|
||||
6. Build the recipe TOML (mirrors `Cargoxx.toml`'s deps + `[meta]`).
|
||||
7. POST `/repos/<registry>/contents` with `{branch: "master",
|
||||
new_branch: "publish/<name>-<version>", files: [...]}` — a
|
||||
single atomic commit creating both `recipes/<name>/versions/
|
||||
<v>.toml` and (for new packages) `recipes/<name>/maintainers.txt`
|
||||
pre-populated with the publisher's Gitea username (`tea api
|
||||
/user`).
|
||||
8. POST `/repos/<registry>/pulls` to open the PR.
|
||||
|
||||
`--dry-run` prints the recipe TOML and exits without any network
|
||||
ops. `--registry <owner>/<repo>` overrides the default
|
||||
(`$CARGOXX_REGISTRY`, falling back to `mozart/cargoxx-pkgs`).
|
||||
Authentication comes from `tea login` — no separate token plumbing.
|
||||
|
||||
Manifest schema gains three optional `[package]` fields used in the
|
||||
recipe's `[meta]` block: `description`, `repository`, `homepage`.
|
||||
|
||||
The cargoxx wrapper (`flake.nix`'s `cargoxxRuntimePath`) now bundles
|
||||
`pkgs.tea` so the binary can shell out to it on non-Nix hosts.
|
||||
|
||||
Verified end-to-end against
|
||||
`https://git.amadey.xyz/mozart/cargoxx-pkgs`: from a
|
||||
`mozart/greeter` checkout, `cargoxx publish` opens PR #3 with
|
||||
branch `publish/greeter-0.1.0` containing a fresh
|
||||
`recipes/greeter/versions/0.1.0.toml` and a new
|
||||
`recipes/greeter/maintainers.txt`.
|
||||
|
||||
@@ -4,7 +4,10 @@ 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)
|
||||
project(cargoxx VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
# Generated by cargoxx — do not edit.
|
||||
# Source of truth: ../Cargoxx.toml
|
||||
@@ -43,6 +46,7 @@ target_sources(cargoxx
|
||||
../src/cli/cmd_clean.cpp
|
||||
../src/cli/cmd_linkdb_add.cpp
|
||||
../src/cli/cmd_new.cpp
|
||||
../src/cli/cmd_publish.cpp
|
||||
../src/cli/cmd_remove.cpp
|
||||
../src/cli/cmd_run.cpp
|
||||
../src/cli/cmd_test.cpp
|
||||
@@ -59,6 +63,7 @@ target_sources(cargoxx
|
||||
../src/manifest/parser.cpp
|
||||
../src/manifest/writer.cpp
|
||||
../src/resolver/brute_scan.cpp
|
||||
../src/resolver/cargoxx_pkgs_probe.cpp
|
||||
../src/resolver/conan_probe.cpp
|
||||
../src/resolver/discover.cpp
|
||||
../src/resolver/findmodule_scan.cpp
|
||||
@@ -75,20 +80,70 @@ target_sources(cargoxx
|
||||
../src/util/levenshtein.cpp
|
||||
../src/util/semver.cpp
|
||||
)
|
||||
target_compile_features(cargoxx PUBLIC cxx_std_23)
|
||||
target_include_directories(cargoxx SYSTEM PRIVATE ../third_party)
|
||||
target_link_libraries(cargoxx PUBLIC
|
||||
reproc
|
||||
SQLite::SQLite3
|
||||
)
|
||||
|
||||
# ----- install + package-config + pkg-config -----
|
||||
install(TARGETS cargoxx
|
||||
EXPORT cargoxxTargets
|
||||
FILE_SET CXX_MODULES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cargoxx
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(EXPORT cargoxxTargets
|
||||
FILE cargoxxTargets.cmake
|
||||
NAMESPACE cargoxx::
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cargoxx)
|
||||
|
||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/cargoxxConfig.cmake.in [[
|
||||
@PACKAGE_INIT@
|
||||
include(CMakeFindDependencyMacro)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cargoxxTargets.cmake")
|
||||
check_required_components(cargoxx)
|
||||
]])
|
||||
configure_package_config_file(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/cargoxxConfig.cmake.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/cargoxxConfig.cmake
|
||||
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cargoxx)
|
||||
write_basic_package_version_file(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/cargoxxConfigVersion.cmake
|
||||
VERSION ${PROJECT_VERSION}
|
||||
COMPATIBILITY SameMajorVersion)
|
||||
install(FILES
|
||||
${CMAKE_CURRENT_BINARY_DIR}/cargoxxConfig.cmake
|
||||
${CMAKE_CURRENT_BINARY_DIR}/cargoxxConfigVersion.cmake
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cargoxx)
|
||||
|
||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/cargoxx.pc.in [[
|
||||
prefix=@CMAKE_INSTALL_PREFIX@
|
||||
exec_prefix=${prefix}
|
||||
libdir=${prefix}/${CMAKE_INSTALL_LIBDIR}
|
||||
includedir=${prefix}/${CMAKE_INSTALL_INCLUDEDIR}
|
||||
|
||||
Name: @PROJECT_NAME@
|
||||
Version: @PROJECT_VERSION@
|
||||
Description: @PROJECT_NAME@
|
||||
Cflags: -I${includedir}
|
||||
Libs: -L${libdir} -l@PROJECT_NAME@
|
||||
]])
|
||||
configure_file(${CMAKE_CURRENT_BINARY_DIR}/cargoxx.pc.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/cargoxx.pc @ONLY)
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/cargoxx.pc
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
|
||||
# ----- binary target -----
|
||||
add_executable(cargoxx_bin ../src/main.cpp)
|
||||
set_target_properties(cargoxx_bin PROPERTIES OUTPUT_NAME cargoxx)
|
||||
set_target_properties(cargoxx_bin PROPERTIES
|
||||
OUTPUT_NAME cargoxx
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||
target_link_libraries(cargoxx_bin PRIVATE
|
||||
cargoxx
|
||||
reproc
|
||||
SQLite::SQLite3
|
||||
)
|
||||
install(TARGETS cargoxx_bin RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
||||
# ----- tests -----
|
||||
enable_testing()
|
||||
@@ -147,6 +202,15 @@ target_link_libraries(test_cmd_new PRIVATE
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
catch_discover_tests(test_cmd_new)
|
||||
add_executable(test_cmd_publish_validation ../tests/cmd_publish_validation.cpp)
|
||||
target_link_libraries(test_cmd_publish_validation PRIVATE
|
||||
cargoxx
|
||||
reproc
|
||||
SQLite::SQLite3
|
||||
Catch2::Catch2
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
catch_discover_tests(test_cmd_publish_validation)
|
||||
add_executable(test_cmd_remove ../tests/cmd_remove.cpp)
|
||||
target_link_libraries(test_cmd_remove PRIVATE
|
||||
cargoxx
|
||||
|
||||
448
docs/library-reuse-and-publish.md
Normal file
448
docs/library-reuse-and-publish.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# Library reuse + public registry
|
||||
|
||||
Design plan for two interlocking features:
|
||||
|
||||
1. **Library reuse** — cargoxx-built libraries are consumable by other
|
||||
cargoxx projects, by plain CMake projects (`find_package`), and by
|
||||
plain clang invocations (`pkg-config`).
|
||||
2. **Public registry + publish** — a Gitea-hosted package registry
|
||||
(`cargoxx-pkgs`), a `cargoxx publish` CLI, and self-hosted CI that
|
||||
validates PRs, builds, pushes derivations to a binary cache, and
|
||||
auto-merges.
|
||||
|
||||
Phase 1 covers library reuse end-to-end with local path and git deps.
|
||||
Phase 2 layers the registry on top, with cache-backed substitution so
|
||||
consumers don't recompile.
|
||||
|
||||
---
|
||||
|
||||
## Part 1 — reusable libraries
|
||||
|
||||
### 1.1 Producer install layout
|
||||
|
||||
```
|
||||
$out/
|
||||
├── bin/ # binaries (existing path)
|
||||
├── lib/
|
||||
│ ├── libmylib.a # archive of impl bits
|
||||
│ ├── pkgconfig/mylib.pc # pkg-config descriptor
|
||||
│ └── cmake/mylib/
|
||||
│ ├── mylibConfig.cmake # entry point for find_package
|
||||
│ ├── mylibConfigVersion.cmake # semver gate
|
||||
│ └── mylibTargets.cmake # IMPORTED tgt + CXX_MODULES FILE_SET
|
||||
└── include/mylib/ # public headers + source-form modules
|
||||
```
|
||||
|
||||
Covers three consumption paths:
|
||||
|
||||
- **cargoxx → cargoxx**: consumer's CMakeLists emits
|
||||
`find_package(mylib CONFIG REQUIRED)` + link against `mylib::mylib`.
|
||||
`$out` is on `CMAKE_PREFIX_PATH` via `buildInputs`.
|
||||
- **plain CMake**: same `find_package` call, manual `HINTS` if not on
|
||||
the default prefix path.
|
||||
- **plain clang + pkg-config**: works for non-module libraries
|
||||
(header-only or `.a` with extern API). Module libraries require a
|
||||
CMake-aware consumer — industry-wide pkg-config has no story for
|
||||
module BMIs.
|
||||
|
||||
### 1.2 Codegen changes (`src/codegen/cmake.cpp`)
|
||||
|
||||
When `layout.library` is set, append:
|
||||
|
||||
```cmake
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
install(TARGETS mylib
|
||||
EXPORT mylibTargets
|
||||
FILE_SET CXX_MODULES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mylib
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(EXPORT mylibTargets
|
||||
FILE mylibTargets.cmake
|
||||
NAMESPACE mylib::
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mylib)
|
||||
|
||||
# Config.cmake.in written inline so we don't need an off-tree template file.
|
||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/mylibConfig.cmake.in [[
|
||||
@PACKAGE_INIT@
|
||||
include(CMakeFindDependencyMacro)
|
||||
@FIND_DEPENDENCY_BLOCK@
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/mylibTargets.cmake")
|
||||
check_required_components(mylib)
|
||||
]])
|
||||
configure_package_config_file(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/mylibConfig.cmake.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/mylibConfig.cmake
|
||||
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mylib)
|
||||
write_basic_package_version_file(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake
|
||||
VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion)
|
||||
install(FILES
|
||||
${CMAKE_CURRENT_BINARY_DIR}/mylibConfig.cmake
|
||||
${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mylib)
|
||||
|
||||
# pkg-config descriptor — also inline.
|
||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/mylib.pc.in [[
|
||||
prefix=@CMAKE_INSTALL_PREFIX@
|
||||
exec_prefix=${prefix}
|
||||
libdir=${prefix}/lib
|
||||
includedir=${prefix}/include
|
||||
|
||||
Name: @PROJECT_NAME@
|
||||
Version: @PROJECT_VERSION@
|
||||
Description: @PROJECT_DESCRIPTION@
|
||||
Cflags: -I${includedir}
|
||||
Libs: -L${libdir} -lmylib
|
||||
]])
|
||||
configure_file(${CMAKE_CURRENT_BINARY_DIR}/mylib.pc.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/mylib.pc @ONLY)
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/mylib.pc
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
```
|
||||
|
||||
`@FIND_DEPENDENCY_BLOCK@` is computed by codegen — one `find_dependency(X)`
|
||||
line per cargoxx-source dep, so the transitive graph reconstitutes when
|
||||
a consumer does `find_package(mylib …)`.
|
||||
|
||||
Binaries also get an install rule so `cmake --install` handles
|
||||
everything in one shot:
|
||||
|
||||
```cmake
|
||||
install(TARGETS mylib_bin RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
install(TARGETS tool RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
```
|
||||
|
||||
### 1.3 `buildCppPackage.installPhase` switches to `cmake --install`
|
||||
|
||||
```nix
|
||||
installPhase = ''
|
||||
cmake --install build/release --prefix $out
|
||||
'';
|
||||
```
|
||||
|
||||
Replaces the `cp -a build/release/bin/. $out/bin/` hack — now bins,
|
||||
library archive, headers, cmake config, pc file all land in the right
|
||||
place from one install command.
|
||||
|
||||
### 1.4 Manifest schema — discriminated dep source
|
||||
|
||||
Backward-compat (string form unchanged):
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sqlite = "*" # nixpkgs / linkdb (today)
|
||||
fmt = { version = ">=10" } # nixpkgs explicit (today)
|
||||
mylib = { path = "../mylib" } # cargoxx local path ← NEW
|
||||
remote = { git = "https://gitea/me/remote", rev = "abc123" } # cargoxx git ← NEW
|
||||
shared = { version = "1.0", registry = "cargoxx" } # cargoxx registry ← NEW
|
||||
```
|
||||
|
||||
`manifest::Dependency` grows discriminated source fields. Parser rejects
|
||||
unknown keys in dep tables (today silently ignored).
|
||||
|
||||
### 1.5 Lockfile schema additions
|
||||
|
||||
For cargoxx-source deps the lockfile carries source provenance and the
|
||||
recipe shape (`find_package`, `targets`) — codegen consumes the latter
|
||||
unchanged, so cargoxx deps look like any other linkdb recipe to the
|
||||
emitted CMakeLists.
|
||||
|
||||
Path deps: record relative path.
|
||||
Git deps: record `(url, commit, sha256)`.
|
||||
Registry deps: record only `(registry_rev, registry_attr)`; the source
|
||||
URL + sha256 live in the registry repo, not the consumer's lockfile.
|
||||
|
||||
### 1.6 `buildCppPackage` recursion
|
||||
|
||||
```nix
|
||||
buildCargoxxDep = p:
|
||||
if p.source_kind == "path" then
|
||||
buildCppPackage { src = src + ("/" + p.path); }
|
||||
|
||||
else if p.source_kind == "git" then
|
||||
let depSrc = pkgs.fetchgit {
|
||||
inherit (p) url;
|
||||
rev = p.commit;
|
||||
hash = p.sha256;
|
||||
};
|
||||
in buildCppPackage { src = depSrc; }
|
||||
|
||||
else if p.source_kind == "registry" then
|
||||
# Pull the prebuilt $out from the registry flake — no source fetch,
|
||||
# no recompile. The registry's `packages.<system>.<attr>` derivation
|
||||
# is content-addressed and substituted from the binary cache.
|
||||
let registry = builtins.getFlake
|
||||
"git+https://gitea/.../cargoxx-pkgs?rev=${lock.registry_rev}";
|
||||
in registry.packages.${system}.${p.registry_attr}
|
||||
|
||||
else throw "unknown source_kind: ${p.source_kind}";
|
||||
|
||||
cargoxxDepDrvs = map buildCargoxxDep
|
||||
(builtins.filter (p: p ? source_kind) lock.package);
|
||||
```
|
||||
|
||||
`cargoxxDepDrvs` join `buildInputs`. nixpkgs's `cmakeConfigurePhase`
|
||||
automatically prepends every `buildInputs` `$out` to `CMAKE_PREFIX_PATH`,
|
||||
so consumer `find_package(<dep> CONFIG REQUIRED)` resolves without
|
||||
extra plumbing.
|
||||
|
||||
### 1.7 Resolver flow for cargoxx-source deps
|
||||
|
||||
```
|
||||
manifest dep → cmd_build::resolve_dep()
|
||||
├─ source kind == Nixpkgs/Auto: existing resolver chain
|
||||
├─ source kind == CargoxxPath:
|
||||
│ read ${path}/Cargoxx.toml, take [package].name/version,
|
||||
│ synthesize Recipe { find_package = "<name> CONFIG REQUIRED",
|
||||
│ targets = ["<name>::<name>"] }
|
||||
├─ source kind == CargoxxGit:
|
||||
│ fetch the repo at rev (resolver cache),
|
||||
│ same as CargoxxPath against the checkout
|
||||
└─ source kind == CargoxxRegistry:
|
||||
fetch the registry's recipe TOML, take name + version,
|
||||
synthesize the same Recipe shape.
|
||||
```
|
||||
|
||||
The recipe shape — `find_package(<name> CONFIG REQUIRED)` plus
|
||||
`<name>::<name>` target — matches what the producer's installed
|
||||
`<name>Config.cmake` defines. Codegen needs no cargoxx-dep special case.
|
||||
|
||||
### 1.8 Phasing
|
||||
|
||||
| Phase | Deliverable |
|
||||
|---|---|
|
||||
| 1a | install rules + Config.cmake + .pc; `cmake --install` in buildCppPackage |
|
||||
| 1b | `{ path = "..." }` deps end-to-end (manifest parse + lock + recursive buildCppPackage); e2e fixture |
|
||||
| 1c | `{ git = "...", rev = "..." }` deps via `pkgs.fetchgit` |
|
||||
| 1d | `{ version = "1.0", registry = "cargoxx" }` deps (gated on Phase 2 being live) |
|
||||
|
||||
---
|
||||
|
||||
## Part 2 — Gitea-hosted registry
|
||||
|
||||
### 2.1 `cargoxx-pkgs` repo layout
|
||||
|
||||
```
|
||||
cargoxx-pkgs/
|
||||
├── flake.nix
|
||||
├── recipes/
|
||||
│ └── <name>/
|
||||
│ ├── maintainers.txt # gitea usernames, one per line
|
||||
│ ├── meta.toml # description, homepage, license, repo
|
||||
│ └── versions/
|
||||
│ ├── 1.0.0.toml
|
||||
│ └── 1.0.1.toml
|
||||
└── .gitea/workflows/
|
||||
├── validate-pr.yml
|
||||
└── auto-merge.yml
|
||||
```
|
||||
|
||||
### 2.2 Recipe `versions/<v>.toml`
|
||||
|
||||
```toml
|
||||
schema = 1
|
||||
name = "mylib"
|
||||
version = "1.0.0"
|
||||
|
||||
[source]
|
||||
type = "git"
|
||||
url = "https://gitea/me/mylib"
|
||||
commit = "abc123…" # 40-char
|
||||
sha256 = "sha256-…" # FOD pin
|
||||
|
||||
[dependencies]
|
||||
fmt = "10.2"
|
||||
otherlib = { version = "0.3", registry = "cargoxx" }
|
||||
|
||||
[lock]
|
||||
nixpkgs_rev = "…"
|
||||
flake_utils_rev = "…"
|
||||
cargoxx_rev = "…"
|
||||
|
||||
[meta]
|
||||
description = "…"
|
||||
homepage = "https://…"
|
||||
license = "MIT"
|
||||
```
|
||||
|
||||
### 2.3 `cargoxx-pkgs` flake (the substitution surface)
|
||||
|
||||
```nix
|
||||
{
|
||||
description = "cargoxx package registry";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/<rev>";
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
inputs.cargoxx.url = "git+https://gitea/.../cargoxx?rev=<rev>";
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, cargoxx }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
|
||||
mkPackage = recipeFile:
|
||||
let r = builtins.fromTOML (builtins.readFile recipeFile);
|
||||
in cargoxx.lib.${system}.buildCppPackage {
|
||||
src = pkgs.fetchgit {
|
||||
inherit (r.source) url;
|
||||
rev = r.source.commit;
|
||||
hash = r.source.sha256;
|
||||
};
|
||||
};
|
||||
|
||||
# Enumerate recipes/<name>/versions/<v>.toml, build packagesAttrs.
|
||||
packagesAttrs = …;
|
||||
in {
|
||||
packages = packagesAttrs;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
CI evaluates this flake, builds the new attr, pushes `$out` to the
|
||||
binary cache. Consumers later get a cache hit for the SAME
|
||||
derivation hash via `builtins.getFlake` at the same `registry_rev`.
|
||||
|
||||
### 2.4 Publishing flow (`cargoxx publish`)
|
||||
|
||||
```
|
||||
cargoxx publish [--dry-run]
|
||||
1. Validate Cargoxx.toml has name, version, description, license, repository.
|
||||
2. Validate working tree clean; current commit on a remote branch.
|
||||
3. Validate Cargoxx.lock pins every cargoxx-source dep to a registry version
|
||||
(path = "..." deps disallowed in publish).
|
||||
4. Compute recipe TOML:
|
||||
[source].url = git remote URL
|
||||
[source].commit = HEAD commit
|
||||
[source].sha256 = sha256 of `git archive HEAD`
|
||||
[dependencies] = mirror Cargoxx.toml
|
||||
[lock] = mirror Cargoxx.lock pins
|
||||
5. Clone cargoxx-pkgs (cached under ~/.cache/cargoxx/registry-clone).
|
||||
6. Create branch publish/<name>-<version>; new package → also create
|
||||
recipes/<name>/{maintainers.txt,meta.toml}.
|
||||
7. Write recipes/<name>/versions/<version>.toml; commit.
|
||||
8. Push branch; `tea pr create` (tea login is already set up).
|
||||
```
|
||||
|
||||
### 2.5 Validation bot (`.gitea/workflows/validate-pr.yml`)
|
||||
|
||||
Runs on every PR via self-hosted runners:
|
||||
|
||||
1. **Schema check**: PR touches only `recipes/<name>/**`; recipe TOML
|
||||
validates.
|
||||
2. **Source fixity**: re-fetch `[source].url` at `[source].commit`,
|
||||
recompute sha256, compare.
|
||||
3. **Build**: `nix build .#packages.x86_64-linux.<name>`.
|
||||
4. **Cache push**:
|
||||
```
|
||||
nix copy --to "file:///srv/cargoxx-cache?secret-key=$KEY" \
|
||||
.#packages.x86_64-linux.<name>
|
||||
```
|
||||
`/srv/cargoxx-cache` served by nginx as the binary cache.
|
||||
5. **Dependency closure**: every `[dependencies]` entry exists at the
|
||||
same PR HEAD.
|
||||
6. **Maintainer match**:
|
||||
- New package: PR author becomes maintainer (write
|
||||
`maintainers.txt`).
|
||||
- Existing package: PR author appears in `maintainers.txt`. Else
|
||||
label `needs-human-review`.
|
||||
7. On all-green + maintainer-match: label `auto-merge`.
|
||||
|
||||
`auto-merge.yml` triggers on the `auto-merge` label and merges via
|
||||
the Gitea API.
|
||||
|
||||
Maintainer transfer is a separate PR editing `maintainers.txt`. Same
|
||||
validation: PR author must already be on the list.
|
||||
|
||||
### 2.6 Resolving registry deps (`cargoxx add foo`)
|
||||
|
||||
```
|
||||
cargoxx add foo[@<version>]
|
||||
1. Fetch https://gitea/.../cargoxx-pkgs/raw/branch/master/index.json
|
||||
(the index, emitted by CI, maps name → list of versions).
|
||||
2. Pick the highest matching version (or @<version>).
|
||||
3. Fetch recipes/foo/versions/<v>.toml.
|
||||
4. Write Cargoxx.toml: foo = { version = "<v>", registry = "cargoxx" }.
|
||||
5. Write Cargoxx.lock: source_kind = "registry",
|
||||
registry_attr = "foo_<safe_ver>".
|
||||
6. Also persist lock.registry_rev = <current master commit>.
|
||||
```
|
||||
|
||||
The bundled cargoxx wrapper has `git`, `curl` on PATH, so this works
|
||||
on non-Nix hosts too.
|
||||
|
||||
### 2.7 Binary cache config
|
||||
|
||||
Once the cache URL + signing key are decided, the cargoxx wrapper's
|
||||
`NIX_CONFIG` (in `flake.nix`'s `cargoxxNixConfig`) gains:
|
||||
|
||||
```
|
||||
substituters = https://cache.cargoxx.<gitea-domain>/... https://cache.nixos.org
|
||||
trusted-public-keys = cache.cargoxx.<gitea-domain>:<base64>= cache.nixos.org:6NCH…=
|
||||
```
|
||||
|
||||
Set via `--set-default`, so a user with a different setup can
|
||||
override by exporting `NIX_CONFIG`.
|
||||
|
||||
### 2.8 Hosting
|
||||
|
||||
| Concern | Choice |
|
||||
|---|---|
|
||||
| Repo host | Self-hosted Gitea (your instance). |
|
||||
| CI | Gitea Actions on self-hosted runners (your existing pool). |
|
||||
| Binary cache | Static-HTTPS to start (`nix copy --to file:///srv/cargoxx-cache`); migrate to `attic` (https://github.com/zhaofengli/attic) if scale demands. |
|
||||
| Publishing auth | `tea login` (already configured). |
|
||||
| Architectures | `x86_64-linux` only for v1. aarch64/darwin are follow-ups. |
|
||||
|
||||
### 2.9 Phasing
|
||||
|
||||
| Phase | Deliverable |
|
||||
|---|---|
|
||||
| 2a | `cargoxx-pkgs` repo skeleton + `flake.nix` (zero recipes). |
|
||||
| 2b | `.gitea/workflows/validate-pr.yml`; manual recipe PRs. |
|
||||
| 2c | `cargoxx publish` CLI; `auto-merge.yml`. |
|
||||
| 2d | `cargoxx add` ↔ registry index; CI cache push; wrapper substituters config. |
|
||||
|
||||
---
|
||||
|
||||
## Module BMI strategy
|
||||
|
||||
Following the libstdc++/libc++ model in nixpkgs: ship **source `.cppm`**
|
||||
via `install(TARGETS … FILE_SET CXX_MODULES …)`, ship the **compiled
|
||||
archive** (`libfoo.a`) of impl bits. Consumers regenerate the BMI in
|
||||
their own build (cmake's `FILE_SET CXX_MODULES` handles it when
|
||||
`find_package(foo)` is called). The compiled archive is what's
|
||||
cached — the BMI is cheap to regenerate against an already-cached
|
||||
archive.
|
||||
|
||||
cargoxx already enables `CMAKE_CXX_MODULE_STD ON`, which CMake uses to
|
||||
rebuild the `std` BMI in each consumer project. The same machinery
|
||||
covers cargoxx libraries.
|
||||
|
||||
---
|
||||
|
||||
## Why `buildCppPackage` doesn't fetch source for registry deps
|
||||
|
||||
The registry flake (`cargoxx-pkgs`) owns the
|
||||
`source-fetch → cargoxx-build → cmake-install` pipeline. Its
|
||||
`packages.<system>.<name>` is an input-addressed derivation: same
|
||||
sources + same toolchain pins → same hash → cache hit.
|
||||
|
||||
Consumers reference that derivation via `builtins.getFlake` at the
|
||||
locked `registry_rev`. Nix's binary cache substitutes the `$out`
|
||||
directly — no source clone, no compile. Source fetch + build live
|
||||
inside the registry's eval, run once per recipe at CI time. This is
|
||||
the same model as nixpkgs/`cache.nixos.org`: you don't fetch fmt's
|
||||
source to use it from nixpkgs.
|
||||
|
||||
---
|
||||
|
||||
## Open follow-ups
|
||||
|
||||
1. Gitea domain + cache URL (for wrapper config in Phase 2d).
|
||||
2. Signing key generation
|
||||
(`nix-store --generate-binary-cache-key cache.cargoxx.<domain>
|
||||
<secret> <public>`); private into runner secrets, public into
|
||||
wrapper config.
|
||||
3. Multi-arch (`aarch64-linux`, `*-darwin`) — Phase 3+.
|
||||
4. Conan-style wrap recipes for non-cargoxx libraries — design now,
|
||||
defer implementation.
|
||||
204
flake.lock
generated
204
flake.lock
generated
@@ -1,5 +1,44 @@
|
||||
{
|
||||
"nodes": {
|
||||
"bundlers": {
|
||||
"inputs": {
|
||||
"nix-appimage": "nix-appimage",
|
||||
"nix-bundle": "nix-bundle",
|
||||
"nix-utils": "nix-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1777842037,
|
||||
"narHash": "sha256-E6kwkFsKnU5k/QAX1aNOPfh69G6Im8/EwdRcZR4J0QE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "bundlers",
|
||||
"rev": "7bb70086c2dad3eecae4805f4d758c80e3cba960",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "bundlers",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1733328505,
|
||||
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
@@ -18,7 +57,119 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"locked": {
|
||||
"lastModified": 1623875721,
|
||||
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_3": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"nix-appimage": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"bundlers",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1757920913,
|
||||
"narHash": "sha256-jd0QwCVz4O1sHHkeaZILD/7D6oyalceEJ4EFnWCgm0k=",
|
||||
"owner": "ralismark",
|
||||
"repo": "nix-appimage",
|
||||
"rev": "7946addbc0d97e358a6d7aefe5e82310f0fe6b18",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ralismark",
|
||||
"repo": "nix-appimage",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-bundle": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"bundlers",
|
||||
"nixpkgs"
|
||||
],
|
||||
"utils": "utils"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756736056,
|
||||
"narHash": "sha256-8YFhvulVX3iS4TYnKisA9zSImJeFN21G75HOUUFjzuE=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-bundle",
|
||||
"rev": "eff01593f62794d458ec714090091419194ab64d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-bundle",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-utils": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1744222205,
|
||||
"narHash": "sha256-di1eNHQdpvvyXv6i7Z+S79KF7cQyhTs7AdFHp7q1e3Q=",
|
||||
"owner": "juliosueiras-nix",
|
||||
"repo": "nix-utils",
|
||||
"rev": "53282197ad090c8cf47c96e99bf6c6c3b2cdc7c0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "juliosueiras-nix",
|
||||
"repo": "nix-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1629252929,
|
||||
"narHash": "sha256-Aj20gmGBs8TG7pyaQqgbsqAQ6cB+TVuL18Pk3DPBxcQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3788c68def67ca7949e0864c27638d484389363d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1777954456,
|
||||
"narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=",
|
||||
@@ -36,8 +187,9 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"bundlers": "bundlers",
|
||||
"flake-utils": "flake-utils_3",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
@@ -54,6 +206,54 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
116
flake.nix
116
flake.nix
@@ -4,18 +4,45 @@
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
bundlers.url = "github:NixOS/bundlers";
|
||||
bundlers.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
outputs = { self, nixpkgs, flake-utils, bundlers }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
|
||||
# exec::run shells out to these at runtime; wrap the binary so
|
||||
# they're reachable even outside a nix develop shell.
|
||||
cargoxxRuntimePath = pkgs.lib.makeBinPath [
|
||||
pkgs.nix
|
||||
pkgs.cmake
|
||||
pkgs.ninja
|
||||
pkgs.curl
|
||||
pkgs.git
|
||||
pkgs.tea # used by `cargoxx publish` for Gitea API + auth
|
||||
];
|
||||
|
||||
# Defaults applied to the bundled `nix` so it works on hosts
|
||||
# that don't already have nix set up (Arch/Debian/Fedora users
|
||||
# who install our .pkg.tar.zst / .deb / .rpm). Multi-user mode
|
||||
# would expect a `nixbld` group and a running daemon.
|
||||
# `substituters` includes the cargoxx-pkgs binary cache so
|
||||
# `cargoxx add <pkg>` substitutes prebuilt $out instead of
|
||||
# rebuilding each registry package locally.
|
||||
cargoxxNixConfig = ''
|
||||
experimental-features = nix-command flakes
|
||||
build-users-group =
|
||||
substituters = https://cache.cargoxx.amadey.xyz https://cache.nixos.org
|
||||
trusted-public-keys = cache.cargoxx.amadey.xyz:HQNcKDh9lufWm0M32a06AEiLf1Hr0WoRY3Bp5NnWZxs= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
|
||||
'';
|
||||
|
||||
cargoxx-bin = pkgs.gcc15Stdenv.mkDerivation {
|
||||
pname = "cargoxx";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
nativeBuildInputs = [ pkgs.cmake pkgs.ninja ];
|
||||
nativeBuildInputs = [ pkgs.cmake pkgs.ninja pkgs.makeWrapper ];
|
||||
buildInputs = [ pkgs.sqlite pkgs.reproc pkgs.catch2_3 ];
|
||||
configurePhase = ''
|
||||
cmake -S build -B build/release -G Ninja \
|
||||
@@ -26,7 +53,10 @@
|
||||
'';
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp build/release/cargoxx $out/bin/
|
||||
cp build/release/bin/cargoxx $out/bin/
|
||||
wrapProgram $out/bin/cargoxx \
|
||||
--prefix PATH : ${cargoxxRuntimePath} \
|
||||
--set-default NIX_CONFIG ${pkgs.lib.escapeShellArg cargoxxNixConfig}
|
||||
'';
|
||||
hardeningDisable = [ "all" ];
|
||||
};
|
||||
@@ -36,6 +66,7 @@
|
||||
lock = builtins.fromTOML (builtins.readFile (src + "/Cargoxx.lock"));
|
||||
isDep = p: p ? linkdb_source;
|
||||
isRoot = p: !(isDep p);
|
||||
isCargoxxSource = p: (p.source_kind or "") != "";
|
||||
root = builtins.head (builtins.filter isRoot lock.package);
|
||||
depPkgs = builtins.filter isDep lock.package;
|
||||
pname = if name != null then name else root.name;
|
||||
@@ -44,11 +75,24 @@
|
||||
(builtins.getFlake "github:NixOS/nixpkgs/${rev}")
|
||||
.legacyPackages.${system};
|
||||
|
||||
# cargoxx-source deps recurse into buildCppPackage; the result
|
||||
# joins buildInputs so the consumer's find_package(<dep> CONFIG
|
||||
# REQUIRED) resolves via CMAKE_PREFIX_PATH.
|
||||
evalDep = p:
|
||||
let rev = if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
||||
then p.nixpkgs_rev
|
||||
else lock.nixpkgs_rev;
|
||||
in (pkgsAt rev).${p.nixpkgs_attr};
|
||||
if (p.source_kind or "") == "cargoxx-path" then
|
||||
buildCppPackage { src = src + ("/" + p.source_path); }
|
||||
else if (p.source_kind or "") == "cargoxx-git" then
|
||||
let depSrc = pkgs.fetchgit {
|
||||
url = p.source_git_url;
|
||||
rev = p.source_git_commit;
|
||||
hash = p.source_git_sha256;
|
||||
};
|
||||
in buildCppPackage { src = depSrc; }
|
||||
else
|
||||
let rev = if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
||||
then p.nixpkgs_rev
|
||||
else lock.nixpkgs_rev;
|
||||
in (pkgsAt rev).${p.nixpkgs_attr};
|
||||
|
||||
depInputs = map evalDep depPkgs;
|
||||
|
||||
@@ -63,12 +107,18 @@
|
||||
mkDepTomlEntry = p:
|
||||
let
|
||||
derivation = evalDep p;
|
||||
rev = if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
||||
# For cargoxx-source deps we don't have a nixpkgs rev/attr — the
|
||||
# vendor.toml entry just needs a name + store_path so cargoxx's
|
||||
# offline pathway can find the dep's installed prefix.
|
||||
isCargoxx = (p.source_kind or "") != "";
|
||||
attr = if isCargoxx then "" else p.nixpkgs_attr;
|
||||
rev = if isCargoxx then ""
|
||||
else if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
||||
then p.nixpkgs_rev else lock.nixpkgs_rev;
|
||||
in ''
|
||||
[[dep]]
|
||||
name = "${p.name}"
|
||||
nixpkgs_attr = "${p.nixpkgs_attr}"
|
||||
nixpkgs_attr = "${attr}"
|
||||
nixpkgs_rev = "${rev}"
|
||||
store_path = "${derivation}"
|
||||
'';
|
||||
@@ -97,14 +147,56 @@
|
||||
cargoxx build --release --offline --vendor ${vendorToml}
|
||||
'';
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp build/release/${pname} $out/bin/ 2>/dev/null || \
|
||||
cp build/release/${pname}_bin $out/bin/${pname}
|
||||
cmake --install build/release --prefix $out
|
||||
'';
|
||||
hardeningDisable = [ "all" ];
|
||||
};
|
||||
bundlers-sys = bundlers.bundlers.${system};
|
||||
|
||||
# Arch's pacman expects a tar.zst archive containing a
|
||||
# `.PKGINFO` metadata file plus the file tree rooted at /.
|
||||
# We ship the entire closure under /nix/store; /usr/bin/cargoxx
|
||||
# is a symlink to the in-store wrapper.
|
||||
toArchPkg = drv:
|
||||
let
|
||||
pname = drv.pname or drv.name;
|
||||
version = drv.version or "0";
|
||||
mainProgram = drv.meta.mainProgram or pname;
|
||||
in pkgs.runCommand "${pname}-${version}-1-x86_64.pkg.tar.zst" {
|
||||
nativeBuildInputs = [ pkgs.libarchive pkgs.coreutils ];
|
||||
closureInfo = pkgs.closureInfo { rootPaths = [ drv ]; };
|
||||
} ''
|
||||
stage=$(mktemp -d)
|
||||
mkdir -p $stage/nix/store $stage/usr/bin
|
||||
for p in $(cat $closureInfo/store-paths); do
|
||||
cp -a "$p" $stage/nix/store/
|
||||
done
|
||||
ln -s ${drv}/bin/${mainProgram} $stage/usr/bin/${mainProgram}
|
||||
installed_size=$(du -sk $stage | cut -f1)
|
||||
cat > $stage/.PKGINFO <<EOF
|
||||
pkgname = ${pname}
|
||||
pkgver = ${version}-1
|
||||
pkgdesc = ${drv.meta.description or pname}
|
||||
builddate = 0
|
||||
packager = nix-build
|
||||
size = $installed_size
|
||||
arch = x86_64
|
||||
EOF
|
||||
( cd $stage && bsdtar --zstd -cf $out .PKGINFO nix usr )
|
||||
'';
|
||||
in {
|
||||
packages.default = cargoxx-bin;
|
||||
packages.dockerImage = bundlers-sys.toDockerImage cargoxx-bin;
|
||||
packages.deb = bundlers-sys.toDEB cargoxx-bin;
|
||||
packages.rpm = bundlers-sys.toRPM cargoxx-bin;
|
||||
packages.archpkg = toArchPkg cargoxx-bin;
|
||||
|
||||
# Reusable packaging functions, all of the shape `drv -> drv`.
|
||||
# Mirror the `to*` naming used by github:NixOS/bundlers.
|
||||
lib.toDockerImage = bundlers-sys.toDockerImage;
|
||||
lib.toDEB = bundlers-sys.toDEB;
|
||||
lib.toRPM = bundlers-sys.toRPM;
|
||||
lib.toArchPkg = toArchPkg;
|
||||
lib.buildCppPackage = buildCppPackage;
|
||||
devShells.default = pkgs.gcc15Stdenv.mkDerivation {
|
||||
name = "cargoxx-dev";
|
||||
|
||||
@@ -29,6 +29,17 @@ auto cmd_vendor(const std::filesystem::path& project_root,
|
||||
const std::filesystem::path& output)
|
||||
-> util::Result<void>;
|
||||
|
||||
// Publish the project's current HEAD as a new version recipe in the
|
||||
// cargoxx-pkgs repo (mozart/cargoxx-pkgs). Validates manifest + lockfile,
|
||||
// computes the source sha256 via `nix flake prefetch`, writes
|
||||
// `recipes/<name>/versions/<version>.toml` (and `maintainers.txt` for
|
||||
// new packages) into a `publish/<name>-<version>` branch via the
|
||||
// Gitea contents API, opens a PR via `tea api`. With `dry_run=true`,
|
||||
// prints the recipe TOML and skips all network operations.
|
||||
// Authentication comes from `tea login`.
|
||||
auto cmd_publish(const std::filesystem::path& project_root, bool dry_run)
|
||||
-> util::Result<void>;
|
||||
|
||||
// Builds the project, picks a binary target, and execs it with `args`.
|
||||
// `bin_name` is required when the project declares more than one binary.
|
||||
// Returns the binary's exit code, or an Error if selection or build fails.
|
||||
|
||||
@@ -141,6 +141,81 @@ auto cmd_add(const fs::path& project_root, const std::string& name,
|
||||
}
|
||||
}
|
||||
|
||||
// Probe cargoxx-pkgs (the project's own recipe flake) before falling
|
||||
// through to the nixpkgs/conan/vcpkg chain. If the name has a recipe
|
||||
// there, the dep is identical in shape to a nixpkgs-resolved one —
|
||||
// string-form spec in Cargoxx.toml, lockfile entry pinning the attr
|
||||
// + cargoxx-pkgs repo rev, generated build/flake.nix gets
|
||||
// `inputs.cargoxx-pkgs` and `cargoxx-pkgs.packages.${system}.<attr>`
|
||||
// as a buildInput. Disabled in tests via CARGOXX_NO_AUTORESOLVE.
|
||||
{
|
||||
auto* env = std::getenv("CARGOXX_NO_AUTORESOLVE");
|
||||
const bool autoresolve_disabled = env != nullptr && *env != 0;
|
||||
if (!autoresolve_disabled) {
|
||||
auto hit = resolver::try_cargoxx_pkgs(name, effective_version);
|
||||
if (hit) {
|
||||
// cargoxx-pkgs's flake exposes per-version attrs as
|
||||
// `<n>_<safe>` (e.g. greeter_0_1_1) plus a bare `<n>`
|
||||
// pointing at the latest. Pin the concrete attr so the
|
||||
// lock is hermetic.
|
||||
std::string safe;
|
||||
safe.reserve(hit->version.size());
|
||||
for (char c : hit->version) {
|
||||
safe.push_back((c == '.' || c == '-' || c == '+') ? '_' : c);
|
||||
}
|
||||
auto attr = std::format("{}_{}", name, safe);
|
||||
|
||||
m->dependencies.push_back(manifest::Dependency{
|
||||
.name = name,
|
||||
.version_spec = hit->version,
|
||||
.components = std::move(components),
|
||||
});
|
||||
if (auto r = manifest::write(*m, manifest_path); !r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
|
||||
const auto lock_path = project_root / "Cargoxx.lock";
|
||||
lockfile::Lockfile lock;
|
||||
if (std::error_code ec; std::filesystem::exists(lock_path, ec)) {
|
||||
if (auto parsed = lockfile::parse(lock_path); parsed) {
|
||||
lock = std::move(*parsed);
|
||||
}
|
||||
}
|
||||
if (lock.version == 0) {
|
||||
lock.version = 1;
|
||||
}
|
||||
lockfile::LockfilePackage entry{
|
||||
.name = name,
|
||||
.version = hit->version,
|
||||
.cargoxx_pkgs_attr = attr,
|
||||
.cargoxx_pkgs_rev = hit->repo_rev,
|
||||
.linkdb_source = "cargoxx-pkgs",
|
||||
.find_package = std::format("{} CONFIG REQUIRED", name),
|
||||
.targets = {std::format("{}::{}", name, name)},
|
||||
};
|
||||
bool replaced = false;
|
||||
for (auto& p : lock.packages) {
|
||||
if (p.name == name && p.version == hit->version) {
|
||||
p = entry;
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!replaced) {
|
||||
lock.packages.push_back(std::move(entry));
|
||||
}
|
||||
if (auto r = lockfile::write(lock, lock_path); !r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (hit.error().code != util::ErrorCode::ResolutionUnknownPackage) {
|
||||
return std::unexpected(hit.error());
|
||||
}
|
||||
// Fall through to the linkdb chain.
|
||||
}
|
||||
}
|
||||
|
||||
const auto effective_overlay = overlay_path.value_or(linkdb::default_overlay_path());
|
||||
|
||||
// Drop any auto-discovered overlay rows for this package before
|
||||
|
||||
@@ -74,7 +74,9 @@ auto query_flake_rev(std::string_view flake_ref) -> std::optional<std::string> {
|
||||
auto merge_lockfile(const manifest::Manifest& m,
|
||||
const std::vector<linkdb::Recipe>& recipes,
|
||||
const std::vector<linkdb::Recipe>& dev_recipes,
|
||||
const lockfile::Lockfile& prior) -> lockfile::Lockfile {
|
||||
const lockfile::Lockfile& prior,
|
||||
const std::map<std::string, std::string>& git_sha256s)
|
||||
-> lockfile::Lockfile {
|
||||
auto find_prior = [&](const std::string& name, const std::string& version)
|
||||
-> std::optional<lockfile::LockfilePackage> {
|
||||
for (const auto& p : prior.packages) {
|
||||
@@ -114,24 +116,61 @@ auto merge_lockfile(const manifest::Manifest& m,
|
||||
auto emit_dep = [&](const manifest::Dependency& dep, const linkdb::Recipe& rec) {
|
||||
std::optional<std::string> rev;
|
||||
std::string attr = rec.nixpkgs_attr;
|
||||
std::optional<std::string> cxx_pkgs_attr;
|
||||
std::optional<std::string> cxx_pkgs_rev;
|
||||
if (auto p = find_prior(dep.name, dep.version_spec); p) {
|
||||
rev = p->nixpkgs_rev;
|
||||
if (p->nixpkgs_attr && !p->nixpkgs_attr->empty()) {
|
||||
attr = *p->nixpkgs_attr;
|
||||
}
|
||||
cxx_pkgs_attr = p->cargoxx_pkgs_attr;
|
||||
cxx_pkgs_rev = p->cargoxx_pkgs_rev;
|
||||
}
|
||||
// For cargoxx-pkgs-resolved deps, drop the nixpkgs fields — the
|
||||
// recipe synthesized at `cargoxx add` time left them empty and
|
||||
// the codegen layer prefers cargoxx_pkgs_attr anyway.
|
||||
std::optional<std::string> nix_attr_opt;
|
||||
if (cxx_pkgs_attr && !cxx_pkgs_attr->empty()) {
|
||||
attr.clear();
|
||||
rev.reset();
|
||||
} else if (!attr.empty()) {
|
||||
nix_attr_opt = attr;
|
||||
}
|
||||
std::optional<std::string> source_kind;
|
||||
std::optional<std::string> source_path;
|
||||
std::optional<std::string> source_git_url;
|
||||
std::optional<std::string> source_git_commit;
|
||||
std::optional<std::string> source_git_sha256;
|
||||
if (dep.source == manifest::DepSource::CargoxxPath) {
|
||||
source_kind = "cargoxx-path";
|
||||
source_path = dep.path;
|
||||
} else if (dep.source == manifest::DepSource::CargoxxGit) {
|
||||
source_kind = "cargoxx-git";
|
||||
source_git_url = dep.git_url;
|
||||
source_git_commit = dep.git_rev;
|
||||
if (auto it = git_sha256s.find(dep.name); it != git_sha256s.end()) {
|
||||
source_git_sha256 = it->second;
|
||||
}
|
||||
}
|
||||
lock.packages.push_back(lockfile::LockfilePackage{
|
||||
.name = dep.name,
|
||||
.version = dep.version_spec,
|
||||
.dependencies = {},
|
||||
.nixpkgs_attr = std::move(attr),
|
||||
.nixpkgs_attr = std::move(nix_attr_opt),
|
||||
.nixpkgs_rev = std::move(rev),
|
||||
.cargoxx_pkgs_attr = std::move(cxx_pkgs_attr),
|
||||
.cargoxx_pkgs_rev = std::move(cxx_pkgs_rev),
|
||||
.linkdb_source = rec.source,
|
||||
.find_package = rec.find_package,
|
||||
.targets = rec.targets,
|
||||
.pkg_config_module = rec.pkg_config_module,
|
||||
.brute_force_libs = rec.brute_force_libs,
|
||||
.brute_force_includes = rec.brute_force_includes,
|
||||
.source_kind = std::move(source_kind),
|
||||
.source_path = std::move(source_path),
|
||||
.source_git_url = std::move(source_git_url),
|
||||
.source_git_commit = std::move(source_git_commit),
|
||||
.source_git_sha256 = std::move(source_git_sha256),
|
||||
});
|
||||
};
|
||||
for (std::size_t i = 0; i < m.dependencies.size(); ++i) {
|
||||
@@ -249,11 +288,122 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
// For { path = "../foo" } deps: skip the linkdb chain entirely. Read
|
||||
// the dep's Cargoxx.toml to confirm the name matches, then synthesize
|
||||
// a Recipe that maps to the producer's installed Config.cmake.
|
||||
auto resolve_path_dep = [&](const manifest::Dependency& dep)
|
||||
-> util::Result<linkdb::Recipe> {
|
||||
const auto dep_root = (project_root / *dep.path).lexically_normal();
|
||||
auto dep_manifest = manifest::parse(dep_root / "Cargoxx.toml");
|
||||
if (!dep_manifest) {
|
||||
return std::unexpected(dep_manifest.error());
|
||||
}
|
||||
if (dep_manifest->package.name != dep.name) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::ManifestInvalidField,
|
||||
std::format("path dep '{}' points to a project named '{}'",
|
||||
dep.name, dep_manifest->package.name),
|
||||
std::format("rename the dep, or fix [package].name in {}",
|
||||
(dep_root / "Cargoxx.toml").string()),
|
||||
dep_root / "Cargoxx.toml", std::nullopt,
|
||||
});
|
||||
}
|
||||
return linkdb::Recipe{
|
||||
.nixpkgs_attr = "",
|
||||
.find_package = std::format("{} CONFIG REQUIRED", dep.name),
|
||||
.targets = {std::format("{}::{}", dep.name, dep.name)},
|
||||
.source = "cargoxx-path",
|
||||
.pkg_config_module = std::nullopt,
|
||||
.brute_force_libs = {},
|
||||
.brute_force_includes = {},
|
||||
};
|
||||
};
|
||||
|
||||
// Side channel: (dep name) → SRI hash captured during git resolution,
|
||||
// consumed by merge_lockfile to persist source_git_sha256.
|
||||
std::map<std::string, std::string> git_sha256s;
|
||||
|
||||
// For { git = "...", rev = "..." } deps: if the prior lockfile already
|
||||
// records an SRI hash for this (url, commit), reuse it. Otherwise run
|
||||
// `nix flake prefetch` to fetch + hash the source tree (FOD-compatible),
|
||||
// then verify the dep's Cargoxx.toml name matches.
|
||||
auto resolve_git_dep = [&](const manifest::Dependency& dep)
|
||||
-> util::Result<linkdb::Recipe> {
|
||||
std::optional<std::string> cached_sha;
|
||||
for (const auto& p : prior.packages) {
|
||||
if (p.name == dep.name && p.source_kind == "cargoxx-git" &&
|
||||
p.source_git_url == dep.git_url &&
|
||||
p.source_git_commit == dep.git_rev && p.source_git_sha256) {
|
||||
cached_sha = p.source_git_sha256;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cached_sha) {
|
||||
if (offline) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::BuildCmakeFailed,
|
||||
std::format("git dep '{}' has no cached hash and --offline "
|
||||
"forbids network fetch", dep.name),
|
||||
"run `cargoxx build` once online to populate Cargoxx.lock",
|
||||
std::nullopt, std::nullopt,
|
||||
});
|
||||
}
|
||||
auto flake_ref = std::format("git+{}?rev={}", *dep.git_url, *dep.git_rev);
|
||||
auto prefetched = resolver::prefetch_flake_source(flake_ref);
|
||||
if (!prefetched) {
|
||||
return std::unexpected(prefetched.error());
|
||||
}
|
||||
// Verify name matches by reading the fetched tree's Cargoxx.toml.
|
||||
auto dep_manifest = manifest::parse(
|
||||
std::filesystem::path{prefetched->store_path} / "Cargoxx.toml");
|
||||
if (!dep_manifest) {
|
||||
return std::unexpected(dep_manifest.error());
|
||||
}
|
||||
if (dep_manifest->package.name != dep.name) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::ManifestInvalidField,
|
||||
std::format("git dep '{}' points to a project named '{}'",
|
||||
dep.name, dep_manifest->package.name),
|
||||
"rename the dep or use a repo whose [package].name matches",
|
||||
std::nullopt, std::nullopt,
|
||||
});
|
||||
}
|
||||
cached_sha = prefetched->hash;
|
||||
}
|
||||
git_sha256s[dep.name] = *cached_sha;
|
||||
return linkdb::Recipe{
|
||||
.nixpkgs_attr = "",
|
||||
.find_package = std::format("{} CONFIG REQUIRED", dep.name),
|
||||
.targets = {std::format("{}::{}", dep.name, dep.name)},
|
||||
.source = "cargoxx-git",
|
||||
.pkg_config_module = std::nullopt,
|
||||
.brute_force_libs = {},
|
||||
.brute_force_includes = {},
|
||||
};
|
||||
};
|
||||
|
||||
auto resolve_list = [&](const std::vector<manifest::Dependency>& deps)
|
||||
-> util::Result<std::vector<linkdb::Recipe>> {
|
||||
std::vector<linkdb::Recipe> out;
|
||||
out.reserve(deps.size());
|
||||
for (const auto& dep : deps) {
|
||||
if (dep.source == manifest::DepSource::CargoxxPath) {
|
||||
auto r = resolve_path_dep(dep);
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
out.push_back(std::move(*r));
|
||||
continue;
|
||||
}
|
||||
if (dep.source == manifest::DepSource::CargoxxGit) {
|
||||
auto r = resolve_git_dep(dep);
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
out.push_back(std::move(*r));
|
||||
continue;
|
||||
}
|
||||
if (auto cached = recipe_from_lock(dep.name, dep.version_spec); cached) {
|
||||
out.push_back(std::move(*cached));
|
||||
continue;
|
||||
@@ -282,7 +432,7 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
||||
if (!dev_recipes) {
|
||||
return std::unexpected(dev_recipes.error());
|
||||
}
|
||||
auto lock = merge_lockfile(*m, *recipes, *dev_recipes, prior);
|
||||
auto lock = merge_lockfile(*m, *recipes, *dev_recipes, prior, git_sha256s);
|
||||
|
||||
std::optional<codegen::VendorIndex> vendor_index;
|
||||
if (offline) {
|
||||
|
||||
459
src/cli/cmd_publish.cpp
Normal file
459
src/cli/cmd_publish.cpp
Normal file
@@ -0,0 +1,459 @@
|
||||
module;
|
||||
|
||||
#include <json.hpp>
|
||||
|
||||
module cargoxx.cli;
|
||||
|
||||
import std;
|
||||
import cargoxx.util;
|
||||
import cargoxx.manifest;
|
||||
import cargoxx.lockfile;
|
||||
import cargoxx.exec;
|
||||
|
||||
namespace cargoxx::cli {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
// The one and only publish destination.
|
||||
constexpr std::string_view CARGOXX_PKGS_REPO = "mozart/cargoxx-pkgs";
|
||||
|
||||
auto err(util::ErrorCode code, std::string msg, std::string hint = "")
|
||||
-> util::Error {
|
||||
return util::Error{code, std::move(msg), std::move(hint), std::nullopt,
|
||||
std::nullopt};
|
||||
}
|
||||
|
||||
auto trim(std::string s) -> std::string {
|
||||
auto end = s.find_last_not_of(" \t\r\n");
|
||||
if (end != std::string::npos) {
|
||||
s.erase(end + 1);
|
||||
}
|
||||
auto start = s.find_first_not_of(" \t\r\n");
|
||||
if (start == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
return s.substr(start);
|
||||
}
|
||||
|
||||
// Run a process, return trimmed stdout on success.
|
||||
auto capture(const std::string& prog, std::vector<std::string> args,
|
||||
const fs::path& cwd) -> util::Result<std::string> {
|
||||
auto r = exec::run(prog, args,
|
||||
exec::ExecOptions{
|
||||
.cwd = cwd,
|
||||
.env_overrides = {},
|
||||
.timeout = std::chrono::seconds{60},
|
||||
.inherit_stdio = false,
|
||||
});
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (r->exit_code != 0) {
|
||||
return std::unexpected(err(
|
||||
util::ErrorCode::ExecCommandFailed,
|
||||
std::format("{} failed (exit {}): {}", prog, r->exit_code,
|
||||
r->stderr_text)));
|
||||
}
|
||||
return trim(r->stdout_text);
|
||||
}
|
||||
|
||||
// Normalize a git remote URL to an https form Nix can fetch.
|
||||
// `git@host:owner/repo.git` → `https://host/owner/repo`
|
||||
// `https://host/owner/repo.git` → `https://host/owner/repo`
|
||||
// `ssh://git@host:port/owner/repo.git` → `https://host/owner/repo`
|
||||
auto normalize_remote(std::string url) -> std::string {
|
||||
if (url.starts_with("git@")) {
|
||||
// git@host:owner/repo[.git]
|
||||
auto colon = url.find(':');
|
||||
if (colon != std::string::npos) {
|
||||
auto host = url.substr(4, colon - 4);
|
||||
auto path = url.substr(colon + 1);
|
||||
url = std::format("https://{}/{}", host, path);
|
||||
}
|
||||
} else if (url.starts_with("ssh://git@")) {
|
||||
url.replace(0, std::string_view{"ssh://git@"}.size(), "https://");
|
||||
// Strip :PORT from ssh form.
|
||||
auto slash = url.find('/', 8);
|
||||
auto colon = url.find(':', 8);
|
||||
if (colon != std::string::npos && colon < slash) {
|
||||
url.erase(colon, slash - colon);
|
||||
}
|
||||
}
|
||||
if (url.ends_with(".git")) {
|
||||
url.erase(url.size() - 4);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// Encode a string as base64 (RFC 4648, no line wrap). Inline impl avoids
|
||||
// dragging in another library — Gitea's contents-API requires base64
|
||||
// bodies for file content.
|
||||
auto b64encode(std::string_view bytes) -> std::string {
|
||||
static constexpr std::string_view alphabet =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string out;
|
||||
out.reserve((bytes.size() + 2) / 3 * 4);
|
||||
std::size_t i = 0;
|
||||
while (i + 3 <= bytes.size()) {
|
||||
auto a = static_cast<unsigned char>(bytes[i]);
|
||||
auto b = static_cast<unsigned char>(bytes[i + 1]);
|
||||
auto c = static_cast<unsigned char>(bytes[i + 2]);
|
||||
out += alphabet[(a >> 2) & 0x3f];
|
||||
out += alphabet[((a << 4) | (b >> 4)) & 0x3f];
|
||||
out += alphabet[((b << 2) | (c >> 6)) & 0x3f];
|
||||
out += alphabet[c & 0x3f];
|
||||
i += 3;
|
||||
}
|
||||
if (i < bytes.size()) {
|
||||
auto a = static_cast<unsigned char>(bytes[i]);
|
||||
out += alphabet[(a >> 2) & 0x3f];
|
||||
if (i + 1 == bytes.size()) {
|
||||
out += alphabet[(a << 4) & 0x3f];
|
||||
out += "==";
|
||||
} else {
|
||||
auto b = static_cast<unsigned char>(bytes[i + 1]);
|
||||
out += alphabet[((a << 4) | (b >> 4)) & 0x3f];
|
||||
out += alphabet[(b << 2) & 0x3f];
|
||||
out += '=';
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
auto escape_toml(std::string_view s) -> std::string {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 2);
|
||||
out += '"';
|
||||
for (char c : s) {
|
||||
if (c == '\\' || c == '"') {
|
||||
out += '\\';
|
||||
}
|
||||
out += c;
|
||||
}
|
||||
out += '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
auto build_recipe(const manifest::Manifest& m, const lockfile::Lockfile& lock,
|
||||
std::string_view source_url, std::string_view source_commit,
|
||||
std::string_view source_sha256) -> std::string {
|
||||
std::string out;
|
||||
out += "schema = 1\n";
|
||||
out += std::format("name = {}\n", escape_toml(m.package.name));
|
||||
out += std::format("version = {}\n\n", escape_toml(m.package.version));
|
||||
|
||||
out += "[source]\n";
|
||||
out += "type = \"git\"\n";
|
||||
out += std::format("url = {}\n", escape_toml(source_url));
|
||||
out += std::format("commit = {}\n", escape_toml(source_commit));
|
||||
out += std::format("sha256 = {}\n\n", escape_toml(source_sha256));
|
||||
|
||||
if (!m.dependencies.empty()) {
|
||||
out += "[dependencies]\n";
|
||||
for (const auto& d : m.dependencies) {
|
||||
if (d.source == manifest::DepSource::CargoxxPath) {
|
||||
// Path deps can't be published — caller already rejected.
|
||||
continue;
|
||||
}
|
||||
out += std::format("{} = {}\n", d.name, escape_toml(d.version_spec));
|
||||
}
|
||||
out += "\n";
|
||||
}
|
||||
|
||||
out += "[lock]\n";
|
||||
if (lock.nixpkgs_rev_pin) {
|
||||
out += std::format("nixpkgs_rev = {}\n", escape_toml(*lock.nixpkgs_rev_pin));
|
||||
}
|
||||
if (lock.flake_utils_rev_pin) {
|
||||
out += std::format("flake_utils_rev = {}\n",
|
||||
escape_toml(*lock.flake_utils_rev_pin));
|
||||
}
|
||||
out += "\n";
|
||||
|
||||
out += "[meta]\n";
|
||||
if (m.package.description) {
|
||||
out += std::format("description = {}\n", escape_toml(*m.package.description));
|
||||
} else {
|
||||
out += std::format("description = {}\n", escape_toml(m.package.name));
|
||||
}
|
||||
if (m.package.homepage) {
|
||||
out += std::format("homepage = {}\n", escape_toml(*m.package.homepage));
|
||||
}
|
||||
if (m.package.license) {
|
||||
out += std::format("license = {}\n", escape_toml(*m.package.license));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
auto extract_json_string(std::string_view body, std::string_view key)
|
||||
-> std::optional<std::string> {
|
||||
auto needle = std::format("\"{}\":\"", key);
|
||||
auto pos = body.find(needle);
|
||||
if (pos == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
pos += needle.size();
|
||||
auto end = body.find('"', pos);
|
||||
if (end == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::string{body.substr(pos, end - pos)};
|
||||
}
|
||||
|
||||
auto tea_whoami() -> util::Result<std::string> {
|
||||
auto r = capture("tea", {"api", "/user"}, fs::current_path());
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
auto login = extract_json_string(*r, "login");
|
||||
if (!login) {
|
||||
return std::unexpected(err(util::ErrorCode::Internal,
|
||||
"tea api /user returned no 'login' field"));
|
||||
}
|
||||
return *login;
|
||||
}
|
||||
|
||||
auto path_exists_remote(const std::string& registry, const std::string& path)
|
||||
-> bool {
|
||||
auto r = exec::run("tea",
|
||||
{"api", std::format("/repos/{}/contents/{}", registry, path)},
|
||||
exec::ExecOptions{
|
||||
.cwd = fs::current_path(),
|
||||
.env_overrides = {},
|
||||
.timeout = std::chrono::seconds{30},
|
||||
.inherit_stdio = false,
|
||||
});
|
||||
if (!r) {
|
||||
return false;
|
||||
}
|
||||
return r->exit_code == 0 && r->stdout_text.find("\"name\":") != std::string::npos;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto cmd_publish(const fs::path& project_root, bool dry_run)
|
||||
-> util::Result<void> {
|
||||
// 1. Read manifest + lockfile
|
||||
auto m = manifest::parse(project_root / "Cargoxx.toml");
|
||||
if (!m) {
|
||||
return std::unexpected(m.error());
|
||||
}
|
||||
if (m->package.name.empty() || m->package.version.empty()) {
|
||||
return std::unexpected(err(util::ErrorCode::ManifestInvalidField,
|
||||
"publish requires [package].name and .version"));
|
||||
}
|
||||
if (!m->package.license) {
|
||||
return std::unexpected(err(util::ErrorCode::ManifestInvalidField,
|
||||
"publish requires [package].license"));
|
||||
}
|
||||
|
||||
auto lock_path = project_root / "Cargoxx.lock";
|
||||
if (std::error_code ec; !fs::exists(lock_path, ec)) {
|
||||
return std::unexpected(err(util::ErrorCode::ManifestNotFound,
|
||||
"Cargoxx.lock is missing — run `cargoxx build` first"));
|
||||
}
|
||||
auto lock = lockfile::parse(lock_path);
|
||||
if (!lock) {
|
||||
return std::unexpected(lock.error());
|
||||
}
|
||||
|
||||
// 2. Path deps are disallowed when publishing (they're local-only).
|
||||
for (const auto& d : m->dependencies) {
|
||||
if (d.source == manifest::DepSource::CargoxxPath) {
|
||||
return std::unexpected(err(
|
||||
util::ErrorCode::ManifestInvalidField,
|
||||
std::format("path dep '{}' cannot be published — convert to "
|
||||
"{{ git = ..., rev = ... }} or release the dep "
|
||||
"separately first", d.name)));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Git context.
|
||||
auto remote_url = capture("git", {"remote", "get-url", "origin"}, project_root);
|
||||
if (!remote_url) {
|
||||
return std::unexpected(err(util::ErrorCode::ManifestInvalidField,
|
||||
"no `origin` git remote",
|
||||
"git remote add origin <url>"));
|
||||
}
|
||||
auto source_url = normalize_remote(*remote_url);
|
||||
|
||||
auto head = capture("git", {"rev-parse", "HEAD"}, project_root);
|
||||
if (!head) {
|
||||
return std::unexpected(head.error());
|
||||
}
|
||||
|
||||
// Working tree must be clean — otherwise the published source won't
|
||||
// match what's actually in the repo at HEAD.
|
||||
auto status = capture("git", {"status", "--porcelain"}, project_root);
|
||||
if (!status) {
|
||||
return std::unexpected(status.error());
|
||||
}
|
||||
if (!status->empty()) {
|
||||
return std::unexpected(err(util::ErrorCode::ManifestInvalidField,
|
||||
"git working tree is not clean",
|
||||
"commit or stash changes before publishing"));
|
||||
}
|
||||
|
||||
// 4. Compute source sha256 via nix flake prefetch — the published
|
||||
// recipe pins this; the registry CI re-verifies it.
|
||||
auto flake_ref = std::format("git+{}?rev={}", source_url, *head);
|
||||
auto prefetch = capture("nix",
|
||||
{"--extra-experimental-features",
|
||||
"nix-command flakes", "flake", "prefetch",
|
||||
flake_ref, "--json"},
|
||||
project_root);
|
||||
if (!prefetch) {
|
||||
return std::unexpected(err(util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("nix flake prefetch failed for {}",
|
||||
flake_ref)));
|
||||
}
|
||||
auto source_sha256 = extract_json_string(*prefetch, "hash");
|
||||
if (!source_sha256) {
|
||||
return std::unexpected(err(util::ErrorCode::ResolutionNetworkError,
|
||||
"nix flake prefetch returned no hash"));
|
||||
}
|
||||
|
||||
// 5. Build the recipe TOML.
|
||||
auto recipe = build_recipe(*m, *lock, source_url, *head, *source_sha256);
|
||||
|
||||
if (dry_run) {
|
||||
std::cout << recipe;
|
||||
return {};
|
||||
}
|
||||
|
||||
// 6. Auth.
|
||||
const std::string registry{CARGOXX_PKGS_REPO};
|
||||
|
||||
auto publisher = tea_whoami();
|
||||
if (!publisher) {
|
||||
return std::unexpected(publisher.error());
|
||||
}
|
||||
|
||||
// 7. Compose the API payload — one atomic commit creating the
|
||||
// version recipe, plus maintainers.txt for first-time packages.
|
||||
auto branch = std::format("publish/{}-{}", m->package.name, m->package.version);
|
||||
auto version_path = std::format("recipes/{}/versions/{}.toml",
|
||||
m->package.name, m->package.version);
|
||||
auto maintainers_path = std::format("recipes/{}/maintainers.txt", m->package.name);
|
||||
bool is_new_package = !path_exists_remote(registry, maintainers_path);
|
||||
|
||||
nlohmann::json files = nlohmann::json::array();
|
||||
files.push_back({
|
||||
{"operation", "create"},
|
||||
{"path", version_path},
|
||||
{"content", b64encode(recipe)},
|
||||
});
|
||||
if (is_new_package) {
|
||||
std::string maintainers_body = *publisher + "\n";
|
||||
files.push_back({
|
||||
{"operation", "create"},
|
||||
{"path", maintainers_path},
|
||||
{"content", b64encode(maintainers_body)},
|
||||
});
|
||||
}
|
||||
|
||||
nlohmann::json body = {
|
||||
{"branch", "master"},
|
||||
{"new_branch", branch},
|
||||
{"message", std::format("publish: {} {}", m->package.name, m->package.version)},
|
||||
{"files", files},
|
||||
};
|
||||
|
||||
// tea api with `-d @<file>` reads the body from a file — write it
|
||||
// to a temp path. exec::run doesn't pipe stdin, so a tempfile is
|
||||
// the simplest portable plumbing.
|
||||
auto contents_tmp = fs::temp_directory_path() /
|
||||
std::format("cargoxx-publish-{}.json",
|
||||
std::random_device{}());
|
||||
std::ofstream{contents_tmp} << body.dump();
|
||||
auto contents_r = exec::run(
|
||||
"tea",
|
||||
{"api", "-X", "POST", "-d", std::format("@{}", contents_tmp.string()),
|
||||
std::format("/repos/{}/contents", registry)},
|
||||
exec::ExecOptions{
|
||||
.cwd = project_root,
|
||||
.env_overrides = {},
|
||||
.timeout = std::chrono::seconds{120},
|
||||
.inherit_stdio = false,
|
||||
});
|
||||
std::error_code rm_ec;
|
||||
fs::remove(contents_tmp, rm_ec);
|
||||
if (!contents_r) {
|
||||
return std::unexpected(contents_r.error());
|
||||
}
|
||||
if (contents_r->exit_code != 0) {
|
||||
return std::unexpected(err(
|
||||
util::ErrorCode::ExecCommandFailed,
|
||||
std::format("tea api contents failed (exit {}): {}",
|
||||
contents_r->exit_code, contents_r->stderr_text)));
|
||||
}
|
||||
// Gitea returns API errors with HTTP 4xx; tea writes the error JSON
|
||||
// to stdout with exit code 0. The `"errors"` array key only appears
|
||||
// in error responses, never in success ones — use that as signal.
|
||||
if (contents_r->stdout_text.find("\"errors\"") != std::string::npos) {
|
||||
return std::unexpected(err(
|
||||
util::ErrorCode::ExecCommandFailed,
|
||||
std::format("Gitea contents API rejected the request: {}",
|
||||
contents_r->stdout_text)));
|
||||
}
|
||||
|
||||
// 8. Open the PR.
|
||||
nlohmann::json pr_body = {
|
||||
{"title", std::format("publish: {} {}", m->package.name, m->package.version)},
|
||||
{"body",
|
||||
std::format("Automated publish from `cargoxx publish` by @{}.\n\n"
|
||||
"- source: `{}@{}`\n- sha256: `{}`\n",
|
||||
*publisher, source_url, *head, *source_sha256)},
|
||||
{"head", branch},
|
||||
{"base", "master"},
|
||||
};
|
||||
auto pr_tmp = fs::temp_directory_path() /
|
||||
std::format("cargoxx-pr-{}.json", std::random_device{}());
|
||||
std::ofstream{pr_tmp} << pr_body.dump();
|
||||
auto pr_r = exec::run(
|
||||
"tea",
|
||||
{"api", "-X", "POST", "-d", std::format("@{}", pr_tmp.string()),
|
||||
std::format("/repos/{}/pulls", registry)},
|
||||
exec::ExecOptions{
|
||||
.cwd = project_root,
|
||||
.env_overrides = {},
|
||||
.timeout = std::chrono::seconds{60},
|
||||
.inherit_stdio = false,
|
||||
});
|
||||
std::error_code ec;
|
||||
fs::remove(pr_tmp, ec);
|
||||
if (!pr_r) {
|
||||
return std::unexpected(pr_r.error());
|
||||
}
|
||||
if (pr_r->exit_code != 0) {
|
||||
return std::unexpected(err(
|
||||
util::ErrorCode::ExecCommandFailed,
|
||||
std::format("tea api /pulls failed (exit {}): {}",
|
||||
pr_r->exit_code, pr_r->stderr_text)));
|
||||
}
|
||||
if (pr_r->stdout_text.find("\"errors\"") != std::string::npos) {
|
||||
return std::unexpected(err(
|
||||
util::ErrorCode::ExecCommandFailed,
|
||||
std::format("Gitea /pulls API rejected the request: {}",
|
||||
pr_r->stdout_text)));
|
||||
}
|
||||
|
||||
// Parse the response properly — substring extraction is fooled by
|
||||
// the nested `html_url` inside the `user`/`head`/`base` sub-objects.
|
||||
std::optional<std::string> pr_url;
|
||||
try {
|
||||
auto j = nlohmann::json::parse(pr_r->stdout_text);
|
||||
if (j.contains("html_url") && j["html_url"].is_string()) {
|
||||
pr_url = j["html_url"].get<std::string>();
|
||||
}
|
||||
} catch (const nlohmann::json::exception&) {
|
||||
// Leave pr_url empty; fall through to the placeholder message.
|
||||
}
|
||||
std::cout << std::format(" Opened {}\n",
|
||||
pr_url.value_or("(PR URL not parsed)"));
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace cargoxx::cli
|
||||
@@ -81,7 +81,7 @@ auto cmd_run(const fs::path& project_root, bool release,
|
||||
}
|
||||
|
||||
const std::string profile = release ? "release" : "debug";
|
||||
auto bin_path = project_root / "build" / profile / selected->name;
|
||||
auto bin_path = project_root / "build" / profile / "bin" / selected->name;
|
||||
|
||||
auto r = exec::run(bin_path.string(), args,
|
||||
exec::ExecOptions{
|
||||
|
||||
@@ -43,6 +43,12 @@ auto run(int argc, char** argv) -> int {
|
||||
vendor_cmd->add_option("--output", vendor_output,
|
||||
"Path to write vendor.toml (default ./vendor.toml)");
|
||||
|
||||
auto* publish_cmd = app.add_subcommand(
|
||||
"publish", "Publish the current HEAD as a new recipe in cargoxx-pkgs");
|
||||
bool publish_dry_run = false;
|
||||
publish_cmd->add_flag("--dry-run", publish_dry_run,
|
||||
"Print the recipe TOML; skip all network ops");
|
||||
|
||||
auto* run_cmd = app.add_subcommand("run", "Build and run a binary target");
|
||||
bool run_release = false;
|
||||
std::string run_bin;
|
||||
@@ -152,6 +158,15 @@ auto run(int argc, char** argv) -> int {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (*publish_cmd) {
|
||||
auto r = cmd_publish(cwd, publish_dry_run);
|
||||
if (!r) {
|
||||
std::cerr << util::format(r.error());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (*run_cmd) {
|
||||
std::optional<std::string> bin;
|
||||
if (!run_bin.empty()) {
|
||||
|
||||
@@ -64,7 +64,10 @@ auto emit_header(const manifest::Manifest& m) -> std::string {
|
||||
"set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD \"d0edc3af-4c50-42ea-a356-e2862fe7a444\")\n"
|
||||
"set(CMAKE_CXX_MODULE_STD ON)\n"
|
||||
"\n"
|
||||
"project({} LANGUAGES CXX)\n"
|
||||
"project({} VERSION {} LANGUAGES CXX)\n"
|
||||
"\n"
|
||||
"include(GNUInstallDirs)\n"
|
||||
"include(CMakePackageConfigHelpers)\n"
|
||||
"\n"
|
||||
"# Generated by cargoxx — do not edit.\n"
|
||||
"# Source of truth: ../Cargoxx.toml\n"
|
||||
@@ -77,7 +80,7 @@ auto emit_header(const manifest::Manifest& m) -> std::string {
|
||||
"\n"
|
||||
"add_compile_options(-Wall -Wextra -Wpedantic -Wconversion "
|
||||
"-Wno-missing-field-initializers)\n",
|
||||
m.package.name, edition_to_int(m.package.edition));
|
||||
m.package.name, m.package.version, edition_to_int(m.package.edition));
|
||||
}
|
||||
|
||||
auto emit_find_packages(const std::vector<linkdb::Recipe>& recipes,
|
||||
@@ -153,9 +156,65 @@ auto any_recipe_is_catch2(const std::vector<linkdb::Recipe>& dev_recipes) -> boo
|
||||
return std::ranges::any_of(dev_recipes, recipe_is_catch2);
|
||||
}
|
||||
|
||||
auto emit_library_install_rules(const std::string& package_name) -> std::string {
|
||||
// Installs the static archive + module FILE_SET, exports targets,
|
||||
// generates Config.cmake + Version.cmake via configure_package_config_file,
|
||||
// and writes a basic pkg-config descriptor. Inline file(WRITE …) keeps the
|
||||
// .in templates self-contained in the generated CMakeLists.txt — no
|
||||
// out-of-tree files to manage.
|
||||
return std::format(
|
||||
"\n# ----- install + package-config + pkg-config -----\n"
|
||||
"install(TARGETS {0}\n"
|
||||
" EXPORT {0}Targets\n"
|
||||
" FILE_SET CXX_MODULES DESTINATION ${{CMAKE_INSTALL_INCLUDEDIR}}/{0}\n"
|
||||
" ARCHIVE DESTINATION ${{CMAKE_INSTALL_LIBDIR}})\n"
|
||||
"install(EXPORT {0}Targets\n"
|
||||
" FILE {0}Targets.cmake\n"
|
||||
" NAMESPACE {0}::\n"
|
||||
" DESTINATION ${{CMAKE_INSTALL_LIBDIR}}/cmake/{0})\n"
|
||||
"\n"
|
||||
"file(WRITE ${{CMAKE_CURRENT_BINARY_DIR}}/{0}Config.cmake.in [[\n"
|
||||
"@PACKAGE_INIT@\n"
|
||||
"include(CMakeFindDependencyMacro)\n"
|
||||
"include(\"${{CMAKE_CURRENT_LIST_DIR}}/{0}Targets.cmake\")\n"
|
||||
"check_required_components({0})\n"
|
||||
"]])\n"
|
||||
"configure_package_config_file(\n"
|
||||
" ${{CMAKE_CURRENT_BINARY_DIR}}/{0}Config.cmake.in\n"
|
||||
" ${{CMAKE_CURRENT_BINARY_DIR}}/{0}Config.cmake\n"
|
||||
" INSTALL_DESTINATION ${{CMAKE_INSTALL_LIBDIR}}/cmake/{0})\n"
|
||||
"write_basic_package_version_file(\n"
|
||||
" ${{CMAKE_CURRENT_BINARY_DIR}}/{0}ConfigVersion.cmake\n"
|
||||
" VERSION ${{PROJECT_VERSION}}\n"
|
||||
" COMPATIBILITY SameMajorVersion)\n"
|
||||
"install(FILES\n"
|
||||
" ${{CMAKE_CURRENT_BINARY_DIR}}/{0}Config.cmake\n"
|
||||
" ${{CMAKE_CURRENT_BINARY_DIR}}/{0}ConfigVersion.cmake\n"
|
||||
" DESTINATION ${{CMAKE_INSTALL_LIBDIR}}/cmake/{0})\n"
|
||||
"\n"
|
||||
"file(WRITE ${{CMAKE_CURRENT_BINARY_DIR}}/{0}.pc.in [[\n"
|
||||
"prefix=@CMAKE_INSTALL_PREFIX@\n"
|
||||
"exec_prefix=${{prefix}}\n"
|
||||
"libdir=${{prefix}}/${{CMAKE_INSTALL_LIBDIR}}\n"
|
||||
"includedir=${{prefix}}/${{CMAKE_INSTALL_INCLUDEDIR}}\n"
|
||||
"\n"
|
||||
"Name: @PROJECT_NAME@\n"
|
||||
"Version: @PROJECT_VERSION@\n"
|
||||
"Description: @PROJECT_NAME@\n"
|
||||
"Cflags: -I${{includedir}}\n"
|
||||
"Libs: -L${{libdir}} -l@PROJECT_NAME@\n"
|
||||
"]])\n"
|
||||
"configure_file(${{CMAKE_CURRENT_BINARY_DIR}}/{0}.pc.in\n"
|
||||
" ${{CMAKE_CURRENT_BINARY_DIR}}/{0}.pc @ONLY)\n"
|
||||
"install(FILES ${{CMAKE_CURRENT_BINARY_DIR}}/{0}.pc\n"
|
||||
" DESTINATION ${{CMAKE_INSTALL_LIBDIR}}/pkgconfig)\n",
|
||||
package_name);
|
||||
}
|
||||
|
||||
auto emit_library(const layout::Target& lib, const std::string& package_name,
|
||||
const std::vector<linkdb::Recipe>& recipes,
|
||||
const std::vector<std::string>& include_dirs,
|
||||
manifest::Edition edition,
|
||||
const fs::path& project_root) -> std::string {
|
||||
std::string out = "\n# ----- library target -----\n";
|
||||
out += std::format("add_library({} STATIC)\n", package_name);
|
||||
@@ -172,6 +231,11 @@ auto emit_library(const layout::Target& lib, const std::string& package_name,
|
||||
}
|
||||
}
|
||||
out += ")\n";
|
||||
// PUBLIC cxx_std_NN propagates the standard requirement onto the
|
||||
// exported IMPORTED target, so consumers `find_package`-ing this
|
||||
// library get the right standard for module BMI regeneration.
|
||||
out += std::format("target_compile_features({} PUBLIC cxx_std_{})\n",
|
||||
package_name, edition_to_int(edition));
|
||||
if (!include_dirs.empty()) {
|
||||
out += std::format("target_include_directories({} SYSTEM PRIVATE", package_name);
|
||||
for (const auto& d : include_dirs) {
|
||||
@@ -181,6 +245,8 @@ auto emit_library(const layout::Target& lib, const std::string& package_name,
|
||||
}
|
||||
out += link_block(package_name, "PUBLIC", false, package_name,
|
||||
collect_dep_targets(recipes));
|
||||
|
||||
out += emit_library_install_rules(package_name);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -190,10 +256,14 @@ auto emit_primary_binary(const layout::Target& bin, const std::string& package_n
|
||||
std::string out = "\n# ----- binary target -----\n";
|
||||
out += std::format("add_executable({}_bin {})\n", package_name,
|
||||
rel_to_build(bin.entry, project_root));
|
||||
out += std::format("set_target_properties({}_bin PROPERTIES OUTPUT_NAME {})\n",
|
||||
out += std::format("set_target_properties({}_bin PROPERTIES\n"
|
||||
" OUTPUT_NAME {}\n"
|
||||
" RUNTIME_OUTPUT_DIRECTORY \"${{CMAKE_BINARY_DIR}}/bin\")\n",
|
||||
package_name, package_name);
|
||||
out += link_block(std::format("{}_bin", package_name), "PRIVATE", has_lib, package_name,
|
||||
collect_dep_targets(recipes));
|
||||
out += std::format("install(TARGETS {}_bin RUNTIME DESTINATION ${{CMAKE_INSTALL_BINDIR}})\n",
|
||||
package_name);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -203,8 +273,13 @@ auto emit_extra_binary(const layout::Target& bin, const std::string& package_nam
|
||||
std::string out = std::format("\n# ----- binary target: {} -----\n", bin.name);
|
||||
out += std::format("add_executable({} {})\n", bin.name,
|
||||
rel_to_build(bin.entry, project_root));
|
||||
out += std::format("set_target_properties({} PROPERTIES\n"
|
||||
" RUNTIME_OUTPUT_DIRECTORY \"${{CMAKE_BINARY_DIR}}/bin\")\n",
|
||||
bin.name);
|
||||
out += link_block(bin.name, "PRIVATE", has_lib, package_name,
|
||||
collect_dep_targets(recipes));
|
||||
out += std::format("install(TARGETS {} RUNTIME DESTINATION ${{CMAKE_INSTALL_BINDIR}})\n",
|
||||
bin.name);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -335,7 +410,8 @@ auto cmake_lists(const GenerateInputs& in) -> std::string {
|
||||
|
||||
if (in.layout.library) {
|
||||
out += emit_library(*in.layout.library, pkg_name, in.recipes,
|
||||
in.manifest.build.include_dirs, in.project_root);
|
||||
in.manifest.build.include_dirs,
|
||||
in.manifest.package.edition, in.project_root);
|
||||
}
|
||||
|
||||
const auto* primary = find_primary_bin(in.layout);
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace cargoxx::codegen {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view CARGOXX_PKGS_URL =
|
||||
"git+https://git.amadey.xyz/mozart/cargoxx-pkgs";
|
||||
|
||||
// One pinned dep gets its own nixpkgs flake input. Unpinned deps stay
|
||||
// on the shared `nixpkgs` input (which always tracks nixos-unstable).
|
||||
struct DepBinding {
|
||||
@@ -20,6 +23,15 @@ struct DepBinding {
|
||||
std::optional<std::string> rev; // pinned commit (null → unpinned)
|
||||
};
|
||||
|
||||
// Parallel to DepBinding for cargoxx-pkgs-resolved deps. All deps from
|
||||
// cargoxx-pkgs share a single `cargoxx-pkgs` flake input pinned at
|
||||
// `rev` — if a project ever needs multiple revs simultaneously, this
|
||||
// can grow per-rev inputs the way DepBinding does for nixpkgs.
|
||||
struct CargoxxPkgsBinding {
|
||||
std::string attr; // e.g. "greeter_0_1_1"
|
||||
std::string rev; // cargoxx-pkgs repo HEAD at lock time
|
||||
};
|
||||
|
||||
// Replaces every char outside [a-zA-Z0-9_] with '_'. The result is safe
|
||||
// to use as a Nix identifier (let bindings, lambda destructure params)
|
||||
// and as an attribute name (inputs.<attr>) — Nix permits underscores in
|
||||
@@ -42,34 +54,66 @@ auto sanitize_input_attr(std::string_view name, std::string_view version)
|
||||
return std::format("nixpkgs_{}_{}", sanitize(name), sanitize(version));
|
||||
}
|
||||
|
||||
struct LockfileRef {
|
||||
std::optional<std::string> rev;
|
||||
std::optional<std::string> attr;
|
||||
struct LockfileEntry {
|
||||
std::optional<std::string> nixpkgs_attr;
|
||||
std::optional<std::string> nixpkgs_rev;
|
||||
std::optional<std::string> cargoxx_pkgs_attr;
|
||||
std::optional<std::string> cargoxx_pkgs_rev;
|
||||
};
|
||||
|
||||
auto find_lockfile_ref(const lockfile::Lockfile& lock, const std::string& name,
|
||||
const std::string& version) -> LockfileRef {
|
||||
auto find_lockfile_entry(const lockfile::Lockfile& lock, const std::string& name,
|
||||
const std::string& version) -> LockfileEntry {
|
||||
for (const auto& p : lock.packages) {
|
||||
if (p.name == name && p.version == version) {
|
||||
return LockfileRef{.rev = p.nixpkgs_rev, .attr = p.nixpkgs_attr};
|
||||
return LockfileEntry{
|
||||
.nixpkgs_attr = p.nixpkgs_attr,
|
||||
.nixpkgs_rev = p.nixpkgs_rev,
|
||||
.cargoxx_pkgs_attr = p.cargoxx_pkgs_attr,
|
||||
.cargoxx_pkgs_rev = p.cargoxx_pkgs_rev,
|
||||
};
|
||||
}
|
||||
}
|
||||
return LockfileRef{};
|
||||
return {};
|
||||
}
|
||||
|
||||
auto build_bindings(const GenerateInputs& in) -> std::vector<DepBinding> {
|
||||
std::vector<DepBinding> out;
|
||||
out.reserve(in.manifest.dependencies.size() + in.manifest.dev_dependencies.size());
|
||||
struct Bindings {
|
||||
std::vector<DepBinding> nixpkgs;
|
||||
std::vector<CargoxxPkgsBinding> cargoxx_pkgs;
|
||||
};
|
||||
|
||||
auto build_bindings(const GenerateInputs& in) -> Bindings {
|
||||
Bindings out;
|
||||
auto push = [&](const manifest::Dependency& dep, const linkdb::Recipe& rec) {
|
||||
auto ref = find_lockfile_ref(in.lock, dep.name, dep.version_spec);
|
||||
std::string attr = (ref.attr && !ref.attr->empty()) ? *ref.attr
|
||||
: rec.nixpkgs_attr;
|
||||
out.push_back(DepBinding{
|
||||
// cargoxx-source deps (path/git) don't live in nixpkgs — they're
|
||||
// produced by a recursive buildCppPackage invocation when the
|
||||
// consumer is built via `nix build`. Emitting them here would
|
||||
// generate `pkgs.` (empty attr) in the devshell flake. Skip them;
|
||||
// the cargoxx-build-direct path will pick them up via a separate
|
||||
// pre-build resolution step in a follow-up.
|
||||
if (dep.source == manifest::DepSource::CargoxxPath
|
||||
|| dep.source == manifest::DepSource::CargoxxGit) {
|
||||
return;
|
||||
}
|
||||
auto entry = find_lockfile_entry(in.lock, dep.name, dep.version_spec);
|
||||
// cargoxx-pkgs resolution wins if the lockfile records it.
|
||||
if (entry.cargoxx_pkgs_attr && !entry.cargoxx_pkgs_attr->empty()
|
||||
&& entry.cargoxx_pkgs_rev && !entry.cargoxx_pkgs_rev->empty()) {
|
||||
out.cargoxx_pkgs.push_back(CargoxxPkgsBinding{
|
||||
.attr = *entry.cargoxx_pkgs_attr,
|
||||
.rev = *entry.cargoxx_pkgs_rev,
|
||||
});
|
||||
return;
|
||||
}
|
||||
std::string attr =
|
||||
(entry.nixpkgs_attr && !entry.nixpkgs_attr->empty())
|
||||
? *entry.nixpkgs_attr
|
||||
: rec.nixpkgs_attr;
|
||||
out.nixpkgs.push_back(DepBinding{
|
||||
.name = dep.name,
|
||||
.version = dep.version_spec,
|
||||
.nixpkgs_attr = std::move(attr),
|
||||
.sanitized = sanitize_input_attr(dep.name, dep.version_spec),
|
||||
.rev = std::move(ref.rev),
|
||||
.rev = entry.nixpkgs_rev,
|
||||
});
|
||||
};
|
||||
for (std::size_t i = 0; i < in.manifest.dependencies.size(); ++i) {
|
||||
@@ -99,7 +143,20 @@ auto pinned_inputs_dedup(const std::vector<DepBinding>& bindings)
|
||||
return out;
|
||||
}
|
||||
|
||||
// All cargoxx-pkgs deps share a single `cargoxx-pkgs` flake input.
|
||||
// Picks the first rev seen; if revs diverge across deps the project
|
||||
// is mid-migration and the user should re-run `cargoxx add` for each
|
||||
// stale dep to align them.
|
||||
auto cargoxx_pkgs_rev(const std::vector<CargoxxPkgsBinding>& bs)
|
||||
-> std::optional<std::string> {
|
||||
if (bs.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return bs.front().rev;
|
||||
}
|
||||
|
||||
auto emit_inputs_block(const std::vector<const DepBinding*>& pinned,
|
||||
const std::optional<std::string>& cargoxx_pkgs_rev,
|
||||
const lockfile::Lockfile& lock,
|
||||
const std::optional<VendorIndex>& vendor)
|
||||
-> std::string {
|
||||
@@ -131,18 +188,25 @@ auto emit_inputs_block(const std::vector<const DepBinding*>& pinned,
|
||||
b->sanitized, *b->rev);
|
||||
}
|
||||
}
|
||||
if (cargoxx_pkgs_rev) {
|
||||
out += std::format(" cargoxx-pkgs.url = \"{}?rev={}\";\n",
|
||||
CARGOXX_PKGS_URL, *cargoxx_pkgs_rev);
|
||||
}
|
||||
out += std::format(" flake-utils.url = \"{}\";\n", flake_utils_url);
|
||||
out += " };\n";
|
||||
return out;
|
||||
}
|
||||
|
||||
auto emit_outputs_params(const std::vector<const DepBinding*>& pinned)
|
||||
-> std::string {
|
||||
auto emit_outputs_params(const std::vector<const DepBinding*>& pinned,
|
||||
bool any_cargoxx_pkgs) -> std::string {
|
||||
std::string out = "{ self, nixpkgs";
|
||||
for (const auto* b : pinned) {
|
||||
out += ", ";
|
||||
out += b->sanitized;
|
||||
}
|
||||
if (any_cargoxx_pkgs) {
|
||||
out += ", cargoxx-pkgs";
|
||||
}
|
||||
out += ", flake-utils }";
|
||||
return out;
|
||||
}
|
||||
@@ -163,15 +227,23 @@ auto base_expr(const DepBinding& b) -> std::string {
|
||||
: std::format("pkgs.{}", b.nixpkgs_attr);
|
||||
}
|
||||
|
||||
auto emit_build_inputs(const std::vector<DepBinding>& bindings) -> std::string {
|
||||
auto emit_build_inputs(const std::vector<DepBinding>& nixpkgs_bs,
|
||||
const std::vector<CargoxxPkgsBinding>& cxx_bs)
|
||||
-> std::string {
|
||||
std::set<std::string> seen;
|
||||
std::string out;
|
||||
for (const auto& b : bindings) {
|
||||
for (const auto& b : nixpkgs_bs) {
|
||||
auto expr = base_expr(b);
|
||||
if (seen.insert(expr).second) {
|
||||
out += std::format(" {}\n", expr);
|
||||
}
|
||||
}
|
||||
for (const auto& b : cxx_bs) {
|
||||
auto expr = std::format("cargoxx-pkgs.packages.${{system}}.{}", b.attr);
|
||||
if (seen.insert(expr).second) {
|
||||
out += std::format(" {}\n", expr);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -179,13 +251,14 @@ auto emit_build_inputs(const std::vector<DepBinding>& bindings) -> std::string {
|
||||
|
||||
auto flake_nix(const GenerateInputs& in) -> std::string {
|
||||
auto bindings = build_bindings(in);
|
||||
auto pinned = pinned_inputs_dedup(bindings);
|
||||
auto pinned = pinned_inputs_dedup(bindings.nixpkgs);
|
||||
auto cxx_rev = cargoxx_pkgs_rev(bindings.cargoxx_pkgs);
|
||||
|
||||
std::string out;
|
||||
out += "{\n";
|
||||
out += std::format(" description = \"{}\";\n\n", in.manifest.package.name);
|
||||
|
||||
out += emit_inputs_block(pinned, in.lock, in.vendor);
|
||||
out += emit_inputs_block(pinned, cxx_rev, in.lock, in.vendor);
|
||||
|
||||
const bool any_pkg_config =
|
||||
std::ranges::any_of(in.recipes,
|
||||
@@ -201,7 +274,7 @@ auto flake_nix(const GenerateInputs& in) -> std::string {
|
||||
|
||||
out += "\n";
|
||||
out += " outputs = ";
|
||||
out += emit_outputs_params(pinned);
|
||||
out += emit_outputs_params(pinned, cxx_rev.has_value());
|
||||
out += ":\n"
|
||||
" flake-utils.lib.eachDefaultSystem (system:\n"
|
||||
" let\n"
|
||||
@@ -219,7 +292,7 @@ auto flake_nix(const GenerateInputs& in) -> std::string {
|
||||
}
|
||||
out += " ];\n"
|
||||
" buildInputs = [\n";
|
||||
out += emit_build_inputs(bindings);
|
||||
out += emit_build_inputs(bindings.nixpkgs, bindings.cargoxx_pkgs);
|
||||
out += " ];\n"
|
||||
" hardeningDisable = [\n"
|
||||
" \"all\"\n"
|
||||
|
||||
@@ -104,6 +104,31 @@ auto discover(const fs::path& project_root, const std::string& package_name)
|
||||
.module_units = {},
|
||||
});
|
||||
}
|
||||
// Cargo's src/bin/<name>/main.cpp form: a subdirectory of src/bin/
|
||||
// whose entry point is its own main.cpp. v1 ships the entry only —
|
||||
// sibling .cpp files in the subdir are not collected.
|
||||
if (fs::exists(bin_dir, ec) && !ec) {
|
||||
std::vector<fs::path> subdir_mains;
|
||||
for (const auto& entry : fs::directory_iterator{bin_dir}) {
|
||||
if (!entry.is_directory()) {
|
||||
continue;
|
||||
}
|
||||
auto candidate = entry.path() / "main.cpp";
|
||||
if (fs::exists(candidate, ec) && !ec) {
|
||||
subdir_mains.push_back(candidate);
|
||||
}
|
||||
}
|
||||
std::ranges::sort(subdir_mains);
|
||||
for (const auto& m : subdir_mains) {
|
||||
out.binaries.push_back(Target{
|
||||
.kind = TargetKind::Binary,
|
||||
.name = m.parent_path().filename().string(),
|
||||
.entry = m,
|
||||
.additional_sources = {},
|
||||
.module_units = {},
|
||||
});
|
||||
}
|
||||
}
|
||||
std::ranges::sort(out.binaries, by_name);
|
||||
|
||||
for (const auto& f : top_level_cpp(tests_dir)) {
|
||||
|
||||
@@ -76,6 +76,12 @@ auto parse_package(const toml::table& tbl, const std::filesystem::path& path)
|
||||
if (auto v = tbl["nixpkgs_rev"].value<std::string>()) {
|
||||
pkg.nixpkgs_rev = *v;
|
||||
}
|
||||
if (auto v = tbl["cargoxx_pkgs_attr"].value<std::string>()) {
|
||||
pkg.cargoxx_pkgs_attr = *v;
|
||||
}
|
||||
if (auto v = tbl["cargoxx_pkgs_rev"].value<std::string>()) {
|
||||
pkg.cargoxx_pkgs_rev = *v;
|
||||
}
|
||||
if (auto v = tbl["linkdb_source"].value<std::string>()) {
|
||||
pkg.linkdb_source = *v;
|
||||
}
|
||||
@@ -106,6 +112,21 @@ auto parse_package(const toml::table& tbl, const std::filesystem::path& path)
|
||||
}
|
||||
pkg.brute_force_includes = std::move(*r);
|
||||
}
|
||||
if (auto v = tbl["source_kind"].value<std::string>()) {
|
||||
pkg.source_kind = *v;
|
||||
}
|
||||
if (auto v = tbl["source_path"].value<std::string>()) {
|
||||
pkg.source_path = *v;
|
||||
}
|
||||
if (auto v = tbl["source_git_url"].value<std::string>()) {
|
||||
pkg.source_git_url = *v;
|
||||
}
|
||||
if (auto v = tbl["source_git_commit"].value<std::string>()) {
|
||||
pkg.source_git_commit = *v;
|
||||
}
|
||||
if (auto v = tbl["source_git_sha256"].value<std::string>()) {
|
||||
pkg.source_git_sha256 = *v;
|
||||
}
|
||||
|
||||
return pkg;
|
||||
}
|
||||
@@ -185,6 +206,12 @@ auto write(const Lockfile& lock, const std::filesystem::path& path) -> util::Res
|
||||
if (p.nixpkgs_rev) {
|
||||
tbl.insert_or_assign("nixpkgs_rev", *p.nixpkgs_rev);
|
||||
}
|
||||
if (p.cargoxx_pkgs_attr) {
|
||||
tbl.insert_or_assign("cargoxx_pkgs_attr", *p.cargoxx_pkgs_attr);
|
||||
}
|
||||
if (p.cargoxx_pkgs_rev) {
|
||||
tbl.insert_or_assign("cargoxx_pkgs_rev", *p.cargoxx_pkgs_rev);
|
||||
}
|
||||
if (p.linkdb_source) {
|
||||
tbl.insert_or_assign("linkdb_source", *p.linkdb_source);
|
||||
}
|
||||
@@ -215,6 +242,21 @@ auto write(const Lockfile& lock, const std::filesystem::path& path) -> util::Res
|
||||
}
|
||||
tbl.insert_or_assign("brute_force_includes", std::move(arr));
|
||||
}
|
||||
if (p.source_kind) {
|
||||
tbl.insert_or_assign("source_kind", *p.source_kind);
|
||||
}
|
||||
if (p.source_path) {
|
||||
tbl.insert_or_assign("source_path", *p.source_path);
|
||||
}
|
||||
if (p.source_git_url) {
|
||||
tbl.insert_or_assign("source_git_url", *p.source_git_url);
|
||||
}
|
||||
if (p.source_git_commit) {
|
||||
tbl.insert_or_assign("source_git_commit", *p.source_git_commit);
|
||||
}
|
||||
if (p.source_git_sha256) {
|
||||
tbl.insert_or_assign("source_git_sha256", *p.source_git_sha256);
|
||||
}
|
||||
packages.push_back(std::move(tbl));
|
||||
}
|
||||
root.insert_or_assign("package", std::move(packages));
|
||||
|
||||
@@ -12,12 +12,27 @@ struct LockfilePackage {
|
||||
std::vector<std::string> dependencies;
|
||||
std::optional<std::string> nixpkgs_attr;
|
||||
std::optional<std::string> nixpkgs_rev;
|
||||
// Parallel to nixpkgs_attr/rev: the dep was resolved through the
|
||||
// cargoxx-pkgs flake (https://git.amadey.xyz/mozart/cargoxx-pkgs).
|
||||
// `cargoxx_pkgs_attr` is the attribute name in
|
||||
// `cargoxx-pkgs.packages.<system>` (e.g. "greeter_0_1_1"), and
|
||||
// `cargoxx_pkgs_rev` pins the cargoxx-pkgs repo itself.
|
||||
std::optional<std::string> cargoxx_pkgs_attr;
|
||||
std::optional<std::string> cargoxx_pkgs_rev;
|
||||
std::optional<std::string> linkdb_source;
|
||||
std::optional<std::string> find_package;
|
||||
std::vector<std::string> targets;
|
||||
std::optional<std::string> pkg_config_module;
|
||||
std::vector<std::string> brute_force_libs;
|
||||
std::vector<std::string> brute_force_includes;
|
||||
// For cargoxx-source deps (not nixpkgs/linkdb-resolved).
|
||||
// "cargoxx-path" → source_path only
|
||||
// "cargoxx-git" → source_git_url + source_git_commit + source_git_sha256
|
||||
std::optional<std::string> source_kind;
|
||||
std::optional<std::string> source_path;
|
||||
std::optional<std::string> source_git_url;
|
||||
std::optional<std::string> source_git_commit;
|
||||
std::optional<std::string> source_git_sha256;
|
||||
|
||||
bool operator==(const LockfilePackage&) const = default;
|
||||
};
|
||||
|
||||
@@ -5,10 +5,20 @@ import cargoxx.util;
|
||||
|
||||
export namespace cargoxx::manifest {
|
||||
|
||||
enum class DepSource {
|
||||
Auto, // string form or { version = ... } only → existing resolver chain
|
||||
CargoxxPath, // { path = "../foo" } → recursive cargoxx build
|
||||
CargoxxGit, // { git = "...", rev = "..." } → fetch + recursive cargoxx build
|
||||
};
|
||||
|
||||
struct Dependency {
|
||||
std::string name;
|
||||
std::string version_spec;
|
||||
std::vector<std::string> components;
|
||||
DepSource source = DepSource::Auto;
|
||||
std::optional<std::string> path; // when source == CargoxxPath
|
||||
std::optional<std::string> git_url; // when source == CargoxxGit
|
||||
std::optional<std::string> git_rev; // when source == CargoxxGit (40-char)
|
||||
|
||||
bool operator==(const Dependency&) const = default;
|
||||
};
|
||||
@@ -29,6 +39,9 @@ struct Package {
|
||||
Edition edition = Edition::Cpp23;
|
||||
std::vector<std::string> authors;
|
||||
std::optional<std::string> license;
|
||||
std::optional<std::string> description;
|
||||
std::optional<std::string> repository;
|
||||
std::optional<std::string> homepage;
|
||||
|
||||
bool operator==(const Package&) const = default;
|
||||
};
|
||||
|
||||
@@ -73,6 +73,7 @@ auto extract_string_array(const toml::array& arr, std::string_view field,
|
||||
|
||||
constexpr std::array PACKAGE_KNOWN_KEYS = {
|
||||
"name", "version", "edition", "authors", "license", "description", "repository",
|
||||
"homepage",
|
||||
};
|
||||
|
||||
constexpr std::array BUILD_KNOWN_KEYS = {
|
||||
@@ -138,6 +139,15 @@ auto parse_package(const toml::table& tbl, const std::filesystem::path& path)
|
||||
if (auto license = tbl["license"].value<std::string>()) {
|
||||
pkg.license = *license;
|
||||
}
|
||||
if (auto v = tbl["description"].value<std::string>()) {
|
||||
pkg.description = *v;
|
||||
}
|
||||
if (auto v = tbl["repository"].value<std::string>()) {
|
||||
pkg.repository = *v;
|
||||
}
|
||||
if (auto v = tbl["homepage"].value<std::string>()) {
|
||||
pkg.homepage = *v;
|
||||
}
|
||||
|
||||
return pkg;
|
||||
}
|
||||
@@ -153,12 +163,49 @@ auto parse_dependency(std::string name, const toml::node& value,
|
||||
}
|
||||
|
||||
if (const auto* tbl = value.as_table()) {
|
||||
// Path form: { path = "../foo" } → cargoxx-source dep.
|
||||
if (auto path_str = (*tbl)["path"].value<std::string>()) {
|
||||
dep.source = DepSource::CargoxxPath;
|
||||
dep.path = *path_str;
|
||||
if (auto v = (*tbl)["version"].value<std::string>()) {
|
||||
dep.version_spec = *v;
|
||||
} else {
|
||||
dep.version_spec = "*";
|
||||
}
|
||||
return dep;
|
||||
}
|
||||
|
||||
// Git form: { git = "...", rev = "<40-char>" } → cargoxx-source dep
|
||||
// fetched at the pinned commit. Branch/tag resolution is not yet
|
||||
// supported; require a literal commit.
|
||||
if (auto git_url = (*tbl)["git"].value<std::string>()) {
|
||||
auto rev = (*tbl)["rev"].value<std::string>();
|
||||
if (!rev) {
|
||||
return std::unexpected(err(
|
||||
ErrorCode::ManifestInvalidField,
|
||||
std::format("git dependency '{}' must specify a 'rev' (40-char commit)",
|
||||
dep.name),
|
||||
path, source_pos(value)));
|
||||
}
|
||||
dep.source = DepSource::CargoxxGit;
|
||||
dep.git_url = *git_url;
|
||||
dep.git_rev = *rev;
|
||||
if (auto v = (*tbl)["version"].value<std::string>()) {
|
||||
dep.version_spec = *v;
|
||||
} else {
|
||||
dep.version_spec = "*";
|
||||
}
|
||||
return dep;
|
||||
}
|
||||
|
||||
if (auto v = (*tbl)["version"].value<std::string>()) {
|
||||
dep.version_spec = *v;
|
||||
} else {
|
||||
return std::unexpected(err(
|
||||
ErrorCode::ManifestInvalidField,
|
||||
std::format("dependency '{}' table must have a 'version' string", dep.name),
|
||||
std::format(
|
||||
"dependency '{}' table must have one of: 'version', 'path', 'git'",
|
||||
dep.name),
|
||||
path, source_pos(value)));
|
||||
}
|
||||
if (const auto* comps = (*tbl)["components"].as_array()) {
|
||||
|
||||
@@ -44,12 +44,32 @@ auto build_table(const Manifest& m) -> toml::table {
|
||||
if (m.package.license) {
|
||||
package.insert_or_assign("license", *m.package.license);
|
||||
}
|
||||
if (m.package.description) {
|
||||
package.insert_or_assign("description", *m.package.description);
|
||||
}
|
||||
if (m.package.repository) {
|
||||
package.insert_or_assign("repository", *m.package.repository);
|
||||
}
|
||||
if (m.package.homepage) {
|
||||
package.insert_or_assign("homepage", *m.package.homepage);
|
||||
}
|
||||
root.insert_or_assign("package", std::move(package));
|
||||
|
||||
auto deps_to_table = [](const std::vector<Dependency>& deps) {
|
||||
toml::table out;
|
||||
for (const auto& dep : deps) {
|
||||
if (dep.components.empty()) {
|
||||
if (dep.source == DepSource::CargoxxPath) {
|
||||
toml::table dep_tbl;
|
||||
dep_tbl.insert_or_assign("path", *dep.path);
|
||||
dep_tbl.is_inline(true);
|
||||
out.insert_or_assign(dep.name, std::move(dep_tbl));
|
||||
} else if (dep.source == DepSource::CargoxxGit) {
|
||||
toml::table dep_tbl;
|
||||
dep_tbl.insert_or_assign("git", *dep.git_url);
|
||||
dep_tbl.insert_or_assign("rev", *dep.git_rev);
|
||||
dep_tbl.is_inline(true);
|
||||
out.insert_or_assign(dep.name, std::move(dep_tbl));
|
||||
} else if (dep.components.empty()) {
|
||||
out.insert_or_assign(dep.name, dep.version_spec);
|
||||
} else {
|
||||
toml::table dep_tbl;
|
||||
|
||||
233
src/resolver/cargoxx_pkgs_probe.cpp
Normal file
233
src/resolver/cargoxx_pkgs_probe.cpp
Normal file
@@ -0,0 +1,233 @@
|
||||
module;
|
||||
|
||||
#include <toml.hpp>
|
||||
|
||||
module cargoxx.resolver;
|
||||
|
||||
import std;
|
||||
import cargoxx.util;
|
||||
import cargoxx.exec;
|
||||
|
||||
namespace cargoxx::resolver {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view CARGOXX_PKGS_OWNER = "mozart";
|
||||
constexpr std::string_view CARGOXX_PKGS_REPO = "cargoxx-pkgs";
|
||||
constexpr std::string_view CARGOXX_PKGS_HOST = "https://git.amadey.xyz";
|
||||
|
||||
auto error(util::ErrorCode code, std::string msg) -> util::Error {
|
||||
return util::Error{code, std::move(msg), "", std::nullopt, std::nullopt};
|
||||
}
|
||||
|
||||
auto curl_get(const std::string& url) -> util::Result<std::string> {
|
||||
auto r = exec::run("curl", {"-fsSL", "--max-time", "30", url},
|
||||
exec::ExecOptions{
|
||||
.cwd = {},
|
||||
.env_overrides = {},
|
||||
.timeout = std::chrono::seconds{40},
|
||||
.inherit_stdio = false,
|
||||
});
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (r->exit_code == 22) {
|
||||
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
|
||||
std::format("HTTP 4xx for {}", url)));
|
||||
}
|
||||
if (r->exit_code != 0) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("curl failed (exit {}) fetching {}: {}",
|
||||
r->exit_code, url, r->stderr_text)));
|
||||
}
|
||||
return std::move(r->stdout_text);
|
||||
}
|
||||
|
||||
// Returns the recipe basenames in `versions/` for `<name>`. Hits the
|
||||
// Gitea contents API which returns a JSON array; we don't pull in a
|
||||
// JSON library for this tiny shape — instead we scrape `"name":"X"`
|
||||
// occurrences. The shape is documented at:
|
||||
// /api/swagger#/repository/repoGetContentsList
|
||||
auto extract_filenames(std::string_view json) -> std::vector<std::string> {
|
||||
std::vector<std::string> out;
|
||||
constexpr std::string_view key = R"("name":")";
|
||||
std::size_t pos = 0;
|
||||
while ((pos = json.find(key, pos)) != std::string_view::npos) {
|
||||
pos += key.size();
|
||||
auto end = json.find('"', pos);
|
||||
if (end == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
out.emplace_back(json.substr(pos, end - pos));
|
||||
pos = end + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
auto pick_best_version(const std::vector<std::string>& versions,
|
||||
std::string_view spec) -> std::optional<std::string> {
|
||||
std::optional<std::string> best;
|
||||
for (const auto& v : versions) {
|
||||
if (spec != "*" && !util::satisfies(v, spec)) {
|
||||
continue;
|
||||
}
|
||||
if (!best) {
|
||||
best = v;
|
||||
continue;
|
||||
}
|
||||
// Both v and *best satisfy; pick the higher. `satisfies(v, "> best")`
|
||||
// is the cheapest precedence test using only the existing helper.
|
||||
if (util::satisfies(v, std::format("> {}", *best))) {
|
||||
best = v;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto parse_cargoxx_pkgs_versions(std::string_view contents_json)
|
||||
-> util::Result<std::vector<std::string>> {
|
||||
auto names = extract_filenames(contents_json);
|
||||
std::vector<std::string> out;
|
||||
out.reserve(names.size());
|
||||
for (const auto& n : names) {
|
||||
constexpr std::string_view suffix = ".toml";
|
||||
if (n.size() <= suffix.size() || !n.ends_with(suffix)) {
|
||||
continue;
|
||||
}
|
||||
out.push_back(n.substr(0, n.size() - suffix.size()));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
auto parse_cargoxx_pkgs_recipe(std::string_view body) -> util::Result<PkgsHit> {
|
||||
toml::table root;
|
||||
try {
|
||||
root = toml::parse(std::string{body});
|
||||
} catch (const toml::parse_error& e) {
|
||||
return std::unexpected(error(util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("cargoxx-pkgs recipe parse error: {}",
|
||||
e.description())));
|
||||
}
|
||||
PkgsHit hit;
|
||||
if (auto v = root["version"].value<std::string>()) {
|
||||
hit.version = *v;
|
||||
} else {
|
||||
return std::unexpected(error(util::ErrorCode::ResolutionNetworkError,
|
||||
"cargoxx-pkgs recipe missing 'version'"));
|
||||
}
|
||||
const auto* src = root["source"].as_table();
|
||||
if (!src) {
|
||||
return std::unexpected(error(util::ErrorCode::ResolutionNetworkError,
|
||||
"cargoxx-pkgs recipe missing [source] table"));
|
||||
}
|
||||
if (auto v = (*src)["type"].value<std::string>(); !v || *v != "git") {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
"cargoxx-pkgs recipe [source].type must be 'git'"));
|
||||
}
|
||||
auto url = (*src)["url"].value<std::string>();
|
||||
auto commit = (*src)["commit"].value<std::string>();
|
||||
auto sha = (*src)["sha256"].value<std::string>();
|
||||
if (!url || !commit || !sha) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
"cargoxx-pkgs recipe [source] needs url + commit + sha256"));
|
||||
}
|
||||
hit.source_url = *url;
|
||||
hit.source_commit = *commit;
|
||||
hit.source_sha256 = *sha;
|
||||
return hit;
|
||||
}
|
||||
|
||||
// Returns the HEAD rev of cargoxx-pkgs's `master` branch. Used to pin
|
||||
// `inputs.cargoxx-pkgs.url = ".../?rev=<rev>"` in the consumer's flake.
|
||||
auto query_cargoxx_pkgs_head() -> util::Result<std::string> {
|
||||
auto url = std::format("{}/api/v1/repos/{}/{}/branches/master",
|
||||
CARGOXX_PKGS_HOST, CARGOXX_PKGS_OWNER,
|
||||
CARGOXX_PKGS_REPO);
|
||||
auto body = curl_get(url);
|
||||
if (!body) {
|
||||
return std::unexpected(body.error());
|
||||
}
|
||||
// Tiny ad-hoc extraction; Gitea returns `"commit":{"id":"<sha>", ...}`.
|
||||
constexpr std::string_view key = R"("id":")";
|
||||
auto pos = body->find(key);
|
||||
if (pos == std::string::npos) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("cargoxx-pkgs branches response from {} has no commit id",
|
||||
url)));
|
||||
}
|
||||
pos += key.size();
|
||||
auto end = body->find('"', pos);
|
||||
if (end == std::string::npos) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("cargoxx-pkgs branches response from {} is truncated",
|
||||
url)));
|
||||
}
|
||||
return body->substr(pos, end - pos);
|
||||
}
|
||||
|
||||
auto try_cargoxx_pkgs(const std::string& name, const std::string& version_spec)
|
||||
-> util::Result<PkgsHit> {
|
||||
if (name.empty()) {
|
||||
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
|
||||
"package name is empty"));
|
||||
}
|
||||
// Step 1: list version recipes via the Gitea contents API.
|
||||
auto list_url = std::format(
|
||||
"{}/api/v1/repos/{}/{}/contents/recipes/{}/versions",
|
||||
CARGOXX_PKGS_HOST, CARGOXX_PKGS_OWNER, CARGOXX_PKGS_REPO, name);
|
||||
auto listing = curl_get(list_url);
|
||||
if (!listing) {
|
||||
return std::unexpected(listing.error());
|
||||
}
|
||||
auto versions_r = parse_cargoxx_pkgs_versions(*listing);
|
||||
if (!versions_r) {
|
||||
return std::unexpected(versions_r.error());
|
||||
}
|
||||
if (versions_r->empty()) {
|
||||
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
|
||||
std::format("cargoxx-pkgs has no versions for '{}'",
|
||||
name)));
|
||||
}
|
||||
auto chosen = pick_best_version(*versions_r, version_spec);
|
||||
if (!chosen) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionUnknownPackage,
|
||||
std::format("cargoxx-pkgs has no version of '{}' satisfying '{}'",
|
||||
name, version_spec)));
|
||||
}
|
||||
|
||||
// Step 2: pull the recipe TOML.
|
||||
auto recipe_url = std::format(
|
||||
"{}/{}/{}/raw/branch/master/recipes/{}/versions/{}.toml",
|
||||
CARGOXX_PKGS_HOST, CARGOXX_PKGS_OWNER, CARGOXX_PKGS_REPO, name, *chosen);
|
||||
auto body = curl_get(recipe_url);
|
||||
if (!body) {
|
||||
return std::unexpected(body.error());
|
||||
}
|
||||
auto hit = parse_cargoxx_pkgs_recipe(*body);
|
||||
if (!hit) {
|
||||
return std::unexpected(hit.error());
|
||||
}
|
||||
if (hit->version != *chosen) {
|
||||
return std::unexpected(error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("cargoxx-pkgs recipe at {} declares version '{}' "
|
||||
"but the path implies '{}'",
|
||||
recipe_url, hit->version, *chosen)));
|
||||
}
|
||||
auto rev = query_cargoxx_pkgs_head();
|
||||
if (!rev) {
|
||||
return std::unexpected(rev.error());
|
||||
}
|
||||
hit->repo_rev = *rev;
|
||||
return hit;
|
||||
}
|
||||
|
||||
} // namespace cargoxx::resolver
|
||||
@@ -207,12 +207,31 @@ auto realize_path_at_rev(const std::string& rev, const std::string& attr)
|
||||
return path;
|
||||
}
|
||||
|
||||
auto realize_flake_source(const std::string& flake_ref)
|
||||
-> util::Result<std::string> {
|
||||
namespace {
|
||||
|
||||
auto extract_json_string(std::string_view body, std::string_view key)
|
||||
-> std::optional<std::string> {
|
||||
auto needle = std::format("\"{}\":\"", key);
|
||||
auto pos = body.find(needle);
|
||||
if (pos == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
pos += needle.size();
|
||||
auto end = body.find('"', pos);
|
||||
if (end == std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::string{body.substr(pos, end - pos)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto prefetch_flake_source(const std::string& flake_ref)
|
||||
-> util::Result<PrefetchedSource> {
|
||||
if (flake_ref.empty()) {
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionUnknownPackage,
|
||||
"realize_flake_source: flake_ref is empty"));
|
||||
"prefetch_flake_source: flake_ref is empty"));
|
||||
}
|
||||
std::vector<std::string> args{
|
||||
"--extra-experimental-features", "nix-command flakes",
|
||||
@@ -234,23 +253,33 @@ auto realize_flake_source(const std::string& flake_ref)
|
||||
std::format("nix flake prefetch failed (exit {}): {}",
|
||||
r->exit_code, r->stderr_text)));
|
||||
}
|
||||
std::string_view body = r->stdout_text;
|
||||
constexpr std::string_view key = "\"storePath\":\"";
|
||||
auto pos = body.find(key);
|
||||
if (pos == std::string_view::npos) {
|
||||
auto store_path = extract_json_string(r->stdout_text, "storePath");
|
||||
auto hash = extract_json_string(r->stdout_text, "hash");
|
||||
if (!store_path) {
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
std::format("nix flake prefetch emitted no storePath for '{}'",
|
||||
flake_ref)));
|
||||
}
|
||||
pos += key.size();
|
||||
auto end = body.find('"', pos);
|
||||
if (end == std::string_view::npos) {
|
||||
if (!hash) {
|
||||
return std::unexpected(make_error(
|
||||
util::ErrorCode::ResolutionNetworkError,
|
||||
"nix flake prefetch JSON malformed"));
|
||||
std::format("nix flake prefetch emitted no hash for '{}'",
|
||||
flake_ref)));
|
||||
}
|
||||
return std::string{body.substr(pos, end - pos)};
|
||||
return PrefetchedSource{
|
||||
.store_path = std::move(*store_path),
|
||||
.hash = std::move(*hash),
|
||||
};
|
||||
}
|
||||
|
||||
auto realize_flake_source(const std::string& flake_ref)
|
||||
-> util::Result<std::string> {
|
||||
auto r = prefetch_flake_source(flake_ref);
|
||||
if (!r) {
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
return std::move(r->store_path);
|
||||
}
|
||||
|
||||
} // namespace cargoxx::resolver
|
||||
|
||||
@@ -59,6 +59,53 @@ auto realize_path_at_rev(const std::string& rev, const std::string& attr)
|
||||
auto realize_flake_source(const std::string& flake_ref)
|
||||
-> util::Result<std::string>;
|
||||
|
||||
struct PrefetchedSource {
|
||||
std::string store_path;
|
||||
std::string hash; // SRI form, e.g. "sha256-<base64>"
|
||||
};
|
||||
|
||||
// Result of looking a package up in the cargoxx-pkgs Gitea repo
|
||||
// (https://git.amadey.xyz/mozart/cargoxx-pkgs). Mirrors what
|
||||
// `nixpkgs_probe` returns for the nixpkgs path: a confirmed (name,
|
||||
// version) pair plus the source-of-truth flake rev so cargoxx build
|
||||
// can pin `inputs.cargoxx-pkgs.url = ".../?rev=<repo_rev>"`. The
|
||||
// recipe-source fields (url/commit/sha256) are surfaced for
|
||||
// diagnostics; consumers pull the dep transitively through the
|
||||
// cargoxx-pkgs flake, not by re-fetching directly.
|
||||
struct PkgsHit {
|
||||
std::string version; // the concrete version that matched (e.g. "0.1.1")
|
||||
std::string repo_rev; // HEAD rev of cargoxx-pkgs at lookup time
|
||||
std::string source_url; // [source].url from the recipe TOML (diag)
|
||||
std::string source_commit; // [source].commit (diag)
|
||||
std::string source_sha256; // [source].sha256 (SRI form) (diag)
|
||||
};
|
||||
|
||||
// Pure: parse a Gitea `/contents/<dir>` JSON listing and extract the
|
||||
// versions (basenames stripped of `.toml`). Returns the versions in
|
||||
// the order they appear in the JSON.
|
||||
auto parse_cargoxx_pkgs_versions(std::string_view contents_json)
|
||||
-> util::Result<std::vector<std::string>>;
|
||||
|
||||
// Pure: parse a single recipe TOML (the file at
|
||||
// `recipes/<name>/versions/<v>.toml`) into a PkgsHit. The caller fills
|
||||
// in `version` from the URL it asked for; `source_*` come from the
|
||||
// recipe's `[source]` table.
|
||||
auto parse_cargoxx_pkgs_recipe(std::string_view body) -> util::Result<PkgsHit>;
|
||||
|
||||
// Hits the cargoxx-pkgs Gitea repo for `<name>`'s recipe list, picks
|
||||
// the highest version satisfying `version_spec` (`*` → highest), fetches
|
||||
// the recipe TOML, and returns a PkgsHit. Returns
|
||||
// `ResolutionUnknownPackage` when the package directory or no version
|
||||
// matching the spec exists.
|
||||
auto try_cargoxx_pkgs(const std::string& name, const std::string& version_spec)
|
||||
-> util::Result<PkgsHit>;
|
||||
|
||||
// Same as realize_flake_source but also returns the SRI hash so the
|
||||
// caller can persist it in a lockfile and feed it to `pkgs.fetchgit`
|
||||
// as a fixed-output derivation pin. Used by cargoxx-git deps.
|
||||
auto prefetch_flake_source(const std::string& flake_ref)
|
||||
-> util::Result<PrefetchedSource>;
|
||||
|
||||
// One CMake config-file's IMPORTED targets together with the find_package
|
||||
// expression derived from its filename stem.
|
||||
struct NixCmakeCandidate {
|
||||
|
||||
@@ -69,7 +69,7 @@ TEST_CASE("cmd_build generates files for a no-deps binary project",
|
||||
REQUIRE(std::filesystem::exists(root / "Cargoxx.lock"));
|
||||
|
||||
auto cmake_text = read_file(root / "build" / "CMakeLists.txt");
|
||||
REQUIRE(cmake_text.find("project(hello LANGUAGES CXX)") != std::string::npos);
|
||||
REQUIRE(cmake_text.find("project(hello VERSION 0.1.0 LANGUAGES CXX)") != std::string::npos);
|
||||
REQUIRE(cmake_text.find("add_executable(hello_bin ../src/main.cpp)") !=
|
||||
std::string::npos);
|
||||
|
||||
|
||||
93
tests/cmd_publish_validation.cpp
Normal file
93
tests/cmd_publish_validation.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// Validation gates for `cargoxx publish` that fail BEFORE any network
|
||||
// I/O. The publish flow performs `nix flake prefetch` + tea API calls,
|
||||
// both of which need live infra. Everything tested here happens earlier
|
||||
// — schema checks of Cargoxx.toml + Cargoxx.lock + git state.
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
import cargoxx.cli;
|
||||
import cargoxx.manifest;
|
||||
import cargoxx.lockfile;
|
||||
import cargoxx.util;
|
||||
import std;
|
||||
|
||||
using cargoxx::cli::cmd_publish;
|
||||
using cargoxx::util::ErrorCode;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace manifest = cargoxx::manifest;
|
||||
namespace lockfile = cargoxx::lockfile;
|
||||
|
||||
namespace {
|
||||
|
||||
auto fresh_dir() -> fs::path {
|
||||
auto d = fs::temp_directory_path() /
|
||||
std::format("cargoxx-publish-test-{}", std::random_device{}());
|
||||
fs::create_directories(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
auto write_manifest(const fs::path& dir, const manifest::Manifest& m) {
|
||||
REQUIRE(manifest::write(m, dir / "Cargoxx.toml").has_value());
|
||||
}
|
||||
|
||||
auto write_lock(const fs::path& dir) {
|
||||
lockfile::Lockfile lock{
|
||||
.version = 1,
|
||||
.packages = {lockfile::LockfilePackage{.name = "foo", .version = "0.1.0"}},
|
||||
};
|
||||
REQUIRE(lockfile::write(lock, dir / "Cargoxx.lock").has_value());
|
||||
}
|
||||
|
||||
auto minimal_pkg() -> manifest::Package {
|
||||
return manifest::Package{
|
||||
.name = "foo",
|
||||
.version = "0.1.0",
|
||||
.edition = manifest::Edition::Cpp23,
|
||||
.license = "MIT",
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("publish rejects a manifest missing [package].license",
|
||||
"[cli][publish]") {
|
||||
auto root = fresh_dir();
|
||||
auto pkg = minimal_pkg();
|
||||
pkg.license = std::nullopt;
|
||||
write_manifest(root, manifest::Manifest{pkg, {}, {}});
|
||||
write_lock(root);
|
||||
|
||||
auto r = cmd_publish(root, /*dry_run=*/true);
|
||||
REQUIRE_FALSE(r.has_value());
|
||||
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||
REQUIRE(r.error().message.find("license") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("publish rejects a project without Cargoxx.lock", "[cli][publish]") {
|
||||
auto root = fresh_dir();
|
||||
write_manifest(root, manifest::Manifest{minimal_pkg(), {}, {}});
|
||||
// No lockfile.
|
||||
|
||||
auto r = cmd_publish(root, /*dry_run=*/true);
|
||||
REQUIRE_FALSE(r.has_value());
|
||||
REQUIRE(r.error().code == ErrorCode::ManifestNotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("publish rejects path-form dependencies", "[cli][publish]") {
|
||||
auto root = fresh_dir();
|
||||
auto pkg = minimal_pkg();
|
||||
manifest::Dependency dep{
|
||||
.name = "sibling",
|
||||
.version_spec = "*",
|
||||
.source = manifest::DepSource::CargoxxPath,
|
||||
.path = "../sibling",
|
||||
};
|
||||
write_manifest(root, manifest::Manifest{pkg, {dep}, {}});
|
||||
write_lock(root);
|
||||
|
||||
auto r = cmd_publish(root, /*dry_run=*/true);
|
||||
REQUIRE_FALSE(r.has_value());
|
||||
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||
REQUIRE(r.error().message.find("path dep") != std::string::npos);
|
||||
}
|
||||
@@ -72,10 +72,15 @@ TEST_CASE("cmake_lists for a binary-only project", "[codegen][cmake]") {
|
||||
GenerateInputs in{m, layout, lock, {}, {}, ROOT};
|
||||
|
||||
auto out = cmake_lists(in);
|
||||
REQUIRE(out.find("project(hello LANGUAGES CXX)") != std::string::npos);
|
||||
REQUIRE(out.find("project(hello VERSION 0.1.0 LANGUAGES CXX)") != std::string::npos);
|
||||
REQUIRE(out.find("include(GNUInstallDirs)") != std::string::npos);
|
||||
REQUIRE(out.find("set(CMAKE_CXX_STANDARD 23)") != std::string::npos);
|
||||
REQUIRE(out.find("add_executable(hello_bin ../src/main.cpp)") != std::string::npos);
|
||||
REQUIRE(out.find("set_target_properties(hello_bin PROPERTIES OUTPUT_NAME hello)") !=
|
||||
REQUIRE(out.find("set_target_properties(hello_bin PROPERTIES\n"
|
||||
" OUTPUT_NAME hello\n"
|
||||
" RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/bin\")") !=
|
||||
std::string::npos);
|
||||
REQUIRE(out.find("install(TARGETS hello_bin RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})") !=
|
||||
std::string::npos);
|
||||
REQUIRE(out.find("add_library") == std::string::npos);
|
||||
REQUIRE(out.find("enable_testing") == std::string::npos);
|
||||
@@ -98,6 +103,15 @@ TEST_CASE("cmake_lists for a library-only project", "[codegen][cmake]") {
|
||||
REQUIRE(out.find("FILE_SET CXX_MODULES") != std::string::npos);
|
||||
REQUIRE(out.find("../src/lib.cppm") != std::string::npos);
|
||||
REQUIRE(out.find("add_executable") == std::string::npos);
|
||||
// Library projects emit install rules + Config.cmake + .pc.
|
||||
REQUIRE(out.find("install(TARGETS widget\n EXPORT widgetTargets") !=
|
||||
std::string::npos);
|
||||
REQUIRE(out.find("install(EXPORT widgetTargets") != std::string::npos);
|
||||
REQUIRE(out.find("configure_package_config_file(") != std::string::npos);
|
||||
REQUIRE(out.find("write_basic_package_version_file(") != std::string::npos);
|
||||
REQUIRE(out.find("widget.pc.in") != std::string::npos);
|
||||
REQUIRE(out.find("DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig") !=
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("cmake_lists wires up library + primary binary", "[codegen][cmake]") {
|
||||
@@ -138,6 +152,13 @@ TEST_CASE("cmake_lists emits extra binaries from src/bin/", "[codegen][cmake]")
|
||||
auto out = cmake_lists(in);
|
||||
REQUIRE(out.find("add_executable(app_bin ../src/main.cpp)") != std::string::npos);
|
||||
REQUIRE(out.find("add_executable(tool ../src/bin/tool.cpp)") != std::string::npos);
|
||||
REQUIRE(out.find("set_target_properties(app_bin PROPERTIES\n"
|
||||
" OUTPUT_NAME app\n"
|
||||
" RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/bin\")") !=
|
||||
std::string::npos);
|
||||
REQUIRE(out.find("set_target_properties(tool PROPERTIES\n"
|
||||
" RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/bin\")") !=
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("cmake_lists emits tests with add_test", "[codegen][cmake]") {
|
||||
|
||||
8
tests/e2e/buildCppPackage/src/bin/extra.cpp
Normal file
8
tests/e2e/buildCppPackage/src/bin/extra.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
import std;
|
||||
int main() {
|
||||
nlohmann::json j;
|
||||
j["from"] = "extra";
|
||||
std::println("{}", j["from"].get<std::string>());
|
||||
return 0;
|
||||
}
|
||||
7
tests/e2e/pathDep/Cargoxx.toml
Normal file
7
tests/e2e/pathDep/Cargoxx.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "consumer"
|
||||
version = "0.1.0"
|
||||
edition = "cpp23"
|
||||
|
||||
[dependencies]
|
||||
greeter = { path = "./greeter" }
|
||||
10
tests/e2e/pathDep/flake.nix
Normal file
10
tests/e2e/pathDep/flake.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
description = "e2e cargoxx path-dep smoke";
|
||||
|
||||
inputs.cargoxx.url = "path:../../..";
|
||||
|
||||
outputs = { self, cargoxx }: {
|
||||
packages.x86_64-linux.default =
|
||||
cargoxx.lib.x86_64-linux.buildCppPackage { src = ./.; };
|
||||
};
|
||||
}
|
||||
4
tests/e2e/pathDep/greeter/Cargoxx.toml
Normal file
4
tests/e2e/pathDep/greeter/Cargoxx.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[package]
|
||||
name = "greeter"
|
||||
version = "0.1.0"
|
||||
edition = "cpp23"
|
||||
8
tests/e2e/pathDep/greeter/src/lib.cppm
Normal file
8
tests/e2e/pathDep/greeter/src/lib.cppm
Normal file
@@ -0,0 +1,8 @@
|
||||
export module greeter;
|
||||
import std;
|
||||
|
||||
export namespace greeter {
|
||||
auto hello(std::string_view who) -> std::string {
|
||||
return std::format("Hello from greeter, {}!", who);
|
||||
}
|
||||
} // namespace greeter
|
||||
48
tests/e2e/pathDep/run.sh
Executable file
48
tests/e2e/pathDep/run.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo="$(cd "${here}/../../.." && pwd)"
|
||||
cargoxx_bin="${CARGOXX_BIN:-${repo}/build/debug/cargoxx}"
|
||||
|
||||
if [[ ! -x "${cargoxx_bin}" ]]; then
|
||||
echo "error: cargoxx binary not found at ${cargoxx_bin}" >&2
|
||||
echo "build it first: nix develop --command cmake --build build/debug" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
work="$(mktemp -d -t cargoxx-e2e-pathdep-XXXXXX)"
|
||||
trap 'rm -rf "${work}"' EXIT
|
||||
|
||||
cp -r "${here}/." "${work}/"
|
||||
sed -i "s|path:\\.\\./\\.\\./\\.\\.|path:${repo}|" "${work}/flake.nix"
|
||||
|
||||
cd "${work}"
|
||||
|
||||
echo "=== cargoxx build --no-build in greeter (path dep generates its own lock)"
|
||||
(cd greeter && "${cargoxx_bin}" build --no-build)
|
||||
|
||||
echo "=== cargoxx build --no-build in consumer"
|
||||
"${cargoxx_bin}" build --no-build
|
||||
|
||||
[[ -f Cargoxx.lock ]] || { echo "Cargoxx.lock missing"; exit 1; }
|
||||
grep -q "source_kind = 'cargoxx-path'" Cargoxx.lock || \
|
||||
{ echo "Cargoxx.lock missing source_kind = cargoxx-path"; exit 1; }
|
||||
|
||||
# nix build needs the source tree to be a git tree so 'path:' input copies
|
||||
# Cargoxx.lock into the store. Init a throwaway git here.
|
||||
git init -q
|
||||
git add -A
|
||||
git -c user.email=e2e@cargoxx -c user.name=e2e commit -q -m fixture
|
||||
|
||||
echo "=== nix build .#default"
|
||||
out="$(nix build .#default --no-link --print-out-paths \
|
||||
--extra-experimental-features 'nix-command flakes')"
|
||||
|
||||
[[ -n "${out}" ]] || { echo "nix build produced no output path"; exit 1; }
|
||||
[[ -x "${out}/bin/consumer" ]] || { echo "missing ${out}/bin/consumer"; exit 1; }
|
||||
|
||||
echo "=== execute"
|
||||
"${out}/bin/consumer"
|
||||
|
||||
echo "ok"
|
||||
7
tests/e2e/pathDep/src/main.cpp
Normal file
7
tests/e2e/pathDep/src/main.cpp
Normal file
@@ -0,0 +1,7 @@
|
||||
import std;
|
||||
import greeter;
|
||||
|
||||
int main() {
|
||||
std::println("{}", greeter::hello("world"));
|
||||
return 0;
|
||||
}
|
||||
@@ -131,16 +131,31 @@ TEST_CASE("discover lists src/bin/*.cpp as additional binaries", "[layout]") {
|
||||
REQUIRE(r->binaries[2].name == "pkg");
|
||||
}
|
||||
|
||||
TEST_CASE("discover does not recurse into src/bin/", "[layout]") {
|
||||
TEST_CASE("discover ignores src/bin/<sub>/ subdirs without main.cpp", "[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/main.cpp");
|
||||
p.touch("src/bin/foo.cpp");
|
||||
p.touch("src/bin/sub/nested.cpp"); // should be ignored
|
||||
p.touch("src/bin/sub/nested.cpp"); // no main.cpp at sub/ root, ignored
|
||||
auto r = discover(p.root(), "pkg");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->binaries.size() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("discover lists src/bin/<sub>/main.cpp as a binary named <sub>",
|
||||
"[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/main.cpp");
|
||||
p.touch("src/bin/extra/main.cpp");
|
||||
p.touch("src/bin/extra/helpers.cpp"); // not collected in v1
|
||||
auto r = discover(p.root(), "pkg");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->binaries.size() == 2);
|
||||
REQUIRE(r->binaries[0].name == "extra");
|
||||
REQUIRE(r->binaries[0].entry.filename() == "main.cpp");
|
||||
REQUIRE(r->binaries[0].entry.parent_path().filename() == "extra");
|
||||
REQUIRE(r->binaries[1].name == "pkg");
|
||||
}
|
||||
|
||||
TEST_CASE("discover lists tests/*.cpp", "[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/main.cpp");
|
||||
|
||||
@@ -118,6 +118,57 @@ TEST_CASE("write round-trips lockfile recipe fields", "[lockfile]") {
|
||||
REQUIRE(round_trip(l) == l);
|
||||
}
|
||||
|
||||
TEST_CASE("write round-trips cargoxx-path source fields", "[lockfile]") {
|
||||
Lockfile l{
|
||||
.version = 1,
|
||||
.packages = {
|
||||
LockfilePackage{
|
||||
.name = "mylib",
|
||||
.version = "*",
|
||||
.dependencies = {},
|
||||
.nixpkgs_attr = std::nullopt,
|
||||
.nixpkgs_rev = std::nullopt,
|
||||
.linkdb_source = "cargoxx-path",
|
||||
.find_package = "mylib CONFIG REQUIRED",
|
||||
.targets = {"mylib::mylib"},
|
||||
.pkg_config_module = std::nullopt,
|
||||
.brute_force_libs = {},
|
||||
.brute_force_includes = {},
|
||||
.source_kind = "cargoxx-path",
|
||||
.source_path = "../mylib",
|
||||
},
|
||||
},
|
||||
};
|
||||
REQUIRE(round_trip(l) == l);
|
||||
}
|
||||
|
||||
TEST_CASE("write round-trips cargoxx-git source fields", "[lockfile]") {
|
||||
Lockfile l{
|
||||
.version = 1,
|
||||
.packages = {
|
||||
LockfilePackage{
|
||||
.name = "mylib",
|
||||
.version = "*",
|
||||
.dependencies = {},
|
||||
.nixpkgs_attr = std::nullopt,
|
||||
.nixpkgs_rev = std::nullopt,
|
||||
.linkdb_source = "cargoxx-git",
|
||||
.find_package = "mylib CONFIG REQUIRED",
|
||||
.targets = {"mylib::mylib"},
|
||||
.pkg_config_module = std::nullopt,
|
||||
.brute_force_libs = {},
|
||||
.brute_force_includes = {},
|
||||
.source_kind = "cargoxx-git",
|
||||
.source_path = std::nullopt,
|
||||
.source_git_url = "https://gitea.example/me/mylib",
|
||||
.source_git_commit = "0123456789012345678901234567890123456789",
|
||||
.source_git_sha256 = "sha256-abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123=",
|
||||
},
|
||||
},
|
||||
};
|
||||
REQUIRE(round_trip(l) == l);
|
||||
}
|
||||
|
||||
TEST_CASE("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") {
|
||||
Lockfile l{
|
||||
.version = 1,
|
||||
|
||||
@@ -181,16 +181,21 @@ optimize = "max"
|
||||
REQUIRE(r.error().code == ErrorCode::ManifestUnknownField);
|
||||
}
|
||||
|
||||
TEST_CASE("parse accepts reserved [package] fields", "[manifest][parse]") {
|
||||
TEST_CASE("parse stores description / repository / homepage on Package",
|
||||
"[manifest][parse]") {
|
||||
auto p = write_manifest(R"(
|
||||
[package]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
description = "demo"
|
||||
repository = "https://example.com/foo"
|
||||
homepage = "https://example.com"
|
||||
)");
|
||||
auto r = parse(p);
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->package.description == "demo");
|
||||
REQUIRE(r->package.repository == "https://example.com/foo");
|
||||
REQUIRE(r->package.homepage == "https://example.com");
|
||||
}
|
||||
|
||||
TEST_CASE("parse accepts reserved top-level tables", "[manifest][parse]") {
|
||||
@@ -297,3 +302,79 @@ version = "0.1.0"
|
||||
REQUIRE_FALSE(r.has_value());
|
||||
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||
}
|
||||
|
||||
TEST_CASE("parse recognizes { path = \"...\" } as a cargoxx path dep",
|
||||
"[manifest][parse]") {
|
||||
auto p = write_manifest(R"(
|
||||
[package]
|
||||
name = "consumer"
|
||||
version = "0.1.0"
|
||||
edition = "cpp23"
|
||||
|
||||
[dependencies]
|
||||
mylib = { path = "../mylib" }
|
||||
)");
|
||||
auto r = parse(p);
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->dependencies.size() == 1);
|
||||
const auto& dep = r->dependencies[0];
|
||||
REQUIRE(dep.name == "mylib");
|
||||
REQUIRE(dep.source == cargoxx::manifest::DepSource::CargoxxPath);
|
||||
REQUIRE(dep.path.has_value());
|
||||
REQUIRE(*dep.path == "../mylib");
|
||||
// Version defaults to "*" when only `path` is given.
|
||||
REQUIRE(dep.version_spec == "*");
|
||||
}
|
||||
|
||||
TEST_CASE("parse rejects dep table without version or path",
|
||||
"[manifest][parse]") {
|
||||
auto p = write_manifest(R"(
|
||||
[package]
|
||||
name = "consumer"
|
||||
version = "0.1.0"
|
||||
edition = "cpp23"
|
||||
|
||||
[dependencies]
|
||||
mylib = { components = ["a"] }
|
||||
)");
|
||||
auto r = parse(p);
|
||||
REQUIRE_FALSE(r.has_value());
|
||||
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||
}
|
||||
|
||||
TEST_CASE("parse recognizes { git = \"...\", rev = \"...\" } as a cargoxx git dep",
|
||||
"[manifest][parse]") {
|
||||
auto p = write_manifest(R"(
|
||||
[package]
|
||||
name = "consumer"
|
||||
version = "0.1.0"
|
||||
edition = "cpp23"
|
||||
|
||||
[dependencies]
|
||||
mylib = { git = "https://gitea.example/me/mylib", rev = "0123456789012345678901234567890123456789" }
|
||||
)");
|
||||
auto r = parse(p);
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->dependencies.size() == 1);
|
||||
const auto& dep = r->dependencies[0];
|
||||
REQUIRE(dep.name == "mylib");
|
||||
REQUIRE(dep.source == cargoxx::manifest::DepSource::CargoxxGit);
|
||||
REQUIRE(dep.git_url == "https://gitea.example/me/mylib");
|
||||
REQUIRE(dep.git_rev == "0123456789012345678901234567890123456789");
|
||||
REQUIRE(dep.version_spec == "*");
|
||||
}
|
||||
|
||||
TEST_CASE("parse rejects git dep without rev", "[manifest][parse]") {
|
||||
auto p = write_manifest(R"(
|
||||
[package]
|
||||
name = "consumer"
|
||||
version = "0.1.0"
|
||||
edition = "cpp23"
|
||||
|
||||
[dependencies]
|
||||
mylib = { git = "https://gitea.example/me/mylib" }
|
||||
)");
|
||||
auto r = parse(p);
|
||||
REQUIRE_FALSE(r.has_value());
|
||||
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||
}
|
||||
|
||||
@@ -138,3 +138,44 @@ TEST_CASE("write fails when the target directory does not exist",
|
||||
auto r = write(m, "/nonexistent/dir/Cargoxx.toml");
|
||||
REQUIRE_FALSE(r.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("write round-trips a path-form dependency", "[manifest][write]") {
|
||||
Manifest m{
|
||||
pkg("consumer", "0.1.0"),
|
||||
{Dependency{
|
||||
.name = "mylib",
|
||||
.version_spec = "*",
|
||||
.components = {},
|
||||
.source = cargoxx::manifest::DepSource::CargoxxPath,
|
||||
.path = "../mylib",
|
||||
}},
|
||||
{},
|
||||
};
|
||||
REQUIRE(round_trip(m) == m);
|
||||
}
|
||||
|
||||
TEST_CASE("write round-trips a git-form dependency", "[manifest][write]") {
|
||||
Manifest m{
|
||||
pkg("consumer", "0.1.0"),
|
||||
{Dependency{
|
||||
.name = "mylib",
|
||||
.version_spec = "*",
|
||||
.components = {},
|
||||
.source = cargoxx::manifest::DepSource::CargoxxGit,
|
||||
.git_url = "https://gitea.example/me/mylib",
|
||||
.git_rev = "0123456789012345678901234567890123456789",
|
||||
}},
|
||||
{},
|
||||
};
|
||||
REQUIRE(round_trip(m) == m);
|
||||
}
|
||||
|
||||
TEST_CASE("write round-trips description/repository/homepage",
|
||||
"[manifest][write]") {
|
||||
auto p = pkg("foo", "0.1.0");
|
||||
p.description = "demo library";
|
||||
p.repository = "https://gitea.example/me/foo";
|
||||
p.homepage = "https://example.com/foo";
|
||||
Manifest m{p, {}, {}};
|
||||
REQUIRE(round_trip(m) == m);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user