Files
canon/canon/canon.cpp
T
paulhorn 651fa4d89b Add canon add target, add test, add dep and doctor
- add target lib|exe <name>: another library or executable in a source
  directory named after it (P1204R0's rule applied per target); a new
  executable links the project's library
- add test <name>: tests/<name>/driver.cpp for a library, or a run of an
  executable, with --arg and --expect
- add dep <dependency>: link a library of this project (no cycles, no
  executables) or an installed package's imported target such as
  fmt::fmt, adding its find_package()
- doctor: read-only check of the files against .canon.toml and the
  P1204R0 layout; errors for missing declared files or broken markers,
  warnings for unbuilt sources, undeclared tests, a hand-edited managed
  block, .h/.cc extensions and include/ or src/ directories
- every add command takes --target; the CLI rejects options a command
  does not take
- manifest: packages, test args, and validation of target names, shared
  source directories and dependencies
- templates moved to canon/templates.hpp
- tests: multi-target and exe-tests scenarios; every scenario ends with
  canon doctor; CANON_CHECK accepts expressions containing commas; the
  CLI test no longer reads a destroyed temporary

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:38:59 +02:00

230 lines
8.7 KiB
C++

#include <canon/add.hpp>
#include <canon/cli.hpp>
#include <canon/details/overloaded.hpp>
#include <canon/doctor.hpp>
#include <canon/executor.hpp>
#include <canon/generate.hpp>
#include <canon/name.hpp>
#include <canon/plan.hpp>
#include <canon/project.hpp>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <expected>
#include <filesystem>
#include <format>
#include <print>
#include <string>
#include <string_view>
#include <vector>
namespace fs = std::filesystem;
namespace {
constexpr int usage_error = 2;
// Quotes a path for pasting into a shell, only when it needs it.
std::string shell_quote(const std::string& s) {
const bool safe = !s.empty() && s.find_first_not_of("abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789/._-+") == std::string::npos;
if (safe) {
return s;
}
std::string out = "'";
for (const char c : s) {
out += c == '\'' ? std::string("'\\''") : std::string(1, c);
}
return out + "'";
}
// Absolute path for messages, with the home directory shown as ~.
std::string display_path(const fs::path& path) {
std::error_code error;
const std::string absolute = fs::absolute(path, error).lexically_normal().string();
if (error) {
return path.string();
}
const char* home_env = std::getenv("HOME");
if (home_env == nullptr || *home_env == '\0') {
return absolute;
}
std::string home = fs::path(home_env).lexically_normal().string();
if (home.size() > 1 && home.ends_with('/')) {
home.pop_back();
}
if (absolute == home || absolute.starts_with(home + "/")) {
return "~" + absolute.substr(home.size());
}
return absolute;
}
// Applies (or dry-runs) the plan and prints what changed.
bool apply_and_report(const canon::plan& plan, const fs::path& root, bool dry_run) {
const auto report = canon::apply(plan, root, {.dry_run = dry_run});
if (!report) {
std::println(stderr, "canon: {}", report.error());
return false;
}
for (const auto& line : *report) {
std::println("{}", line);
}
if (dry_run) {
std::println("\nDry run: nothing was written.");
}
return true;
}
int run_new(const canon::new_command& cmd) {
const auto name = canon::parse_project_name(cmd.kind, cmd.name);
if (!name) {
std::println(stderr, "canon: {}", name.error());
return usage_error;
}
const fs::path& directory = cmd.options.directory;
std::error_code ignored;
if (!fs::is_directory(directory, ignored)) {
std::println(stderr, "canon: directory {} does not exist", directory.string());
return EXIT_FAILURE;
}
const fs::path destination = (directory / name->name).lexically_normal();
if (fs::exists(destination, ignored) &&
!(fs::is_directory(destination, ignored) && fs::is_empty(destination, ignored))) {
std::println(stderr, "canon: {} already exists and is not an empty directory", destination.string());
return EXIT_FAILURE;
}
if (!apply_and_report(canon::generate_new_project(*name), directory, cmd.options.dry_run)) {
return EXIT_FAILURE;
}
if (cmd.options.dry_run) {
return EXIT_SUCCESS;
}
const std::string_view kind = cmd.kind == canon::project_kind::library ? "library" : "executable";
std::println("\nCreated {} {} in {}.\n\nNext:\n cd {}\n cmake -S . -B build\n"
" cmake --build build\n ctest --test-dir build --output-on-failure",
kind, name->name, display_path(destination), shell_quote(destination.string()));
return EXIT_SUCCESS;
}
// Shared by the add commands: load the project, plan, apply, report.
template <class Planner>
int run_add(const canon::project_options& options, Planner make_plan, std::string_view done) {
const auto project = canon::load_project(options.directory);
if (!project) {
std::println(stderr, "canon: {}", project.error());
return EXIT_FAILURE;
}
const std::expected<canon::plan, std::string> plan = make_plan(*project);
if (!plan) {
std::println(stderr, "canon: {}", plan.error());
return EXIT_FAILURE;
}
std::println("{} ({}):", project->declared.project.name, display_path(project->root));
if (!apply_and_report(*plan, project->root, options.dry_run)) {
return EXIT_FAILURE;
}
if (!options.dry_run) {
std::println("\n{}", done);
}
return EXIT_SUCCESS;
}
int run_doctor(const canon::doctor_command& cmd) {
const auto project = canon::load_project(cmd.directory);
if (!project) {
std::println(stderr, "canon: {}", project.error());
return EXIT_FAILURE;
}
const auto findings = canon::diagnose(*project, canon::list_project_files(project->root));
std::println("{} ({}):", project->declared.project.name, display_path(project->root));
if (findings.empty()) {
std::println(" No problems found.");
return EXIT_SUCCESS;
}
int errors = 0;
for (const canon::finding& f : findings) {
errors += f.level == canon::severity::error ? 1 : 0;
std::println(" {:<8} {}: {}", canon::to_string(f.level), f.path, f.message);
}
const auto warnings = static_cast<int>(findings.size()) - errors;
std::println("\n{} {}, {} {}.", errors, errors == 1 ? "error" : "errors", warnings,
warnings == 1 ? "warning" : "warnings");
return errors > 0 ? EXIT_FAILURE : EXIT_SUCCESS;
}
constexpr std::string_view rebuild = "Rebuild with: cmake --build build";
std::string target_suffix(const std::string& target) {
return target.empty() ? "" : std::format(" to {}", target);
}
} // namespace
int main(int argc, char* argv[]) try {
const std::vector<std::string_view> args(argv + 1, argv + argc);
const auto command = canon::parse_command_line(args);
if (!command) {
std::println(stderr, "canon: {}\nRun 'canon --help' for usage.", command.error());
return usage_error;
}
using namespace canon;
return std::visit(
details::overloaded{
[](const help_command&) {
std::print("{}", usage());
return EXIT_SUCCESS;
},
[](const version_command&) {
std::println("canon {}", CANON_VERSION);
return EXIT_SUCCESS;
},
[](const new_command& cmd) { return run_new(cmd); },
[](const add_module_command& cmd) {
return run_add(
cmd.options, [&](const project& p) { return plan_add_module(p, cmd.request); },
std::format("Added module {}{}. {}", cmd.request.path, target_suffix(cmd.request.target),
rebuild));
},
[](const add_unit_test_command& cmd) {
return run_add(
cmd.options, [&](const project& p) { return plan_add_unit_test(p, cmd.request); },
std::format("Added a unit test for module {}. {}", cmd.request.path, rebuild));
},
[](const add_target_command& cmd) {
const bool library = cmd.request.kind == project_kind::library;
return run_add(
cmd.options, [&](const project& p) { return plan_add_target(p, cmd.request); },
library ? std::format("Added library {}. Link it into another target with:\n"
" canon add dep {} --target <target>",
cmd.request.name, cmd.request.name)
: std::format("Added executable {}. {}", cmd.request.name, rebuild));
},
[](const add_test_command& cmd) {
return run_add(
cmd.options, [&](const project& p) { return plan_add_test(p, cmd.request); },
std::format("Added test {}. Build and run it with:\n"
" cmake --build build && ctest --test-dir build -R '^{}$'",
cmd.request.name, cmd.request.name));
},
[](const add_dep_command& cmd) {
return run_add(
cmd.options, [&](const project& p) { return plan_add_dep(p, cmd.request); },
std::format("Added dependency {}{}. {}", cmd.request.dependency,
target_suffix(cmd.request.target), rebuild));
},
[](const doctor_command& cmd) { return run_doctor(cmd); },
},
*command);
} catch (const std::exception& e) {
std::println(stderr, "canon: internal error: {}", e.what());
return EXIT_FAILURE;
}