[M8] reusable libraries: install layout + cargoxx-path deps
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,6 +3,13 @@
|
|||||||
!/build/CMakeLists.txt
|
!/build/CMakeLists.txt
|
||||||
/result
|
/result
|
||||||
/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
|
# CMake
|
||||||
CMakeCache.txt
|
CMakeCache.txt
|
||||||
|
|||||||
63
CHANGELOG.md
63
CHANGELOG.md
@@ -435,3 +435,66 @@ All notable changes to cargoxx will be documented in this file.
|
|||||||
Added `inputs.bundlers.url = "github:NixOS/bundlers"` (with
|
Added `inputs.bundlers.url = "github:NixOS/bundlers"` (with
|
||||||
`inputs.nixpkgs.follows = "nixpkgs"`) to keep the closure aligned
|
`inputs.nixpkgs.follows = "nixpkgs"`) to keep the closure aligned
|
||||||
with the project's pinned nixpkgs.
|
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.
|
||||||
|
|||||||
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.
|
||||||
29
flake.nix
29
flake.nix
@@ -60,6 +60,7 @@
|
|||||||
lock = builtins.fromTOML (builtins.readFile (src + "/Cargoxx.lock"));
|
lock = builtins.fromTOML (builtins.readFile (src + "/Cargoxx.lock"));
|
||||||
isDep = p: p ? linkdb_source;
|
isDep = p: p ? linkdb_source;
|
||||||
isRoot = p: !(isDep p);
|
isRoot = p: !(isDep p);
|
||||||
|
isCargoxxSource = p: (p.source_kind or "") != "";
|
||||||
root = builtins.head (builtins.filter isRoot lock.package);
|
root = builtins.head (builtins.filter isRoot lock.package);
|
||||||
depPkgs = builtins.filter isDep lock.package;
|
depPkgs = builtins.filter isDep lock.package;
|
||||||
pname = if name != null then name else root.name;
|
pname = if name != null then name else root.name;
|
||||||
@@ -68,11 +69,17 @@
|
|||||||
(builtins.getFlake "github:NixOS/nixpkgs/${rev}")
|
(builtins.getFlake "github:NixOS/nixpkgs/${rev}")
|
||||||
.legacyPackages.${system};
|
.legacyPackages.${system};
|
||||||
|
|
||||||
|
# cargoxx-path deps recurse into buildCppPackage on the sibling
|
||||||
|
# source tree; the result joins buildInputs so the consumer's
|
||||||
|
# find_package(<dep> CONFIG REQUIRED) resolves via CMAKE_PREFIX_PATH.
|
||||||
evalDep = p:
|
evalDep = p:
|
||||||
let rev = if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
if (p.source_kind or "") == "cargoxx-path" then
|
||||||
then p.nixpkgs_rev
|
buildCppPackage { src = src + ("/" + p.source_path); }
|
||||||
else lock.nixpkgs_rev;
|
else
|
||||||
in (pkgsAt rev).${p.nixpkgs_attr};
|
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;
|
depInputs = map evalDep depPkgs;
|
||||||
|
|
||||||
@@ -87,12 +94,18 @@
|
|||||||
mkDepTomlEntry = p:
|
mkDepTomlEntry = p:
|
||||||
let
|
let
|
||||||
derivation = evalDep p;
|
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.
|
||||||
|
isPath = (p.source_kind or "") == "cargoxx-path";
|
||||||
|
attr = if isPath then "" else p.nixpkgs_attr;
|
||||||
|
rev = if isPath then ""
|
||||||
|
else if (p ? nixpkgs_rev) && (p.nixpkgs_rev != "")
|
||||||
then p.nixpkgs_rev else lock.nixpkgs_rev;
|
then p.nixpkgs_rev else lock.nixpkgs_rev;
|
||||||
in ''
|
in ''
|
||||||
[[dep]]
|
[[dep]]
|
||||||
name = "${p.name}"
|
name = "${p.name}"
|
||||||
nixpkgs_attr = "${p.nixpkgs_attr}"
|
nixpkgs_attr = "${attr}"
|
||||||
nixpkgs_rev = "${rev}"
|
nixpkgs_rev = "${rev}"
|
||||||
store_path = "${derivation}"
|
store_path = "${derivation}"
|
||||||
'';
|
'';
|
||||||
@@ -121,9 +134,7 @@
|
|||||||
cargoxx build --release --offline --vendor ${vendorToml}
|
cargoxx build --release --offline --vendor ${vendorToml}
|
||||||
'';
|
'';
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
mkdir -p $out/bin
|
cmake --install build/release --prefix $out
|
||||||
cp build/release/${pname} $out/bin/ 2>/dev/null || \
|
|
||||||
cp build/release/${pname}_bin $out/bin/${pname}
|
|
||||||
'';
|
'';
|
||||||
hardeningDisable = [ "all" ];
|
hardeningDisable = [ "all" ];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ auto merge_lockfile(const manifest::Manifest& m,
|
|||||||
attr = *p->nixpkgs_attr;
|
attr = *p->nixpkgs_attr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
std::optional<std::string> source_kind;
|
||||||
|
std::optional<std::string> source_path;
|
||||||
|
if (dep.source == manifest::DepSource::CargoxxPath) {
|
||||||
|
source_kind = "cargoxx-path";
|
||||||
|
source_path = dep.path;
|
||||||
|
}
|
||||||
lock.packages.push_back(lockfile::LockfilePackage{
|
lock.packages.push_back(lockfile::LockfilePackage{
|
||||||
.name = dep.name,
|
.name = dep.name,
|
||||||
.version = dep.version_spec,
|
.version = dep.version_spec,
|
||||||
@@ -132,6 +138,8 @@ auto merge_lockfile(const manifest::Manifest& m,
|
|||||||
.pkg_config_module = rec.pkg_config_module,
|
.pkg_config_module = rec.pkg_config_module,
|
||||||
.brute_force_libs = rec.brute_force_libs,
|
.brute_force_libs = rec.brute_force_libs,
|
||||||
.brute_force_includes = rec.brute_force_includes,
|
.brute_force_includes = rec.brute_force_includes,
|
||||||
|
.source_kind = std::move(source_kind),
|
||||||
|
.source_path = std::move(source_path),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
for (std::size_t i = 0; i < m.dependencies.size(); ++i) {
|
for (std::size_t i = 0; i < m.dependencies.size(); ++i) {
|
||||||
@@ -249,11 +257,50 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
|||||||
return std::nullopt;
|
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 = {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
auto resolve_list = [&](const std::vector<manifest::Dependency>& deps)
|
auto resolve_list = [&](const std::vector<manifest::Dependency>& deps)
|
||||||
-> util::Result<std::vector<linkdb::Recipe>> {
|
-> util::Result<std::vector<linkdb::Recipe>> {
|
||||||
std::vector<linkdb::Recipe> out;
|
std::vector<linkdb::Recipe> out;
|
||||||
out.reserve(deps.size());
|
out.reserve(deps.size());
|
||||||
for (const auto& dep : deps) {
|
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 (auto cached = recipe_from_lock(dep.name, dep.version_spec); cached) {
|
if (auto cached = recipe_from_lock(dep.name, dep.version_spec); cached) {
|
||||||
out.push_back(std::move(*cached));
|
out.push_back(std::move(*cached));
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ auto cmd_run(const fs::path& project_root, bool release,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::string profile = release ? "release" : "debug";
|
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,
|
auto r = exec::run(bin_path.string(), args,
|
||||||
exec::ExecOptions{
|
exec::ExecOptions{
|
||||||
|
|||||||
@@ -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_EXPERIMENTAL_CXX_IMPORT_STD \"d0edc3af-4c50-42ea-a356-e2862fe7a444\")\n"
|
||||||
"set(CMAKE_CXX_MODULE_STD ON)\n"
|
"set(CMAKE_CXX_MODULE_STD ON)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"project({} LANGUAGES CXX)\n"
|
"project({} VERSION {} LANGUAGES CXX)\n"
|
||||||
|
"\n"
|
||||||
|
"include(GNUInstallDirs)\n"
|
||||||
|
"include(CMakePackageConfigHelpers)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"# Generated by cargoxx — do not edit.\n"
|
"# Generated by cargoxx — do not edit.\n"
|
||||||
"# Source of truth: ../Cargoxx.toml\n"
|
"# Source of truth: ../Cargoxx.toml\n"
|
||||||
@@ -77,7 +80,7 @@ auto emit_header(const manifest::Manifest& m) -> std::string {
|
|||||||
"\n"
|
"\n"
|
||||||
"add_compile_options(-Wall -Wextra -Wpedantic -Wconversion "
|
"add_compile_options(-Wall -Wextra -Wpedantic -Wconversion "
|
||||||
"-Wno-missing-field-initializers)\n",
|
"-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,
|
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);
|
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,
|
auto emit_library(const layout::Target& lib, const std::string& package_name,
|
||||||
const std::vector<linkdb::Recipe>& recipes,
|
const std::vector<linkdb::Recipe>& recipes,
|
||||||
const std::vector<std::string>& include_dirs,
|
const std::vector<std::string>& include_dirs,
|
||||||
|
manifest::Edition edition,
|
||||||
const fs::path& project_root) -> std::string {
|
const fs::path& project_root) -> std::string {
|
||||||
std::string out = "\n# ----- library target -----\n";
|
std::string out = "\n# ----- library target -----\n";
|
||||||
out += std::format("add_library({} STATIC)\n", package_name);
|
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";
|
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()) {
|
if (!include_dirs.empty()) {
|
||||||
out += std::format("target_include_directories({} SYSTEM PRIVATE", package_name);
|
out += std::format("target_include_directories({} SYSTEM PRIVATE", package_name);
|
||||||
for (const auto& d : include_dirs) {
|
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,
|
out += link_block(package_name, "PUBLIC", false, package_name,
|
||||||
collect_dep_targets(recipes));
|
collect_dep_targets(recipes));
|
||||||
|
|
||||||
|
out += emit_library_install_rules(package_name);
|
||||||
return out;
|
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";
|
std::string out = "\n# ----- binary target -----\n";
|
||||||
out += std::format("add_executable({}_bin {})\n", package_name,
|
out += std::format("add_executable({}_bin {})\n", package_name,
|
||||||
rel_to_build(bin.entry, project_root));
|
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);
|
package_name, package_name);
|
||||||
out += link_block(std::format("{}_bin", package_name), "PRIVATE", has_lib, package_name,
|
out += link_block(std::format("{}_bin", package_name), "PRIVATE", has_lib, package_name,
|
||||||
collect_dep_targets(recipes));
|
collect_dep_targets(recipes));
|
||||||
|
out += std::format("install(TARGETS {}_bin RUNTIME DESTINATION ${{CMAKE_INSTALL_BINDIR}})\n",
|
||||||
|
package_name);
|
||||||
return out;
|
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);
|
std::string out = std::format("\n# ----- binary target: {} -----\n", bin.name);
|
||||||
out += std::format("add_executable({} {})\n", bin.name,
|
out += std::format("add_executable({} {})\n", bin.name,
|
||||||
rel_to_build(bin.entry, project_root));
|
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,
|
out += link_block(bin.name, "PRIVATE", has_lib, package_name,
|
||||||
collect_dep_targets(recipes));
|
collect_dep_targets(recipes));
|
||||||
|
out += std::format("install(TARGETS {} RUNTIME DESTINATION ${{CMAKE_INSTALL_BINDIR}})\n",
|
||||||
|
bin.name);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +410,8 @@ auto cmake_lists(const GenerateInputs& in) -> std::string {
|
|||||||
|
|
||||||
if (in.layout.library) {
|
if (in.layout.library) {
|
||||||
out += emit_library(*in.layout.library, pkg_name, in.recipes,
|
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);
|
const auto* primary = find_primary_bin(in.layout);
|
||||||
|
|||||||
@@ -104,6 +104,31 @@ auto discover(const fs::path& project_root, const std::string& package_name)
|
|||||||
.module_units = {},
|
.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);
|
std::ranges::sort(out.binaries, by_name);
|
||||||
|
|
||||||
for (const auto& f : top_level_cpp(tests_dir)) {
|
for (const auto& f : top_level_cpp(tests_dir)) {
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ auto parse_package(const toml::table& tbl, const std::filesystem::path& path)
|
|||||||
}
|
}
|
||||||
pkg.brute_force_includes = std::move(*r);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
return pkg;
|
return pkg;
|
||||||
}
|
}
|
||||||
@@ -215,6 +221,12 @@ auto write(const Lockfile& lock, const std::filesystem::path& path) -> util::Res
|
|||||||
}
|
}
|
||||||
tbl.insert_or_assign("brute_force_includes", std::move(arr));
|
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);
|
||||||
|
}
|
||||||
packages.push_back(std::move(tbl));
|
packages.push_back(std::move(tbl));
|
||||||
}
|
}
|
||||||
root.insert_or_assign("package", std::move(packages));
|
root.insert_or_assign("package", std::move(packages));
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ struct LockfilePackage {
|
|||||||
std::optional<std::string> pkg_config_module;
|
std::optional<std::string> pkg_config_module;
|
||||||
std::vector<std::string> brute_force_libs;
|
std::vector<std::string> brute_force_libs;
|
||||||
std::vector<std::string> brute_force_includes;
|
std::vector<std::string> brute_force_includes;
|
||||||
|
// For cargoxx-source deps (not nixpkgs/linkdb-resolved). v1 supports
|
||||||
|
// "cargoxx-path"; "cargoxx-git" / "cargoxx-registry" land in 1c/1d.
|
||||||
|
std::optional<std::string> source_kind;
|
||||||
|
std::optional<std::string> source_path;
|
||||||
|
|
||||||
bool operator==(const LockfilePackage&) const = default;
|
bool operator==(const LockfilePackage&) const = default;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ import cargoxx.util;
|
|||||||
|
|
||||||
export namespace cargoxx::manifest {
|
export namespace cargoxx::manifest {
|
||||||
|
|
||||||
|
enum class DepSource {
|
||||||
|
Auto, // string form or { version = ... } only → existing resolver chain
|
||||||
|
CargoxxPath, // { path = "../foo" } → recursive cargoxx build
|
||||||
|
};
|
||||||
|
|
||||||
struct Dependency {
|
struct Dependency {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string version_spec;
|
std::string version_spec;
|
||||||
std::vector<std::string> components;
|
std::vector<std::string> components;
|
||||||
|
DepSource source = DepSource::Auto;
|
||||||
|
std::optional<std::string> path; // when source == CargoxxPath
|
||||||
|
|
||||||
bool operator==(const Dependency&) const = default;
|
bool operator==(const Dependency&) const = default;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -153,12 +153,27 @@ auto parse_dependency(std::string name, const toml::node& value,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (const auto* tbl = value.as_table()) {
|
if (const auto* tbl = value.as_table()) {
|
||||||
|
// Path form takes precedence: { path = "../foo" } → cargoxx-source dep.
|
||||||
|
// Version is optional in the path form (defaulting to "*"); the dep's
|
||||||
|
// own Cargoxx.toml supplies the real version at resolve time.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
if (auto v = (*tbl)["version"].value<std::string>()) {
|
if (auto v = (*tbl)["version"].value<std::string>()) {
|
||||||
dep.version_spec = *v;
|
dep.version_spec = *v;
|
||||||
} else {
|
} else {
|
||||||
return std::unexpected(err(
|
return std::unexpected(err(
|
||||||
ErrorCode::ManifestInvalidField,
|
ErrorCode::ManifestInvalidField,
|
||||||
std::format("dependency '{}' table must have a 'version' string", dep.name),
|
std::format("dependency '{}' table must have a 'version' or 'path' string",
|
||||||
|
dep.name),
|
||||||
path, source_pos(value)));
|
path, source_pos(value)));
|
||||||
}
|
}
|
||||||
if (const auto* comps = (*tbl)["components"].as_array()) {
|
if (const auto* comps = (*tbl)["components"].as_array()) {
|
||||||
|
|||||||
@@ -49,7 +49,12 @@ auto build_table(const Manifest& m) -> toml::table {
|
|||||||
auto deps_to_table = [](const std::vector<Dependency>& deps) {
|
auto deps_to_table = [](const std::vector<Dependency>& deps) {
|
||||||
toml::table out;
|
toml::table out;
|
||||||
for (const auto& dep : deps) {
|
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.components.empty()) {
|
||||||
out.insert_or_assign(dep.name, dep.version_spec);
|
out.insert_or_assign(dep.name, dep.version_spec);
|
||||||
} else {
|
} else {
|
||||||
toml::table dep_tbl;
|
toml::table dep_tbl;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ TEST_CASE("cmd_build generates files for a no-deps binary project",
|
|||||||
REQUIRE(std::filesystem::exists(root / "Cargoxx.lock"));
|
REQUIRE(std::filesystem::exists(root / "Cargoxx.lock"));
|
||||||
|
|
||||||
auto cmake_text = read_file(root / "build" / "CMakeLists.txt");
|
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)") !=
|
REQUIRE(cmake_text.find("add_executable(hello_bin ../src/main.cpp)") !=
|
||||||
std::string::npos);
|
std::string::npos);
|
||||||
|
|
||||||
|
|||||||
@@ -72,10 +72,15 @@ TEST_CASE("cmake_lists for a binary-only project", "[codegen][cmake]") {
|
|||||||
GenerateInputs in{m, layout, lock, {}, {}, ROOT};
|
GenerateInputs in{m, layout, lock, {}, {}, ROOT};
|
||||||
|
|
||||||
auto out = cmake_lists(in);
|
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("set(CMAKE_CXX_STANDARD 23)") != std::string::npos);
|
||||||
REQUIRE(out.find("add_executable(hello_bin ../src/main.cpp)") != 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);
|
std::string::npos);
|
||||||
REQUIRE(out.find("add_library") == std::string::npos);
|
REQUIRE(out.find("add_library") == std::string::npos);
|
||||||
REQUIRE(out.find("enable_testing") == 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("FILE_SET CXX_MODULES") != std::string::npos);
|
||||||
REQUIRE(out.find("../src/lib.cppm") != std::string::npos);
|
REQUIRE(out.find("../src/lib.cppm") != std::string::npos);
|
||||||
REQUIRE(out.find("add_executable") == 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]") {
|
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);
|
auto out = cmake_lists(in);
|
||||||
REQUIRE(out.find("add_executable(app_bin ../src/main.cpp)") != std::string::npos);
|
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("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]") {
|
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");
|
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;
|
TempProject p;
|
||||||
p.touch("src/main.cpp");
|
p.touch("src/main.cpp");
|
||||||
p.touch("src/bin/foo.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");
|
auto r = discover(p.root(), "pkg");
|
||||||
REQUIRE(r.has_value());
|
REQUIRE(r.has_value());
|
||||||
REQUIRE(r->binaries.size() == 2);
|
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]") {
|
TEST_CASE("discover lists tests/*.cpp", "[layout]") {
|
||||||
TempProject p;
|
TempProject p;
|
||||||
p.touch("src/main.cpp");
|
p.touch("src/main.cpp");
|
||||||
|
|||||||
@@ -118,6 +118,30 @@ TEST_CASE("write round-trips lockfile recipe fields", "[lockfile]") {
|
|||||||
REQUIRE(round_trip(l) == l);
|
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("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") {
|
TEST_CASE("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") {
|
||||||
Lockfile l{
|
Lockfile l{
|
||||||
.version = 1,
|
.version = 1,
|
||||||
|
|||||||
@@ -297,3 +297,42 @@ version = "0.1.0"
|
|||||||
REQUIRE_FALSE(r.has_value());
|
REQUIRE_FALSE(r.has_value());
|
||||||
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user