{ | { | ||||
"packages": { | "packages": { | ||||
"ctre": { | |||||
"2.7.0": { | |||||
"depends": {}, | |||||
"description": "A compile-time PCRE (almost) compatible regular expression matcher", | |||||
"git": { | |||||
"auto-lib": "hanickadot/ctre", | |||||
"ref": "v2.7", | |||||
"url": "https://github.com/hanickadot/compile-time-regular-expressions.git" | |||||
} | |||||
} | |||||
}, | |||||
"fmt": { | "fmt": { | ||||
"0.10.0": { | "0.10.0": { | ||||
"depends": {}, | "depends": {}, |
Uses: vob/semester | Uses: vob/semester | ||||
Uses: pubgrub/pubgrub | Uses: pubgrub/pubgrub | ||||
Uses: vob/json5 | Uses: vob/json5 | ||||
Uses: hanickadot/ctre |
"pubgrub/pubgrub", | "pubgrub/pubgrub", | ||||
"vob/json5", | "vob/json5", | ||||
"vob/semester", | "vob/semester", | ||||
"hanickadot/ctre", | |||||
] | ] | ||||
} | } |
Depends: pubgrub 0.2.0 | Depends: pubgrub 0.2.0 | ||||
Depends: vob-json5 0.1.5 | Depends: vob-json5 0.1.5 | ||||
Depends: vob-semester 0.1.0 | Depends: vob-semester 0.1.0 | ||||
Depends: ctre 2.7.0 | |||||
Test-Driver: Catch-Main | Test-Driver: Catch-Main |
"semver": "0.2.1", | "semver": "0.2.1", | ||||
"pubgrub": "0.2.0", | "pubgrub": "0.2.0", | ||||
"vob-json5": "0.1.5", | "vob-json5": "0.1.5", | ||||
"vob-semester": "0.1.0" | |||||
"vob-semester": "0.1.0", | |||||
"ctre": "2.7.0", | |||||
}, | }, | ||||
"test_driver": "Catch-Main" | "test_driver": "Catch-Main" | ||||
} | } |
lib_reqs.include_paths.push_back(lib.library_().public_include_dir()); | lib_reqs.include_paths.push_back(lib.library_().public_include_dir()); | ||||
lib_reqs.uses = lib.library_().manifest().uses; | lib_reqs.uses = lib.library_().manifest().uses; | ||||
lib_reqs.links = lib.library_().manifest().links; | lib_reqs.links = lib.library_().manifest().links; | ||||
if (const auto& arc = lib.create_archive()) { | |||||
if (const auto& arc = lib.archive_plan()) { | |||||
lib_reqs.linkable_path = out_root / arc->calc_archive_file_path(toolchain); | lib_reqs.linkable_path = out_root / arc->calc_archive_file_path(toolchain); | ||||
} | } | ||||
auto gen_incdir_opt = lib.generated_include_dir(); | |||||
if (gen_incdir_opt) { | |||||
lib_reqs.include_paths.push_back(out_root / *gen_incdir_opt); | |||||
} | |||||
} | } | ||||
} | } | ||||
return ureqs; | return ureqs; | ||||
for (auto&& link : lib.links()) { | for (auto&& link : lib.links()) { | ||||
out << "Links: " << link.namespace_ << "/" << link.name << '\n'; | out << "Links: " << link.namespace_ << "/" << link.name << '\n'; | ||||
} | } | ||||
if (auto&& arc = lib.create_archive()) { | |||||
if (auto&& arc = lib.archive_plan()) { | |||||
out << "Path: " | out << "Path: " | ||||
<< (env.output_root / arc->calc_archive_file_path(env.toolchain)).generic_string() | << (env.output_root / arc->calc_archive_file_path(env.toolchain)).generic_string() | ||||
<< '\n'; | << '\n'; | ||||
generate_compdb(plan, env); | generate_compdb(plan, env); | ||||
} | } | ||||
plan.render_all(env); | |||||
dds::stopwatch sw; | dds::stopwatch sw; | ||||
plan.compile_all(env, params.parallel_jobs); | plan.compile_all(env, params.parallel_jobs); | ||||
spdlog::info("Compilation completed in {:n}ms", sw.elapsed_ms().count()); | spdlog::info("Compilation completed in {:n}ms", sw.elapsed_ms().count()); |
* Return a range iterating over ever file compilation defined in the given build plan | * Return a range iterating over ever file compilation defined in the given build plan | ||||
*/ | */ | ||||
inline auto iter_compilations(const build_plan& plan) { | inline auto iter_compilations(const build_plan& plan) { | ||||
auto lib_compiles = // | |||||
iter_libraries(plan) // | |||||
| ranges::views::transform(&library_plan::create_archive) // | |||||
| ranges::views::filter([&](auto&& opt) { return bool(opt); }) // | |||||
| ranges::views::transform([&](auto&& opt) -> auto& { return opt->compile_files(); }) // | |||||
| ranges::views::join // | |||||
auto lib_compiles = // | |||||
iter_libraries(plan) // | |||||
| ranges::views::transform(&library_plan::archive_plan) // | |||||
| ranges::views::filter([&](auto&& opt) { return bool(opt); }) // | |||||
| ranges::views::transform([&](auto&& opt) -> auto& { | |||||
return opt->file_compilations(); | |||||
}) // | |||||
| ranges::views::join // | |||||
; | ; | ||||
auto exe_compiles = // | auto exe_compiles = // |
/** | /** | ||||
* Get the compilation plans for this library. | * Get the compilation plans for this library. | ||||
*/ | */ | ||||
auto& compile_files() const noexcept { return _compile_files; } | |||||
auto& file_compilations() const noexcept { return _compile_files; } | |||||
/** | /** | ||||
* Perform the actual archive generation. Expects all compilations to have | * Perform the actual archive generation. Expects all compilations to have |
compile_command_info compile_file_plan::generate_compile_command(build_env_ref env) const { | compile_command_info compile_file_plan::generate_compile_command(build_env_ref env) const { | ||||
compile_file_spec spec{_source.path, calc_object_file_path(env)}; | compile_file_spec spec{_source.path, calc_object_file_path(env)}; | ||||
spec.enable_warnings = _rules.enable_warnings(); | spec.enable_warnings = _rules.enable_warnings(); | ||||
extend(spec.include_dirs, _rules.include_dirs()); | |||||
for (auto dirpath : _rules.include_dirs()) { | |||||
if (!dirpath.is_absolute()) { | |||||
dirpath = env.output_root / dirpath; | |||||
} | |||||
dirpath = fs::weakly_canonical(dirpath); | |||||
spec.include_dirs.push_back(std::move(dirpath)); | |||||
} | |||||
for (const auto& use : _rules.uses()) { | for (const auto& use : _rules.uses()) { | ||||
extend(spec.external_include_dirs, env.ureqs.include_paths(use)); | extend(spec.external_include_dirs, env.ureqs.include_paths(use)); | ||||
} | } | ||||
} | } | ||||
fs::path compile_file_plan::calc_object_file_path(const build_env& env) const noexcept { | fs::path compile_file_plan::calc_object_file_path(const build_env& env) const noexcept { | ||||
// `relpath` is just the path from the root of the source directory to the source file. | |||||
auto relpath = fs::relative(_source.path, _source.basis_path); | |||||
auto relpath = _source.relative_path(); | |||||
// The full output directory is prefixed by `_subdir` | // The full output directory is prefixed by `_subdir` | ||||
auto ret = env.output_root / _subdir / relpath; | auto ret = env.output_root / _subdir / relpath; | ||||
ret.replace_filename(relpath.filename().string() + env.toolchain.object_suffix()); | ret.replace_filename(relpath.filename().string() + env.toolchain.object_suffix()); |
for (const lm::usage& links : _links) { | for (const lm::usage& links : _links) { | ||||
extend(spec.inputs, env.ureqs.link_paths(links)); | extend(spec.inputs, env.ureqs.link_paths(links)); | ||||
} | } | ||||
if (lib.create_archive()) { | |||||
if (lib.archive_plan()) { | |||||
// The associated library has compiled components. Add the static library a as a linker | // The associated library has compiled components. Add the static library a as a linker | ||||
// input | // input | ||||
spec.inputs.push_back(env.output_root | spec.inputs.push_back(env.output_root | ||||
/ lib.create_archive()->calc_archive_file_path(env.toolchain)); | |||||
/ lib.archive_plan()->calc_archive_file_path(env.toolchain)); | |||||
} | } | ||||
// The main object should be a linker input, of course. | // The main object should be a linker input, of course. |
#include <range/v3/view/concat.hpp> | #include <range/v3/view/concat.hpp> | ||||
#include <range/v3/view/filter.hpp> | #include <range/v3/view/filter.hpp> | ||||
#include <range/v3/view/join.hpp> | #include <range/v3/view/join.hpp> | ||||
#include <range/v3/view/repeat.hpp> | |||||
#include <range/v3/view/transform.hpp> | #include <range/v3/view/transform.hpp> | ||||
#include <range/v3/view/zip.hpp> | |||||
#include <spdlog/spdlog.h> | #include <spdlog/spdlog.h> | ||||
return exceptions.empty(); | return exceptions.empty(); | ||||
} | } | ||||
template <typename T, typename Range> | |||||
decltype(auto) pair_up(T& left, Range& right) { | |||||
auto rep = ranges::view::repeat(left); | |||||
return ranges::view::zip(rep, right); | |||||
} | |||||
} // namespace | } // namespace | ||||
void build_plan::render_all(build_env_ref env) const { | |||||
auto templates = _packages // | |||||
| ranges::view::transform(&package_plan::libraries) // | |||||
| ranges::view::join // | |||||
| ranges::view::transform([](const auto& lib) { return pair_up(lib, lib.templates()); }) // | |||||
| ranges::view::join; | |||||
for (const auto& [lib, tmpl] : templates) { | |||||
tmpl.render(env, lib.library_()); | |||||
} | |||||
} | |||||
void build_plan::compile_all(const build_env& env, int njobs) const { | void build_plan::compile_all(const build_env& env, int njobs) const { | ||||
auto okay = dds::compile_all(iter_compilations(*this), env, njobs); | auto okay = dds::compile_all(iter_compilations(*this), env, njobs); | ||||
if (!okay) { | if (!okay) { | ||||
void build_plan::archive_all(const build_env& env, int njobs) const { | void build_plan::archive_all(const build_env& env, int njobs) const { | ||||
auto okay = parallel_run(iter_libraries(*this), njobs, [&](const library_plan& lib) { | auto okay = parallel_run(iter_libraries(*this), njobs, [&](const library_plan& lib) { | ||||
if (lib.create_archive()) { | |||||
lib.create_archive()->archive(env); | |||||
if (lib.archive_plan()) { | |||||
lib.archive_plan()->archive(env); | |||||
} | } | ||||
}); | }); | ||||
if (!okay) { | if (!okay) { |
* All of the packages in this plan | * All of the packages in this plan | ||||
*/ | */ | ||||
auto& packages() const noexcept { return _packages; } | auto& packages() const noexcept { return _packages; } | ||||
/** | |||||
* Render all config templates in the plan. | |||||
*/ | |||||
void render_all(const build_env& env) const; | |||||
/** | /** | ||||
* Compile all files in the plan. | * Compile all files in the plan. | ||||
*/ | */ |
#include <range/v3/view/transform.hpp> | #include <range/v3/view/transform.hpp> | ||||
#include <cassert> | #include <cassert> | ||||
#include <string> | |||||
using namespace dds; | using namespace dds; | ||||
namespace { | |||||
const std::string gen_dir_qual = "__dds/gen"; | |||||
fs::path rebase_gen_incdir(path_ref subdir) { return gen_dir_qual / subdir; } | |||||
} // namespace | |||||
std::optional<fs::path> library_plan::generated_include_dir() const noexcept { | |||||
if (_templates.empty()) { | |||||
return std::nullopt; | |||||
} | |||||
return rebase_gen_incdir(output_subdirectory()); | |||||
} | |||||
library_plan library_plan::create(const library_root& lib, | library_plan library_plan::create(const library_root& lib, | ||||
const library_build_params& params, | const library_build_params& params, | ||||
std::optional<std::string_view> qual_name_) { | std::optional<std::string_view> qual_name_) { | ||||
std::vector<source_file> app_sources; | std::vector<source_file> app_sources; | ||||
std::vector<source_file> test_sources; | std::vector<source_file> test_sources; | ||||
std::vector<source_file> lib_sources; | std::vector<source_file> lib_sources; | ||||
std::vector<source_file> template_sources; | |||||
auto qual_name = std::string(qual_name_.value_or(lib.manifest().name)); | auto qual_name = std::string(qual_name_.value_or(lib.manifest().name)); | ||||
app_sources.push_back(sfile); | app_sources.push_back(sfile); | ||||
} else if (sfile.kind == source_kind::source) { | } else if (sfile.kind == source_kind::source) { | ||||
lib_sources.push_back(sfile); | lib_sources.push_back(sfile); | ||||
} else if (sfile.kind == source_kind::header_template) { | |||||
template_sources.push_back(sfile); | |||||
} else { | } else { | ||||
assert(sfile.kind == source_kind::header); | assert(sfile.kind == source_kind::header); | ||||
} | } | ||||
compile_rules.enable_warnings() = params.enable_warnings; | compile_rules.enable_warnings() = params.enable_warnings; | ||||
compile_rules.uses() = lib.manifest().uses; | compile_rules.uses() = lib.manifest().uses; | ||||
const auto codegen_subdir = rebase_gen_incdir(params.out_subdir); | |||||
if (!template_sources.empty()) { | |||||
compile_rules.include_dirs().push_back(codegen_subdir); | |||||
} | |||||
// Convert the library sources into their respective file compilation plans. | // Convert the library sources into their respective file compilation plans. | ||||
auto lib_compile_files = // | auto lib_compile_files = // | ||||
lib_sources // | lib_sources // | ||||
// If we have any compiled library files, generate a static library archive | // If we have any compiled library files, generate a static library archive | ||||
// for this library | // for this library | ||||
std::optional<create_archive_plan> create_archive; | |||||
std::optional<create_archive_plan> archive_plan; | |||||
if (!lib_compile_files.empty()) { | if (!lib_compile_files.empty()) { | ||||
create_archive.emplace(lib.manifest().name, | |||||
qual_name, | |||||
params.out_subdir, | |||||
std::move(lib_compile_files)); | |||||
archive_plan.emplace(lib.manifest().name, | |||||
qual_name, | |||||
params.out_subdir, | |||||
std::move(lib_compile_files)); | |||||
} | } | ||||
// Collect the paths to linker inputs that should be used when generating executables for this | // Collect the paths to linker inputs that should be used when generating executables for this | ||||
// Pick a subdir based on app/test | // Pick a subdir based on app/test | ||||
const auto subdir_base = is_test ? params.out_subdir / "test" : params.out_subdir; | const auto subdir_base = is_test ? params.out_subdir / "test" : params.out_subdir; | ||||
// Put test/app executables in a further subdirectory based on the source file path | // Put test/app executables in a further subdirectory based on the source file path | ||||
const auto subdir | |||||
= subdir_base / fs::relative(source.path.parent_path(), lib.src_source_root().path); | |||||
const auto subdir = subdir_base / source.relative_path().parent_path(); | |||||
// Pick compile rules based on app/test | // Pick compile rules based on app/test | ||||
auto rules = is_test ? test_rules : compile_rules; | auto rules = is_test ? test_rules : compile_rules; | ||||
// Pick input libs based on app/test | // Pick input libs based on app/test | ||||
link_executables.emplace_back(std::move(exe)); | link_executables.emplace_back(std::move(exe)); | ||||
} | } | ||||
std::vector<render_template_plan> render_templates; | |||||
for (const auto& sf : template_sources) { | |||||
render_templates.emplace_back(sf, codegen_subdir); | |||||
} | |||||
// Done! | // Done! | ||||
return library_plan{lib, qual_name, std::move(create_archive), std::move(link_executables)}; | |||||
return library_plan{lib, | |||||
qual_name, | |||||
params.out_subdir, | |||||
std::move(archive_plan), | |||||
std::move(link_executables), | |||||
std::move(render_templates)}; | |||||
} | } |
#include <dds/build/plan/archive.hpp> | #include <dds/build/plan/archive.hpp> | ||||
#include <dds/build/plan/exe.hpp> | #include <dds/build/plan/exe.hpp> | ||||
#include <dds/build/plan/template.hpp> | |||||
#include <dds/library/root.hpp> | #include <dds/library/root.hpp> | ||||
#include <dds/usage_reqs.hpp> | #include <dds/usage_reqs.hpp> | ||||
#include <dds/util/fs.hpp> | #include <dds/util/fs.hpp> | ||||
library_root _lib; | library_root _lib; | ||||
/// The qualified name of the library | /// The qualified name of the library | ||||
std::string _qual_name; | std::string _qual_name; | ||||
/// The library's subdirectory within the output directory | |||||
fs::path _subdir; | |||||
/// The `create_archive_plan` for this library, if applicable | /// The `create_archive_plan` for this library, if applicable | ||||
std::optional<create_archive_plan> _create_archive; | std::optional<create_archive_plan> _create_archive; | ||||
/// The executables that should be linked as part of this library's build | /// The executables that should be linked as part of this library's build | ||||
std::vector<link_executable_plan> _link_exes; | std::vector<link_executable_plan> _link_exes; | ||||
/// The templates that must be rendered for this library | |||||
std::vector<render_template_plan> _templates; | |||||
public: | public: | ||||
/** | /** | ||||
*/ | */ | ||||
library_plan(library_root lib, | library_plan(library_root lib, | ||||
std::string_view qual_name, | std::string_view qual_name, | ||||
fs::path subdir, | |||||
std::optional<create_archive_plan> ar, | std::optional<create_archive_plan> ar, | ||||
std::vector<link_executable_plan> exes) | |||||
std::vector<link_executable_plan> exes, | |||||
std::vector<render_template_plan> tmpls) | |||||
: _lib(std::move(lib)) | : _lib(std::move(lib)) | ||||
, _qual_name(qual_name) | , _qual_name(qual_name) | ||||
, _subdir(std::move(subdir)) | |||||
, _create_archive(std::move(ar)) | , _create_archive(std::move(ar)) | ||||
, _link_exes(std::move(exes)) {} | |||||
, _link_exes(std::move(exes)) | |||||
, _templates(std::move(tmpls)) {} | |||||
/** | /** | ||||
* Get the underlying library object | * Get the underlying library object | ||||
* Get the qualified name of the library, as if for a libman usage requirement | * Get the qualified name of the library, as if for a libman usage requirement | ||||
*/ | */ | ||||
auto& qualified_name() const noexcept { return _qual_name; } | auto& qualified_name() const noexcept { return _qual_name; } | ||||
/** | |||||
* The output subdirectory of this library plan | |||||
*/ | |||||
path_ref output_subdirectory() const noexcept { return _subdir; } | |||||
/** | /** | ||||
* The directory that defines the source root of the library. | * The directory that defines the source root of the library. | ||||
*/ | */ | ||||
* A `create_archive_plan` object, or `nullopt`, depending on if this library has compiled | * A `create_archive_plan` object, or `nullopt`, depending on if this library has compiled | ||||
* components | * components | ||||
*/ | */ | ||||
auto& create_archive() const noexcept { return _create_archive; } | |||||
auto& archive_plan() const noexcept { return _create_archive; } | |||||
/** | |||||
* The template rendering plans for this library. | |||||
*/ | |||||
auto& templates() const noexcept { return _templates; } | |||||
/** | /** | ||||
* The executables that should be created by this library | * The executables that should be created by this library | ||||
*/ | */ | ||||
* The library identifiers that are linked by this library | * The library identifiers that are linked by this library | ||||
*/ | */ | ||||
auto& links() const noexcept { return _lib.manifest().links; } | auto& links() const noexcept { return _lib.manifest().links; } | ||||
/** | |||||
* The path to the directory that should be added for the #include search | |||||
* path for this library, relative to the build root. Returns `nullopt` if | |||||
* this library has no generated headers. | |||||
*/ | |||||
std::optional<fs::path> generated_include_dir() const noexcept; | |||||
/** | /** | ||||
* Named constructor: Create a new `library_plan` automatically from some build-time parameters. | * Named constructor: Create a new `library_plan` automatically from some build-time parameters. | ||||
std::optional<std::string_view> qual_name); | std::optional<std::string_view> qual_name); | ||||
}; | }; | ||||
} // namespace dds | |||||
} // namespace dds |
#include <dds/build/plan/template.hpp> | |||||
#include <dds/error/errors.hpp> | |||||
#include <dds/library/root.hpp> | |||||
#include <dds/util/fs.hpp> | |||||
#include <dds/util/string.hpp> | |||||
#include <ctre.hpp> | |||||
#include <semester/json.hpp> | |||||
#include <string> | |||||
#include <string_view> | |||||
using namespace dds; | |||||
using json_data = semester::basic_data<semester::json_traits<std::allocator<void>>>; | |||||
namespace { | |||||
static constexpr ctll::fixed_string IDENT_RE{"([_a-zA-Z]\\w*)(.*)"}; | |||||
std::string_view skip(std::string_view in) { | |||||
auto nspace_pos = in.find_first_not_of(" \t\n\r\f"); | |||||
in = in.substr(nspace_pos); | |||||
if (starts_with(in, "/*")) { | |||||
// It's a block comment. Find the block-end marker. | |||||
auto block_end = in.find("*/"); | |||||
if (block_end == in.npos) { | |||||
throw_user_error<errc::template_error>("Unterminated block comment"); | |||||
} | |||||
in = in.substr(block_end + 2); | |||||
// Recursively skip some more | |||||
return skip(in); | |||||
} | |||||
if (starts_with(in, "//")) { | |||||
more: | |||||
// It's a line comment. Find the next not-continued newline | |||||
auto cn_nl = in.find("\\\n"); | |||||
auto nl = in.find("\n"); | |||||
if (cn_nl < nl) { | |||||
// The next newline is a continuation of the comment. Keep looking | |||||
in = in.substr(nl + 1); | |||||
goto more; | |||||
} | |||||
if (nl == in.npos) { | |||||
// We've reached the end. Okay. | |||||
return in.substr(nl); | |||||
} | |||||
} | |||||
// Not a comment, and not whitespace. Okay. | |||||
return in; | |||||
} | |||||
std::string stringify(const json_data& dat) { | |||||
if (dat.is_bool()) { | |||||
return dat.as_bool() ? "true" : "false"; | |||||
} else if (dat.is_double()) { | |||||
return std::to_string(dat.as_double()); | |||||
} else if (dat.is_null()) { | |||||
return "nullptr"; | |||||
} else if (dat.is_string()) { | |||||
/// XXX: This probably isn't quite enough sanitization for edge cases. | |||||
auto str = dat.as_string(); | |||||
str = replace(str, "\n", "\\n"); | |||||
str = replace(str, "\"", "\\\""); | |||||
return "\"" + str + "\""; | |||||
} else { | |||||
throw_user_error<errc::template_error>("Cannot render un-stringable data type"); | |||||
} | |||||
} | |||||
std::pair<std::string, std::string_view> eval_expr_tail(std::string_view in, const json_data& dat) { | |||||
in = skip(in); | |||||
if (starts_with(in, ".")) { | |||||
// Accessing a subproperty of the data | |||||
in.remove_prefix(1); | |||||
in = skip(in); | |||||
// We _must_ see an identifier | |||||
auto [is_ident, ident, tail] = ctre::match<IDENT_RE>(in); | |||||
if (!is_ident) { | |||||
throw_user_error<errc::template_error>("Expected identifier following dot `.`"); | |||||
} | |||||
if (!dat.is_mapping()) { | |||||
throw_user_error<errc::template_error>("Cannot use dot `.` on non-mapping object"); | |||||
} | |||||
auto& map = dat.as_mapping(); | |||||
auto found = map.find(ident.to_view()); | |||||
if (found == map.end()) { | |||||
throw_user_error<errc::template_error>("No subproperty '{}'", ident.to_view()); | |||||
} | |||||
return eval_expr_tail(tail, found->second); | |||||
} | |||||
return {stringify(dat), in}; | |||||
} | |||||
std::pair<std::string, std::string_view> eval_primary_expr(std::string_view in, | |||||
const json_data& dat) { | |||||
in = skip(in); | |||||
if (in.empty()) { | |||||
throw_user_error<errc::template_error>("Expected primary expression"); | |||||
} | |||||
if (in.front() == '(') { | |||||
in = in.substr(1); | |||||
auto [ret, tail] = eval_primary_expr(in, dat); | |||||
if (!starts_with(tail, ")")) { | |||||
throw_user_error<errc::template_error>( | |||||
"Expected closing parenthesis `)` following expression"); | |||||
} | |||||
return {ret, tail.substr(1)}; | |||||
} | |||||
auto [is_ident, ident, tail_1] = ctre::match<IDENT_RE>(in); | |||||
if (is_ident) { | |||||
auto& map = dat.as_mapping(); | |||||
auto found = map.find(ident.to_view()); | |||||
if (found == map.end()) { | |||||
throw_user_error<errc::template_error>("Unknown top-level identifier '{}'", | |||||
ident.to_view()); | |||||
} | |||||
return eval_expr_tail(tail_1, found->second); | |||||
} | |||||
return {"nope", in}; | |||||
} | |||||
std::string render_template(std::string_view tmpl, const library_root& lib) { | |||||
std::string acc; | |||||
std::string_view MARKER_STRING = "__dds"; | |||||
// Fill out a data structure that will be exposed to the template | |||||
json_data dat = json_data::mapping_type({ | |||||
{ | |||||
"lib", | |||||
json_data::mapping_type{ | |||||
{"name", lib.manifest().name}, | |||||
{"root", lib.path().string()}, | |||||
}, | |||||
}, | |||||
}); | |||||
while (!tmpl.empty()) { | |||||
// Find the next marker in the template string | |||||
auto next_marker = tmpl.find(MARKER_STRING); | |||||
if (next_marker == tmpl.npos) { | |||||
// We've reached the end of the template. Stop | |||||
acc.append(tmpl); | |||||
break; | |||||
} | |||||
// Append the string up to the next marker | |||||
acc.append(tmpl.substr(0, next_marker)); | |||||
// Consume up to the next marker | |||||
tmpl = tmpl.substr(next_marker + MARKER_STRING.size()); | |||||
auto next_not_space = tmpl.find_first_not_of(" \t"); | |||||
if (next_not_space == tmpl.npos || tmpl[next_not_space] != '(') { | |||||
throw_user_error<errc::template_error>( | |||||
"Expected `(` following `__dds` identifier in template file"); | |||||
} | |||||
auto [inner, tail] = eval_primary_expr(tmpl, dat); | |||||
acc.append(inner); | |||||
tmpl = tail; | |||||
} | |||||
return acc; | |||||
} | |||||
} // namespace | |||||
void render_template_plan::render(build_env_ref env, const library_root& lib) const { | |||||
auto content = slurp_file(_source.path); | |||||
// Calculate the destination of the template rendering | |||||
auto dest = env.output_root / _subdir / _source.relative_path(); | |||||
dest.replace_filename(dest.stem().stem().filename().string() + dest.extension().string()); | |||||
fs::create_directories(dest.parent_path()); | |||||
auto result = render_template(content, lib); | |||||
if (fs::is_regular_file(dest)) { | |||||
auto existing_content = slurp_file(dest); | |||||
if (result == existing_content) { | |||||
/// The content of the file has not changed. Do not write a file. | |||||
return; | |||||
} | |||||
} | |||||
auto ofile = open(dest, std::ios::binary | std::ios::out); | |||||
ofile << result; | |||||
ofile.close(); // Throw any exceptions while closing the file | |||||
} |
#pragma once | |||||
#include <dds/build/plan/base.hpp> | |||||
#include <dds/source/file.hpp> | |||||
#include <utility> | |||||
namespace dds { | |||||
class library_root; | |||||
class render_template_plan { | |||||
/** | |||||
* The source file that defines the config template | |||||
*/ | |||||
source_file _source; | |||||
/** | |||||
* The subdirectory in which the template should be rendered. | |||||
*/ | |||||
fs::path _subdir; | |||||
public: | |||||
/** | |||||
* Create a new instance | |||||
* @param sf The source file of the template | |||||
* @param subdir The subdirectort into which the template should render | |||||
*/ | |||||
render_template_plan(source_file sf, path_ref subdir) | |||||
: _source(std::move(sf)) | |||||
, _subdir(subdir) {} | |||||
/** | |||||
* Render the template into its output directory | |||||
*/ | |||||
void render(build_env_ref, const library_root& owning_library) const; | |||||
}; | |||||
} // namespace dds |
invalid_lib_filesystem, | invalid_lib_filesystem, | ||||
invalid_pkg_filesystem, | invalid_pkg_filesystem, | ||||
template_error, | |||||
}; | }; | ||||
std::string error_reference_of(errc) noexcept; | std::string error_reference_of(errc) noexcept; |
auto ext_found | auto ext_found | ||||
= std::lower_bound(header_exts.begin(), header_exts.end(), p.extension(), std::less<>()); | = std::lower_bound(header_exts.begin(), header_exts.end(), p.extension(), std::less<>()); | ||||
if (ext_found != header_exts.end() && *ext_found == p.extension()) { | if (ext_found != header_exts.end() && *ext_found == p.extension()) { | ||||
auto stem = p.stem(); | |||||
if (stem.extension() == ".config") { | |||||
return source_kind::header_template; | |||||
} | |||||
return source_kind::header; | return source_kind::header; | ||||
} | } | ||||
return std::nullopt; | return std::nullopt; | ||||
} | } | ||||
if (ends_with(p.stem().string(), ".test")) { | |||||
if (p.stem().extension() == ".test") { | |||||
return source_kind::test; | return source_kind::test; | ||||
} | } | ||||
if (ends_with(p.stem().string(), ".main")) { | |||||
if (p.stem().extension() == ".main") { | |||||
return source_kind::app; | return source_kind::app; | ||||
} | } | ||||
enum class source_kind { | enum class source_kind { | ||||
header, | header, | ||||
header_template, | |||||
source, | source, | ||||
test, | test, | ||||
app, | app, | ||||
std::optional<source_kind> infer_source_kind(path_ref) noexcept; | std::optional<source_kind> infer_source_kind(path_ref) noexcept; | ||||
struct source_file { | struct source_file { | ||||
fs::path path; | |||||
fs::path basis_path; | |||||
/** | |||||
* The actual path to the file | |||||
*/ | |||||
fs::path path; | |||||
/** | |||||
* The path to source root that contains the file in question | |||||
*/ | |||||
fs::path basis_path; | |||||
/** | |||||
* The kind of the source file | |||||
*/ | |||||
source_kind kind; | source_kind kind; | ||||
static std::optional<source_file> from_path(path_ref path, path_ref base_path) noexcept; | static std::optional<source_file> from_path(path_ref path, path_ref base_path) noexcept; | ||||
fs::path relative_path() const noexcept { return fs::relative(path, basis_path); } | |||||
}; | }; | ||||
using source_list = std::vector<source_file>; | using source_list = std::vector<source_file>; |
#include <dds/source/file.hpp> | |||||
#include <catch2/catch.hpp> | |||||
using dds::source_kind; | |||||
TEST_CASE("Infer source kind") { | |||||
using dds::infer_source_kind; | |||||
auto k = infer_source_kind("foo.h"); | |||||
CHECK(k == source_kind::header); | |||||
CHECK(infer_source_kind("foo.hpp") == source_kind::header); | |||||
CHECK_FALSE(infer_source_kind("foo.txt")); // Not a source file extension | |||||
CHECK(infer_source_kind("foo.hh") == source_kind::header); | |||||
CHECK(infer_source_kind("foo.config.hpp") == source_kind::header_template); | |||||
} |
#pragma once | |||||
#include <string> | |||||
int config_file_value = 42; |
#include <info.hpp> | |||||
#include <cassert> | |||||
int main() { assert(config_file_value == 42); } |
{ | |||||
"name": "test-library" | |||||
} |
{ | |||||
"name": "test-simple", | |||||
"version": "1.2.3-gamma", | |||||
"namespace": "test" | |||||
} |
#pragma once | |||||
#include <string_view> | |||||
std::string_view lib_name = __dds(lib.name); |
#include <simple/config.hpp> | |||||
#include <cassert> | |||||
int main() { assert(lib_name == "test-library"); } |
import pytest | |||||
from time import sleep | |||||
from tests import DDS, dds_fixture_conf_1 | |||||
@dds_fixture_conf_1('copy_only') | |||||
def test_config_template(dds: DDS): | |||||
generated_fpath = dds.build_dir / '__dds/gen/info.hpp' | |||||
assert not generated_fpath.is_file() | |||||
dds.build() | |||||
assert generated_fpath.is_file() | |||||
# Check that re-running the build will not update the generated file (the | |||||
# file's content has not changed. Re-generating it would invalidate the | |||||
# cache and force a false-rebuild.) | |||||
start_time = generated_fpath.stat().st_mtime | |||||
sleep(0.1) # Wait just long enough to register a new stamp time | |||||
dds.build() | |||||
new_time = generated_fpath.stat().st_mtime | |||||
assert new_time == start_time | |||||
@dds_fixture_conf_1('simple') | |||||
def test_simple_substitution(dds: DDS): | |||||
dds.build() |
full_cmd = itertools.chain([self.dds_exe], cmd) | full_cmd = itertools.chain([self.dds_exe], cmd) | ||||
return proc.run(full_cmd, cwd=cwd or self.source_root) | return proc.run(full_cmd, cwd=cwd or self.source_root) | ||||
def run(self, cmd: proc.CommandLine, *, | |||||
cwd: Path = None) -> subprocess.CompletedProcess: | |||||
def run(self, cmd: proc.CommandLine, *, cwd: Path = None, | |||||
check=True) -> subprocess.CompletedProcess: | |||||
cmdline = list(proc.flatten_cmd(cmd)) | cmdline = list(proc.flatten_cmd(cmd)) | ||||
res = self.run_unchecked(cmd, cwd=cwd) | res = self.run_unchecked(cmd, cwd=cwd) | ||||
if res.returncode != 0: | |||||
if res.returncode != 0 and check: | |||||
raise subprocess.CalledProcessError( | raise subprocess.CalledProcessError( | ||||
res.returncode, [self.dds_exe] + cmdline, res.stdout) | res.returncode, [self.dds_exe] + cmdline, res.stdout) | ||||
return res | return res | ||||
toolchain: str = None, | toolchain: str = None, | ||||
apps: bool = True, | apps: bool = True, | ||||
warnings: bool = True, | warnings: bool = True, | ||||
tests: bool = True) -> subprocess.CompletedProcess: | |||||
return self.run([ | |||||
'build', | |||||
f'--out={self.build_dir}', | |||||
f'--toolchain={toolchain or self.default_builtin_toolchain}', | |||||
f'--catalog={self.catalog_path}', | |||||
f'--repo-dir={self.repo_dir}', | |||||
['--no-tests'] if not tests else [], | |||||
['--no-apps'] if not apps else [], | |||||
['--no-warnings'] if not warnings else [], | |||||
self.project_dir_arg, | |||||
]) | |||||
tests: bool = True, | |||||
check: bool = True) -> subprocess.CompletedProcess: | |||||
return self.run( | |||||
[ | |||||
'build', | |||||
f'--out={self.build_dir}', | |||||
f'--toolchain={toolchain or self.default_builtin_toolchain}', | |||||
f'--catalog={self.catalog_path}', | |||||
f'--repo-dir={self.repo_dir}', | |||||
['--no-tests'] if not tests else [], | |||||
['--no-apps'] if not apps else [], | |||||
['--no-warnings'] if not warnings else [], | |||||
self.project_dir_arg, | |||||
], | |||||
check=check, | |||||
) | |||||
def sdist_create(self) -> subprocess.CompletedProcess: | def sdist_create(self) -> subprocess.CompletedProcess: | ||||
return self.run([ | return self.run([ |
'neo-concepts': '^0.2.1', | 'neo-concepts': '^0.2.1', | ||||
}), | }), | ||||
]), | ]), | ||||
Package('ctre', [ | |||||
Version( | |||||
'2.7.0', | |||||
description= | |||||
'A compile-time PCRE (almost) compatible regular expression matcher', | |||||
remote=Git( | |||||
'https://github.com/hanickadot/compile-time-regular-expressions.git', | |||||
'v2.7', | |||||
auto_lib='hanickadot/ctre', | |||||
)) | |||||
]), | |||||
many_versions( | many_versions( | ||||
'spdlog', | 'spdlog', | ||||
( | ( |