[M3] add lockfile types + parse/write

This commit is contained in:
2026-05-08 12:25:49 +00:00
parent cafa403a58
commit 86e88f236f
6 changed files with 351 additions and 0 deletions

175
src/lockfile/lockfile.cpp Normal file
View 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

View File

@@ -1,4 +1,34 @@
export module cargoxx.lockfile;
import std;
import cargoxx.util;
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