[M1] add cargoxx new
This commit is contained in:
@@ -36,3 +36,10 @@ All notable changes to cargoxx will be documented in this file.
|
|||||||
sorts results for deterministic output, and returns `LayoutNoTarget`
|
sorts results for deterministic output, and returns `LayoutNoTarget`
|
||||||
when neither a library nor any binary is present.
|
when neither a library nor any binary is present.
|
||||||
`tests/layout_discovery.cpp` covers 12 cases.
|
`tests/layout_discovery.cpp` covers 12 cases.
|
||||||
|
- `cargoxx new <name>` and `cargoxx new --lib <name>` scaffold a project
|
||||||
|
directory with `Cargoxx.toml`, `src/main.cpp` or `src/lib.cppm`, and a
|
||||||
|
minimal `.gitignore`. Hyphens in the package name are mapped to
|
||||||
|
underscores when generating the module/namespace identifier.
|
||||||
|
Codegen of `flake.nix` / `CMakeLists.txt` is intentionally deferred to M3.
|
||||||
|
CLI11 v2.6.2 vendored at `third_party/CLI11.hpp`; entry point is now
|
||||||
|
`cargoxx::cli::run(argc, argv)`. `tests/cmd_new.cpp` covers 9 cases.
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ 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/cli/cmd_new.cpp
|
||||||
|
src/cli/run.cpp
|
||||||
PUBLIC
|
PUBLIC
|
||||||
FILE_SET CXX_MODULES FILES
|
FILE_SET CXX_MODULES FILES
|
||||||
src/lib.cppm
|
src/lib.cppm
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
export module cargoxx.cli;
|
export module cargoxx.cli;
|
||||||
|
|
||||||
|
import std;
|
||||||
import cargoxx.util;
|
import cargoxx.util;
|
||||||
import cargoxx.exec;
|
|
||||||
import cargoxx.manifest;
|
import cargoxx.manifest;
|
||||||
import cargoxx.lockfile;
|
|
||||||
import cargoxx.layout;
|
export namespace cargoxx::cli {
|
||||||
import cargoxx.linkdb;
|
|
||||||
import cargoxx.resolver;
|
auto cmd_new(const std::string& name, bool lib_only,
|
||||||
import cargoxx.codegen;
|
const std::filesystem::path& parent_dir) -> util::Result<void>;
|
||||||
|
|
||||||
|
auto run(int argc, char** argv) -> int;
|
||||||
|
|
||||||
|
} // namespace cargoxx::cli
|
||||||
|
|||||||
158
src/cli/cmd_new.cpp
Normal file
158
src/cli/cmd_new.cpp
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
module cargoxx.cli;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.util;
|
||||||
|
import cargoxx.manifest;
|
||||||
|
|
||||||
|
namespace cargoxx::cli {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::string_view MAIN_CPP_TEMPLATE = R"(import std;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::println("Hello from {}!", "@@NAME@@");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr std::string_view LIB_CPPM_TEMPLATE = R"(export module @@MODULE@@;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
|
||||||
|
export namespace @@MODULE@@ {
|
||||||
|
auto greeting() -> std::string_view {
|
||||||
|
return "Hello from @@NAME@@!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr std::string_view GITIGNORE_TEMPLATE = "/build/\n";
|
||||||
|
|
||||||
|
auto is_valid_pkg_name(std::string_view s) -> bool {
|
||||||
|
if (s.empty() || std::isdigit(static_cast<unsigned char>(s[0]))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (char c : s) {
|
||||||
|
bool ok = std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '-';
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto sanitize_id(std::string_view s) -> std::string {
|
||||||
|
std::string out{s};
|
||||||
|
std::ranges::replace(out, '-', '_');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto substitute(std::string_view tmpl, std::string_view marker, std::string_view value)
|
||||||
|
-> std::string {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(tmpl.size());
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (pos < tmpl.size()) {
|
||||||
|
auto next = tmpl.find(marker, pos);
|
||||||
|
if (next == std::string_view::npos) {
|
||||||
|
out.append(tmpl.substr(pos));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.append(tmpl.substr(pos, next - pos));
|
||||||
|
out.append(value);
|
||||||
|
pos = next + marker.size();
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto write_text(const std::filesystem::path& path, std::string_view content)
|
||||||
|
-> util::Result<void> {
|
||||||
|
std::ofstream out{path};
|
||||||
|
if (!out) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::Internal,
|
||||||
|
std::format("cannot open for writing: {}", path.string()),
|
||||||
|
"", path, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out << content;
|
||||||
|
if (!out) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::Internal,
|
||||||
|
std::format("write failed: {}", path.string()),
|
||||||
|
"", path, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto cmd_new(const std::string& name, bool lib_only,
|
||||||
|
const std::filesystem::path& parent_dir) -> util::Result<void> {
|
||||||
|
if (!is_valid_pkg_name(name)) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::LayoutInvalidName,
|
||||||
|
std::format("invalid package name '{}'", name),
|
||||||
|
"names must match [a-zA-Z_][a-zA-Z0-9_-]*",
|
||||||
|
std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto root = parent_dir / name;
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::exists(root, ec)) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::Internal,
|
||||||
|
std::format("destination '{}' already exists", root.string()),
|
||||||
|
"remove it or pick a different name",
|
||||||
|
root, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::create_directories(root / "src", ec);
|
||||||
|
if (ec) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::Internal,
|
||||||
|
std::format("cannot create '{}': {}", (root / "src").string(), ec.message()),
|
||||||
|
"", root / "src", std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest::Manifest m{
|
||||||
|
.package = manifest::Package{
|
||||||
|
.name = name,
|
||||||
|
.version = "0.1.0",
|
||||||
|
.edition = manifest::Edition::Cpp23,
|
||||||
|
.authors = {},
|
||||||
|
.license = std::nullopt,
|
||||||
|
},
|
||||||
|
.dependencies = {},
|
||||||
|
.build = {},
|
||||||
|
};
|
||||||
|
if (auto r = manifest::write(m, root / "Cargoxx.toml"); !r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lib_only) {
|
||||||
|
auto content = substitute(LIB_CPPM_TEMPLATE, "@@MODULE@@", sanitize_id(name));
|
||||||
|
content = substitute(content, "@@NAME@@", name);
|
||||||
|
if (auto r = write_text(root / "src" / "lib.cppm", content); !r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auto content = substitute(MAIN_CPP_TEMPLATE, "@@NAME@@", name);
|
||||||
|
if (auto r = write_text(root / "src" / "main.cpp", content); !r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto r = write_text(root / ".gitignore", GITIGNORE_TEMPLATE); !r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::cli
|
||||||
48
src/cli/run.cpp
Normal file
48
src/cli/run.cpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
module;
|
||||||
|
|
||||||
|
#include <CLI11.hpp>
|
||||||
|
|
||||||
|
module cargoxx.cli;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.util;
|
||||||
|
|
||||||
|
namespace cargoxx::cli {
|
||||||
|
|
||||||
|
auto run(int argc, char** argv) -> int {
|
||||||
|
CLI::App app{"cargoxx — a Cargo-style project tool for modern C++"};
|
||||||
|
app.require_subcommand(1);
|
||||||
|
|
||||||
|
auto* new_cmd = app.add_subcommand("new", "Create a new cargoxx project");
|
||||||
|
std::string new_name;
|
||||||
|
bool new_lib = false;
|
||||||
|
new_cmd->add_option("name", new_name, "Project name")->required();
|
||||||
|
new_cmd->add_flag("--lib", new_lib, "Create a library project");
|
||||||
|
|
||||||
|
try {
|
||||||
|
app.parse(argc, argv);
|
||||||
|
} catch (const CLI::ParseError& e) {
|
||||||
|
return app.exit(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*new_cmd) {
|
||||||
|
std::error_code ec;
|
||||||
|
auto cwd = std::filesystem::current_path(ec);
|
||||||
|
if (ec) {
|
||||||
|
std::cerr << std::format("error: cannot determine current working directory: {}\n",
|
||||||
|
ec.message());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
auto r = cmd_new(new_name, new_lib, cwd);
|
||||||
|
if (!r) {
|
||||||
|
std::cerr << util::format(r.error());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::cout << std::format(" Created `{}` project\n", new_name);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::cli
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import cargoxx;
|
import cargoxx.cli;
|
||||||
import std;
|
|
||||||
|
|
||||||
int main(int /*argc*/, char** /*argv*/) {
|
int main(int argc, char** argv) {
|
||||||
std::println("Hello, cargoxx!");
|
return cargoxx::cli::run(argc, argv);
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ cargoxx_add_test(util_error)
|
|||||||
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(cmd_new)
|
||||||
|
|||||||
110
tests/cmd_new.cpp
Normal file
110
tests/cmd_new.cpp
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
import cargoxx.cli;
|
||||||
|
import cargoxx.manifest;
|
||||||
|
import cargoxx.util;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using cargoxx::cli::cmd_new;
|
||||||
|
using cargoxx::util::ErrorCode;
|
||||||
|
namespace manifest = cargoxx::manifest;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto fresh_dir() -> std::filesystem::path {
|
||||||
|
auto d = std::filesystem::temp_directory_path() /
|
||||||
|
std::format("cargoxx-cmd_new-test-{}", std::random_device{}());
|
||||||
|
std::filesystem::create_directories(d);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto read_file(const std::filesystem::path& p) -> std::string {
|
||||||
|
std::ifstream in{p};
|
||||||
|
return std::string{std::istreambuf_iterator<char>(in), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("cmd_new --bin scaffolds a binary project", "[cli][new]") {
|
||||||
|
auto parent = fresh_dir();
|
||||||
|
REQUIRE(cmd_new("hello", false, parent).has_value());
|
||||||
|
|
||||||
|
auto root = parent / "hello";
|
||||||
|
REQUIRE(std::filesystem::exists(root / "Cargoxx.toml"));
|
||||||
|
REQUIRE(std::filesystem::exists(root / "src" / "main.cpp"));
|
||||||
|
REQUIRE_FALSE(std::filesystem::exists(root / "src" / "lib.cppm"));
|
||||||
|
REQUIRE(std::filesystem::exists(root / ".gitignore"));
|
||||||
|
|
||||||
|
auto m = manifest::parse(root / "Cargoxx.toml");
|
||||||
|
REQUIRE(m.has_value());
|
||||||
|
REQUIRE(m->package.name == "hello");
|
||||||
|
REQUIRE(m->package.version == "0.1.0");
|
||||||
|
REQUIRE(m->package.edition == manifest::Edition::Cpp23);
|
||||||
|
|
||||||
|
auto main_src = read_file(root / "src" / "main.cpp");
|
||||||
|
REQUIRE(main_src.find("import std;") != std::string::npos);
|
||||||
|
REQUIRE(main_src.find("\"hello\"") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("cmd_new --lib scaffolds a library project", "[cli][new]") {
|
||||||
|
auto parent = fresh_dir();
|
||||||
|
REQUIRE(cmd_new("widget", true, parent).has_value());
|
||||||
|
|
||||||
|
auto root = parent / "widget";
|
||||||
|
REQUIRE(std::filesystem::exists(root / "src" / "lib.cppm"));
|
||||||
|
REQUIRE_FALSE(std::filesystem::exists(root / "src" / "main.cpp"));
|
||||||
|
|
||||||
|
auto lib_src = read_file(root / "src" / "lib.cppm");
|
||||||
|
REQUIRE(lib_src.find("export module widget;") != std::string::npos);
|
||||||
|
REQUIRE(lib_src.find("export namespace widget") != std::string::npos);
|
||||||
|
REQUIRE(lib_src.find("Hello from widget!") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("cmd_new replaces hyphens with underscores in module name",
|
||||||
|
"[cli][new]") {
|
||||||
|
auto parent = fresh_dir();
|
||||||
|
REQUIRE(cmd_new("my-project", true, parent).has_value());
|
||||||
|
|
||||||
|
auto m = manifest::parse(parent / "my-project" / "Cargoxx.toml");
|
||||||
|
REQUIRE(m->package.name == "my-project");
|
||||||
|
|
||||||
|
auto lib_src = read_file(parent / "my-project" / "src" / "lib.cppm");
|
||||||
|
REQUIRE(lib_src.find("export module my_project;") != std::string::npos);
|
||||||
|
REQUIRE(lib_src.find("Hello from my-project!") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("cmd_new fails when target directory exists", "[cli][new]") {
|
||||||
|
auto parent = fresh_dir();
|
||||||
|
std::filesystem::create_directories(parent / "exists");
|
||||||
|
auto r = cmd_new("exists", false, parent);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::Internal);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("cmd_new rejects an empty name", "[cli][new]") {
|
||||||
|
auto parent = fresh_dir();
|
||||||
|
auto r = cmd_new("", false, parent);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::LayoutInvalidName);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("cmd_new rejects a name starting with a digit", "[cli][new]") {
|
||||||
|
auto parent = fresh_dir();
|
||||||
|
auto r = cmd_new("1bad", false, parent);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::LayoutInvalidName);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("cmd_new rejects a name with a space", "[cli][new]") {
|
||||||
|
auto parent = fresh_dir();
|
||||||
|
auto r = cmd_new("foo bar", false, parent);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::LayoutInvalidName);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("cmd_new writes a minimal .gitignore", "[cli][new]") {
|
||||||
|
auto parent = fresh_dir();
|
||||||
|
REQUIRE(cmd_new("foo", false, parent).has_value());
|
||||||
|
auto content = read_file(parent / "foo" / ".gitignore");
|
||||||
|
REQUIRE(content.find("/build/") != std::string::npos);
|
||||||
|
}
|
||||||
12216
third_party/CLI11.hpp
vendored
Normal file
12216
third_party/CLI11.hpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user