#include "breeze/Modules.hpp" #include #include #include #include "breeze/CMakeText.hpp" #include "breeze/Error.hpp" #include "breeze/Fs.hpp" #include "breeze/Text.hpp" namespace breeze { namespace sfs = std::filesystem; std::string kindName(ModuleKind kind) { switch (kind) { case ModuleKind::Exec: return "exec"; case ModuleKind::Lib: return "lib"; case ModuleKind::Obj: return "obj"; case ModuleKind::Test: return "test"; case ModuleKind::Example: return "example"; } return "?"; } namespace { struct MacroInfo { const char* name; ModuleKind kind; LibType libType; }; const std::array Macros = {{ {"add_lib_module", ModuleKind::Lib, LibType::Unspecified}, {"add_static_lib_module", ModuleKind::Lib, LibType::Static}, {"add_shared_lib_module", ModuleKind::Lib, LibType::Shared}, {"add_objects_module", ModuleKind::Obj, LibType::Unspecified}, {"add_exe_module", ModuleKind::Exec, LibType::Unspecified}, {"add_test_module", ModuleKind::Test, LibType::Unspecified}, {"add_example_module", ModuleKind::Example, LibType::Unspecified}, }}; const MacroInfo* macroInfo(const std::string& name) { std::string lowered = text::lower(name); for (const MacroInfo& m : Macros) { if (lowered == m.name) return &m; } return nullptr; } bool isModuleCall(const std::string& name) { return macroInfo(name) != nullptr; } bool isKeyword(const std::string& s) { return s == "OUTPUT_NAME" || s == "TYPE" || s == "DEPENDS" || s == "EXTRA_SOURCES" || s == "NO_DOCS"; } Module toModule(const cmake::Call& call, const sfs::path& dir, ModuleKind /*category*/) { const MacroInfo* info = macroInfo(call.name); Module m; m.macro = text::lower(call.name); m.dir = dir; m.dirName = dir.filename().string(); m.kind = info->kind; m.libType = info->libType; std::string mode; bool haveId = false; for (const cmake::Token& t : call.args) { if (!t.quoted && isKeyword(t.text)) { mode = t.text; continue; } if (!haveId) { m.id = t.text; haveId = true; } else if (mode == "OUTPUT_NAME") { m.outputName = t.text; } else if (mode == "TYPE") { std::string v = text::lower(t.text); m.libType = v == "shared" ? LibType::Shared : v == "static" ? LibType::Static : m.libType; } else if (mode == "DEPENDS") { m.deps.push_back(t.text); } } return m; } void scanDirectory(const sfs::path& container, ModuleKind category, std::vector& out, bool skipContainers) { std::error_code ec; if (!sfs::is_directory(container, ec)) return; std::vector dirs; for (const sfs::directory_entry& e : sfs::directory_iterator(container, ec)) { if (!e.is_directory()) continue; std::string name = e.path().filename().string(); if (skipContainers && (name == "tests" || name == "examples") && !sfs::exists(e.path() / "CMakeLists.txt")) continue; if (sfs::is_regular_file(e.path() / "CMakeLists.txt", ec)) dirs.push_back(e.path()); } std::sort(dirs.begin(), dirs.end()); for (const sfs::path& dir : dirs) { std::optional text = fs::tryReadText(dir / "CMakeLists.txt"); if (!text) continue; for (const cmake::Call& call : cmake::findCalls(*text, isModuleCall)) { if (call.args.empty()) continue; out.push_back(toModule(call, dir, category)); } } } sfs::path moduleDirectory(const Project& project, ModuleKind kind, const std::string& name) { sfs::path source = project.root / "source"; if (kind == ModuleKind::Test) return source / "tests" / name; if (kind == ModuleKind::Example) return source / "examples" / name; return source / name; } // plus the kind's suffix, unless the name already ends that way // (a module called UtilTest is util_test, not util_test_test). std::string defaultId(ModuleKind kind, const std::string& name) { std::string base = text::snakeCase(name); std::string suffix = "_" + kindName(kind); if (kind == ModuleKind::Exec) suffix = "_exe"; return text::endsWith(base, suffix) ? base : base + suffix; } std::string applyTemplate(std::string t, const std::string& name, const std::string& snake) { t = text::replaceAll(std::move(t), "@NAME@", name); t = text::replaceAll(std::move(t), "@SNAKE@", snake); return t; } const char* const MainCpp = R"(#include int main() { std::cout << "Hello from @NAME@\n"; return 0; } )"; const char* const LibHeader = R"(#pragma once #include namespace @SNAKE@ { /// A placeholder: replace it with the module's real interface. std::string describe(); } )"; const char* const LibSource = R"(#include "@SNAKE@/@NAME@.hpp" namespace @SNAKE@ { std::string describe() { return "@NAME@"; } } )"; const char* const TestMain = R"(// A test is an executable that exits with 0 when everything holds; ctest runs it. int main() { return 0; } )"; } std::vector scanModules(const Project& project) { std::vector modules; sfs::path source = project.root / "source"; scanDirectory(source, ModuleKind::Exec, modules, true); scanDirectory(source / "tests", ModuleKind::Test, modules, false); scanDirectory(source / "examples", ModuleKind::Example, modules, false); return modules; } const Module* findModule(const std::vector& modules, const std::string& idOrDir) { for (const Module& m : modules) { if (m.id == idOrDir) return &m; } for (const Module& m : modules) { if (m.dirName == idOrDir) return &m; } // A directory name typed in another case, when only one module could mean it. const Module* found = nullptr; for (const Module& m : modules) { if (text::lower(m.dirName) == text::lower(idOrDir)) { if (found != nullptr) return nullptr; found = &m; } } return found; } namespace { const Module& require(const std::vector& modules, const std::string& idOrDir) { if (const Module* m = findModule(modules, idOrDir)) return *m; throw Error("there is no module '" + idOrDir + "' (see `breeze ls`)"); } } Module addModule(const Project& project, const NewModule& request) { if (!text::isName(request.name)) throw Error("'" + request.name + "' is not a valid module name: it starts with a letter and has only " "letters, digits and '_' (it becomes a directory and a C++ namespace)"); if (request.kind == ModuleKind::Obj && !project.sdk.supportsObjects) throw Error("this project's SDK (" + project.sdk.describe() + ") has no objects modules. Move to an SDK that " "has them with " + (project.sdk.layout == SdkLayout::Legacy ? "`breeze sdk migrate `" : "`breeze sdk upgrade`")); if (request.kind == ModuleKind::Lib && request.libType == LibType::Unspecified) throw Error("a library is either --static or --shared; say which"); std::string id = request.id ? *request.id : defaultId(request.kind, request.name); if (!text::isTargetId(id)) throw Error("'" + id + "' is not a valid module id (letters, digits, '_', '.', '+', '-')"); std::vector existing = scanModules(project); for (const Module& m : existing) { if (m.id == id) throw Error("the id '" + id + "' is already taken by the module in source/" + m.dirName + (m.kind == ModuleKind::Test ? " (tests)" : "")); } sfs::path dir = moduleDirectory(project, request.kind, request.name); if (sfs::exists(dir)) throw Error("'" + fs::generic(sfs::relative(dir, project.root)) + "' already exists"); std::vector deps; for (const std::string& d : request.deps) { if (d == id || d == request.name) throw Error("a module can't depend on itself ('" + d + "')"); const Module* dep = findModule(existing, d); if (dep != nullptr) { if (dep->kind == ModuleKind::Exec || dep->kind == ModuleKind::Test || dep->kind == ModuleKind::Example) throw Error("'" + d + "' is " + (dep->kind == ModuleKind::Exec || dep->kind == ModuleKind::Example ? "an " : "a ") + kindName(dep->kind) + " module: only libraries and objects modules can be depended on"); deps.push_back(dep->id); } else if (text::isTargetId(d)) { deps.push_back(d); // a target from elsewhere: a dependency, a system library } else { throw Error("'" + d + "' is not a module of this project or a valid target name"); } } std::string snake = text::snakeCase(request.name); std::string call; switch (request.kind) { case ModuleKind::Exec: call = "add_exe_module(" + id + " OUTPUT_NAME " + cmake::quote(snake); break; case ModuleKind::Lib: call = "add_lib_module(" + id + " TYPE " + (request.libType == LibType::Shared ? "SHARED" : "STATIC") + " OUTPUT_NAME " + cmake::quote(snake); break; case ModuleKind::Obj: call = "add_objects_module(" + id + " OUTPUT_NAME " + cmake::quote(snake); break; case ModuleKind::Test: call = "add_test_module(" + id; break; case ModuleKind::Example: call = "add_example_module(" + id; break; } if (!deps.empty()) call += " DEPENDS " + text::join(deps, " "); call += ")\n"; const char* comment = nullptr; switch (request.kind) { case ModuleKind::Exec: comment = "# An executable: every .cpp in this directory, headers in include/.\n"; break; case ModuleKind::Lib: comment = "# A library: sources in src/, public headers in include/.\n"; break; case ModuleKind::Obj: comment = "# An objects module: compiled to object files that whatever names it in " "DEPENDS takes in.\n"; break; case ModuleKind::Test: comment = "# A test: an executable ctest runs (built with BUILD_TESTS=ON).\n"; break; case ModuleKind::Example: comment = "# An example: an executable (built with BUILD_EXAMPLES=ON).\n"; break; } fs::writeText(dir / "CMakeLists.txt", std::string(comment) + call); if (request.kind == ModuleKind::Lib || request.kind == ModuleKind::Obj) { fs::writeText(dir / "include" / snake / (request.name + ".hpp"), applyTemplate(LibHeader, request.name, snake)); fs::writeText(dir / "src" / (request.name + ".cpp"), applyTemplate(LibSource, request.name, snake)); } else if (request.kind == ModuleKind::Test) { fs::writeText(dir / "main.cpp", TestMain); } else { fs::writeText(dir / "main.cpp", applyTemplate(MainCpp, request.name, snake)); } std::vector now = scanModules(project); const Module* created = findModule(now, id); if (created == nullptr) throw Error("internal error: the new module '" + id + "' was not found after writing it"); return *created; } std::vector dependentsOf(const std::vector& modules, const Module& module, bool recursive) { std::vector out; std::set seen; std::vector queue{module.id}; while (!queue.empty()) { std::string current = queue.front(); queue.erase(queue.begin()); for (const Module& m : modules) { if (std::find(m.deps.begin(), m.deps.end(), current) == m.deps.end()) continue; if (seen.insert(m.id).second) { out.push_back(m.id); if (recursive) queue.push_back(m.id); } } if (!recursive) break; } return out; } std::vector dependenciesOf(const std::vector& modules, const Module& module, bool recursive) { std::vector out; std::set seen{module.id}; std::vector queue{module.id}; while (!queue.empty()) { std::string current = queue.front(); queue.erase(queue.begin()); const Module* m = findModule(modules, current); if (m == nullptr) continue; for (const std::string& d : m->deps) { if (seen.insert(d).second) { out.push_back(d); if (recursive) queue.push_back(d); } } if (!recursive) break; } return out; } void deleteModule(const Project& project, const std::string& idOrDir) { std::vector modules = scanModules(project); const Module& m = require(modules, idOrDir); std::vector users = dependentsOf(modules, m, false); if (!users.empty()) throw Error("'" + m.id + "' can't be deleted: " + text::join(users, ", ") + (users.size() == 1 ? " depends" : " depend") + " on it"); fs::removeTree(m.dir); } namespace { struct Edit { cmake::Token token; std::string value; }; // Applies replacements from the end of the file backwards so earlier offsets stay valid. void rewrite(const sfs::path& file, std::string text, std::vector edits) { std::sort(edits.begin(), edits.end(), [](const Edit& a, const Edit& b) { return a.token.begin > b.token.begin; }); for (const Edit& e : edits) cmake::replaceToken(text, e.token, e.value); fs::writeText(file, text); } } RenameResult renameModule(const Project& project, const std::string& idOrDir, const std::optional& newName, const std::optional& newId) { if (!newName && !newId) throw Error("nothing to rename: give --name and/or --id"); std::vector modules = scanModules(project); const Module& m = require(modules, idOrDir); RenameResult result; result.oldId = m.id; result.newId = newId ? *newId : m.id; result.oldDir = m.dirName; result.newDir = newName ? *newName : m.dirName; if (newName && !text::isName(*newName)) throw Error("'" + *newName + "' is not a valid module name"); if (newId && !text::isTargetId(*newId)) throw Error("'" + *newId + "' is not a valid module id"); if (result.newId == result.oldId && result.newDir == result.oldDir) throw Error("the module already has that name and id"); if (result.newId != result.oldId) { for (const Module& other : modules) { if (other.id == result.newId) throw Error("the id '" + result.newId + "' is already taken"); } } sfs::path newDirPath = m.dir.parent_path() / result.newDir; if (result.newDir != result.oldDir && sfs::exists(newDirPath)) throw Error("'" + fs::generic(sfs::relative(newDirPath, project.root)) + "' already exists"); // Everything is rewritten at the files' present locations first; the directory moves last. std::set files; for (const Module& other : modules) files.insert(other.dir / "CMakeLists.txt"); for (const sfs::path& file : files) { std::string text = fs::readText(file); std::vector edits; bool isOwn = file == m.dir / "CMakeLists.txt"; bool touchedDependency = false; for (const cmake::Call& call : cmake::findCalls(text, isModuleCall)) { if (call.args.empty()) continue; if (isOwn && result.newId != result.oldId && call.args.front().text == result.oldId) edits.push_back({call.args.front(), result.newId}); std::string mode; bool first = true; for (const cmake::Token& t : call.args) { if (!t.quoted && isKeyword(t.text)) { mode = t.text; continue; } if (first) { first = false; continue; } if (mode == "DEPENDS" && result.newId != result.oldId && t.text == result.oldId) { edits.push_back({t, result.newId}); touchedDependency = true; } } } if (edits.empty()) continue; rewrite(file, text, std::move(edits)); if (touchedDependency) { std::string dirName = file.parent_path().filename().string(); if (!isOwn) result.updatedDependents.push_back(dirName); } } if (result.newDir != result.oldDir) { std::error_code ec; sfs::rename(m.dir, newDirPath, ec); if (ec) throw Error("cannot rename '" + fs::generic(m.dir) + "': " + ec.message()); } return result; } }