[M3] add lockfile types + parse/write
This commit is contained in:
@@ -57,6 +57,10 @@ All notable changes to cargoxx will be documented in this file.
|
|||||||
are deferred to the M2 follow-up commit.
|
are deferred to the M2 follow-up commit.
|
||||||
`tests/linkdb_lookup.cpp` covers 13 cases including a smoke test that
|
`tests/linkdb_lookup.cpp` covers 13 cases including a smoke test that
|
||||||
resolves all 25 curated packages.
|
resolves all 25 curated packages.
|
||||||
|
- `cargoxx.lockfile`: `Lockfile`, `LockfilePackage` types and `parse(path)` /
|
||||||
|
`write(lock, path)` matching the format in `SPEC.md` §5. Also
|
||||||
|
`Lockfile::nixpkgs_rev()` returns the shared revision (codegen will
|
||||||
|
consume this in M3). `tests/lockfile_round_trip.cpp` covers 9 cases.
|
||||||
- SQLite overlay: `Database::open(overlay_path)` now opens (and creates,
|
- SQLite overlay: `Database::open(overlay_path)` now opens (and creates,
|
||||||
if missing) a per-user `linkdb.sqlite` cache, applying the schema from
|
if missing) a per-user `linkdb.sqlite` cache, applying the schema from
|
||||||
`SPEC.md` §9 idempotently. Default path is
|
`SPEC.md` §9 idempotently. Default path is
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ target_sources(cargoxx
|
|||||||
src/manifest/parser.cpp
|
src/manifest/parser.cpp
|
||||||
src/manifest/writer.cpp
|
src/manifest/writer.cpp
|
||||||
src/layout/layout.cpp
|
src/layout/layout.cpp
|
||||||
|
src/lockfile/lockfile.cpp
|
||||||
src/linkdb/recipe.cpp
|
src/linkdb/recipe.cpp
|
||||||
src/linkdb/curated.cpp
|
src/linkdb/curated.cpp
|
||||||
src/linkdb/overlay.cpp
|
src/linkdb/overlay.cpp
|
||||||
|
|||||||
175
src/lockfile/lockfile.cpp
Normal file
175
src/lockfile/lockfile.cpp
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
module;
|
||||||
|
|
||||||
|
#include <toml.hpp>
|
||||||
|
|
||||||
|
module cargoxx.lockfile;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.util;
|
||||||
|
|
||||||
|
namespace cargoxx::lockfile {
|
||||||
|
|
||||||
|
auto Lockfile::nixpkgs_rev() const -> std::optional<std::string> {
|
||||||
|
for (const auto& p : packages) {
|
||||||
|
if (p.nixpkgs_rev && !p.nixpkgs_rev->empty()) {
|
||||||
|
return p.nixpkgs_rev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using util::Error;
|
||||||
|
using util::ErrorCode;
|
||||||
|
|
||||||
|
auto err(ErrorCode code, std::string msg, std::filesystem::path path) -> Error {
|
||||||
|
return Error{code, std::move(msg), "", std::move(path), std::nullopt};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto extract_string_array(const toml::array& arr, std::string_view field,
|
||||||
|
const std::filesystem::path& path)
|
||||||
|
-> util::Result<std::vector<std::string>> {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
out.reserve(arr.size());
|
||||||
|
for (const auto& el : arr) {
|
||||||
|
if (auto s = el.value<std::string>()) {
|
||||||
|
out.push_back(*s);
|
||||||
|
} else {
|
||||||
|
return std::unexpected(err(ErrorCode::ManifestInvalidField,
|
||||||
|
std::format("'{}' must be an array of strings", field),
|
||||||
|
path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parse_package(const toml::table& tbl, const std::filesystem::path& path)
|
||||||
|
-> util::Result<LockfilePackage> {
|
||||||
|
LockfilePackage pkg;
|
||||||
|
|
||||||
|
if (auto v = tbl["name"].value<std::string>()) {
|
||||||
|
pkg.name = *v;
|
||||||
|
} else {
|
||||||
|
return std::unexpected(
|
||||||
|
err(ErrorCode::ManifestInvalidField, "lockfile package missing 'name'", path));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto v = tbl["version"].value<std::string>()) {
|
||||||
|
pkg.version = *v;
|
||||||
|
} else {
|
||||||
|
return std::unexpected(
|
||||||
|
err(ErrorCode::ManifestInvalidField, "lockfile package missing 'version'", path));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* deps = tbl["dependencies"].as_array()) {
|
||||||
|
auto r = extract_string_array(*deps, "dependencies", path);
|
||||||
|
if (!r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
pkg.dependencies = std::move(*r);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto v = tbl["nixpkgs_attr"].value<std::string>()) {
|
||||||
|
pkg.nixpkgs_attr = *v;
|
||||||
|
}
|
||||||
|
if (auto v = tbl["nixpkgs_rev"].value<std::string>()) {
|
||||||
|
pkg.nixpkgs_rev = *v;
|
||||||
|
}
|
||||||
|
if (auto v = tbl["linkdb_source"].value<std::string>()) {
|
||||||
|
pkg.linkdb_source = *v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto parse(const std::filesystem::path& path) -> util::Result<Lockfile> {
|
||||||
|
std::error_code ec;
|
||||||
|
if (!std::filesystem::exists(path, ec) || ec) {
|
||||||
|
return std::unexpected(err(ErrorCode::ManifestNotFound,
|
||||||
|
std::format("lockfile not found: {}", path.string()), path));
|
||||||
|
}
|
||||||
|
|
||||||
|
toml::table root;
|
||||||
|
try {
|
||||||
|
root = toml::parse_file(path.string());
|
||||||
|
} catch (const toml::parse_error& e) {
|
||||||
|
return std::unexpected(err(ErrorCode::ManifestParseError,
|
||||||
|
std::format("toml parse error: {}", e.description()), path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Lockfile lock;
|
||||||
|
if (auto v = root["version"].value<int>()) {
|
||||||
|
lock.version = *v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* arr = root["package"].as_array()) {
|
||||||
|
lock.packages.reserve(arr->size());
|
||||||
|
for (const auto& el : *arr) {
|
||||||
|
const auto* tbl = el.as_table();
|
||||||
|
if (!tbl) {
|
||||||
|
return std::unexpected(err(ErrorCode::ManifestInvalidField,
|
||||||
|
"[[package]] entries must be tables", path));
|
||||||
|
}
|
||||||
|
auto p = parse_package(*tbl, path);
|
||||||
|
if (!p) {
|
||||||
|
return std::unexpected(p.error());
|
||||||
|
}
|
||||||
|
lock.packages.push_back(std::move(*p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto write(const Lockfile& lock, const std::filesystem::path& path) -> util::Result<void> {
|
||||||
|
toml::table root;
|
||||||
|
root.insert_or_assign("version", lock.version);
|
||||||
|
|
||||||
|
toml::array packages;
|
||||||
|
for (const auto& p : lock.packages) {
|
||||||
|
toml::table tbl;
|
||||||
|
tbl.insert_or_assign("name", p.name);
|
||||||
|
tbl.insert_or_assign("version", p.version);
|
||||||
|
if (!p.dependencies.empty()) {
|
||||||
|
toml::array deps;
|
||||||
|
for (const auto& d : p.dependencies) {
|
||||||
|
deps.push_back(d);
|
||||||
|
}
|
||||||
|
tbl.insert_or_assign("dependencies", std::move(deps));
|
||||||
|
}
|
||||||
|
if (p.nixpkgs_attr) {
|
||||||
|
tbl.insert_or_assign("nixpkgs_attr", *p.nixpkgs_attr);
|
||||||
|
}
|
||||||
|
if (p.nixpkgs_rev) {
|
||||||
|
tbl.insert_or_assign("nixpkgs_rev", *p.nixpkgs_rev);
|
||||||
|
}
|
||||||
|
if (p.linkdb_source) {
|
||||||
|
tbl.insert_or_assign("linkdb_source", *p.linkdb_source);
|
||||||
|
}
|
||||||
|
packages.push_back(std::move(tbl));
|
||||||
|
}
|
||||||
|
root.insert_or_assign("package", std::move(packages));
|
||||||
|
|
||||||
|
std::ofstream out{path};
|
||||||
|
if (!out) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::Internal,
|
||||||
|
std::format("cannot open lockfile for writing: {}", path.string()),
|
||||||
|
"", path, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out << root << '\n';
|
||||||
|
if (!out) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::Internal,
|
||||||
|
std::format("write failed: {}", path.string()),
|
||||||
|
"", path, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::lockfile
|
||||||
@@ -1,4 +1,34 @@
|
|||||||
export module cargoxx.lockfile;
|
export module cargoxx.lockfile;
|
||||||
|
|
||||||
|
import std;
|
||||||
import cargoxx.util;
|
import cargoxx.util;
|
||||||
import cargoxx.manifest;
|
import cargoxx.manifest;
|
||||||
|
|
||||||
|
export namespace cargoxx::lockfile {
|
||||||
|
|
||||||
|
struct LockfilePackage {
|
||||||
|
std::string name;
|
||||||
|
std::string version;
|
||||||
|
std::vector<std::string> dependencies; // "<name> <version>" entries; non-empty for the root
|
||||||
|
std::optional<std::string> nixpkgs_attr;
|
||||||
|
std::optional<std::string> nixpkgs_rev;
|
||||||
|
std::optional<std::string> linkdb_source;
|
||||||
|
|
||||||
|
bool operator==(const LockfilePackage&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Lockfile {
|
||||||
|
int version = 1;
|
||||||
|
std::vector<LockfilePackage> packages;
|
||||||
|
|
||||||
|
bool operator==(const Lockfile&) const = default;
|
||||||
|
|
||||||
|
// The nixpkgs revision is shared across every dep package per SPEC §5.
|
||||||
|
// Returns the first non-empty rev seen, or nullopt if no deps are pinned.
|
||||||
|
[[nodiscard]] auto nixpkgs_rev() const -> std::optional<std::string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto parse(const std::filesystem::path& path) -> util::Result<Lockfile>;
|
||||||
|
auto write(const Lockfile& lock, const std::filesystem::path& path) -> util::Result<void>;
|
||||||
|
|
||||||
|
} // namespace cargoxx::lockfile
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ cargoxx_add_test(semver_satisfies)
|
|||||||
cargoxx_add_test(manifest_parse)
|
cargoxx_add_test(manifest_parse)
|
||||||
cargoxx_add_test(manifest_write)
|
cargoxx_add_test(manifest_write)
|
||||||
cargoxx_add_test(layout_discovery)
|
cargoxx_add_test(layout_discovery)
|
||||||
|
cargoxx_add_test(lockfile_round_trip)
|
||||||
cargoxx_add_test(linkdb_lookup)
|
cargoxx_add_test(linkdb_lookup)
|
||||||
cargoxx_add_test(linkdb_overlay)
|
cargoxx_add_test(linkdb_overlay)
|
||||||
cargoxx_add_test(cmd_new)
|
cargoxx_add_test(cmd_new)
|
||||||
|
|||||||
140
tests/lockfile_round_trip.cpp
Normal file
140
tests/lockfile_round_trip.cpp
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
import cargoxx.lockfile;
|
||||||
|
import cargoxx.util;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using cargoxx::lockfile::Lockfile;
|
||||||
|
using cargoxx::lockfile::LockfilePackage;
|
||||||
|
using cargoxx::lockfile::parse;
|
||||||
|
using cargoxx::lockfile::write;
|
||||||
|
using cargoxx::util::ErrorCode;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto tmp_path() -> std::filesystem::path {
|
||||||
|
auto d = std::filesystem::temp_directory_path() /
|
||||||
|
std::format("cargoxx-lockfile-test-{}", std::random_device{}());
|
||||||
|
std::filesystem::create_directories(d);
|
||||||
|
return d / "Cargoxx.lock";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto root_pkg(std::string name, std::string version,
|
||||||
|
std::vector<std::string> deps = {}) -> LockfilePackage {
|
||||||
|
return LockfilePackage{
|
||||||
|
.name = std::move(name),
|
||||||
|
.version = std::move(version),
|
||||||
|
.dependencies = std::move(deps),
|
||||||
|
.nixpkgs_attr = std::nullopt,
|
||||||
|
.nixpkgs_rev = std::nullopt,
|
||||||
|
.linkdb_source = std::nullopt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto dep_pkg(std::string name, std::string version, std::string attr, std::string rev,
|
||||||
|
std::string source = "curated") -> LockfilePackage {
|
||||||
|
return LockfilePackage{
|
||||||
|
.name = std::move(name),
|
||||||
|
.version = std::move(version),
|
||||||
|
.dependencies = {},
|
||||||
|
.nixpkgs_attr = std::move(attr),
|
||||||
|
.nixpkgs_rev = std::move(rev),
|
||||||
|
.linkdb_source = std::move(source),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto round_trip(const Lockfile& l) -> Lockfile {
|
||||||
|
auto path = tmp_path();
|
||||||
|
REQUIRE(write(l, path).has_value());
|
||||||
|
auto parsed = parse(path);
|
||||||
|
REQUIRE(parsed.has_value());
|
||||||
|
return *parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips a minimal lockfile", "[lockfile]") {
|
||||||
|
Lockfile l{1, {root_pkg("my-project", "0.1.0")}};
|
||||||
|
REQUIRE(round_trip(l) == l);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips a lockfile with deps", "[lockfile]") {
|
||||||
|
Lockfile l{
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
root_pkg("my-project", "0.1.0", {"fmt 10.2.1", "spdlog 1.13.0"}),
|
||||||
|
dep_pkg("fmt", "10.2.1", "fmt_10", "8a3f...c2d1"),
|
||||||
|
dep_pkg("spdlog", "1.13.0", "spdlog", "8a3f...c2d1"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
REQUIRE(round_trip(l) == l);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") {
|
||||||
|
Lockfile l{
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
root_pkg("p", "0.1.0", {"fmt 10.2.1"}),
|
||||||
|
dep_pkg("fmt", "10.2.1", "fmt_10", "abc123"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
REQUIRE(l.nixpkgs_rev() == "abc123");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Lockfile::nixpkgs_rev is nullopt when no deps", "[lockfile]") {
|
||||||
|
Lockfile l{1, {root_pkg("p", "0.1.0")}};
|
||||||
|
REQUIRE_FALSE(l.nixpkgs_rev().has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parse rejects a missing file", "[lockfile]") {
|
||||||
|
auto r = parse("/nonexistent/Cargoxx.lock");
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ManifestNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parse rejects malformed toml", "[lockfile]") {
|
||||||
|
auto path = tmp_path();
|
||||||
|
std::ofstream{path} << "version = \nname = \"x\"\n";
|
||||||
|
auto r = parse(path);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ManifestParseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parse rejects a package missing name", "[lockfile]") {
|
||||||
|
auto path = tmp_path();
|
||||||
|
std::ofstream{path} << R"(
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
version = "0.1.0"
|
||||||
|
)";
|
||||||
|
auto r = parse(path);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parse rejects a package missing version", "[lockfile]") {
|
||||||
|
auto path = tmp_path();
|
||||||
|
std::ofstream{path} << R"(
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "p"
|
||||||
|
)";
|
||||||
|
auto r = parse(path);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parse defaults version to 1 when omitted", "[lockfile]") {
|
||||||
|
auto path = tmp_path();
|
||||||
|
std::ofstream{path} << R"(
|
||||||
|
[[package]]
|
||||||
|
name = "p"
|
||||||
|
version = "0.1.0"
|
||||||
|
)";
|
||||||
|
auto r = parse(path);
|
||||||
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->version == 1);
|
||||||
|
REQUIRE(r->packages.size() == 1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user