Add project files
This commit is contained in:
commit
f2fe6b8855
114 files changed
+20525
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user