[M1] add manifest::write
This commit is contained in:
@@ -25,3 +25,7 @@ All notable changes to cargoxx will be documented in this file.
|
|||||||
`[package]` / `[build]` and unknown top-level keys are rejected; reserved
|
`[package]` / `[build]` and unknown top-level keys are rejected; reserved
|
||||||
fields (`description`, `repository`, `[dev-dependencies]`, `[features]`,
|
fields (`description`, `repository`, `[dev-dependencies]`, `[features]`,
|
||||||
`[workspace]`) are accepted. `tests/manifest_parse.cpp` covers 17 cases.
|
`[workspace]`) are accepted. `tests/manifest_parse.cpp` covers 17 cases.
|
||||||
|
- `manifest::write(m, path)` serializes a `Manifest` as TOML using toml++.
|
||||||
|
Dependencies are emitted alphabetically (matches Cargo). Round-trip
|
||||||
|
property is exercised by `tests/manifest_write.cpp` (9 cases).
|
||||||
|
Defaulted `operator==` on the manifest structs supports comparison.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ target_sources(cargoxx
|
|||||||
PRIVATE
|
PRIVATE
|
||||||
src/util/error.cpp
|
src/util/error.cpp
|
||||||
src/manifest/parser.cpp
|
src/manifest/parser.cpp
|
||||||
|
src/manifest/writer.cpp
|
||||||
PUBLIC
|
PUBLIC
|
||||||
FILE_SET CXX_MODULES FILES
|
FILE_SET CXX_MODULES FILES
|
||||||
src/lib.cppm
|
src/lib.cppm
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ struct Dependency {
|
|||||||
std::string name;
|
std::string name;
|
||||||
std::string version_spec;
|
std::string version_spec;
|
||||||
std::vector<std::string> components;
|
std::vector<std::string> components;
|
||||||
|
|
||||||
|
bool operator==(const Dependency&) const = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BuildSettings {
|
struct BuildSettings {
|
||||||
bool warnings_as_errors = false;
|
bool warnings_as_errors = false;
|
||||||
std::vector<std::string> sanitizers;
|
std::vector<std::string> sanitizers;
|
||||||
|
|
||||||
|
bool operator==(const BuildSettings&) const = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class Edition { Cpp20, Cpp23, Cpp26 };
|
enum class Edition { Cpp20, Cpp23, Cpp26 };
|
||||||
@@ -24,14 +28,19 @@ struct Package {
|
|||||||
Edition edition = Edition::Cpp23;
|
Edition edition = Edition::Cpp23;
|
||||||
std::vector<std::string> authors;
|
std::vector<std::string> authors;
|
||||||
std::optional<std::string> license;
|
std::optional<std::string> license;
|
||||||
|
|
||||||
|
bool operator==(const Package&) const = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Manifest {
|
struct Manifest {
|
||||||
Package package;
|
Package package;
|
||||||
std::vector<Dependency> dependencies;
|
std::vector<Dependency> dependencies;
|
||||||
BuildSettings build;
|
BuildSettings build;
|
||||||
|
|
||||||
|
bool operator==(const Manifest&) const = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto parse(const std::filesystem::path& path) -> util::Result<Manifest>;
|
auto parse(const std::filesystem::path& path) -> util::Result<Manifest>;
|
||||||
|
auto write(const Manifest& m, const std::filesystem::path& path) -> util::Result<void>;
|
||||||
|
|
||||||
} // namespace cargoxx::manifest
|
} // namespace cargoxx::manifest
|
||||||
|
|||||||
103
src/manifest/writer.cpp
Normal file
103
src/manifest/writer.cpp
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
module;
|
||||||
|
|
||||||
|
#include <toml.hpp>
|
||||||
|
|
||||||
|
module cargoxx.manifest;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.util;
|
||||||
|
|
||||||
|
namespace cargoxx::manifest {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto edition_to_string(Edition e) -> std::string_view {
|
||||||
|
switch (e) {
|
||||||
|
case Edition::Cpp20:
|
||||||
|
return "cpp20";
|
||||||
|
case Edition::Cpp23:
|
||||||
|
return "cpp23";
|
||||||
|
case Edition::Cpp26:
|
||||||
|
return "cpp26";
|
||||||
|
}
|
||||||
|
return "cpp23";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto to_array(const std::vector<std::string>& xs) -> toml::array {
|
||||||
|
toml::array out;
|
||||||
|
for (const auto& x : xs) {
|
||||||
|
out.push_back(x);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto build_table(const Manifest& m) -> toml::table {
|
||||||
|
toml::table root;
|
||||||
|
|
||||||
|
toml::table package;
|
||||||
|
package.insert_or_assign("name", m.package.name);
|
||||||
|
package.insert_or_assign("version", m.package.version);
|
||||||
|
package.insert_or_assign("edition", std::string{edition_to_string(m.package.edition)});
|
||||||
|
if (!m.package.authors.empty()) {
|
||||||
|
package.insert_or_assign("authors", to_array(m.package.authors));
|
||||||
|
}
|
||||||
|
if (m.package.license) {
|
||||||
|
package.insert_or_assign("license", *m.package.license);
|
||||||
|
}
|
||||||
|
root.insert_or_assign("package", std::move(package));
|
||||||
|
|
||||||
|
if (!m.dependencies.empty()) {
|
||||||
|
toml::table deps;
|
||||||
|
for (const auto& dep : m.dependencies) {
|
||||||
|
if (dep.components.empty()) {
|
||||||
|
deps.insert_or_assign(dep.name, dep.version_spec);
|
||||||
|
} else {
|
||||||
|
toml::table dep_tbl;
|
||||||
|
dep_tbl.insert_or_assign("version", dep.version_spec);
|
||||||
|
dep_tbl.insert_or_assign("components", to_array(dep.components));
|
||||||
|
dep_tbl.is_inline(true);
|
||||||
|
deps.insert_or_assign(dep.name, std::move(dep_tbl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.insert_or_assign("dependencies", std::move(deps));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.build.warnings_as_errors || !m.build.sanitizers.empty()) {
|
||||||
|
toml::table build;
|
||||||
|
if (m.build.warnings_as_errors) {
|
||||||
|
build.insert_or_assign("warnings_as_errors", true);
|
||||||
|
}
|
||||||
|
if (!m.build.sanitizers.empty()) {
|
||||||
|
build.insert_or_assign("sanitizers", to_array(m.build.sanitizers));
|
||||||
|
}
|
||||||
|
root.insert_or_assign("build", std::move(build));
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto write(const Manifest& m, const std::filesystem::path& path) -> util::Result<void> {
|
||||||
|
auto tbl = build_table(m);
|
||||||
|
|
||||||
|
std::ofstream out{path};
|
||||||
|
if (!out) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::Internal,
|
||||||
|
std::format("cannot open manifest for writing: {}", path.string()),
|
||||||
|
"", path, std::nullopt});
|
||||||
|
}
|
||||||
|
|
||||||
|
out << tbl << '\n';
|
||||||
|
if (!out) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::Internal,
|
||||||
|
std::format("write failed: {}", path.string()),
|
||||||
|
"", path, std::nullopt});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::manifest
|
||||||
@@ -9,3 +9,4 @@ endfunction()
|
|||||||
|
|
||||||
cargoxx_add_test(util_error)
|
cargoxx_add_test(util_error)
|
||||||
cargoxx_add_test(manifest_parse)
|
cargoxx_add_test(manifest_parse)
|
||||||
|
cargoxx_add_test(manifest_write)
|
||||||
|
|||||||
132
tests/manifest_write.cpp
Normal file
132
tests/manifest_write.cpp
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
import cargoxx.manifest;
|
||||||
|
import cargoxx.util;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using cargoxx::manifest::BuildSettings;
|
||||||
|
using cargoxx::manifest::Dependency;
|
||||||
|
using cargoxx::manifest::Edition;
|
||||||
|
using cargoxx::manifest::Manifest;
|
||||||
|
using cargoxx::manifest::Package;
|
||||||
|
using cargoxx::manifest::parse;
|
||||||
|
using cargoxx::manifest::write;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto tmp_path() -> std::filesystem::path {
|
||||||
|
auto dir = std::filesystem::temp_directory_path() /
|
||||||
|
std::format("cargoxx-write-test-{}", std::random_device{}());
|
||||||
|
std::filesystem::create_directories(dir);
|
||||||
|
return dir / "Cargoxx.toml";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pkg(std::string name, std::string version, Edition ed = Edition::Cpp23,
|
||||||
|
std::vector<std::string> authors = {},
|
||||||
|
std::optional<std::string> license = std::nullopt) -> Package {
|
||||||
|
return Package{
|
||||||
|
.name = std::move(name),
|
||||||
|
.version = std::move(version),
|
||||||
|
.edition = ed,
|
||||||
|
.authors = std::move(authors),
|
||||||
|
.license = std::move(license),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto dep(std::string name, std::string version,
|
||||||
|
std::vector<std::string> components = {}) -> Dependency {
|
||||||
|
return Dependency{
|
||||||
|
.name = std::move(name),
|
||||||
|
.version_spec = std::move(version),
|
||||||
|
.components = std::move(components),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto round_trip(const Manifest& m) -> Manifest {
|
||||||
|
auto path = tmp_path();
|
||||||
|
REQUIRE(write(m, path).has_value());
|
||||||
|
auto parsed = parse(path);
|
||||||
|
REQUIRE(parsed.has_value());
|
||||||
|
return *parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips a minimal manifest", "[manifest][write]") {
|
||||||
|
Manifest m{pkg("foo", "0.1.0"), {}, {}};
|
||||||
|
REQUIRE(round_trip(m) == m);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips authors and license", "[manifest][write]") {
|
||||||
|
Manifest m{pkg("foo", "0.1.0", Edition::Cpp20, {"Ada", "Grace"}, "MIT"), {}, {}};
|
||||||
|
REQUIRE(round_trip(m) == m);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips a string-form dependency", "[manifest][write]") {
|
||||||
|
Manifest m{pkg("foo", "0.1.0"), {dep("fmt", "10.2")}, {}};
|
||||||
|
REQUIRE(round_trip(m) == m);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips a table-form dependency with components",
|
||||||
|
"[manifest][write]") {
|
||||||
|
Manifest m{pkg("foo", "0.1.0"), {dep("boost", "1.84", {"filesystem", "system"})}, {}};
|
||||||
|
REQUIRE(round_trip(m) == m);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write sorts dependencies alphabetically (matches Cargo)",
|
||||||
|
"[manifest][write]") {
|
||||||
|
Manifest m{
|
||||||
|
pkg("foo", "0.1.0"),
|
||||||
|
{
|
||||||
|
dep("fmt", "10.2"),
|
||||||
|
dep("spdlog", "1.13"),
|
||||||
|
dep("boost", "1.84", {"filesystem"}),
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
auto rt = round_trip(m);
|
||||||
|
REQUIRE(rt.dependencies.size() == 3);
|
||||||
|
REQUIRE(rt.dependencies[0].name == "boost");
|
||||||
|
REQUIRE(rt.dependencies[1].name == "fmt");
|
||||||
|
REQUIRE(rt.dependencies[2].name == "spdlog");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips build settings", "[manifest][write]") {
|
||||||
|
Manifest m{
|
||||||
|
pkg("foo", "0.1.0"),
|
||||||
|
{},
|
||||||
|
BuildSettings{.warnings_as_errors = true,
|
||||||
|
.sanitizers = {"address", "undefined"}},
|
||||||
|
};
|
||||||
|
REQUIRE(round_trip(m) == m);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write omits empty optional sections", "[manifest][write]") {
|
||||||
|
Manifest m{pkg("foo", "0.1.0"), {}, {}};
|
||||||
|
auto path = tmp_path();
|
||||||
|
REQUIRE(write(m, path).has_value());
|
||||||
|
|
||||||
|
std::ifstream in{path};
|
||||||
|
std::string content{std::istreambuf_iterator<char>(in), {}};
|
||||||
|
REQUIRE(content.find("[dependencies]") == std::string::npos);
|
||||||
|
REQUIRE(content.find("[build]") == std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write overwrites an existing file", "[manifest][write]") {
|
||||||
|
auto path = tmp_path();
|
||||||
|
std::ofstream{path} << "# stale content\n[package]\nname = \"old\"\n";
|
||||||
|
|
||||||
|
Manifest m{pkg("new", "1.0.0"), {}, {}};
|
||||||
|
REQUIRE(write(m, path).has_value());
|
||||||
|
|
||||||
|
auto parsed = parse(path);
|
||||||
|
REQUIRE(parsed.has_value());
|
||||||
|
REQUIRE(parsed->package.name == "new");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write fails when the target directory does not exist",
|
||||||
|
"[manifest][write]") {
|
||||||
|
Manifest m{pkg("foo", "0.1.0"), {}, {}};
|
||||||
|
auto r = write(m, "/nonexistent/dir/Cargoxx.toml");
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user