Compare commits
10 Commits
a757149f99
...
65a749f088
| Author | SHA1 | Date | |
|---|---|---|---|
| 65a749f088 | |||
| 94e658fdf1 | |||
| 01b3c28d6c | |||
| 8bbfcf7657 | |||
| 8b396bcd0f | |||
| c46f3aa1f0 | |||
| 73aebf183e | |||
| 9b6014b82d | |||
| 6c4933f282 | |||
| 4f9b6f1827 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
# Build outputs
|
# Build outputs
|
||||||
/build/
|
/build/*
|
||||||
|
!/build/CMakeLists.txt
|
||||||
/result
|
/result
|
||||||
/result-*
|
/result-*
|
||||||
|
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.30)
|
|
||||||
|
|
||||||
# Opt into experimental C++ modules dyndep + `import std;` support.
|
|
||||||
# Required until CMake declares these stable; see CMake docs for the current UUID.
|
|
||||||
set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
|
|
||||||
set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444")
|
|
||||||
set(CMAKE_CXX_MODULE_STD ON)
|
|
||||||
|
|
||||||
project(cargoxx LANGUAGES CXX VERSION 0.1.0)
|
|
||||||
|
|
||||||
# Phase 0: hand-written CMakeLists.txt. Replaced by generated build/CMakeLists.txt
|
|
||||||
# at milestone M3 once cargoxx can build itself. See TECH_SPEC.md §15.
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 23)
|
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
||||||
set(CMAKE_CXX_EXTENSIONS ON)
|
|
||||||
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
|
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
|
||||||
|
|
||||||
if(NOT CMAKE_BUILD_TYPE)
|
|
||||||
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# TECH_SPEC.md §17: -Wall -Wextra -Wpedantic -Wconversion, -Werror in CI.
|
|
||||||
add_compile_options(-Wall -Wextra -Wpedantic -Wconversion)
|
|
||||||
|
|
||||||
option(CARGOXX_WERROR "Treat warnings as errors" OFF)
|
|
||||||
if(CARGOXX_WERROR)
|
|
||||||
add_compile_options(-Werror)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
find_package(SQLite3 REQUIRED)
|
|
||||||
find_package(reproc REQUIRED)
|
|
||||||
|
|
||||||
# ----- cargoxx library: module units + implementation units -----
|
|
||||||
add_library(cargoxx STATIC)
|
|
||||||
target_include_directories(cargoxx SYSTEM PRIVATE third_party)
|
|
||||||
target_sources(cargoxx
|
|
||||||
PRIVATE
|
|
||||||
src/util/error.cpp
|
|
||||||
src/util/semver.cpp
|
|
||||||
src/manifest/parser.cpp
|
|
||||||
src/manifest/writer.cpp
|
|
||||||
src/layout/layout.cpp
|
|
||||||
src/lockfile/lockfile.cpp
|
|
||||||
src/linkdb/database.cpp
|
|
||||||
src/linkdb/overlay.cpp
|
|
||||||
src/codegen/flake.cpp
|
|
||||||
src/codegen/cmake.cpp
|
|
||||||
src/exec/subprocess.cpp
|
|
||||||
src/resolver/nixpkgs_probe.cpp
|
|
||||||
src/resolver/nix_cmake_scan.cpp
|
|
||||||
src/resolver/conan_probe.cpp
|
|
||||||
src/resolver/vcpkg_probe.cpp
|
|
||||||
src/resolver/verify_link.cpp
|
|
||||||
src/resolver/discover.cpp
|
|
||||||
src/resolver/search_devbox.cpp
|
|
||||||
src/resolver/nixpkgs_git.cpp
|
|
||||||
src/resolver/version_resolve.cpp
|
|
||||||
src/cli/cmd_new.cpp
|
|
||||||
src/cli/cmd_build.cpp
|
|
||||||
src/cli/cmd_run.cpp
|
|
||||||
src/cli/cmd_test.cpp
|
|
||||||
src/cli/cmd_clean.cpp
|
|
||||||
src/cli/cmd_add.cpp
|
|
||||||
src/cli/cmd_remove.cpp
|
|
||||||
src/cli/run.cpp
|
|
||||||
PUBLIC
|
|
||||||
FILE_SET CXX_MODULES FILES
|
|
||||||
src/lib.cppm
|
|
||||||
src/util/util.cppm
|
|
||||||
src/exec/exec.cppm
|
|
||||||
src/manifest/manifest.cppm
|
|
||||||
src/lockfile/lockfile.cppm
|
|
||||||
src/layout/layout.cppm
|
|
||||||
src/linkdb/linkdb.cppm
|
|
||||||
src/resolver/resolver.cppm
|
|
||||||
src/codegen/codegen.cppm
|
|
||||||
src/cli/cli.cppm
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(cargoxx PRIVATE SQLite::SQLite3 reproc)
|
|
||||||
|
|
||||||
# ----- cargoxx binary -----
|
|
||||||
add_executable(cargoxx_bin src/main.cpp)
|
|
||||||
set_target_properties(cargoxx_bin PROPERTIES OUTPUT_NAME cargoxx)
|
|
||||||
target_link_libraries(cargoxx_bin PRIVATE cargoxx)
|
|
||||||
|
|
||||||
# ----- tests -----
|
|
||||||
option(CARGOXX_BUILD_TESTS "Build cargoxx tests" ON)
|
|
||||||
if(CARGOXX_BUILD_TESTS)
|
|
||||||
enable_testing()
|
|
||||||
add_subdirectory(tests)
|
|
||||||
endif()
|
|
||||||
24
Cargoxx.lock
Normal file
24
Cargoxx.lock
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
version = 1
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
dependencies = [ 'reproc *', 'sqlite *', 'catch2_3 *' ]
|
||||||
|
name = 'cargoxx'
|
||||||
|
version = '0.1.0'
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
linkdb_source = 'nix-probe'
|
||||||
|
name = 'reproc'
|
||||||
|
nixpkgs_attr = 'reproc'
|
||||||
|
version = '*'
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
linkdb_source = 'cmake-findmodule'
|
||||||
|
name = 'sqlite'
|
||||||
|
nixpkgs_attr = 'sqlite'
|
||||||
|
version = '*'
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
linkdb_source = 'nix-probe'
|
||||||
|
name = 'catch2_3'
|
||||||
|
nixpkgs_attr = 'catch2_3'
|
||||||
|
version = '*'
|
||||||
@@ -5,11 +5,12 @@ edition = "cpp23"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sqlite3 = "*"
|
sqlite = "*"
|
||||||
reproc = "*"
|
reproc = "*"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
catch2 = "*"
|
catch2_3 = "*"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
warnings_as_errors = false
|
warnings_as_errors = false
|
||||||
|
include_dirs = ["third_party"]
|
||||||
|
|||||||
17
TECH_SPEC.md
17
TECH_SPEC.md
@@ -619,18 +619,17 @@ User-facing errors are formatted via `util::format(Error)` and printed to stderr
|
|||||||
|
|
||||||
## 15. Bootstrap and self-hosting
|
## 15. Bootstrap and self-hosting
|
||||||
|
|
||||||
Three phases.
|
**Phase 0 (historical) — hand-written `CMakeLists.txt` and `flake.nix` at the repo root.**
|
||||||
|
|
||||||
**Phase 0 — hand-written CMake (commits before milestone M3).**
|
**Phase 2 (current, since M6) — fully self-hosted.**
|
||||||
`CMakeLists.txt` and `flake.nix` at the repo root are written by humans. `cargoxx` builds `cargoxx`.
|
`Cargoxx.toml` describes cargoxx's own deps with nixpkgs names: `sqlite`, `reproc`, `catch2_3`. `cargoxx build` runs the auto-resolver chain (nixpkgs probe → realize → nix_cmake_scan → pc_scan), confirms each recipe via verify_link, and generates `build/CMakeLists.txt` and the root `flake.nix`. Both files are committed (tracked) so the build is reproducible without first building cargoxx, and `[build].include_dirs = ["third_party"]` keeps the vendored headers on the include path.
|
||||||
|
|
||||||
**Phase 1 — generated CMake, hand-written flake.**
|
Bootstrap path:
|
||||||
At milestone M3 (codegen complete), commit a populated `Cargoxx.toml`. Generate `build/CMakeLists.txt` from it. Delete the root `CMakeLists.txt`. The root `flake.nix` stays hand-written because cargoxx doesn't know about its own host-language deps yet.
|
```
|
||||||
|
pre-built cargoxx → cargoxx build → next cargoxx
|
||||||
|
```
|
||||||
|
|
||||||
**Phase 2 — fully self-hosted.**
|
A clean clone with an empty `~/.cache/cargoxx/linkdb.sqlite` auto-resolves all three deps on first `cargoxx build` (sqlite goes through pkg-config because nixpkgs ships no SQLite3Config.cmake; reproc/catch2_3 go through nix_cmake_scan). For continuity, a known-good cargoxx binary is shipped as a release artifact; from-scratch bootstrap is not in v0.1 scope.
|
||||||
At milestone M5, vendor all third-party headers into `third_party/` and have cargoxx generate the flake too. The bootstrap path becomes: pre-built cargoxx binary → run `cargoxx build` → produce next cargoxx.
|
|
||||||
|
|
||||||
For continuity, ship a known-good cargoxx binary as a release artifact. Anyone bootstrapping from source clones the repo, downloads the latest release binary, and runs `./bootstrap-cargoxx build`. If we ever want to bootstrap from absolute zero, `scripts/bootstrap-build.sh` does it with a hand-written CMake invocation.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
408
build/CMakeLists.txt
Normal file
408
build/CMakeLists.txt
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.30)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Generated by cargoxx — do not edit.
|
||||||
|
# Source of truth: ../Cargoxx.toml
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS ON)
|
||||||
|
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
add_compile_options(-Wall -Wextra -Wpedantic -Wconversion)
|
||||||
|
|
||||||
|
# ----- dependencies -----
|
||||||
|
find_package(reproc CONFIG REQUIRED)
|
||||||
|
find_package(SQLite3 REQUIRED)
|
||||||
|
find_package(Catch2 CONFIG REQUIRED)
|
||||||
|
|
||||||
|
# ----- library target -----
|
||||||
|
add_library(cargoxx STATIC)
|
||||||
|
target_sources(cargoxx
|
||||||
|
PUBLIC
|
||||||
|
FILE_SET CXX_MODULES BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. FILES
|
||||||
|
../src/cli/cli.cppm
|
||||||
|
../src/codegen/codegen.cppm
|
||||||
|
../src/exec/exec.cppm
|
||||||
|
../src/layout/layout.cppm
|
||||||
|
../src/lib.cppm
|
||||||
|
../src/linkdb/linkdb.cppm
|
||||||
|
../src/lockfile/lockfile.cppm
|
||||||
|
../src/manifest/manifest.cppm
|
||||||
|
../src/resolver/resolver.cppm
|
||||||
|
../src/util/util.cppm
|
||||||
|
PRIVATE
|
||||||
|
../src/cli/cmd_add.cpp
|
||||||
|
../src/cli/cmd_build.cpp
|
||||||
|
../src/cli/cmd_clean.cpp
|
||||||
|
../src/cli/cmd_linkdb_add.cpp
|
||||||
|
../src/cli/cmd_new.cpp
|
||||||
|
../src/cli/cmd_remove.cpp
|
||||||
|
../src/cli/cmd_run.cpp
|
||||||
|
../src/cli/cmd_test.cpp
|
||||||
|
../src/cli/run.cpp
|
||||||
|
../src/codegen/cmake.cpp
|
||||||
|
../src/codegen/flake.cpp
|
||||||
|
../src/exec/subprocess.cpp
|
||||||
|
../src/layout/layout.cpp
|
||||||
|
../src/linkdb/database.cpp
|
||||||
|
../src/linkdb/overlay.cpp
|
||||||
|
../src/lockfile/lockfile.cpp
|
||||||
|
../src/manifest/parser.cpp
|
||||||
|
../src/manifest/writer.cpp
|
||||||
|
../src/resolver/brute_scan.cpp
|
||||||
|
../src/resolver/conan_probe.cpp
|
||||||
|
../src/resolver/discover.cpp
|
||||||
|
../src/resolver/findmodule_scan.cpp
|
||||||
|
../src/resolver/fuzzy_listing.cpp
|
||||||
|
../src/resolver/nix_cmake_scan.cpp
|
||||||
|
../src/resolver/nixpkgs_git.cpp
|
||||||
|
../src/resolver/nixpkgs_probe.cpp
|
||||||
|
../src/resolver/pc_scan.cpp
|
||||||
|
../src/resolver/search_devbox.cpp
|
||||||
|
../src/resolver/vcpkg_probe.cpp
|
||||||
|
../src/resolver/verify_link.cpp
|
||||||
|
../src/resolver/version_resolve.cpp
|
||||||
|
../src/util/error.cpp
|
||||||
|
../src/util/levenshtein.cpp
|
||||||
|
../src/util/semver.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(cargoxx SYSTEM PRIVATE ../third_party)
|
||||||
|
target_link_libraries(cargoxx PUBLIC
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- binary target -----
|
||||||
|
add_executable(cargoxx_bin ../src/main.cpp)
|
||||||
|
set_target_properties(cargoxx_bin PROPERTIES OUTPUT_NAME cargoxx)
|
||||||
|
target_link_libraries(cargoxx_bin PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- tests -----
|
||||||
|
enable_testing()
|
||||||
|
include(Catch)
|
||||||
|
add_executable(test_brute_scan_parse ../tests/brute_scan_parse.cpp)
|
||||||
|
target_link_libraries(test_brute_scan_parse PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_brute_scan_parse)
|
||||||
|
add_executable(test_cmd_add ../tests/cmd_add.cpp)
|
||||||
|
target_link_libraries(test_cmd_add PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_cmd_add)
|
||||||
|
add_executable(test_cmd_build ../tests/cmd_build.cpp)
|
||||||
|
target_link_libraries(test_cmd_build PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_cmd_build)
|
||||||
|
add_executable(test_cmd_clean ../tests/cmd_clean.cpp)
|
||||||
|
target_link_libraries(test_cmd_clean PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_cmd_clean)
|
||||||
|
add_executable(test_cmd_linkdb_add ../tests/cmd_linkdb_add.cpp)
|
||||||
|
target_link_libraries(test_cmd_linkdb_add PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_cmd_linkdb_add)
|
||||||
|
add_executable(test_cmd_new ../tests/cmd_new.cpp)
|
||||||
|
target_link_libraries(test_cmd_new PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_cmd_new)
|
||||||
|
add_executable(test_cmd_remove ../tests/cmd_remove.cpp)
|
||||||
|
target_link_libraries(test_cmd_remove PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_cmd_remove)
|
||||||
|
add_executable(test_cmd_run ../tests/cmd_run.cpp)
|
||||||
|
target_link_libraries(test_cmd_run PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_cmd_run)
|
||||||
|
add_executable(test_codegen_cmake ../tests/codegen_cmake.cpp)
|
||||||
|
target_link_libraries(test_codegen_cmake PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_codegen_cmake)
|
||||||
|
add_executable(test_codegen_flake ../tests/codegen_flake.cpp)
|
||||||
|
target_link_libraries(test_codegen_flake PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_codegen_flake)
|
||||||
|
add_executable(test_conan_probe_live ../tests/conan_probe_live.cpp)
|
||||||
|
target_link_libraries(test_conan_probe_live PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_conan_probe_live)
|
||||||
|
add_executable(test_conan_probe_parse ../tests/conan_probe_parse.cpp)
|
||||||
|
target_link_libraries(test_conan_probe_parse PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_conan_probe_parse)
|
||||||
|
add_executable(test_devbox_resolve_live ../tests/devbox_resolve_live.cpp)
|
||||||
|
target_link_libraries(test_devbox_resolve_live PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_devbox_resolve_live)
|
||||||
|
add_executable(test_devbox_resolve_parse ../tests/devbox_resolve_parse.cpp)
|
||||||
|
target_link_libraries(test_devbox_resolve_parse PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_devbox_resolve_parse)
|
||||||
|
add_executable(test_exec_run ../tests/exec_run.cpp)
|
||||||
|
target_link_libraries(test_exec_run PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_exec_run)
|
||||||
|
add_executable(test_findmodule_scan_live ../tests/findmodule_scan_live.cpp)
|
||||||
|
target_link_libraries(test_findmodule_scan_live PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_findmodule_scan_live)
|
||||||
|
add_executable(test_last_failure_dir ../tests/last_failure_dir.cpp)
|
||||||
|
target_link_libraries(test_last_failure_dir PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_last_failure_dir)
|
||||||
|
add_executable(test_layout_discovery ../tests/layout_discovery.cpp)
|
||||||
|
target_link_libraries(test_layout_discovery PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_layout_discovery)
|
||||||
|
add_executable(test_levenshtein ../tests/levenshtein.cpp)
|
||||||
|
target_link_libraries(test_levenshtein PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_levenshtein)
|
||||||
|
add_executable(test_linkdb_lookup ../tests/linkdb_lookup.cpp)
|
||||||
|
target_link_libraries(test_linkdb_lookup PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_linkdb_lookup)
|
||||||
|
add_executable(test_linkdb_overlay ../tests/linkdb_overlay.cpp)
|
||||||
|
target_link_libraries(test_linkdb_overlay PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_linkdb_overlay)
|
||||||
|
add_executable(test_lockfile_round_trip ../tests/lockfile_round_trip.cpp)
|
||||||
|
target_link_libraries(test_lockfile_round_trip PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_lockfile_round_trip)
|
||||||
|
add_executable(test_manifest_parse ../tests/manifest_parse.cpp)
|
||||||
|
target_link_libraries(test_manifest_parse PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_manifest_parse)
|
||||||
|
add_executable(test_manifest_write ../tests/manifest_write.cpp)
|
||||||
|
target_link_libraries(test_manifest_write PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_manifest_write)
|
||||||
|
add_executable(test_nix_cmake_scan_live ../tests/nix_cmake_scan_live.cpp)
|
||||||
|
target_link_libraries(test_nix_cmake_scan_live PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_nix_cmake_scan_live)
|
||||||
|
add_executable(test_nix_cmake_scan_parse ../tests/nix_cmake_scan_parse.cpp)
|
||||||
|
target_link_libraries(test_nix_cmake_scan_parse PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_nix_cmake_scan_parse)
|
||||||
|
add_executable(test_nixpkgs_git_resolve ../tests/nixpkgs_git_resolve.cpp)
|
||||||
|
target_link_libraries(test_nixpkgs_git_resolve PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_nixpkgs_git_resolve)
|
||||||
|
add_executable(test_nixpkgs_probe_live ../tests/nixpkgs_probe_live.cpp)
|
||||||
|
target_link_libraries(test_nixpkgs_probe_live PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_nixpkgs_probe_live)
|
||||||
|
add_executable(test_nixpkgs_probe_parse ../tests/nixpkgs_probe_parse.cpp)
|
||||||
|
target_link_libraries(test_nixpkgs_probe_parse PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_nixpkgs_probe_parse)
|
||||||
|
add_executable(test_pc_scan_parse ../tests/pc_scan_parse.cpp)
|
||||||
|
target_link_libraries(test_pc_scan_parse PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_pc_scan_parse)
|
||||||
|
add_executable(test_semver_satisfies ../tests/semver_satisfies.cpp)
|
||||||
|
target_link_libraries(test_semver_satisfies PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_semver_satisfies)
|
||||||
|
add_executable(test_util_error ../tests/util_error.cpp)
|
||||||
|
target_link_libraries(test_util_error PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_util_error)
|
||||||
|
add_executable(test_vcpkg_probe_live ../tests/vcpkg_probe_live.cpp)
|
||||||
|
target_link_libraries(test_vcpkg_probe_live PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_vcpkg_probe_live)
|
||||||
|
add_executable(test_vcpkg_probe_parse ../tests/vcpkg_probe_parse.cpp)
|
||||||
|
target_link_libraries(test_vcpkg_probe_parse PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_vcpkg_probe_parse)
|
||||||
|
add_executable(test_verify_link_unit ../tests/verify_link_unit.cpp)
|
||||||
|
target_link_libraries(test_verify_link_unit PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_verify_link_unit)
|
||||||
18
flake.nix
18
flake.nix
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
description = "cargoxx — Cargo-style frontend for modern C++";
|
description = "cargoxx";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
@@ -11,21 +11,21 @@
|
|||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs { inherit system; };
|
||||||
in {
|
in {
|
||||||
devShells.default = pkgs.gcc15Stdenv.mkDerivation {
|
devShell = pkgs.gcc15Stdenv.mkDerivation {
|
||||||
name = "cargoxx-dev";
|
name = "shell";
|
||||||
version = "0.1.0";
|
version = "1.0";
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkgs.cmake
|
|
||||||
pkgs.ninja
|
pkgs.ninja
|
||||||
pkgs.git
|
pkgs.cmake
|
||||||
pkgs.pkg-config
|
|
||||||
];
|
];
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
pkgs.sqlite
|
|
||||||
pkgs.reproc
|
pkgs.reproc
|
||||||
|
pkgs.sqlite
|
||||||
pkgs.catch2_3
|
pkgs.catch2_3
|
||||||
];
|
];
|
||||||
hardeningDisable = [ "all" ];
|
hardeningDisable = [
|
||||||
|
"all"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,17 @@ auto cmd_add(const std::filesystem::path& project_root, const std::string& name,
|
|||||||
auto cmd_remove(const std::filesystem::path& project_root, const std::string& name)
|
auto cmd_remove(const std::filesystem::path& project_root, const std::string& name)
|
||||||
-> util::Result<void>;
|
-> util::Result<void>;
|
||||||
|
|
||||||
|
// Inserts a manual recipe into the SQLite linkdb overlay. Equivalent to
|
||||||
|
// the auto-discover path's confirmed row, but user-provided. Use it
|
||||||
|
// when nixpkgs ships a CMake FindModule (no <X>Config.cmake) or when the
|
||||||
|
// scanner can't pick the right target automatically.
|
||||||
|
auto cmd_linkdb_add(const std::string& package, const std::string& version_range,
|
||||||
|
const std::string& find_package_text,
|
||||||
|
std::vector<std::string> targets,
|
||||||
|
const std::string& nixpkgs_attr,
|
||||||
|
std::optional<std::filesystem::path> overlay_path = std::nullopt)
|
||||||
|
-> util::Result<void>;
|
||||||
|
|
||||||
auto run(int argc, char** argv) -> int;
|
auto run(int argc, char** argv) -> int;
|
||||||
|
|
||||||
} // namespace cargoxx::cli
|
} // namespace cargoxx::cli
|
||||||
|
|||||||
@@ -77,9 +77,12 @@ auto record_lockfile_rev(const fs::path& project_root, const std::string& name,
|
|||||||
return lockfile::write(lock, lock_path);
|
return lockfile::write(lock, lock_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drives the resolver chain (Conan → vcpkg → nix-cmake-scan), running a
|
// Drives the resolver chain (Conan → vcpkg → nix-cmake-scan → pc-scan),
|
||||||
// real `cmd_build` against each candidate via verify_link. On success the
|
// running a real `cmd_build` against each candidate via verify_link.
|
||||||
// overlay carries a confirmed row for the package.
|
// On success the overlay carries a confirmed row for the package.
|
||||||
|
// Every probe's scratch project is preserved under
|
||||||
|
// `<XDG>/cargoxx/last-failure/<name>/<NN>-<probe>/` for inspection;
|
||||||
|
// the dir is wiped clean at the start of each call.
|
||||||
auto run_auto_resolution(const std::string& name, const std::string& version,
|
auto run_auto_resolution(const std::string& name, const std::string& version,
|
||||||
const std::vector<std::string>& components,
|
const std::vector<std::string>& components,
|
||||||
const fs::path& overlay_path) -> util::Result<void> {
|
const fs::path& overlay_path) -> util::Result<void> {
|
||||||
@@ -87,17 +90,18 @@ auto run_auto_resolution(const std::string& name, const std::string& version,
|
|||||||
return cmd_build(root, /*no_build=*/false, /*release=*/false,
|
return cmd_build(root, /*no_build=*/false, /*release=*/false,
|
||||||
/*target=*/std::nullopt, overlay_path);
|
/*target=*/std::nullopt, overlay_path);
|
||||||
};
|
};
|
||||||
const auto scratch_root =
|
const auto scratch_root = resolver::last_failure_dir(name);
|
||||||
std::filesystem::temp_directory_path() /
|
std::error_code ec;
|
||||||
std::format("cargoxx-discover-{}", std::random_device{}());
|
std::filesystem::remove_all(scratch_root, ec);
|
||||||
|
std::filesystem::create_directories(scratch_root, ec);
|
||||||
|
|
||||||
auto disc = resolver::discover(name, version, components, overlay_path,
|
auto disc = resolver::discover(name, version, components, overlay_path,
|
||||||
scratch_root, build_fn);
|
scratch_root, build_fn);
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
std::filesystem::remove_all(scratch_root, ec);
|
|
||||||
|
|
||||||
if (!disc) {
|
if (!disc) {
|
||||||
|
std::cerr << std::format(
|
||||||
|
"note: every probe attempt's scratch project is preserved at\n"
|
||||||
|
" {}/ — re-run cmake inside any subdir to reproduce.\n",
|
||||||
|
scratch_root.string());
|
||||||
return std::unexpected(disc.error());
|
return std::unexpected(disc.error());
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import cargoxx.linkdb;
|
|||||||
import cargoxx.lockfile;
|
import cargoxx.lockfile;
|
||||||
import cargoxx.codegen;
|
import cargoxx.codegen;
|
||||||
import cargoxx.exec;
|
import cargoxx.exec;
|
||||||
|
import cargoxx.resolver;
|
||||||
|
|
||||||
namespace cargoxx::cli {
|
namespace cargoxx::cli {
|
||||||
|
|
||||||
@@ -146,17 +147,49 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
|
|||||||
return std::unexpected(layout_result.error());
|
return std::unexpected(layout_result.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto db = linkdb::Database::open(std::move(overlay_path));
|
const auto effective_overlay = overlay_path.value_or(linkdb::default_overlay_path());
|
||||||
|
auto db = linkdb::Database::open(effective_overlay);
|
||||||
if (!db) {
|
if (!db) {
|
||||||
return std::unexpected(db.error());
|
return std::unexpected(db.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto auto_resolve = [&](const std::string& name, const std::string& version,
|
||||||
|
const std::vector<std::string>& components)
|
||||||
|
-> util::Result<void> {
|
||||||
|
auto build_fn = [&](const fs::path& root) {
|
||||||
|
return cmd_build(root, /*no_build=*/false, /*release=*/false,
|
||||||
|
/*target=*/std::nullopt, effective_overlay);
|
||||||
|
};
|
||||||
|
const auto scratch_root = resolver::last_failure_dir(name);
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove_all(scratch_root, ec);
|
||||||
|
std::filesystem::create_directories(scratch_root, ec);
|
||||||
|
auto disc = resolver::discover(name, version, components,
|
||||||
|
effective_overlay, scratch_root, build_fn);
|
||||||
|
if (!disc) {
|
||||||
|
std::cerr << std::format(
|
||||||
|
"note: every probe attempt's scratch project is preserved at\n"
|
||||||
|
" {}/ — re-run cmake inside any subdir to reproduce.\n",
|
||||||
|
scratch_root.string());
|
||||||
|
return std::unexpected(disc.error());
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
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) {
|
||||||
auto r = db->resolve(dep.name, dep.version_spec, dep.components);
|
auto r = db->resolve(dep.name, dep.version_spec, dep.components);
|
||||||
|
if (!r && r.error().code == util::ErrorCode::LinkdbUnknownPackage) {
|
||||||
|
if (auto resolved = auto_resolve(dep.name, dep.version_spec,
|
||||||
|
dep.components);
|
||||||
|
!resolved) {
|
||||||
|
return std::unexpected(resolved.error());
|
||||||
|
}
|
||||||
|
r = db->resolve(dep.name, dep.version_spec, dep.components);
|
||||||
|
}
|
||||||
if (!r) {
|
if (!r) {
|
||||||
return std::unexpected(r.error());
|
return std::unexpected(r.error());
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/cli/cmd_linkdb_add.cpp
Normal file
31
src/cli/cmd_linkdb_add.cpp
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
module cargoxx.cli;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.linkdb;
|
||||||
|
import cargoxx.util;
|
||||||
|
|
||||||
|
namespace cargoxx::cli {
|
||||||
|
|
||||||
|
auto cmd_linkdb_add(const std::string& package, const std::string& version_range,
|
||||||
|
const std::string& find_package_text,
|
||||||
|
std::vector<std::string> targets,
|
||||||
|
const std::string& nixpkgs_attr,
|
||||||
|
std::optional<std::filesystem::path> overlay_path)
|
||||||
|
-> util::Result<void> {
|
||||||
|
auto db = linkdb::Database::open(std::move(overlay_path));
|
||||||
|
if (!db) {
|
||||||
|
return std::unexpected(db.error());
|
||||||
|
}
|
||||||
|
if (auto r = db->evict_auto_recipes(package); !r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
linkdb::Recipe r{
|
||||||
|
.nixpkgs_attr = nixpkgs_attr,
|
||||||
|
.find_package = find_package_text,
|
||||||
|
.targets = std::move(targets),
|
||||||
|
.source = "manual",
|
||||||
|
};
|
||||||
|
return db->add_manual(package, version_range, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::cli
|
||||||
@@ -58,6 +58,28 @@ auto run(int argc, char** argv) -> int {
|
|||||||
std::string remove_name;
|
std::string remove_name;
|
||||||
remove_cmd->add_option("name", remove_name, "Package name to remove")->required();
|
remove_cmd->add_option("name", remove_name, "Package name to remove")->required();
|
||||||
|
|
||||||
|
auto* linkdb_cmd =
|
||||||
|
app.add_subcommand("linkdb", "Manage the link database");
|
||||||
|
linkdb_cmd->require_subcommand(1);
|
||||||
|
auto* linkdb_add_cmd =
|
||||||
|
linkdb_cmd->add_subcommand("add", "Insert a manual recipe");
|
||||||
|
std::string ldb_package;
|
||||||
|
std::string ldb_version = "*";
|
||||||
|
std::string ldb_find_package;
|
||||||
|
std::string ldb_targets;
|
||||||
|
std::string ldb_nixpkgs_attr;
|
||||||
|
linkdb_add_cmd->add_option("package", ldb_package, "Package name")->required();
|
||||||
|
linkdb_add_cmd->add_option("--version", ldb_version, "Version range (default: *)");
|
||||||
|
linkdb_add_cmd->add_option("--find-package", ldb_find_package,
|
||||||
|
"Body of the find_package(...) call (e.g. \"fmt CONFIG REQUIRED\")")
|
||||||
|
->required();
|
||||||
|
linkdb_add_cmd->add_option("--targets", ldb_targets,
|
||||||
|
"Comma-separated CMake targets (e.g. fmt::fmt)")
|
||||||
|
->required();
|
||||||
|
linkdb_add_cmd->add_option("--nixpkgs-attr", ldb_nixpkgs_attr,
|
||||||
|
"nixpkgs attribute name (e.g. fmt_10)")
|
||||||
|
->required();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
app.parse(argc, argv);
|
app.parse(argc, argv);
|
||||||
} catch (const CLI::ParseError& e) {
|
} catch (const CLI::ParseError& e) {
|
||||||
@@ -175,6 +197,31 @@ auto run(int argc, char** argv) -> int {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (*linkdb_add_cmd) {
|
||||||
|
std::vector<std::string> targets;
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (pos <= ldb_targets.size()) {
|
||||||
|
auto comma = ldb_targets.find(',', pos);
|
||||||
|
auto piece = ldb_targets.substr(
|
||||||
|
pos, comma == std::string::npos ? ldb_targets.size() - pos : comma - pos);
|
||||||
|
if (!piece.empty()) {
|
||||||
|
targets.push_back(std::move(piece));
|
||||||
|
}
|
||||||
|
if (comma == std::string::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos = comma + 1;
|
||||||
|
}
|
||||||
|
auto r = cmd_linkdb_add(ldb_package, ldb_version, ldb_find_package,
|
||||||
|
std::move(targets), ldb_nixpkgs_attr);
|
||||||
|
if (!r) {
|
||||||
|
std::cerr << util::format(r.error());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::cout << std::format(" Added manual recipe for {}\n", ldb_package);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,11 +86,60 @@ auto emit_find_packages(const std::vector<linkdb::Recipe>& recipes,
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
std::string out = "\n# ----- dependencies -----\n";
|
std::string out = "\n# ----- dependencies -----\n";
|
||||||
for (const auto& r : recipes) {
|
|
||||||
|
bool pkgconfig_emitted = false;
|
||||||
|
auto emit_one = [&](const linkdb::Recipe& r) {
|
||||||
|
if (!r.brute_force_libs.empty() || !r.brute_force_includes.empty()) {
|
||||||
|
// Synthesize a single INTERFACE IMPORTED target named after
|
||||||
|
// the first entry in `targets` (e.g. `<pkg>::<pkg>`). No
|
||||||
|
// find_package — every artifact path is baked in.
|
||||||
|
if (r.targets.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& target = r.targets.front();
|
||||||
|
out += std::format("add_library({} INTERFACE IMPORTED)\n", target);
|
||||||
|
if (!r.brute_force_libs.empty()) {
|
||||||
|
out += std::format("set_property(TARGET {} APPEND PROPERTY "
|
||||||
|
"INTERFACE_LINK_LIBRARIES",
|
||||||
|
target);
|
||||||
|
for (const auto& l : r.brute_force_libs) {
|
||||||
|
out += std::format("\n \"{}\"", l);
|
||||||
|
}
|
||||||
|
out += ")\n";
|
||||||
|
}
|
||||||
|
if (!r.brute_force_includes.empty()) {
|
||||||
|
out += std::format("set_property(TARGET {} APPEND PROPERTY "
|
||||||
|
"INTERFACE_INCLUDE_DIRECTORIES",
|
||||||
|
target);
|
||||||
|
for (const auto& i : r.brute_force_includes) {
|
||||||
|
out += std::format("\n \"{}\"", i);
|
||||||
|
}
|
||||||
|
out += ")\n";
|
||||||
|
}
|
||||||
|
} else if (r.pkg_config_module && !r.pkg_config_module->empty()) {
|
||||||
|
if (!pkgconfig_emitted) {
|
||||||
|
out += "find_package(PkgConfig REQUIRED)\n";
|
||||||
|
pkgconfig_emitted = true;
|
||||||
|
}
|
||||||
|
std::string upper;
|
||||||
|
upper.reserve(r.pkg_config_module->size());
|
||||||
|
for (char c : *r.pkg_config_module) {
|
||||||
|
upper += std::isalnum(static_cast<unsigned char>(c))
|
||||||
|
? static_cast<char>(std::toupper(
|
||||||
|
static_cast<unsigned char>(c)))
|
||||||
|
: '_';
|
||||||
|
}
|
||||||
|
out += std::format("pkg_check_modules({} REQUIRED IMPORTED_TARGET {})\n",
|
||||||
|
upper, *r.pkg_config_module);
|
||||||
|
} else {
|
||||||
out += std::format("find_package({})\n", r.find_package);
|
out += std::format("find_package({})\n", r.find_package);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
for (const auto& r : recipes) {
|
||||||
|
emit_one(r);
|
||||||
|
}
|
||||||
for (const auto& r : dev_recipes) {
|
for (const auto& r : dev_recipes) {
|
||||||
out += std::format("find_package({})\n", r.find_package);
|
emit_one(r);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -111,7 +160,7 @@ auto emit_library(const layout::Target& lib, const std::string& package_name,
|
|||||||
out += std::format("add_library({} STATIC)\n", package_name);
|
out += std::format("add_library({} STATIC)\n", package_name);
|
||||||
out += std::format("target_sources({}\n", package_name);
|
out += std::format("target_sources({}\n", package_name);
|
||||||
out += " PUBLIC\n";
|
out += " PUBLIC\n";
|
||||||
out += " FILE_SET CXX_MODULES FILES\n";
|
out += " FILE_SET CXX_MODULES BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. FILES\n";
|
||||||
for (const auto& m : lib.module_units) {
|
for (const auto& m : lib.module_units) {
|
||||||
out += std::format(" {}\n", rel_to_build(m, project_root));
|
out += std::format(" {}\n", rel_to_build(m, project_root));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,25 +59,24 @@ auto find_lockfile_ref(const lockfile::Lockfile& lock, const std::string& name,
|
|||||||
|
|
||||||
auto build_bindings(const GenerateInputs& in) -> std::vector<DepBinding> {
|
auto build_bindings(const GenerateInputs& in) -> std::vector<DepBinding> {
|
||||||
std::vector<DepBinding> out;
|
std::vector<DepBinding> out;
|
||||||
out.reserve(in.manifest.dependencies.size());
|
out.reserve(in.manifest.dependencies.size() + in.manifest.dev_dependencies.size());
|
||||||
for (std::size_t i = 0; i < in.manifest.dependencies.size(); ++i) {
|
auto push = [&](const manifest::Dependency& dep, const linkdb::Recipe& rec) {
|
||||||
const auto& dep = in.manifest.dependencies[i];
|
|
||||||
const auto& rec = in.recipes[i];
|
|
||||||
auto ref = find_lockfile_ref(in.lock, dep.name, dep.version_spec);
|
auto ref = find_lockfile_ref(in.lock, dep.name, dep.version_spec);
|
||||||
// For pinned deps the lockfile's nixpkgs_attr is authoritative
|
|
||||||
// (it came from devbox's attr_paths for this specific rev). The
|
|
||||||
// curated recipe's attr only applies to nixos-unstable, so it's
|
|
||||||
// wrong when the dep pulls from a different rev.
|
|
||||||
std::string attr = (ref.attr && !ref.attr->empty()) ? *ref.attr
|
std::string attr = (ref.attr && !ref.attr->empty()) ? *ref.attr
|
||||||
: rec.nixpkgs_attr;
|
: rec.nixpkgs_attr;
|
||||||
DepBinding b{
|
out.push_back(DepBinding{
|
||||||
.name = dep.name,
|
.name = dep.name,
|
||||||
.version = dep.version_spec,
|
.version = dep.version_spec,
|
||||||
.nixpkgs_attr = std::move(attr),
|
.nixpkgs_attr = std::move(attr),
|
||||||
.sanitized = sanitize_input_attr(dep.name, dep.version_spec),
|
.sanitized = sanitize_input_attr(dep.name, dep.version_spec),
|
||||||
.rev = std::move(ref.rev),
|
.rev = std::move(ref.rev),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
out.push_back(std::move(b));
|
for (std::size_t i = 0; i < in.manifest.dependencies.size(); ++i) {
|
||||||
|
push(in.manifest.dependencies[i], in.recipes[i]);
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < in.manifest.dev_dependencies.size(); ++i) {
|
||||||
|
push(in.manifest.dev_dependencies[i], in.dev_recipes[i]);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -168,6 +167,18 @@ auto flake_nix(const GenerateInputs& in) -> std::string {
|
|||||||
|
|
||||||
out += emit_inputs_block(pinned);
|
out += emit_inputs_block(pinned);
|
||||||
|
|
||||||
|
const bool any_pkg_config =
|
||||||
|
std::ranges::any_of(in.recipes,
|
||||||
|
[](const linkdb::Recipe& r) {
|
||||||
|
return r.pkg_config_module &&
|
||||||
|
!r.pkg_config_module->empty();
|
||||||
|
}) ||
|
||||||
|
std::ranges::any_of(in.dev_recipes,
|
||||||
|
[](const linkdb::Recipe& r) {
|
||||||
|
return r.pkg_config_module &&
|
||||||
|
!r.pkg_config_module->empty();
|
||||||
|
});
|
||||||
|
|
||||||
out += "\n";
|
out += "\n";
|
||||||
out += " outputs = ";
|
out += " outputs = ";
|
||||||
out += emit_outputs_params(pinned);
|
out += emit_outputs_params(pinned);
|
||||||
@@ -182,8 +193,11 @@ auto flake_nix(const GenerateInputs& in) -> std::string {
|
|||||||
" version = \"1.0\";\n"
|
" version = \"1.0\";\n"
|
||||||
" nativeBuildInputs = [\n"
|
" nativeBuildInputs = [\n"
|
||||||
" pkgs.ninja\n"
|
" pkgs.ninja\n"
|
||||||
" pkgs.cmake\n"
|
" pkgs.cmake\n";
|
||||||
" ];\n"
|
if (any_pkg_config) {
|
||||||
|
out += " pkgs.pkg-config\n";
|
||||||
|
}
|
||||||
|
out += " ];\n"
|
||||||
" buildInputs = [\n";
|
" buildInputs = [\n";
|
||||||
out += emit_build_inputs(bindings);
|
out += emit_build_inputs(bindings);
|
||||||
out += " ];\n"
|
out += " ];\n"
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ auto Database::resolve(const std::string& package, const std::string& version,
|
|||||||
.find_package = row.find_package,
|
.find_package = row.find_package,
|
||||||
.targets = row.targets,
|
.targets = row.targets,
|
||||||
.source = row.source,
|
.source = row.source,
|
||||||
|
.pkg_config_module = row.pkg_config_module,
|
||||||
|
.brute_force_libs = row.brute_force_libs,
|
||||||
|
.brute_force_includes = row.brute_force_includes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return std::unexpected(util::Error{
|
return std::unexpected(util::Error{
|
||||||
|
|||||||
@@ -15,6 +15,24 @@ struct Recipe {
|
|||||||
std::vector<std::string> targets; // post-substitution
|
std::vector<std::string> targets; // post-substitution
|
||||||
std::string source; // 'curated' | 'manual' | etc.
|
std::string source; // 'curated' | 'manual' | etc.
|
||||||
|
|
||||||
|
// When set, the dep is consumed via PkgConfig instead of a normal
|
||||||
|
// find_package. Codegen emits
|
||||||
|
// find_package(PkgConfig REQUIRED)
|
||||||
|
// pkg_check_modules(<UPPER> REQUIRED IMPORTED_TARGET <pkg_config_module>)
|
||||||
|
// and the recipe's `targets` list holds the synthesized
|
||||||
|
// PkgConfig::<UPPER> entries. `find_package` for these recipes is
|
||||||
|
// the literal string "PkgConfig REQUIRED" — kept consistent so
|
||||||
|
// overlay rows don't need a sentinel.
|
||||||
|
std::optional<std::string> pkg_config_module;
|
||||||
|
|
||||||
|
// Set by the brute-force probe (the last-resort discover stage).
|
||||||
|
// When non-empty, codegen skips `find_package(...)` and instead
|
||||||
|
// synthesizes an INTERFACE IMPORTED target named in `targets[0]`
|
||||||
|
// (which is `<pkg>::<pkg>`) with these absolute lib paths +
|
||||||
|
// include dirs.
|
||||||
|
std::vector<std::string> brute_force_libs;
|
||||||
|
std::vector<std::string> brute_force_includes;
|
||||||
|
|
||||||
bool operator==(const Recipe&) const = default;
|
bool operator==(const Recipe&) const = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,6 +47,9 @@ struct OverlayRow {
|
|||||||
std::vector<std::string> targets;
|
std::vector<std::string> targets;
|
||||||
std::string source;
|
std::string source;
|
||||||
std::int64_t verified_at = 0;
|
std::int64_t verified_at = 0;
|
||||||
|
std::optional<std::string> pkg_config_module;
|
||||||
|
std::vector<std::string> brute_force_libs;
|
||||||
|
std::vector<std::string> brute_force_includes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// RAII wrapper for an open sqlite3 connection used by the overlay database.
|
// RAII wrapper for an open sqlite3 connection used by the overlay database.
|
||||||
|
|||||||
@@ -90,6 +90,42 @@ auto overlay_open(const std::filesystem::path& path)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schema migrations. SQLite ADD COLUMN errors when the column
|
||||||
|
// already exists; treat "duplicate column" as success.
|
||||||
|
auto add_column = [&](const char* sql) -> util::Result<void> {
|
||||||
|
char* mig_err = nullptr;
|
||||||
|
if (sqlite3_exec(state->handle(), sql, nullptr, nullptr, &mig_err) !=
|
||||||
|
SQLITE_OK) {
|
||||||
|
if (mig_err && std::string_view{mig_err}.find("duplicate column") ==
|
||||||
|
std::string_view::npos) {
|
||||||
|
std::string msg = std::format("cannot migrate overlay schema: {}",
|
||||||
|
mig_err ? mig_err : "?");
|
||||||
|
sqlite3_free(mig_err);
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::LinkdbCorrupt, std::move(msg), "", path,
|
||||||
|
std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sqlite3_free(mig_err);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
if (auto r = add_column(
|
||||||
|
"ALTER TABLE recipes ADD COLUMN pkg_config_module TEXT");
|
||||||
|
!r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
if (auto r = add_column(
|
||||||
|
"ALTER TABLE recipes ADD COLUMN brute_force_libs TEXT");
|
||||||
|
!r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
if (auto r = add_column(
|
||||||
|
"ALTER TABLE recipes ADD COLUMN brute_force_includes TEXT");
|
||||||
|
!r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +135,8 @@ auto overlay_insert_manual(OverlayState& state, const std::string& package,
|
|||||||
constexpr const char* SQL =
|
constexpr const char* SQL =
|
||||||
"INSERT OR REPLACE INTO recipes "
|
"INSERT OR REPLACE INTO recipes "
|
||||||
"(package, version_range, nixpkgs_attr, find_package, targets, components, source, "
|
"(package, version_range, nixpkgs_attr, find_package, targets, components, source, "
|
||||||
" verified_at) "
|
" verified_at, pkg_config_module, brute_force_libs, brute_force_includes) "
|
||||||
"VALUES (?, ?, ?, ?, ?, NULL, 'manual', ?)";
|
"VALUES (?, ?, ?, ?, ?, NULL, 'manual', ?, ?, ?, ?)";
|
||||||
|
|
||||||
sqlite3* db = state.handle();
|
sqlite3* db = state.handle();
|
||||||
sqlite3_stmt* stmt = nullptr;
|
sqlite3_stmt* stmt = nullptr;
|
||||||
@@ -109,6 +145,8 @@ auto overlay_insert_manual(OverlayState& state, const std::string& package,
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto targets_str = nlohmann::json(r.targets).dump();
|
auto targets_str = nlohmann::json(r.targets).dump();
|
||||||
|
auto libs_str = nlohmann::json(r.brute_force_libs).dump();
|
||||||
|
auto incs_str = nlohmann::json(r.brute_force_includes).dump();
|
||||||
auto now = std::chrono::duration_cast<std::chrono::seconds>(
|
auto now = std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
std::chrono::system_clock::now().time_since_epoch())
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
.count();
|
.count();
|
||||||
@@ -119,6 +157,13 @@ auto overlay_insert_manual(OverlayState& state, const std::string& package,
|
|||||||
sqlite3_bind_text(stmt, 4, r.find_package.c_str(), -1, SQLITE_TRANSIENT);
|
sqlite3_bind_text(stmt, 4, r.find_package.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_text(stmt, 5, targets_str.c_str(), -1, SQLITE_TRANSIENT);
|
sqlite3_bind_text(stmt, 5, targets_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_int64(stmt, 6, now);
|
sqlite3_bind_int64(stmt, 6, now);
|
||||||
|
if (r.pkg_config_module) {
|
||||||
|
sqlite3_bind_text(stmt, 7, r.pkg_config_module->c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
} else {
|
||||||
|
sqlite3_bind_null(stmt, 7);
|
||||||
|
}
|
||||||
|
sqlite3_bind_text(stmt, 8, libs_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 9, incs_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
|
||||||
auto rc = sqlite3_step(stmt);
|
auto rc = sqlite3_step(stmt);
|
||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
@@ -137,8 +182,8 @@ auto overlay_insert(OverlayState& state, const std::string& package,
|
|||||||
constexpr const char* SQL =
|
constexpr const char* SQL =
|
||||||
"INSERT OR REPLACE INTO recipes "
|
"INSERT OR REPLACE INTO recipes "
|
||||||
"(package, version_range, nixpkgs_attr, find_package, targets, components, source, "
|
"(package, version_range, nixpkgs_attr, find_package, targets, components, source, "
|
||||||
" verified_at) "
|
" verified_at, pkg_config_module, brute_force_libs, brute_force_includes) "
|
||||||
"VALUES (?, ?, ?, ?, ?, NULL, ?, ?)";
|
"VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)";
|
||||||
|
|
||||||
sqlite3* db = state.handle();
|
sqlite3* db = state.handle();
|
||||||
sqlite3_stmt* stmt = nullptr;
|
sqlite3_stmt* stmt = nullptr;
|
||||||
@@ -147,6 +192,8 @@ auto overlay_insert(OverlayState& state, const std::string& package,
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto targets_str = nlohmann::json(r.targets).dump();
|
auto targets_str = nlohmann::json(r.targets).dump();
|
||||||
|
auto libs_str = nlohmann::json(r.brute_force_libs).dump();
|
||||||
|
auto incs_str = nlohmann::json(r.brute_force_includes).dump();
|
||||||
|
|
||||||
sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT);
|
sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_text(stmt, 2, version_range.c_str(), -1, SQLITE_TRANSIENT);
|
sqlite3_bind_text(stmt, 2, version_range.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
@@ -155,6 +202,13 @@ auto overlay_insert(OverlayState& state, const std::string& package,
|
|||||||
sqlite3_bind_text(stmt, 5, targets_str.c_str(), -1, SQLITE_TRANSIENT);
|
sqlite3_bind_text(stmt, 5, targets_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_text(stmt, 6, source.c_str(), -1, SQLITE_TRANSIENT);
|
sqlite3_bind_text(stmt, 6, source.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_int64(stmt, 7, verified_at);
|
sqlite3_bind_int64(stmt, 7, verified_at);
|
||||||
|
if (r.pkg_config_module) {
|
||||||
|
sqlite3_bind_text(stmt, 8, r.pkg_config_module->c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
} else {
|
||||||
|
sqlite3_bind_null(stmt, 8);
|
||||||
|
}
|
||||||
|
sqlite3_bind_text(stmt, 9, libs_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 10, incs_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
|
||||||
auto rc = sqlite3_step(stmt);
|
auto rc = sqlite3_step(stmt);
|
||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
@@ -245,7 +299,8 @@ auto overlay_evict_auto(OverlayState& state, const std::string& package)
|
|||||||
auto overlay_query(OverlayState& state, const std::string& package)
|
auto overlay_query(OverlayState& state, const std::string& package)
|
||||||
-> util::Result<std::vector<OverlayRow>> {
|
-> util::Result<std::vector<OverlayRow>> {
|
||||||
constexpr const char* SQL =
|
constexpr const char* SQL =
|
||||||
"SELECT version_range, nixpkgs_attr, find_package, targets, source, verified_at "
|
"SELECT version_range, nixpkgs_attr, find_package, targets, source, verified_at, "
|
||||||
|
" pkg_config_module, brute_force_libs, brute_force_includes "
|
||||||
"FROM recipes WHERE package = ?";
|
"FROM recipes WHERE package = ?";
|
||||||
|
|
||||||
sqlite3* db = state.handle();
|
sqlite3* db = state.handle();
|
||||||
@@ -276,6 +331,26 @@ auto overlay_query(OverlayState& state, const std::string& package)
|
|||||||
}
|
}
|
||||||
row.source = column_text(stmt, 4);
|
row.source = column_text(stmt, 4);
|
||||||
row.verified_at = sqlite3_column_int64(stmt, 5);
|
row.verified_at = sqlite3_column_int64(stmt, 5);
|
||||||
|
if (sqlite3_column_type(stmt, 6) != SQLITE_NULL) {
|
||||||
|
row.pkg_config_module = column_text(stmt, 6);
|
||||||
|
}
|
||||||
|
auto parse_str_array = [&](int col, std::vector<std::string>& out_arr) {
|
||||||
|
if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
auto txt = column_text(stmt, col);
|
||||||
|
if (txt.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out_arr =
|
||||||
|
nlohmann::json::parse(txt).get<std::vector<std::string>>();
|
||||||
|
} catch (const nlohmann::json::exception&) {
|
||||||
|
// legacy/manual rows may have stored garbage; ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
parse_str_array(7, row.brute_force_libs);
|
||||||
|
parse_str_array(8, row.brute_force_includes);
|
||||||
out.push_back(std::move(row));
|
out.push_back(std::move(row));
|
||||||
}
|
}
|
||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
|
|||||||
81
src/resolver/brute_scan.cpp
Normal file
81
src/resolver/brute_scan.cpp
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
module cargoxx.resolver;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.util;
|
||||||
|
|
||||||
|
namespace cargoxx::resolver {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto is_lib_filename(const fs::path& p) -> bool {
|
||||||
|
auto name = p.filename().string();
|
||||||
|
if (!name.starts_with("lib")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto ext = p.extension().string();
|
||||||
|
if (ext == ".a") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (ext == ".so" || ext == ".dylib") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// .so.<N>, .so.<N>.<M>, ... — common shared-lib versioning. Use a
|
||||||
|
// looser check: if the name contains ".so." or ".dylib." anywhere
|
||||||
|
// after the lib prefix, accept it.
|
||||||
|
return name.find(".so.") != std::string::npos ||
|
||||||
|
name.find(".dylib.") != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto brute_scan(const fs::path& store_path, const std::string& package_name)
|
||||||
|
-> util::Result<BruteCandidate> {
|
||||||
|
if (package_name.empty()) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
"brute_scan: package name is empty",
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const auto lib_dir = store_path / "lib";
|
||||||
|
const auto include_dir = store_path / "include";
|
||||||
|
|
||||||
|
BruteCandidate out;
|
||||||
|
std::error_code ec;
|
||||||
|
|
||||||
|
if (fs::exists(lib_dir, ec) && !ec) {
|
||||||
|
for (const auto& entry : fs::directory_iterator{
|
||||||
|
lib_dir, fs::directory_options::skip_permission_denied, ec}) {
|
||||||
|
if (!entry.is_regular_file() && !entry.is_symlink()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!is_lib_filename(entry.path())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.lib_files.push_back(entry.path().string());
|
||||||
|
}
|
||||||
|
std::ranges::sort(out.lib_files);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs::exists(include_dir, ec) && !ec) {
|
||||||
|
// For include/, expose the top-level directory itself (e.g.
|
||||||
|
// `<store>/include`) — that's what `#include <pkg/foo.h>`
|
||||||
|
// expects. Adding every subdir would also work, but is noisier
|
||||||
|
// and provokes name collisions across deps.
|
||||||
|
out.include_dirs.push_back(include_dir.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out.lib_files.empty() && out.include_dirs.empty()) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no libs or headers under '{}'", store_path.string()),
|
||||||
|
"", store_path, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::resolver
|
||||||
@@ -48,6 +48,61 @@ auto recipe_from_nix_scan(const NixCmakeCandidate& n,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PkgConfig-shaped recipe. The synthesized `PkgConfig::<NAME>` target
|
||||||
|
// name must match what CMake's `pkg_check_modules(... IMPORTED_TARGET
|
||||||
|
// <module>)` produces: the third arg of pkg_check_modules becomes the
|
||||||
|
// prefix variable AND the imported-target suffix. We pass the
|
||||||
|
// uppercased module stem so the target is `PkgConfig::<UPPER>`.
|
||||||
|
auto upper_pc_name(std::string_view module_name) -> std::string {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(module_name.size());
|
||||||
|
for (char c : module_name) {
|
||||||
|
if (std::isalnum(static_cast<unsigned char>(c))) {
|
||||||
|
out += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
|
} else {
|
||||||
|
out += '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto recipe_from_pc(const PcCandidate& p, const std::string& nixpkgs_attr,
|
||||||
|
const std::string& source) -> linkdb::Recipe {
|
||||||
|
auto upper = upper_pc_name(p.pc_module);
|
||||||
|
return linkdb::Recipe{
|
||||||
|
.nixpkgs_attr = nixpkgs_attr,
|
||||||
|
.find_package = "PkgConfig REQUIRED",
|
||||||
|
.targets = {std::format("PkgConfig::{}", upper)},
|
||||||
|
.source = source,
|
||||||
|
.pkg_config_module = p.pc_module,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto recipe_from_findmodule(const FindModuleCandidate& fm,
|
||||||
|
const std::string& nixpkgs_attr,
|
||||||
|
const std::string& source) -> linkdb::Recipe {
|
||||||
|
return linkdb::Recipe{
|
||||||
|
.nixpkgs_attr = nixpkgs_attr,
|
||||||
|
.find_package = fm.find_package,
|
||||||
|
.targets = fm.targets,
|
||||||
|
.source = source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto recipe_from_brute(const BruteCandidate& b, const std::string& name,
|
||||||
|
const std::string& nixpkgs_attr, const std::string& source)
|
||||||
|
-> linkdb::Recipe {
|
||||||
|
return linkdb::Recipe{
|
||||||
|
.nixpkgs_attr = nixpkgs_attr,
|
||||||
|
// No find_package — codegen synthesizes the target directly.
|
||||||
|
.find_package = "",
|
||||||
|
.targets = {std::format("{}::{}", name, name)},
|
||||||
|
.source = source,
|
||||||
|
.brute_force_libs = b.lib_files,
|
||||||
|
.brute_force_includes = b.include_dirs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
struct Candidate {
|
struct Candidate {
|
||||||
std::string source;
|
std::string source;
|
||||||
linkdb::Recipe recipe;
|
linkdb::Recipe recipe;
|
||||||
@@ -56,7 +111,7 @@ struct Candidate {
|
|||||||
auto try_verify(const Candidate& cand, const std::string& name,
|
auto try_verify(const Candidate& cand, const std::string& name,
|
||||||
const std::string& version_spec,
|
const std::string& version_spec,
|
||||||
const std::vector<std::string>& components,
|
const std::vector<std::string>& components,
|
||||||
const fs::path& overlay_path, const fs::path& scratch_root,
|
const fs::path& overlay_path, const fs::path& scratch_path,
|
||||||
const BuildFn& build_fn) -> util::Result<void> {
|
const BuildFn& build_fn) -> util::Result<void> {
|
||||||
VerifyLinkRequest req{
|
VerifyLinkRequest req{
|
||||||
.candidate = cand.recipe,
|
.candidate = cand.recipe,
|
||||||
@@ -65,7 +120,7 @@ auto try_verify(const Candidate& cand, const std::string& name,
|
|||||||
.version_spec = version_spec,
|
.version_spec = version_spec,
|
||||||
.components = components,
|
.components = components,
|
||||||
.overlay_path = overlay_path,
|
.overlay_path = overlay_path,
|
||||||
.scratch_root = scratch_root,
|
.scratch_path = scratch_path,
|
||||||
};
|
};
|
||||||
return verify_link(req, build_fn);
|
return verify_link(req, build_fn);
|
||||||
}
|
}
|
||||||
@@ -83,10 +138,10 @@ auto discover(const std::string& name, const std::string& version_spec,
|
|||||||
|
|
||||||
std::vector<Candidate> candidates;
|
std::vector<Candidate> candidates;
|
||||||
|
|
||||||
if (auto c = conan_probe(name); c) {
|
if (auto c = conan_probe_fuzzy(name); c) {
|
||||||
candidates.push_back({"conan", recipe_from_conan(*c, name, "conan")});
|
candidates.push_back({"conan", recipe_from_conan(*c, name, "conan")});
|
||||||
}
|
}
|
||||||
if (auto v = vcpkg_probe(name); v) {
|
if (auto v = vcpkg_probe_fuzzy(name); v) {
|
||||||
candidates.push_back({"vcpkg", recipe_from_vcpkg(*v, name, "vcpkg")});
|
candidates.push_back({"vcpkg", recipe_from_vcpkg(*v, name, "vcpkg")});
|
||||||
}
|
}
|
||||||
// Multi-output nix packages keep CMake configs in the `dev` output.
|
// Multi-output nix packages keep CMake configs in the `dev` output.
|
||||||
@@ -109,16 +164,45 @@ auto discover(const std::string& name, const std::string& version_spec,
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
};
|
};
|
||||||
std::optional<NixCmakeCandidate> scan_hit;
|
std::optional<NixCmakeCandidate> scan_hit;
|
||||||
|
std::optional<PcCandidate> pc_hit;
|
||||||
|
std::string realized_dev_path;
|
||||||
if (!info->dev_path.empty()) {
|
if (!info->dev_path.empty()) {
|
||||||
scan_hit = realize_and_scan("dev");
|
scan_hit = realize_and_scan("dev");
|
||||||
|
if (auto realized = realize_path(std::format("{}.dev", name)); realized) {
|
||||||
|
realized_dev_path = *realized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!scan_hit) {
|
if (!scan_hit) {
|
||||||
scan_hit = realize_and_scan("");
|
scan_hit = realize_and_scan("");
|
||||||
}
|
}
|
||||||
|
if (realized_dev_path.empty()) {
|
||||||
|
if (auto realized = realize_path(name); realized) {
|
||||||
|
realized_dev_path = *realized;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (scan_hit) {
|
if (scan_hit) {
|
||||||
candidates.push_back(
|
candidates.push_back(
|
||||||
{"nix-probe", recipe_from_nix_scan(*scan_hit, name, "nix-probe")});
|
{"nix-probe", recipe_from_nix_scan(*scan_hit, name, "nix-probe")});
|
||||||
}
|
}
|
||||||
|
if (auto fm = findmodule_scan(name); fm) {
|
||||||
|
candidates.push_back(
|
||||||
|
{"cmake-findmodule", recipe_from_findmodule(*fm, name, "cmake-findmodule")});
|
||||||
|
}
|
||||||
|
if (!realized_dev_path.empty()) {
|
||||||
|
if (auto p = pc_scan(fs::path{realized_dev_path}, name); p) {
|
||||||
|
pc_hit = std::move(*p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pc_hit) {
|
||||||
|
candidates.push_back(
|
||||||
|
{"pkg-config", recipe_from_pc(*pc_hit, name, "pkg-config")});
|
||||||
|
}
|
||||||
|
if (!realized_dev_path.empty()) {
|
||||||
|
if (auto b = brute_scan(fs::path{realized_dev_path}, name); b) {
|
||||||
|
candidates.push_back(
|
||||||
|
{"brute-force", recipe_from_brute(*b, name, name, "brute-force")});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (candidates.empty()) {
|
if (candidates.empty()) {
|
||||||
return std::unexpected(error(
|
return std::unexpected(error(
|
||||||
@@ -132,9 +216,12 @@ auto discover(const std::string& name, const std::string& version_spec,
|
|||||||
std::format("no candidate for '{}' verified", name), "",
|
std::format("no candidate for '{}' verified", name), "",
|
||||||
std::nullopt, std::nullopt,
|
std::nullopt, std::nullopt,
|
||||||
};
|
};
|
||||||
for (auto& cand : candidates) {
|
for (std::size_t i = 0; i < candidates.size(); ++i) {
|
||||||
|
auto& cand = candidates[i];
|
||||||
|
auto subdir = std::format("{:02}-{}", i + 1, cand.source);
|
||||||
|
auto scratch_path = scratch_root / subdir;
|
||||||
auto verified = try_verify(cand, name, version_spec, components, overlay_path,
|
auto verified = try_verify(cand, name, version_spec, components, overlay_path,
|
||||||
scratch_root, build_fn);
|
scratch_path, build_fn);
|
||||||
if (verified) {
|
if (verified) {
|
||||||
return Discovered{
|
return Discovered{
|
||||||
.recipe = std::move(cand.recipe),
|
.recipe = std::move(cand.recipe),
|
||||||
@@ -147,4 +234,17 @@ auto discover(const std::string& name, const std::string& version_spec,
|
|||||||
return std::unexpected(last_error);
|
return std::unexpected(last_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto last_failure_dir(const std::string& package_name) -> fs::path {
|
||||||
|
auto base = []() -> fs::path {
|
||||||
|
if (auto* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) {
|
||||||
|
return fs::path{xdg} / "cargoxx" / "last-failure";
|
||||||
|
}
|
||||||
|
if (auto* home = std::getenv("HOME"); home && *home) {
|
||||||
|
return fs::path{home} / ".cache" / "cargoxx" / "last-failure";
|
||||||
|
}
|
||||||
|
return fs::current_path() / ".cargoxx-last-failure";
|
||||||
|
}();
|
||||||
|
return base / package_name;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace cargoxx::resolver
|
} // namespace cargoxx::resolver
|
||||||
|
|||||||
213
src/resolver/findmodule_scan.cpp
Normal file
213
src/resolver/findmodule_scan.cpp
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
module cargoxx.resolver;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.exec;
|
||||||
|
import cargoxx.util;
|
||||||
|
|
||||||
|
namespace cargoxx::resolver {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Mirrors nix_cmake_scan / pc_scan. Kept local; Phase A refactor will
|
||||||
|
// extract this to a shared `name_match` module.
|
||||||
|
auto normalize(std::string_view s) -> std::string {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(s.size());
|
||||||
|
for (char c : s) {
|
||||||
|
out += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
}
|
||||||
|
if (out.size() > 3 && out.starts_with("lib")) {
|
||||||
|
out.erase(0, 3);
|
||||||
|
}
|
||||||
|
auto is_vchar = [](char c) {
|
||||||
|
return std::isdigit(static_cast<unsigned char>(c)) || c == '.'
|
||||||
|
|| c == '-' || c == '_';
|
||||||
|
};
|
||||||
|
std::size_t end = out.size();
|
||||||
|
while (end > 0 && is_vchar(out[end - 1])) {
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
bool has_digit = false;
|
||||||
|
for (auto i = end; i < out.size(); ++i) {
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
|
||||||
|
has_digit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (has_digit) {
|
||||||
|
out.erase(end);
|
||||||
|
if (!out.empty() && (out.back() == '-' || out.back() == '_')) {
|
||||||
|
out.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string compact;
|
||||||
|
compact.reserve(out.size());
|
||||||
|
for (char c : out) {
|
||||||
|
if (std::isalnum(static_cast<unsigned char>(c))) {
|
||||||
|
compact += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto match_score(std::string_view stem, std::string_view pkg) -> int {
|
||||||
|
auto s = normalize(stem);
|
||||||
|
auto q = normalize(pkg);
|
||||||
|
if (q.empty()) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (s == q) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!s.empty() && (s.starts_with(q) || q.starts_with(s))) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find CMake's bundled Modules dir by running a one-line script that
|
||||||
|
// prints `CMAKE_ROOT`. We can't use `cmake -E capabilities` because
|
||||||
|
// CMake 4.x dropped the `cmakeRoot` field; `cmake -P` of a script with
|
||||||
|
// `message("${CMAKE_ROOT}")` is the portable path. The message text
|
||||||
|
// goes to stderr in `cmake -P` mode.
|
||||||
|
auto find_modules_dir() -> std::optional<fs::path> {
|
||||||
|
auto script = fs::temp_directory_path() /
|
||||||
|
std::format("cargoxx-findroot-{}.cmake",
|
||||||
|
std::random_device{}());
|
||||||
|
{
|
||||||
|
std::ofstream out{script};
|
||||||
|
if (!out) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
out << "message(\"${CMAKE_ROOT}\")\n";
|
||||||
|
}
|
||||||
|
auto r = exec::run("cmake", {"-P", script.string()},
|
||||||
|
exec::ExecOptions{
|
||||||
|
.cwd = fs::current_path(),
|
||||||
|
.env_overrides = {},
|
||||||
|
.timeout = std::chrono::seconds{5},
|
||||||
|
.inherit_stdio = false,
|
||||||
|
});
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(script, ec);
|
||||||
|
if (!r || r->exit_code != 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
// The message goes to stderr in script mode; trim and use it.
|
||||||
|
std::string_view body = r->stderr_text;
|
||||||
|
while (!body.empty() && (body.back() == '\n' || body.back() == '\r' ||
|
||||||
|
body.back() == ' ' || body.back() == '\t')) {
|
||||||
|
body.remove_suffix(1);
|
||||||
|
}
|
||||||
|
if (body.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
fs::path modules = fs::path{std::string{body}} / "Modules";
|
||||||
|
if (!fs::exists(modules, ec) || ec) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip a leading "Find" and trailing ".cmake" from a filename to get
|
||||||
|
// the find_package stem.
|
||||||
|
auto module_stem(const fs::path& path) -> std::string {
|
||||||
|
auto s = path.stem().string(); // e.g. "FindSQLite3"
|
||||||
|
if (s.starts_with("Find")) {
|
||||||
|
s.erase(0, 4);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto findmodule_scan(const std::string& package_name)
|
||||||
|
-> util::Result<FindModuleCandidate> {
|
||||||
|
auto modules_dir = find_modules_dir();
|
||||||
|
if (!modules_dir) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
"cannot locate CMake's bundled Modules directory",
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Match {
|
||||||
|
int score;
|
||||||
|
std::string stem;
|
||||||
|
fs::path path;
|
||||||
|
};
|
||||||
|
std::vector<Match> matches;
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
for (const auto& entry : fs::directory_iterator{
|
||||||
|
*modules_dir, fs::directory_options::skip_permission_denied, ec}) {
|
||||||
|
if (!entry.is_regular_file()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto name = entry.path().filename().string();
|
||||||
|
if (!name.starts_with("Find") || !name.ends_with(".cmake")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto stem = module_stem(entry.path());
|
||||||
|
matches.push_back(Match{
|
||||||
|
.score = match_score(stem, package_name),
|
||||||
|
.stem = std::move(stem),
|
||||||
|
.path = entry.path(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.empty()) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no Find*.cmake under '{}'", modules_dir->string()),
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ranges::stable_sort(matches, [](const Match& a, const Match& b) {
|
||||||
|
if (a.score != b.score) {
|
||||||
|
return a.score < b.score;
|
||||||
|
}
|
||||||
|
if (a.stem.size() != b.stem.size()) {
|
||||||
|
return a.stem.size() < b.stem.size();
|
||||||
|
}
|
||||||
|
return a.stem < b.stem;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matches.front().score >= 2) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no Find*.cmake matches package name '{}'", package_name),
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& winner = matches.front();
|
||||||
|
|
||||||
|
// FindModules rarely declare IMPORTED targets the same way Config
|
||||||
|
// files do, so scan_imported_targets often comes back empty.
|
||||||
|
// Default to CMake's modern convention `<X>::<X>` and let the
|
||||||
|
// namespaced-from-the-module-body pick override when present.
|
||||||
|
std::vector<std::string> targets;
|
||||||
|
std::ifstream in{winner.path};
|
||||||
|
if (in) {
|
||||||
|
std::string text{std::istreambuf_iterator<char>{in}, {}};
|
||||||
|
targets = scan_imported_targets(text);
|
||||||
|
}
|
||||||
|
if (targets.empty()) {
|
||||||
|
targets.push_back(std::format("{}::{}", winner.stem, winner.stem));
|
||||||
|
} else {
|
||||||
|
targets = filter_public_targets(std::move(targets), winner.stem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FindModuleCandidate{
|
||||||
|
.find_package = std::format("{} REQUIRED", winner.stem),
|
||||||
|
.targets = std::move(targets),
|
||||||
|
.module_file = winner.path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::resolver
|
||||||
193
src/resolver/fuzzy_listing.cpp
Normal file
193
src/resolver/fuzzy_listing.cpp
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
module;
|
||||||
|
|
||||||
|
#include <json.hpp>
|
||||||
|
|
||||||
|
module cargoxx.resolver;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.exec;
|
||||||
|
import cargoxx.util;
|
||||||
|
|
||||||
|
namespace cargoxx::resolver {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto network_error(std::string msg) -> util::Error {
|
||||||
|
return util::Error{util::ErrorCode::ResolutionNetworkError, std::move(msg),
|
||||||
|
"", std::nullopt, std::nullopt};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fetch_tree_paths(const std::string& url) -> util::Result<std::vector<std::string>> {
|
||||||
|
auto r = exec::run("curl", {"-fsSL", "--max-time", "20", url},
|
||||||
|
exec::ExecOptions{
|
||||||
|
.cwd = {},
|
||||||
|
.env_overrides = {},
|
||||||
|
.timeout = std::chrono::seconds{30},
|
||||||
|
.inherit_stdio = false,
|
||||||
|
});
|
||||||
|
if (!r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
if (r->exit_code != 0) {
|
||||||
|
return std::unexpected(network_error(std::format(
|
||||||
|
"curl failed (exit {}): {}", r->exit_code, r->stderr_text)));
|
||||||
|
}
|
||||||
|
nlohmann::json j;
|
||||||
|
try {
|
||||||
|
j = nlohmann::json::parse(r->stdout_text);
|
||||||
|
} catch (const nlohmann::json::parse_error& e) {
|
||||||
|
return std::unexpected(
|
||||||
|
network_error(std::format("tree listing not valid JSON: {}", e.what())));
|
||||||
|
}
|
||||||
|
if (!j.contains("tree") || !j["tree"].is_array()) {
|
||||||
|
return std::unexpected(network_error("tree listing missing 'tree' array"));
|
||||||
|
}
|
||||||
|
std::vector<std::string> out;
|
||||||
|
for (const auto& entry : j["tree"]) {
|
||||||
|
if (entry.contains("path") && entry["path"].is_string()) {
|
||||||
|
out.push_back(entry["path"].get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cache_root() -> fs::path {
|
||||||
|
if (auto* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) {
|
||||||
|
return fs::path{xdg} / "cargoxx";
|
||||||
|
}
|
||||||
|
if (auto* home = std::getenv("HOME"); home && *home) {
|
||||||
|
return fs::path{home} / ".cache" / "cargoxx";
|
||||||
|
}
|
||||||
|
return fs::temp_directory_path() / "cargoxx";
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto INDEX_TTL = std::chrono::hours{24};
|
||||||
|
|
||||||
|
auto load_or_fetch(const std::string& cache_key, const std::string& url)
|
||||||
|
-> util::Result<std::vector<std::string>> {
|
||||||
|
auto path = cache_root() / std::format("{}-index.txt", cache_key);
|
||||||
|
std::error_code ec;
|
||||||
|
if (fs::exists(path, ec) && !ec) {
|
||||||
|
auto age = std::chrono::system_clock::now() -
|
||||||
|
std::chrono::file_clock::to_sys(fs::last_write_time(path));
|
||||||
|
if (age < INDEX_TTL) {
|
||||||
|
std::ifstream in{path};
|
||||||
|
if (in) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(in, line)) {
|
||||||
|
if (!line.empty()) {
|
||||||
|
out.push_back(std::move(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!out.empty()) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto fresh = fetch_tree_paths(url);
|
||||||
|
if (!fresh) {
|
||||||
|
return std::unexpected(fresh.error());
|
||||||
|
}
|
||||||
|
fs::create_directories(path.parent_path(), ec);
|
||||||
|
if (std::ofstream out{path}; out) {
|
||||||
|
for (const auto& p : *fresh) {
|
||||||
|
out << p << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Levenshtein top-k filter with a max-distance gate of ⌈len/4⌉ (min 1).
|
||||||
|
auto top_fuzzy(std::string_view query, const std::vector<std::string>& corpus,
|
||||||
|
std::size_t k) -> std::vector<std::string> {
|
||||||
|
const std::size_t cap = std::max<std::size_t>(1, (query.size() + 3) / 4);
|
||||||
|
struct Scored {
|
||||||
|
std::size_t dist;
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
std::vector<Scored> scored;
|
||||||
|
scored.reserve(corpus.size());
|
||||||
|
for (const auto& c : corpus) {
|
||||||
|
auto d = util::levenshtein(query, c);
|
||||||
|
if (d <= cap) {
|
||||||
|
scored.push_back({d, c});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::ranges::sort(scored, [](const auto& a, const auto& b) {
|
||||||
|
if (a.dist != b.dist) {
|
||||||
|
return a.dist < b.dist;
|
||||||
|
}
|
||||||
|
return a.name < b.name;
|
||||||
|
});
|
||||||
|
std::vector<std::string> out;
|
||||||
|
for (std::size_t i = 0; i < std::min(k, scored.size()); ++i) {
|
||||||
|
out.push_back(std::move(scored[i].name));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto FUZZY_K = std::size_t{3};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto conan_probe_fuzzy(const std::string& name) -> util::Result<ConanRecipe> {
|
||||||
|
if (auto exact = conan_probe(name); exact) {
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
auto index = load_or_fetch(
|
||||||
|
"conan",
|
||||||
|
"https://api.github.com/repos/conan-io/conan-center-index/git/trees/master:recipes");
|
||||||
|
if (!index) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no Conan recipe for '{}' and index fetch failed", name),
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
auto candidates = top_fuzzy(name, *index, FUZZY_K);
|
||||||
|
for (const auto& cand : candidates) {
|
||||||
|
if (auto r = conan_probe(cand); r) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no Conan recipe matches '{}' (tried exact + fuzzy top-{})",
|
||||||
|
name, FUZZY_K),
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto vcpkg_probe_fuzzy(const std::string& name) -> util::Result<VcpkgRecipe> {
|
||||||
|
if (auto exact = vcpkg_probe(name); exact) {
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
auto index = load_or_fetch(
|
||||||
|
"vcpkg",
|
||||||
|
"https://api.github.com/repos/microsoft/vcpkg/git/trees/master:ports");
|
||||||
|
if (!index) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no vcpkg port for '{}' and index fetch failed", name),
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
auto candidates = top_fuzzy(name, *index, FUZZY_K);
|
||||||
|
for (const auto& cand : candidates) {
|
||||||
|
if (auto r = vcpkg_probe(cand); r) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no vcpkg port matches '{}' (tried exact + fuzzy top-{})",
|
||||||
|
name, FUZZY_K),
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::resolver
|
||||||
161
src/resolver/pc_scan.cpp
Normal file
161
src/resolver/pc_scan.cpp
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
module cargoxx.resolver;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.util;
|
||||||
|
|
||||||
|
namespace cargoxx::resolver {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Mirrors `normalize` in nix_cmake_scan.cpp. Kept local rather than
|
||||||
|
// extracted so each scanner is self-contained; a future Phase A
|
||||||
|
// refactor will hoist this into a shared module.
|
||||||
|
auto normalize(std::string_view s) -> std::string {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(s.size());
|
||||||
|
for (char c : s) {
|
||||||
|
out += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
}
|
||||||
|
if (out.size() > 3 && out.starts_with("lib")) {
|
||||||
|
out.erase(0, 3);
|
||||||
|
}
|
||||||
|
auto is_vchar = [](char c) {
|
||||||
|
return std::isdigit(static_cast<unsigned char>(c)) || c == '.'
|
||||||
|
|| c == '-' || c == '_';
|
||||||
|
};
|
||||||
|
std::size_t end = out.size();
|
||||||
|
while (end > 0 && is_vchar(out[end - 1])) {
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
bool has_digit = false;
|
||||||
|
for (auto i = end; i < out.size(); ++i) {
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
|
||||||
|
has_digit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (has_digit) {
|
||||||
|
out.erase(end);
|
||||||
|
if (!out.empty() && (out.back() == '-' || out.back() == '_')) {
|
||||||
|
out.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string compact;
|
||||||
|
compact.reserve(out.size());
|
||||||
|
for (char c : out) {
|
||||||
|
if (std::isalnum(static_cast<unsigned char>(c))) {
|
||||||
|
compact += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same scoring as nix_cmake_scan: 0 exact, 1 prefix-either, 2 fallback.
|
||||||
|
auto match_score(std::string_view stem, std::string_view pkg) -> int {
|
||||||
|
auto s = normalize(stem);
|
||||||
|
auto q = normalize(pkg);
|
||||||
|
if (q.empty()) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (s == q) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!s.empty() && (s.starts_with(q) || q.starts_with(s))) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive: read a few KB to verify the .pc has at least one Name: or
|
||||||
|
// Libs: line. We don't parse the file — `pkg_check_modules` does that
|
||||||
|
// at CMake time — but a sanity check rejects empty/junk files.
|
||||||
|
auto looks_like_pc(const fs::path& path) -> bool {
|
||||||
|
std::ifstream in{path};
|
||||||
|
if (!in) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char buf[4096];
|
||||||
|
in.read(buf, sizeof(buf));
|
||||||
|
std::string_view chunk{buf, static_cast<std::size_t>(in.gcount())};
|
||||||
|
return chunk.find("\nName:") != std::string_view::npos ||
|
||||||
|
chunk.starts_with("Name:") ||
|
||||||
|
chunk.find("\nLibs:") != std::string_view::npos ||
|
||||||
|
chunk.starts_with("Libs:");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto pc_scan(const fs::path& store_path, const std::string& package_name)
|
||||||
|
-> util::Result<PcCandidate> {
|
||||||
|
const auto pc_dir = store_path / "lib" / "pkgconfig";
|
||||||
|
std::error_code ec;
|
||||||
|
if (!fs::exists(pc_dir, ec) || ec) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no pkgconfig directory under '{}'", pc_dir.string()),
|
||||||
|
"", store_path, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Match {
|
||||||
|
int score;
|
||||||
|
std::string stem;
|
||||||
|
fs::path path;
|
||||||
|
};
|
||||||
|
std::vector<Match> matches;
|
||||||
|
|
||||||
|
for (const auto& entry : fs::directory_iterator{
|
||||||
|
pc_dir, fs::directory_options::skip_permission_denied, ec}) {
|
||||||
|
if (!entry.is_regular_file()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.path().extension() != ".pc") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!looks_like_pc(entry.path())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto stem = entry.path().stem().string();
|
||||||
|
matches.push_back(Match{
|
||||||
|
.score = match_score(stem, package_name),
|
||||||
|
.stem = stem,
|
||||||
|
.path = entry.path(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.empty()) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no usable .pc file under '{}'", pc_dir.string()),
|
||||||
|
"", store_path, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ranges::stable_sort(matches, [](const Match& a, const Match& b) {
|
||||||
|
if (a.score != b.score) {
|
||||||
|
return a.score < b.score;
|
||||||
|
}
|
||||||
|
if (a.stem.size() != b.stem.size()) {
|
||||||
|
return a.stem.size() < b.stem.size();
|
||||||
|
}
|
||||||
|
return a.stem < b.stem;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matches.front().score >= 2) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no .pc file under '{}' matches package name '{}'",
|
||||||
|
pc_dir.string(), package_name),
|
||||||
|
"", store_path, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return PcCandidate{
|
||||||
|
.pc_module = std::move(matches.front().stem),
|
||||||
|
.pc_file = std::move(matches.front().path),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::resolver
|
||||||
@@ -77,6 +77,52 @@ auto nix_cmake_scan(const std::filesystem::path& store_path,
|
|||||||
const std::string& package_name)
|
const std::string& package_name)
|
||||||
-> util::Result<NixCmakeCandidate>;
|
-> util::Result<NixCmakeCandidate>;
|
||||||
|
|
||||||
|
// A CMake builtin FindModule-shaped recipe. CMake ships ~160 `Find*.cmake`
|
||||||
|
// modules with the installation; for libraries that have no `Config.cmake`
|
||||||
|
// but a corresponding builtin (sqlite has `FindSQLite3.cmake`, openssl has
|
||||||
|
// `FindOpenSSL.cmake`, threads has `FindThreads.cmake`, …) the recipe
|
||||||
|
// emits `find_package(<X> REQUIRED)` without the CONFIG keyword.
|
||||||
|
struct FindModuleCandidate {
|
||||||
|
std::string find_package; // e.g. "SQLite3 REQUIRED"
|
||||||
|
std::vector<std::string> targets; // e.g. ["SQLite::SQLite3"]
|
||||||
|
std::filesystem::path module_file; // the FindX.cmake we matched
|
||||||
|
};
|
||||||
|
|
||||||
|
// Walks CMake's bundled `Modules/Find*.cmake` and picks the best match
|
||||||
|
// for `package_name`. Returns ResolutionUnknownPackage when no module
|
||||||
|
// scores acceptably.
|
||||||
|
auto findmodule_scan(const std::string& package_name)
|
||||||
|
-> util::Result<FindModuleCandidate>;
|
||||||
|
|
||||||
|
// A pkg-config-shaped recipe: the package ships a `.pc` file rather
|
||||||
|
// than a CMake config. Consumed via `find_package(PkgConfig REQUIRED)`
|
||||||
|
// + `pkg_check_modules(<NAME> REQUIRED IMPORTED_TARGET <pc_module>)`,
|
||||||
|
// linking against the generated `PkgConfig::<NAME>` target.
|
||||||
|
struct PcCandidate {
|
||||||
|
std::string pc_module; // file stem, e.g. "sqlite3"
|
||||||
|
std::filesystem::path pc_file; // path to the .pc on disk
|
||||||
|
};
|
||||||
|
|
||||||
|
// Walks <store_path>/lib/pkgconfig/*.pc and picks the best match for
|
||||||
|
// `package_name`. Returns ResolutionUnknownPackage when no `.pc` file
|
||||||
|
// is present or none scores acceptably.
|
||||||
|
auto pc_scan(const std::filesystem::path& store_path,
|
||||||
|
const std::string& package_name)
|
||||||
|
-> util::Result<PcCandidate>;
|
||||||
|
|
||||||
|
// Last-resort brute-force: every shared/static lib + every include
|
||||||
|
// directory under the store path is wrapped in a synthetic
|
||||||
|
// `<pkg>::<pkg>` INTERFACE IMPORTED target. Used when nothing more
|
||||||
|
// structured matched.
|
||||||
|
struct BruteCandidate {
|
||||||
|
std::vector<std::string> lib_files; // abs paths to lib*.{a,so,dylib}
|
||||||
|
std::vector<std::string> include_dirs; // abs paths under include/
|
||||||
|
};
|
||||||
|
|
||||||
|
auto brute_scan(const std::filesystem::path& store_path,
|
||||||
|
const std::string& package_name)
|
||||||
|
-> util::Result<BruteCandidate>;
|
||||||
|
|
||||||
// Output of a conan-center-index recipe scrape.
|
// Output of a conan-center-index recipe scrape.
|
||||||
struct ConanRecipe {
|
struct ConanRecipe {
|
||||||
std::string find_package; // e.g. "fmt CONFIG REQUIRED"
|
std::string find_package; // e.g. "fmt CONFIG REQUIRED"
|
||||||
@@ -97,6 +143,13 @@ auto parse_conanfile(std::string_view conanfile_text, const std::string& fallbac
|
|||||||
// ResolutionNetworkError.
|
// ResolutionNetworkError.
|
||||||
auto conan_probe(const std::string& name) -> util::Result<ConanRecipe>;
|
auto conan_probe(const std::string& name) -> util::Result<ConanRecipe>;
|
||||||
|
|
||||||
|
// Like `conan_probe`, but on exact-name miss falls back to a fuzzy
|
||||||
|
// match against the conan-center-index recipe listing using
|
||||||
|
// Levenshtein distance ≤ ⌈len/4⌉. Returns the first fuzzy candidate
|
||||||
|
// whose conanfile.py parses cleanly. Internal-only: the user's
|
||||||
|
// originally-typed package name is preserved by the caller.
|
||||||
|
auto conan_probe_fuzzy(const std::string& name) -> util::Result<ConanRecipe>;
|
||||||
|
|
||||||
// Output of a microsoft/vcpkg port usage-file scrape.
|
// Output of a microsoft/vcpkg port usage-file scrape.
|
||||||
struct VcpkgRecipe {
|
struct VcpkgRecipe {
|
||||||
std::string find_package; // e.g. "fmt CONFIG REQUIRED"
|
std::string find_package; // e.g. "fmt CONFIG REQUIRED"
|
||||||
@@ -115,6 +168,10 @@ auto parse_vcpkg_usage(std::string_view usage_text)
|
|||||||
// ResolutionUnknownPackage; transport errors → ResolutionNetworkError.
|
// ResolutionUnknownPackage; transport errors → ResolutionNetworkError.
|
||||||
auto vcpkg_probe(const std::string& name) -> util::Result<VcpkgRecipe>;
|
auto vcpkg_probe(const std::string& name) -> util::Result<VcpkgRecipe>;
|
||||||
|
|
||||||
|
// Like `vcpkg_probe`, but on exact-name miss falls back to fuzzy
|
||||||
|
// matching against the vcpkg/ports listing (Levenshtein ≤ ⌈len/4⌉).
|
||||||
|
auto vcpkg_probe_fuzzy(const std::string& name) -> util::Result<VcpkgRecipe>;
|
||||||
|
|
||||||
// Caller-supplied closure that runs `cargoxx build` (or any equivalent
|
// Caller-supplied closure that runs `cargoxx build` (or any equivalent
|
||||||
// build) on a project rooted at the given path. Injected so the resolver
|
// build) on a project rooted at the given path. Injected so the resolver
|
||||||
// stays decoupled from `cargoxx.cli`.
|
// stays decoupled from `cargoxx.cli`.
|
||||||
@@ -123,12 +180,17 @@ using BuildFn =
|
|||||||
|
|
||||||
struct VerifyLinkRequest {
|
struct VerifyLinkRequest {
|
||||||
linkdb::Recipe candidate; // recipe under test
|
linkdb::Recipe candidate; // recipe under test
|
||||||
std::string source; // "conan" | "vcpkg" | "nix-probe"
|
std::string source; // "conan" | "vcpkg" | "nix-probe" | "pkg-config" | …
|
||||||
std::string package_name;
|
std::string package_name;
|
||||||
std::string version_spec; // user-supplied spec (e.g. "*", "1.2")
|
std::string version_spec; // user-supplied spec (e.g. "*", "1.2")
|
||||||
std::vector<std::string> components;
|
std::vector<std::string> components;
|
||||||
std::filesystem::path overlay_path; // sqlite file we read/write
|
std::filesystem::path overlay_path; // sqlite file we read/write
|
||||||
std::filesystem::path scratch_root; // parent dir for the tmp project
|
// Exact directory the verify project lives in. verify_link creates
|
||||||
|
// it (and its `src/` subdir) but never deletes it; the caller is
|
||||||
|
// responsible for cleanup. discover() puts each probe's project at
|
||||||
|
// a distinct path under `<last-failure-dir>/<NN>-<probe>/` so
|
||||||
|
// every attempt is preserved for inspection.
|
||||||
|
std::filesystem::path scratch_path;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scaffolds a tiny Cargoxx project under `req.scratch_root`, writes a
|
// Scaffolds a tiny Cargoxx project under `req.scratch_root`, writes a
|
||||||
@@ -147,20 +209,34 @@ struct Discovered {
|
|||||||
std::string source;
|
std::string source;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Walks the full auto-resolution chain for a package not present in the
|
// Walks the full auto-resolution chain for a package not present in
|
||||||
// curated linkdb or the user's overlay:
|
// the user's overlay. Each candidate produced by a probe gets its own
|
||||||
// 1. nixpkgs_probe(name) — confirms the attribute exists, captures
|
// verify_link attempt at
|
||||||
// version + out_path
|
// <scratch_root>/<NN>-<probe>/
|
||||||
// 2. for each of conan_probe, vcpkg_probe, nix_cmake_scan(out_path,…):
|
// e.g. `01-conan/`, `02-vcpkg/`, `03-nix-probe/`, `04-pkg-config/`.
|
||||||
// build a candidate linkdb::Recipe, run verify_link on it, return
|
// Subdirs are NOT cleaned up — they're meant for the user to inspect
|
||||||
// on first success
|
// after a failed `cargoxx add`. The caller wipes `<scratch_root>`
|
||||||
// 3. all candidates failed → ResolutionUnsatisfiable
|
// clean at the start of each invocation (cmd_add / cmd_build).
|
||||||
|
//
|
||||||
|
// Returns `Discovered` on the first verify_link success;
|
||||||
|
// `ResolutionUnsatisfiable` when all probes are exhausted; or the
|
||||||
|
// underlying error from `nixpkgs_probe`.
|
||||||
auto discover(const std::string& name, const std::string& version_spec,
|
auto discover(const std::string& name, const std::string& version_spec,
|
||||||
const std::vector<std::string>& components,
|
const std::vector<std::string>& components,
|
||||||
const std::filesystem::path& overlay_path,
|
const std::filesystem::path& overlay_path,
|
||||||
const std::filesystem::path& scratch_root, const BuildFn& build_fn)
|
const std::filesystem::path& scratch_root, const BuildFn& build_fn)
|
||||||
-> util::Result<Discovered>;
|
-> util::Result<Discovered>;
|
||||||
|
|
||||||
|
// The on-disk parent dir that holds per-package verify_link scratch
|
||||||
|
// projects. Resolves to:
|
||||||
|
// $XDG_CACHE_HOME/cargoxx/last-failure/<pkg>/ (when XDG_CACHE_HOME is set)
|
||||||
|
// $HOME/.cache/cargoxx/last-failure/<pkg>/ (else if HOME is set)
|
||||||
|
// <cwd>/.cargoxx-last-failure/<pkg>/ (fallback)
|
||||||
|
// Each `cargoxx add <pkg>` (and `cmd_build`'s auto-resolve) wipes
|
||||||
|
// this dir clean, then discover() repopulates it with one subdir per
|
||||||
|
// probe attempt.
|
||||||
|
auto last_failure_dir(const std::string& package_name) -> std::filesystem::path;
|
||||||
|
|
||||||
// Output of devbox's /v1/resolve API. We capture only the fields cargoxx
|
// Output of devbox's /v1/resolve API. We capture only the fields cargoxx
|
||||||
// uses; the response carries far more metadata (license, summary, per-
|
// uses; the response carries far more metadata (license, summary, per-
|
||||||
// system store hashes) that we deliberately ignore.
|
// system store hashes) that we deliberately ignore.
|
||||||
|
|||||||
@@ -31,24 +31,6 @@ auto write_text(const fs::path& path, std::string_view content) -> util::Result<
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
class TmpProject {
|
|
||||||
public:
|
|
||||||
explicit TmpProject(fs::path root) : root_(std::move(root)) {}
|
|
||||||
~TmpProject() {
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(root_, ec);
|
|
||||||
}
|
|
||||||
TmpProject(const TmpProject&) = delete;
|
|
||||||
TmpProject& operator=(const TmpProject&) = delete;
|
|
||||||
TmpProject(TmpProject&&) = delete;
|
|
||||||
TmpProject& operator=(TmpProject&&) = delete;
|
|
||||||
|
|
||||||
[[nodiscard]] auto path() const -> const fs::path& { return root_; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
fs::path root_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
|
auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
|
||||||
@@ -61,9 +43,6 @@ auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the provisional overlay row. It persists through the build's
|
|
||||||
// own Database::open() call, which is how the candidate recipe gets
|
|
||||||
// surfaced to cmake_lists codegen via Database::resolve.
|
|
||||||
{
|
{
|
||||||
auto db = linkdb::Database::open(req.overlay_path);
|
auto db = linkdb::Database::open(req.overlay_path);
|
||||||
if (!db) {
|
if (!db) {
|
||||||
@@ -74,26 +53,12 @@ auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
|
|||||||
!r) {
|
!r) {
|
||||||
return std::unexpected(r.error());
|
return std::unexpected(r.error());
|
||||||
}
|
}
|
||||||
} // close db before re-opening it inside build_fn
|
|
||||||
|
|
||||||
// Scaffold a tmp project. We bypass cargoxx::cli::cmd_new to avoid a
|
|
||||||
// resolver-on-cli dependency cycle; the manifest + src/main.cpp are
|
|
||||||
// exactly what cmd_build needs for its codegen.
|
|
||||||
std::error_code ec;
|
|
||||||
fs::create_directories(req.scratch_root, ec);
|
|
||||||
if (ec) {
|
|
||||||
return std::unexpected(io_error(
|
|
||||||
std::format("cannot create scratch root: {}", ec.message()),
|
|
||||||
req.scratch_root));
|
|
||||||
}
|
}
|
||||||
auto proj_root = req.scratch_root /
|
|
||||||
std::format("cargoxx-verify-{}-{}", req.package_name,
|
|
||||||
std::random_device{}());
|
|
||||||
TmpProject scope{proj_root};
|
|
||||||
|
|
||||||
|
const auto& proj_root = req.scratch_path;
|
||||||
|
std::error_code ec;
|
||||||
fs::create_directories(proj_root / "src", ec);
|
fs::create_directories(proj_root / "src", ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
// Roll back the provisional row before bailing.
|
|
||||||
auto db = linkdb::Database::open(req.overlay_path);
|
auto db = linkdb::Database::open(req.overlay_path);
|
||||||
if (db) {
|
if (db) {
|
||||||
(void)db->abort_provisional(req.package_name, req.version_spec,
|
(void)db->abort_provisional(req.package_name, req.version_spec,
|
||||||
@@ -129,8 +94,6 @@ auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
|
|||||||
return std::unexpected(r.error());
|
return std::unexpected(r.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty main — exercises find_package + target + linker without
|
|
||||||
// requiring per-package symbol knowledge.
|
|
||||||
if (auto r = write_text(proj_root / "src" / "main.cpp", "int main() {}\n"); !r) {
|
if (auto r = write_text(proj_root / "src" / "main.cpp", "int main() {}\n"); !r) {
|
||||||
auto db = linkdb::Database::open(req.overlay_path);
|
auto db = linkdb::Database::open(req.overlay_path);
|
||||||
if (db) {
|
if (db) {
|
||||||
|
|||||||
30
src/util/levenshtein.cpp
Normal file
30
src/util/levenshtein.cpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
module cargoxx.util;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
|
||||||
|
namespace cargoxx::util {
|
||||||
|
|
||||||
|
auto levenshtein(std::string_view a, std::string_view b) -> std::size_t {
|
||||||
|
if (a.size() < b.size()) {
|
||||||
|
std::swap(a, b);
|
||||||
|
}
|
||||||
|
std::vector<std::size_t> prev(b.size() + 1);
|
||||||
|
std::vector<std::size_t> curr(b.size() + 1);
|
||||||
|
std::iota(prev.begin(), prev.end(), std::size_t{0});
|
||||||
|
|
||||||
|
for (std::size_t i = 1; i <= a.size(); ++i) {
|
||||||
|
curr[0] = i;
|
||||||
|
for (std::size_t j = 1; j <= b.size(); ++j) {
|
||||||
|
auto cost = (a[i - 1] == b[j - 1]) ? std::size_t{0} : std::size_t{1};
|
||||||
|
curr[j] = std::min({
|
||||||
|
prev[j] + 1,
|
||||||
|
curr[j - 1] + 1,
|
||||||
|
prev[j - 1] + cost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
std::swap(prev, curr);
|
||||||
|
}
|
||||||
|
return prev[b.size()];
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::util
|
||||||
@@ -47,6 +47,11 @@ using Result = std::expected<T, Error>;
|
|||||||
|
|
||||||
auto format(const Error& e) -> std::string;
|
auto format(const Error& e) -> std::string;
|
||||||
|
|
||||||
|
// Classic Levenshtein edit distance. Used by the resolver's
|
||||||
|
// Conan/vcpkg fuzzy match when the user's nixpkgs name doesn't appear
|
||||||
|
// verbatim in those repositories' indexes (e.g. `sqlite` ↔ `sqlite3`).
|
||||||
|
auto levenshtein(std::string_view a, std::string_view b) -> std::size_t;
|
||||||
|
|
||||||
// Returns true if `version` (e.g. "10.2", "1.84.0") satisfies `range`.
|
// Returns true if `version` (e.g. "10.2", "1.84.0") satisfies `range`.
|
||||||
//
|
//
|
||||||
// Supported range syntax:
|
// Supported range syntax:
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
find_package(Catch2 3 REQUIRED CONFIG)
|
|
||||||
include(Catch)
|
|
||||||
|
|
||||||
function(cargoxx_add_test name)
|
|
||||||
add_executable(${name} ${name}.cpp)
|
|
||||||
target_link_libraries(${name} PRIVATE cargoxx Catch2::Catch2WithMain)
|
|
||||||
catch_discover_tests(${name})
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
cargoxx_add_test(util_error)
|
|
||||||
cargoxx_add_test(semver_satisfies)
|
|
||||||
cargoxx_add_test(exec_run)
|
|
||||||
cargoxx_add_test(manifest_parse)
|
|
||||||
cargoxx_add_test(manifest_write)
|
|
||||||
cargoxx_add_test(layout_discovery)
|
|
||||||
cargoxx_add_test(lockfile_round_trip)
|
|
||||||
cargoxx_add_test(linkdb_lookup)
|
|
||||||
cargoxx_add_test(linkdb_overlay)
|
|
||||||
cargoxx_add_test(codegen_flake)
|
|
||||||
cargoxx_add_test(codegen_cmake)
|
|
||||||
cargoxx_add_test(cmd_new)
|
|
||||||
cargoxx_add_test(cmd_build)
|
|
||||||
cargoxx_add_test(cmd_run)
|
|
||||||
cargoxx_add_test(cmd_clean)
|
|
||||||
cargoxx_add_test(cmd_add)
|
|
||||||
cargoxx_add_test(cmd_remove)
|
|
||||||
cargoxx_add_test(nixpkgs_probe_parse)
|
|
||||||
cargoxx_add_test(nixpkgs_probe_live)
|
|
||||||
cargoxx_add_test(nix_cmake_scan_parse)
|
|
||||||
cargoxx_add_test(nix_cmake_scan_live)
|
|
||||||
cargoxx_add_test(conan_probe_parse)
|
|
||||||
cargoxx_add_test(conan_probe_live)
|
|
||||||
cargoxx_add_test(vcpkg_probe_parse)
|
|
||||||
cargoxx_add_test(vcpkg_probe_live)
|
|
||||||
cargoxx_add_test(verify_link_unit)
|
|
||||||
cargoxx_add_test(devbox_resolve_parse)
|
|
||||||
cargoxx_add_test(devbox_resolve_live)
|
|
||||||
cargoxx_add_test(nixpkgs_git_resolve)
|
|
||||||
89
tests/brute_scan_parse.cpp
Normal file
89
tests/brute_scan_parse.cpp
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
import cargoxx.resolver;
|
||||||
|
import cargoxx.util;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using cargoxx::resolver::brute_scan;
|
||||||
|
using cargoxx::util::ErrorCode;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto fresh_store() -> std::filesystem::path {
|
||||||
|
auto d = std::filesystem::temp_directory_path() /
|
||||||
|
std::format("cargoxx-brute-scan-{}", std::random_device{}());
|
||||||
|
std::filesystem::create_directories(d / "lib");
|
||||||
|
std::filesystem::create_directories(d / "include");
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_lib(const std::filesystem::path& store, std::string_view name) {
|
||||||
|
std::ofstream{store / "lib" / std::string{name}};
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_include(const std::filesystem::path& store, std::string_view rel) {
|
||||||
|
auto p = store / "include" / rel;
|
||||||
|
std::filesystem::create_directories(p.parent_path());
|
||||||
|
std::ofstream{p};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("brute_scan collects lib*.a and lib*.so files",
|
||||||
|
"[resolver][brute_scan]") {
|
||||||
|
auto store = fresh_store();
|
||||||
|
touch_lib(store, "libfoo.a");
|
||||||
|
touch_lib(store, "libbar.so");
|
||||||
|
touch_lib(store, "libbaz.so.1.2.3");
|
||||||
|
touch_lib(store, "not-a-lib.txt");
|
||||||
|
touch_include(store, "foo.h");
|
||||||
|
|
||||||
|
auto r = brute_scan(store, "foo");
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->lib_files.size() == 3);
|
||||||
|
auto has = [&](std::string_view suffix) {
|
||||||
|
return std::ranges::any_of(r->lib_files, [&](const auto& p) {
|
||||||
|
return std::string_view{p}.ends_with(suffix);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
REQUIRE(has("libfoo.a"));
|
||||||
|
REQUIRE(has("libbar.so"));
|
||||||
|
REQUIRE(has("libbaz.so.1.2.3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("brute_scan exposes include/ as a single search directory",
|
||||||
|
"[resolver][brute_scan]") {
|
||||||
|
auto store = fresh_store();
|
||||||
|
touch_lib(store, "libsdl.a");
|
||||||
|
touch_include(store, "SDL2/SDL.h");
|
||||||
|
touch_include(store, "SDL2/SDL_video.h");
|
||||||
|
|
||||||
|
auto r = brute_scan(store, "sdl");
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->include_dirs.size() == 1);
|
||||||
|
REQUIRE(std::string_view{r->include_dirs[0]}.ends_with("/include"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("brute_scan errors when neither libs nor headers are present",
|
||||||
|
"[resolver][brute_scan]") {
|
||||||
|
auto d = std::filesystem::temp_directory_path() /
|
||||||
|
std::format("cargoxx-brute-empty-{}", std::random_device{}());
|
||||||
|
std::filesystem::create_directories(d);
|
||||||
|
|
||||||
|
auto r = brute_scan(d, "ghost");
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("brute_scan emits sorted lib paths for deterministic codegen",
|
||||||
|
"[resolver][brute_scan]") {
|
||||||
|
auto store = fresh_store();
|
||||||
|
touch_lib(store, "libzz.a");
|
||||||
|
touch_lib(store, "libaa.a");
|
||||||
|
touch_lib(store, "libmm.so");
|
||||||
|
|
||||||
|
auto r = brute_scan(store, "x");
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->lib_files.size() == 3);
|
||||||
|
REQUIRE(std::ranges::is_sorted(r->lib_files));
|
||||||
|
}
|
||||||
@@ -156,7 +156,7 @@ TEST_CASE("cmd_build fails for an unknown dep", "[cli][build]") {
|
|||||||
|
|
||||||
auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent));
|
auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent));
|
||||||
REQUIRE_FALSE(r.has_value());
|
REQUIRE_FALSE(r.has_value());
|
||||||
REQUIRE(r.error().code == ErrorCode::LinkdbUnknownPackage);
|
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("cmd_build --release writes a release-typed build/CMakeLists",
|
TEST_CASE("cmd_build --release writes a release-typed build/CMakeLists",
|
||||||
|
|||||||
65
tests/cmd_linkdb_add.cpp
Normal file
65
tests/cmd_linkdb_add.cpp
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
import cargoxx.cli;
|
||||||
|
import cargoxx.linkdb;
|
||||||
|
import cargoxx.util;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using cargoxx::cli::cmd_linkdb_add;
|
||||||
|
using cargoxx::linkdb::Database;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto fresh_overlay() -> std::filesystem::path {
|
||||||
|
auto d = std::filesystem::temp_directory_path() /
|
||||||
|
std::format("cargoxx-linkdb-add-test-{}", std::random_device{}());
|
||||||
|
std::filesystem::create_directories(d);
|
||||||
|
return d / "overlay.sqlite";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("cmd_linkdb_add inserts a recipe that resolve() can read back",
|
||||||
|
"[cli][linkdb_add]") {
|
||||||
|
auto overlay = fresh_overlay();
|
||||||
|
|
||||||
|
auto r = cmd_linkdb_add(
|
||||||
|
"sqlite3", "*", "SQLite3 REQUIRED", {"SQLite::SQLite3"}, "sqlite",
|
||||||
|
overlay);
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
|
||||||
|
auto db = Database::open(overlay);
|
||||||
|
REQUIRE(db.has_value());
|
||||||
|
auto rec = db->resolve("sqlite3", "*");
|
||||||
|
REQUIRE(rec.has_value());
|
||||||
|
REQUIRE(rec->source == "manual");
|
||||||
|
REQUIRE(rec->find_package == "SQLite3 REQUIRED");
|
||||||
|
REQUIRE(rec->targets == std::vector<std::string>{"SQLite::SQLite3"});
|
||||||
|
REQUIRE(rec->nixpkgs_attr == "sqlite");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("cmd_linkdb_add evicts auto-discovered rows for the same package",
|
||||||
|
"[cli][linkdb_add]") {
|
||||||
|
auto overlay = fresh_overlay();
|
||||||
|
auto db = Database::open(overlay);
|
||||||
|
REQUIRE(db.has_value());
|
||||||
|
|
||||||
|
// Seed an auto-source row first.
|
||||||
|
cargoxx::linkdb::Recipe auto_recipe{
|
||||||
|
.nixpkgs_attr = "sqlite",
|
||||||
|
.find_package = "auto CONFIG REQUIRED",
|
||||||
|
.targets = {"auto::auto"},
|
||||||
|
.source = "nix-probe",
|
||||||
|
};
|
||||||
|
REQUIRE(db->insert_provisional("sqlite3", "*", auto_recipe, "nix-probe")
|
||||||
|
.has_value());
|
||||||
|
REQUIRE(db->confirm_provisional("sqlite3", "*", "nix-probe").has_value());
|
||||||
|
|
||||||
|
REQUIRE(cmd_linkdb_add("sqlite3", "*", "SQLite3 REQUIRED",
|
||||||
|
{"SQLite::SQLite3"}, "sqlite", overlay)
|
||||||
|
.has_value());
|
||||||
|
|
||||||
|
auto rec = db->resolve("sqlite3", "*");
|
||||||
|
REQUIRE(rec.has_value());
|
||||||
|
REQUIRE(rec->source == "manual");
|
||||||
|
}
|
||||||
@@ -95,7 +95,7 @@ TEST_CASE("cmake_lists for a library-only project", "[codegen][cmake]") {
|
|||||||
|
|
||||||
auto out = cmake_lists(in);
|
auto out = cmake_lists(in);
|
||||||
REQUIRE(out.find("add_library(widget STATIC)") != std::string::npos);
|
REQUIRE(out.find("add_library(widget STATIC)") != std::string::npos);
|
||||||
REQUIRE(out.find("FILE_SET CXX_MODULES FILES") != 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);
|
||||||
}
|
}
|
||||||
@@ -342,3 +342,70 @@ TEST_CASE("cmake_lists threads dev_recipes through find_package and tests",
|
|||||||
auto block = out.substr(link, end - link);
|
auto block = out.substr(link, end - link);
|
||||||
REQUIRE(block.find("Catch2::Catch2WithMain") != std::string::npos);
|
REQUIRE(block.find("Catch2::Catch2WithMain") != std::string::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("cmake_lists emits pkg_check_modules for pkg_config recipes",
|
||||||
|
"[codegen][cmake]") {
|
||||||
|
Manifest m{
|
||||||
|
.package = pkg("app"),
|
||||||
|
.dependencies = {{.name = "sqlite", .version_spec = "*"}},
|
||||||
|
};
|
||||||
|
DiscoveredLayout layout{
|
||||||
|
.library = std::nullopt,
|
||||||
|
.binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")},
|
||||||
|
.tests = {},
|
||||||
|
.examples = {},
|
||||||
|
};
|
||||||
|
Lockfile lock = lock_minimal();
|
||||||
|
Recipe sqlite{
|
||||||
|
.nixpkgs_attr = "sqlite",
|
||||||
|
.find_package = "PkgConfig REQUIRED",
|
||||||
|
.targets = {"PkgConfig::SQLITE3"},
|
||||||
|
.source = "pkg-config",
|
||||||
|
.pkg_config_module = "sqlite3",
|
||||||
|
};
|
||||||
|
GenerateInputs in{m, layout, lock, {sqlite}, {}, ROOT};
|
||||||
|
|
||||||
|
auto out = cmake_lists(in);
|
||||||
|
REQUIRE(out.find("find_package(PkgConfig REQUIRED)") != std::string::npos);
|
||||||
|
REQUIRE(out.find("pkg_check_modules(SQLITE3 REQUIRED IMPORTED_TARGET sqlite3)") !=
|
||||||
|
std::string::npos);
|
||||||
|
auto link = out.find("target_link_libraries(app_bin PRIVATE");
|
||||||
|
REQUIRE(link != std::string::npos);
|
||||||
|
auto block = out.substr(link, out.find(')', link) - link);
|
||||||
|
REQUIRE(block.find("PkgConfig::SQLITE3") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("cmake_lists synthesizes INTERFACE IMPORTED target for brute-force "
|
||||||
|
"recipes",
|
||||||
|
"[codegen][cmake]") {
|
||||||
|
Manifest m{
|
||||||
|
.package = pkg("app"),
|
||||||
|
.dependencies = {{.name = "obscure", .version_spec = "*"}},
|
||||||
|
};
|
||||||
|
DiscoveredLayout layout{
|
||||||
|
.library = std::nullopt,
|
||||||
|
.binaries = {src_target(TargetKind::Binary, "app", "src/main.cpp")},
|
||||||
|
.tests = {},
|
||||||
|
.examples = {},
|
||||||
|
};
|
||||||
|
Lockfile lock = lock_minimal();
|
||||||
|
Recipe brute{
|
||||||
|
.nixpkgs_attr = "obscure",
|
||||||
|
.find_package = "",
|
||||||
|
.targets = {"obscure::obscure"},
|
||||||
|
.source = "brute-force",
|
||||||
|
.brute_force_libs = {"/nix/store/abc-obscure/lib/libobscure.a"},
|
||||||
|
.brute_force_includes = {"/nix/store/abc-obscure/include"},
|
||||||
|
};
|
||||||
|
GenerateInputs in{m, layout, lock, {brute}, {}, ROOT};
|
||||||
|
|
||||||
|
auto out = cmake_lists(in);
|
||||||
|
REQUIRE(out.find("add_library(obscure::obscure INTERFACE IMPORTED)") !=
|
||||||
|
std::string::npos);
|
||||||
|
REQUIRE(out.find("INTERFACE_LINK_LIBRARIES") != std::string::npos);
|
||||||
|
REQUIRE(out.find("/nix/store/abc-obscure/lib/libobscure.a") !=
|
||||||
|
std::string::npos);
|
||||||
|
REQUIRE(out.find("INTERFACE_INCLUDE_DIRECTORIES") != std::string::npos);
|
||||||
|
REQUIRE(out.find("/nix/store/abc-obscure/include") != std::string::npos);
|
||||||
|
REQUIRE(out.find("find_package()") == std::string::npos);
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,6 +73,37 @@ auto dep_pkg(std::string name, std::string version,
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("flake_nix adds pkgs.pkg-config to nativeBuildInputs only when needed",
|
||||||
|
"[codegen][flake]") {
|
||||||
|
Manifest m{pkg("app"), {dep("sqlite", "*")}, {}};
|
||||||
|
DiscoveredLayout layout{};
|
||||||
|
Lockfile lock{1, {root_pkg("app", "0.1.0")}};
|
||||||
|
std::vector<Recipe> recipes = {Recipe{
|
||||||
|
.nixpkgs_attr = "sqlite",
|
||||||
|
.find_package = "PkgConfig REQUIRED",
|
||||||
|
.targets = {"PkgConfig::SQLITE3"},
|
||||||
|
.source = "pkg-config",
|
||||||
|
.pkg_config_module = "sqlite3",
|
||||||
|
}};
|
||||||
|
GenerateInputs in{m, layout, lock, recipes, {}, "/tmp/app"};
|
||||||
|
|
||||||
|
auto out = flake_nix(in);
|
||||||
|
REQUIRE(out.find("pkgs.pkg-config") != std::string::npos);
|
||||||
|
REQUIRE(out.find("pkgs.sqlite") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("flake_nix omits pkgs.pkg-config when no recipe needs it",
|
||||||
|
"[codegen][flake]") {
|
||||||
|
Manifest m{pkg("hello"), {dep("fmt", "*")}, {}};
|
||||||
|
DiscoveredLayout layout{};
|
||||||
|
Lockfile lock{1, {root_pkg("hello", "0.1.0")}};
|
||||||
|
std::vector<Recipe> recipes = {recipe("fmt_10")};
|
||||||
|
GenerateInputs in{m, layout, lock, recipes, {}, "/tmp/hello"};
|
||||||
|
|
||||||
|
auto out = flake_nix(in);
|
||||||
|
REQUIRE(out.find("pkgs.pkg-config") == std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("flake_nix always emits the shared nixos-unstable nixpkgs input",
|
TEST_CASE("flake_nix always emits the shared nixos-unstable nixpkgs input",
|
||||||
"[codegen][flake]") {
|
"[codegen][flake]") {
|
||||||
Manifest m{pkg("hello"), {}, {}};
|
Manifest m{pkg("hello"), {}, {}};
|
||||||
|
|||||||
67
tests/findmodule_scan_live.cpp
Normal file
67
tests/findmodule_scan_live.cpp
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
import cargoxx.resolver;
|
||||||
|
import cargoxx.util;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using cargoxx::resolver::findmodule_scan;
|
||||||
|
using cargoxx::util::ErrorCode;
|
||||||
|
|
||||||
|
// `findmodule_scan` shells out to `cmake -P` to discover CMAKE_ROOT.
|
||||||
|
// Gate the test on the same env var we use for other live probes so
|
||||||
|
// CI without cmake can still pass.
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto live_tests_enabled() -> bool {
|
||||||
|
auto* e = std::getenv("CARGOXX_NETWORK_TESTS");
|
||||||
|
return e != nullptr && *e != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("findmodule_scan picks FindSQLite3 for 'sqlite'",
|
||||||
|
"[resolver][findmodule_scan][live]") {
|
||||||
|
if (!live_tests_enabled()) {
|
||||||
|
SKIP("CARGOXX_NETWORK_TESTS not set");
|
||||||
|
}
|
||||||
|
auto r = findmodule_scan("sqlite");
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->find_package == "SQLite3 REQUIRED");
|
||||||
|
REQUIRE_FALSE(r->targets.empty());
|
||||||
|
auto has = [&](std::string_view t) {
|
||||||
|
return std::ranges::find(r->targets, std::string{t}) != r->targets.end();
|
||||||
|
};
|
||||||
|
REQUIRE((has("SQLite3::SQLite3") || has("SQLite::SQLite3")));
|
||||||
|
REQUIRE(r->module_file.filename() == "FindSQLite3.cmake");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("findmodule_scan picks FindZLIB for 'zlib'",
|
||||||
|
"[resolver][findmodule_scan][live]") {
|
||||||
|
if (!live_tests_enabled()) {
|
||||||
|
SKIP("CARGOXX_NETWORK_TESTS not set");
|
||||||
|
}
|
||||||
|
auto r = findmodule_scan("zlib");
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->find_package == "ZLIB REQUIRED");
|
||||||
|
REQUIRE(r->module_file.filename() == "FindZLIB.cmake");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("findmodule_scan picks FindThreads for 'threads'",
|
||||||
|
"[resolver][findmodule_scan][live]") {
|
||||||
|
if (!live_tests_enabled()) {
|
||||||
|
SKIP("CARGOXX_NETWORK_TESTS not set");
|
||||||
|
}
|
||||||
|
auto r = findmodule_scan("threads");
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->find_package == "Threads REQUIRED");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("findmodule_scan errors for a totally unknown name",
|
||||||
|
"[resolver][findmodule_scan][live]") {
|
||||||
|
if (!live_tests_enabled()) {
|
||||||
|
SKIP("CARGOXX_NETWORK_TESTS not set");
|
||||||
|
}
|
||||||
|
auto r = findmodule_scan("definitelynotacmaketmodule12345");
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
|
||||||
|
}
|
||||||
59
tests/last_failure_dir.cpp
Normal file
59
tests/last_failure_dir.cpp
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
import cargoxx.resolver;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using cargoxx::resolver::last_failure_dir;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct EnvScope {
|
||||||
|
EnvScope(const char* k, std::optional<std::string> v) : key(k) {
|
||||||
|
if (auto* prior = std::getenv(key)) {
|
||||||
|
previous = std::string{prior};
|
||||||
|
}
|
||||||
|
if (v) {
|
||||||
|
setenv(key, v->c_str(), 1);
|
||||||
|
} else {
|
||||||
|
unsetenv(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
~EnvScope() {
|
||||||
|
if (previous) {
|
||||||
|
setenv(key, previous->c_str(), 1);
|
||||||
|
} else {
|
||||||
|
unsetenv(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const char* key;
|
||||||
|
std::optional<std::string> previous;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("last_failure_dir honors XDG_CACHE_HOME when set",
|
||||||
|
"[resolver][last_failure_dir]") {
|
||||||
|
EnvScope xdg{"XDG_CACHE_HOME", "/tmp/xdg-test"};
|
||||||
|
|
||||||
|
auto p = last_failure_dir("sqlite");
|
||||||
|
REQUIRE(p.string() == "/tmp/xdg-test/cargoxx/last-failure/sqlite");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("last_failure_dir falls back to $HOME/.cache when XDG is unset",
|
||||||
|
"[resolver][last_failure_dir]") {
|
||||||
|
EnvScope xdg{"XDG_CACHE_HOME", std::nullopt};
|
||||||
|
EnvScope home{"HOME", "/tmp/home-test"};
|
||||||
|
|
||||||
|
auto p = last_failure_dir("fmt");
|
||||||
|
REQUIRE(p.string() == "/tmp/home-test/.cache/cargoxx/last-failure/fmt");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("last_failure_dir uses cwd-based fallback when neither var is set",
|
||||||
|
"[resolver][last_failure_dir]") {
|
||||||
|
EnvScope xdg{"XDG_CACHE_HOME", std::nullopt};
|
||||||
|
EnvScope home{"HOME", std::nullopt};
|
||||||
|
|
||||||
|
auto p = last_failure_dir("obscure");
|
||||||
|
REQUIRE(p.filename() == "obscure");
|
||||||
|
REQUIRE(p.parent_path().filename() == ".cargoxx-last-failure");
|
||||||
|
}
|
||||||
36
tests/levenshtein.cpp
Normal file
36
tests/levenshtein.cpp
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
import cargoxx.util;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using cargoxx::util::levenshtein;
|
||||||
|
|
||||||
|
TEST_CASE("levenshtein of equal strings is zero", "[util][levenshtein]") {
|
||||||
|
REQUIRE(levenshtein("", "") == 0);
|
||||||
|
REQUIRE(levenshtein("sqlite", "sqlite") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("levenshtein counts a single suffix character", "[util][levenshtein]") {
|
||||||
|
REQUIRE(levenshtein("sqlite", "sqlite3") == 1);
|
||||||
|
REQUIRE(levenshtein("fmt", "fmtlib") == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("levenshtein is symmetric", "[util][levenshtein]") {
|
||||||
|
REQUIRE(levenshtein("sqlite", "sqlite3") == levenshtein("sqlite3", "sqlite"));
|
||||||
|
REQUIRE(levenshtein("abseil-cpp", "absl") == levenshtein("absl", "abseil-cpp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("levenshtein counts a single substitution", "[util][levenshtein]") {
|
||||||
|
REQUIRE(levenshtein("kitten", "sitten") == 1);
|
||||||
|
REQUIRE(levenshtein("kitten", "kittes") == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("levenshtein matches the classic kitten/sitting example",
|
||||||
|
"[util][levenshtein]") {
|
||||||
|
REQUIRE(levenshtein("kitten", "sitting") == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("levenshtein handles empty inputs", "[util][levenshtein]") {
|
||||||
|
REQUIRE(levenshtein("", "abc") == 3);
|
||||||
|
REQUIRE(levenshtein("abc", "") == 3);
|
||||||
|
}
|
||||||
89
tests/pc_scan_parse.cpp
Normal file
89
tests/pc_scan_parse.cpp
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
import cargoxx.resolver;
|
||||||
|
import cargoxx.util;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using cargoxx::resolver::pc_scan;
|
||||||
|
using cargoxx::util::ErrorCode;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto fresh_store() -> std::filesystem::path {
|
||||||
|
auto d = std::filesystem::temp_directory_path() /
|
||||||
|
std::format("cargoxx-pc-scan-{}", std::random_device{}());
|
||||||
|
std::filesystem::create_directories(d / "lib" / "pkgconfig");
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_pc(const std::filesystem::path& store, std::string_view name,
|
||||||
|
std::string_view content =
|
||||||
|
"Name: x\nDescription: x\nLibs: -lx\n") {
|
||||||
|
std::ofstream{store / "lib" / "pkgconfig" / std::string{name}} << content;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("pc_scan picks the exact-name .pc when present",
|
||||||
|
"[resolver][pc_scan]") {
|
||||||
|
auto store = fresh_store();
|
||||||
|
touch_pc(store, "sqlite3.pc");
|
||||||
|
|
||||||
|
auto r = pc_scan(store, "sqlite3");
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->pc_module == "sqlite3");
|
||||||
|
REQUIRE(r->pc_file.filename() == "sqlite3.pc");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("pc_scan picks sqlite3.pc for nixpkgs name 'sqlite'",
|
||||||
|
"[resolver][pc_scan]") {
|
||||||
|
auto store = fresh_store();
|
||||||
|
touch_pc(store, "sqlite3.pc");
|
||||||
|
|
||||||
|
auto r = pc_scan(store, "sqlite");
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->pc_module == "sqlite3");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("pc_scan picks the best match among multiple .pc files",
|
||||||
|
"[resolver][pc_scan]") {
|
||||||
|
auto store = fresh_store();
|
||||||
|
touch_pc(store, "zlib.pc");
|
||||||
|
touch_pc(store, "sqlite3.pc");
|
||||||
|
touch_pc(store, "unrelated.pc");
|
||||||
|
|
||||||
|
auto r = pc_scan(store, "zlib");
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->pc_module == "zlib");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("pc_scan returns ResolutionUnknownPackage when nothing matches",
|
||||||
|
"[resolver][pc_scan]") {
|
||||||
|
auto store = fresh_store();
|
||||||
|
touch_pc(store, "totally-unrelated.pc");
|
||||||
|
|
||||||
|
auto r = pc_scan(store, "sqlite");
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("pc_scan errors when lib/pkgconfig is missing",
|
||||||
|
"[resolver][pc_scan]") {
|
||||||
|
auto d = std::filesystem::temp_directory_path() /
|
||||||
|
std::format("cargoxx-pc-empty-{}", std::random_device{}());
|
||||||
|
std::filesystem::create_directories(d);
|
||||||
|
|
||||||
|
auto r = pc_scan(d, "sqlite");
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("pc_scan skips .pc files that look like junk",
|
||||||
|
"[resolver][pc_scan]") {
|
||||||
|
auto store = fresh_store();
|
||||||
|
touch_pc(store, "sqlite3.pc", "this is not a real pc file\n");
|
||||||
|
|
||||||
|
auto r = pc_scan(store, "sqlite3");
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ResolutionUnknownPackage);
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ auto make_request(const std::filesystem::path& parent) -> VerifyLinkRequest {
|
|||||||
.version_spec = "*",
|
.version_spec = "*",
|
||||||
.components = {},
|
.components = {},
|
||||||
.overlay_path = parent / "overlay.sqlite",
|
.overlay_path = parent / "overlay.sqlite",
|
||||||
.scratch_root = parent / "scratch",
|
.scratch_path = parent / "scratch" / "01-conan",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,8 @@ TEST_CASE("verify_link rolls the provisional row back when the build fails",
|
|||||||
REQUIRE(rec.error().code == cargoxx::util::ErrorCode::LinkdbUnknownPackage);
|
REQUIRE(rec.error().code == cargoxx::util::ErrorCode::LinkdbUnknownPackage);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("verify_link cleans up its scratch project", "[resolver][verify_link]") {
|
TEST_CASE("verify_link preserves its scratch project for inspection",
|
||||||
|
"[resolver][verify_link]") {
|
||||||
auto parent = fresh_dir();
|
auto parent = fresh_dir();
|
||||||
auto req = make_request(parent);
|
auto req = make_request(parent);
|
||||||
std::filesystem::path captured;
|
std::filesystem::path captured;
|
||||||
@@ -111,6 +112,7 @@ TEST_CASE("verify_link cleans up its scratch project", "[resolver][verify_link]"
|
|||||||
return cargoxx::util::Result<void>{};
|
return cargoxx::util::Result<void>{};
|
||||||
});
|
});
|
||||||
|
|
||||||
REQUIRE_FALSE(captured.empty());
|
REQUIRE(captured == req.scratch_path);
|
||||||
REQUIRE_FALSE(std::filesystem::exists(captured));
|
REQUIRE(std::filesystem::exists(captured / "Cargoxx.toml"));
|
||||||
|
REQUIRE(std::filesystem::exists(captured / "src" / "main.cpp"));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user