[M1] add layout::discover
This commit is contained in:
@@ -29,3 +29,10 @@ All notable changes to cargoxx will be documented in this file.
|
||||
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.
|
||||
- `cargoxx.layout` public types (`Target`, `TargetKind`, `DiscoveredLayout`)
|
||||
and `discover(project_root, package_name)`. Walks `src/` recursively
|
||||
for the library (excluding the `src/bin/` subtree), enumerates
|
||||
`src/main.cpp`, `src/bin/*.cpp`, `tests/*.cpp`, `examples/*.cpp` flat,
|
||||
sorts results for deterministic output, and returns `LayoutNoTarget`
|
||||
when neither a library nor any binary is present.
|
||||
`tests/layout_discovery.cpp` covers 12 cases.
|
||||
|
||||
@@ -37,6 +37,7 @@ target_sources(cargoxx
|
||||
src/util/error.cpp
|
||||
src/manifest/parser.cpp
|
||||
src/manifest/writer.cpp
|
||||
src/layout/layout.cpp
|
||||
PUBLIC
|
||||
FILE_SET CXX_MODULES FILES
|
||||
src/lib.cppm
|
||||
|
||||
142
src/layout/layout.cpp
Normal file
142
src/layout/layout.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
module cargoxx.layout;
|
||||
|
||||
import std;
|
||||
import cargoxx.util;
|
||||
|
||||
namespace cargoxx::layout {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
auto top_level_cpp(const fs::path& dir) -> std::vector<fs::path> {
|
||||
std::vector<fs::path> out;
|
||||
std::error_code ec;
|
||||
if (!fs::exists(dir, ec) || ec) {
|
||||
return out;
|
||||
}
|
||||
for (const auto& entry : fs::directory_iterator{dir}) {
|
||||
if (entry.is_regular_file() && entry.path().extension() == ".cpp") {
|
||||
out.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
std::ranges::sort(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
void walk_library_sources(const fs::path& src_dir, const fs::path& lib_path,
|
||||
const fs::path& main_path, std::vector<fs::path>& cppm_out,
|
||||
std::vector<fs::path>& cpp_out) {
|
||||
std::error_code ec;
|
||||
if (!fs::exists(src_dir, ec) || ec) {
|
||||
return;
|
||||
}
|
||||
for (const auto& entry : fs::recursive_directory_iterator{src_dir}) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
const auto& p = entry.path();
|
||||
// Exclude the bin/ subtree — it holds extra binary roots, not library sources.
|
||||
auto rel = fs::relative(p, src_dir);
|
||||
if (!rel.empty() && rel.begin()->string() == "bin") {
|
||||
continue;
|
||||
}
|
||||
if (p == lib_path || p == main_path) {
|
||||
continue;
|
||||
}
|
||||
const auto ext = p.extension();
|
||||
if (ext == ".cppm") {
|
||||
cppm_out.push_back(p);
|
||||
} else if (ext == ".cpp") {
|
||||
cpp_out.push_back(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto by_name(const Target& a, const Target& b) -> bool {
|
||||
return a.name < b.name;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto discover(const fs::path& project_root, const std::string& package_name)
|
||||
-> util::Result<DiscoveredLayout> {
|
||||
const auto src_dir = project_root / "src";
|
||||
const auto lib_path = src_dir / "lib.cppm";
|
||||
const auto main_path = src_dir / "main.cpp";
|
||||
const auto bin_dir = src_dir / "bin";
|
||||
const auto tests_dir = project_root / "tests";
|
||||
const auto examples_dir = project_root / "examples";
|
||||
|
||||
DiscoveredLayout out;
|
||||
|
||||
std::error_code ec;
|
||||
if (fs::exists(lib_path, ec) && !ec) {
|
||||
std::vector<fs::path> cppm{lib_path};
|
||||
std::vector<fs::path> cpp;
|
||||
walk_library_sources(src_dir, lib_path, main_path, cppm, cpp);
|
||||
std::ranges::sort(cppm);
|
||||
std::ranges::sort(cpp);
|
||||
out.library = Target{
|
||||
.kind = TargetKind::Library,
|
||||
.name = package_name,
|
||||
.entry = lib_path,
|
||||
.additional_sources = std::move(cpp),
|
||||
.module_units = std::move(cppm),
|
||||
};
|
||||
}
|
||||
|
||||
if (fs::exists(main_path, ec) && !ec) {
|
||||
out.binaries.push_back(Target{
|
||||
.kind = TargetKind::Binary,
|
||||
.name = package_name,
|
||||
.entry = main_path,
|
||||
.additional_sources = {},
|
||||
.module_units = {},
|
||||
});
|
||||
}
|
||||
for (const auto& f : top_level_cpp(bin_dir)) {
|
||||
out.binaries.push_back(Target{
|
||||
.kind = TargetKind::Binary,
|
||||
.name = f.stem().string(),
|
||||
.entry = f,
|
||||
.additional_sources = {},
|
||||
.module_units = {},
|
||||
});
|
||||
}
|
||||
std::ranges::sort(out.binaries, by_name);
|
||||
|
||||
for (const auto& f : top_level_cpp(tests_dir)) {
|
||||
out.tests.push_back(Target{
|
||||
.kind = TargetKind::Test,
|
||||
.name = f.stem().string(),
|
||||
.entry = f,
|
||||
.additional_sources = {},
|
||||
.module_units = {},
|
||||
});
|
||||
}
|
||||
|
||||
for (const auto& f : top_level_cpp(examples_dir)) {
|
||||
out.examples.push_back(Target{
|
||||
.kind = TargetKind::Example,
|
||||
.name = f.stem().string(),
|
||||
.entry = f,
|
||||
.additional_sources = {},
|
||||
.module_units = {},
|
||||
});
|
||||
}
|
||||
|
||||
if (!out.library && out.binaries.empty()) {
|
||||
return std::unexpected(util::Error{
|
||||
util::ErrorCode::LayoutNoTarget,
|
||||
"no target found",
|
||||
"expected one of: src/main.cpp, src/lib.cppm",
|
||||
project_root,
|
||||
std::nullopt,
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace cargoxx::layout
|
||||
@@ -1,3 +1,32 @@
|
||||
export module cargoxx.layout;
|
||||
|
||||
import std;
|
||||
import cargoxx.util;
|
||||
|
||||
export namespace cargoxx::layout {
|
||||
|
||||
enum class TargetKind { Library, Binary, Test, Example };
|
||||
|
||||
struct Target {
|
||||
TargetKind kind;
|
||||
std::string name;
|
||||
std::filesystem::path entry;
|
||||
std::vector<std::filesystem::path> additional_sources;
|
||||
std::vector<std::filesystem::path> module_units;
|
||||
|
||||
bool operator==(const Target&) const = default;
|
||||
};
|
||||
|
||||
struct DiscoveredLayout {
|
||||
std::optional<Target> library;
|
||||
std::vector<Target> binaries;
|
||||
std::vector<Target> tests;
|
||||
std::vector<Target> examples;
|
||||
|
||||
bool operator==(const DiscoveredLayout&) const = default;
|
||||
};
|
||||
|
||||
auto discover(const std::filesystem::path& project_root, const std::string& package_name)
|
||||
-> util::Result<DiscoveredLayout>;
|
||||
|
||||
} // namespace cargoxx::layout
|
||||
|
||||
@@ -10,3 +10,4 @@ endfunction()
|
||||
cargoxx_add_test(util_error)
|
||||
cargoxx_add_test(manifest_parse)
|
||||
cargoxx_add_test(manifest_write)
|
||||
cargoxx_add_test(layout_discovery)
|
||||
|
||||
193
tests/layout_discovery.cpp
Normal file
193
tests/layout_discovery.cpp
Normal file
@@ -0,0 +1,193 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
import cargoxx.layout;
|
||||
import cargoxx.util;
|
||||
import std;
|
||||
|
||||
using cargoxx::layout::DiscoveredLayout;
|
||||
using cargoxx::layout::discover;
|
||||
using cargoxx::layout::Target;
|
||||
using cargoxx::layout::TargetKind;
|
||||
using cargoxx::util::ErrorCode;
|
||||
|
||||
namespace {
|
||||
|
||||
class TempProject {
|
||||
public:
|
||||
TempProject() {
|
||||
root_ = std::filesystem::temp_directory_path() /
|
||||
std::format("cargoxx-layout-test-{}", std::random_device{}());
|
||||
std::filesystem::create_directories(root_);
|
||||
}
|
||||
|
||||
~TempProject() {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(root_, ec);
|
||||
}
|
||||
|
||||
TempProject(const TempProject&) = delete;
|
||||
TempProject& operator=(const TempProject&) = delete;
|
||||
TempProject(TempProject&&) = delete;
|
||||
TempProject& operator=(TempProject&&) = delete;
|
||||
|
||||
auto root() const -> const std::filesystem::path& { return root_; }
|
||||
|
||||
auto touch(std::string_view rel) -> std::filesystem::path {
|
||||
auto p = root_ / rel;
|
||||
std::filesystem::create_directories(p.parent_path());
|
||||
std::ofstream{p} << "// stub\n";
|
||||
return p;
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path root_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("discover returns LayoutNoTarget for an empty src", "[layout]") {
|
||||
TempProject p;
|
||||
std::filesystem::create_directories(p.root() / "src");
|
||||
auto r = discover(p.root(), "foo");
|
||||
REQUIRE_FALSE(r.has_value());
|
||||
REQUIRE(r.error().code == ErrorCode::LayoutNoTarget);
|
||||
}
|
||||
|
||||
TEST_CASE("discover finds main.cpp as a binary", "[layout]") {
|
||||
TempProject p;
|
||||
auto main = p.touch("src/main.cpp");
|
||||
auto r = discover(p.root(), "foo");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE_FALSE(r->library.has_value());
|
||||
REQUIRE(r->binaries.size() == 1);
|
||||
REQUIRE(r->binaries[0].kind == TargetKind::Binary);
|
||||
REQUIRE(r->binaries[0].name == "foo");
|
||||
REQUIRE(r->binaries[0].entry == main);
|
||||
}
|
||||
|
||||
TEST_CASE("discover finds lib.cppm as a library", "[layout]") {
|
||||
TempProject p;
|
||||
auto lib = p.touch("src/lib.cppm");
|
||||
auto r = discover(p.root(), "foo");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->library.has_value());
|
||||
REQUIRE(r->library->kind == TargetKind::Library);
|
||||
REQUIRE(r->library->name == "foo");
|
||||
REQUIRE(r->library->entry == lib);
|
||||
REQUIRE(r->library->module_units == std::vector{lib});
|
||||
REQUIRE(r->library->additional_sources.empty());
|
||||
REQUIRE(r->binaries.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("discover supports lib + main together", "[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/lib.cppm");
|
||||
p.touch("src/main.cpp");
|
||||
auto r = discover(p.root(), "foo");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->library.has_value());
|
||||
REQUIRE(r->binaries.size() == 1);
|
||||
REQUIRE(r->binaries[0].entry.filename() == "main.cpp");
|
||||
}
|
||||
|
||||
TEST_CASE("discover collects nested .cppm and .cpp into the library",
|
||||
"[layout]") {
|
||||
TempProject p;
|
||||
auto lib = p.touch("src/lib.cppm");
|
||||
auto inner_cppm = p.touch("src/internal/foo.cppm");
|
||||
auto inner_cpp = p.touch("src/internal/foo.cpp");
|
||||
auto r = discover(p.root(), "foo");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->library->module_units.size() == 2);
|
||||
REQUIRE(std::ranges::find(r->library->module_units, lib) !=
|
||||
r->library->module_units.end());
|
||||
REQUIRE(std::ranges::find(r->library->module_units, inner_cppm) !=
|
||||
r->library->module_units.end());
|
||||
REQUIRE(r->library->additional_sources == std::vector{inner_cpp});
|
||||
}
|
||||
|
||||
TEST_CASE("discover excludes src/bin/* from the library walk", "[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/lib.cppm");
|
||||
p.touch("src/bin/tool.cpp");
|
||||
auto r = discover(p.root(), "foo");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->library->additional_sources.empty());
|
||||
REQUIRE(r->binaries.size() == 1);
|
||||
REQUIRE(r->binaries[0].name == "tool");
|
||||
}
|
||||
|
||||
TEST_CASE("discover lists src/bin/*.cpp as additional binaries", "[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/main.cpp");
|
||||
p.touch("src/bin/foo.cpp");
|
||||
p.touch("src/bin/bar.cpp");
|
||||
auto r = discover(p.root(), "pkg");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->binaries.size() == 3);
|
||||
// sorted alphabetically by target name
|
||||
REQUIRE(r->binaries[0].name == "bar");
|
||||
REQUIRE(r->binaries[1].name == "foo");
|
||||
REQUIRE(r->binaries[2].name == "pkg");
|
||||
}
|
||||
|
||||
TEST_CASE("discover does not recurse into src/bin/", "[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/main.cpp");
|
||||
p.touch("src/bin/foo.cpp");
|
||||
p.touch("src/bin/sub/nested.cpp"); // should be ignored
|
||||
auto r = discover(p.root(), "pkg");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->binaries.size() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("discover lists tests/*.cpp", "[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/main.cpp");
|
||||
p.touch("tests/alpha.cpp");
|
||||
p.touch("tests/beta.cpp");
|
||||
p.touch("tests/sub/nested.cpp"); // ignored, no recursion
|
||||
auto r = discover(p.root(), "pkg");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->tests.size() == 2);
|
||||
REQUIRE(r->tests[0].name == "alpha");
|
||||
REQUIRE(r->tests[0].kind == TargetKind::Test);
|
||||
REQUIRE(r->tests[1].name == "beta");
|
||||
}
|
||||
|
||||
TEST_CASE("discover lists examples/*.cpp", "[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/main.cpp");
|
||||
p.touch("examples/demo.cpp");
|
||||
auto r = discover(p.root(), "pkg");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->examples.size() == 1);
|
||||
REQUIRE(r->examples[0].name == "demo");
|
||||
REQUIRE(r->examples[0].kind == TargetKind::Example);
|
||||
}
|
||||
|
||||
TEST_CASE("discover ignores non .cpp/.cppm files", "[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/main.cpp");
|
||||
p.touch("src/notes.txt");
|
||||
p.touch("src/lib.cppm");
|
||||
p.touch("src/internal/header.h"); // headers in src/ are ignored
|
||||
auto r = discover(p.root(), "pkg");
|
||||
REQUIRE(r.has_value());
|
||||
REQUIRE(r->library->module_units.size() == 1);
|
||||
REQUIRE(r->library->additional_sources.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("discover output is deterministic across runs", "[layout]") {
|
||||
TempProject p;
|
||||
p.touch("src/lib.cppm");
|
||||
p.touch("src/zeta.cppm");
|
||||
p.touch("src/alpha.cppm");
|
||||
p.touch("src/middle.cppm");
|
||||
auto r1 = discover(p.root(), "pkg");
|
||||
auto r2 = discover(p.root(), "pkg");
|
||||
REQUIRE(r1.has_value());
|
||||
REQUIRE(r2.has_value());
|
||||
REQUIRE(*r1 == *r2);
|
||||
REQUIRE(std::ranges::is_sorted(r1->library->module_units));
|
||||
}
|
||||
Reference in New Issue
Block a user