Add project files

This commit is contained in:
alejo committed 2026-10-24 09:53:48 -03:00
commit f2fe6b8855
114 files changed
+20525

No files matched your search

+580
View File
@@ -0,0 +1,580 @@
#include "breeze/Modules.hpp"
#include <algorithm>
#include <array>
#include <set>
#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<MacroInfo, 7> 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<Module>& out, bool skipContainers)
{
std::error_code ec;
if (!sfs::is_directory(container, ec))
return;
std::vector<sfs::path> 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<std::string> 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;
}
// <name in snake case> 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 <iostream>
int main()
{
std::cout << "Hello from @NAME@\n";
return 0;
}
)";
const char* const LibHeader = R"(#pragma once
#include <string>
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<Module> scanModules(const Project& project)
{
std::vector<Module> 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<Module>& 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<Module>& 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 <version>`"
: "`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<Module> 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<std::string> 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<Module> 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<std::string> dependentsOf(const std::vector<Module>& modules, const Module& module,
bool recursive)
{
std::vector<std::string> out;
std::set<std::string> seen;
std::vector<std::string> 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<std::string> dependenciesOf(const std::vector<Module>& modules,
const Module& module, bool recursive)
{
std::vector<std::string> out;
std::set<std::string> seen{module.id};
std::vector<std::string> 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<Module> modules = scanModules(project);
const Module& m = require(modules, idOrDir);
std::vector<std::string> 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<Edit> 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<std::string>& newName,
const std::optional<std::string>& newId)
{
if (!newName && !newId)
throw Error("nothing to rename: give --name and/or --id");
std::vector<Module> 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<sfs::path> 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<Edit> 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;
}
}