| @@ -277,6 +277,31 @@ void write_cmake(build_env_ref env, const build_plan& plan, path_ref cmake_out) | |||
| } | |||
| } | |||
| /** | |||
| * @brief Calculate a hash of the directory layout of the given directory. | |||
| * | |||
| * Because a tweaks-dir is specifically designed to have files added/removed within it, and | |||
| * its contents are inspected by `__has_include`, we need to have a way to invalidate any caches | |||
| * when the content of that directory changes. We don't care to hash the contents of the files, | |||
| * since those will already break any caches. | |||
| */ | |||
| std::string hash_tweaks_dir(const fs::path& tweaks_dir) { | |||
| if (!fs::is_directory(tweaks_dir)) { | |||
| return "0"; // No tweaks directory, no cache to bust | |||
| } | |||
| std::vector<fs::path> children{fs::recursive_directory_iterator{tweaks_dir}, | |||
| fs::recursive_directory_iterator{}}; | |||
| std::sort(children.begin(), children.end()); | |||
| // A really simple inline djb2 hash | |||
| std::uint32_t hash = 5381; | |||
| for (auto& p : children) { | |||
| for (std::uint32_t c : fs::weakly_canonical(p).string()) { | |||
| hash = ((hash << 5) + hash) + c; | |||
| } | |||
| } | |||
| return std::to_string(hash); | |||
| } | |||
| template <typename Func> | |||
| void with_build_plan(const build_params& params, | |||
| const std::vector<sdist_target>& sdists, | |||
| @@ -292,11 +317,20 @@ void with_build_plan(const build_params& params, | |||
| params.out_root, | |||
| db, | |||
| toolchain_knobs{ | |||
| .is_tty = stdout_is_a_tty(), | |||
| .is_tty = stdout_is_a_tty(), | |||
| .tweaks_dir = params.tweaks_dir, | |||
| }, | |||
| ureqs, | |||
| }; | |||
| if (env.knobs.tweaks_dir) { | |||
| env.knobs.cache_buster = hash_tweaks_dir(*env.knobs.tweaks_dir); | |||
| dds_log(trace, | |||
| "Build cache-buster value for tweaks-dir [{}] content is '{}'", | |||
| *env.knobs.tweaks_dir, | |||
| *env.knobs.cache_buster); | |||
| } | |||
| if (st.generate_catch2_main) { | |||
| auto catch_lib = prepare_test_driver(params, test_lib::catch_main, env); | |||
| ureqs.add(".dds", "Catch-Main") = catch_lib; | |||
| @@ -13,9 +13,10 @@ struct build_params { | |||
| std::optional<fs::path> existing_lm_index; | |||
| std::optional<fs::path> emit_lmi; | |||
| std::optional<fs::path> emit_cmake{}; | |||
| std::optional<fs::path> tweaks_dir{}; | |||
| dds::toolchain toolchain; | |||
| bool generate_compdb = true; | |||
| int parallel_jobs = 0; | |||
| }; | |||
| } // namespace dds | |||
| } // namespace dds | |||
| @@ -30,6 +30,7 @@ int build(const options& opts) { | |||
| .out_root = opts.out_path.value_or(fs::current_path() / "_build"), | |||
| .existing_lm_index = opts.build.lm_index, | |||
| .emit_lmi = {}, | |||
| .tweaks_dir = opts.build.tweaks_dir, | |||
| .toolchain = opts.load_toolchain(), | |||
| .parallel_jobs = opts.jobs, | |||
| }); | |||
| @@ -18,6 +18,7 @@ int build_deps(const options& opts) { | |||
| .existing_lm_index = {}, | |||
| .emit_lmi = opts.build.lm_index.value_or("INDEX.lmi"), | |||
| .emit_cmake = opts.build_deps.cmake_file, | |||
| .tweaks_dir = opts.build.tweaks_dir, | |||
| .toolchain = opts.load_toolchain(), | |||
| .parallel_jobs = opts.jobs, | |||
| }; | |||
| @@ -11,6 +11,7 @@ int compile_file(const options& opts) { | |||
| .out_root = opts.out_path.value_or(fs::current_path() / "_build"), | |||
| .existing_lm_index = opts.build.lm_index, | |||
| .emit_lmi = {}, | |||
| .tweaks_dir = opts.build.tweaks_dir, | |||
| .toolchain = opts.load_toolchain(), | |||
| .parallel_jobs = opts.jobs, | |||
| }); | |||
| @@ -90,6 +90,17 @@ struct setup { | |||
| .action = put_into(opts.repoman.repo_dir), | |||
| }; | |||
| argument tweaks_dir_arg{ | |||
| .long_spellings = {"tweaks-dir"}, | |||
| .short_spellings = {"TD"}, | |||
| .help | |||
| = "Base directory of " | |||
| "\x1b]8;;https://vector-of-bool.github.io/2020/10/04/lib-configuration.html\x1b\\tweak " | |||
| "headers\x1b]8;;\x1b\\ that should be available to the build.", | |||
| .valname = "<dir>", | |||
| .action = put_into(opts.build.tweaks_dir), | |||
| }; | |||
| void do_setup(argument_parser& parser) noexcept { | |||
| parser.add_argument({ | |||
| .long_spellings = {"log-level"}, | |||
| @@ -189,6 +200,7 @@ struct setup { | |||
| build_cmd.add_argument(lm_index_arg.dup()).help | |||
| = "Path to a libman index file to use for loading project dependencies"; | |||
| build_cmd.add_argument(jobs_arg.dup()); | |||
| build_cmd.add_argument(tweaks_dir_arg.dup()); | |||
| } | |||
| void setup_compile_file_cmd(argument_parser& compile_file_cmd) noexcept { | |||
| @@ -199,6 +211,7 @@ struct setup { | |||
| = "Set the maximum number of files to compile in parallel"; | |||
| compile_file_cmd.add_argument(lm_index_arg.dup()); | |||
| compile_file_cmd.add_argument(out_arg.dup()); | |||
| compile_file_cmd.add_argument(tweaks_dir_arg.dup()); | |||
| compile_file_cmd.add_argument({ | |||
| .help = "One or more source files to compile", | |||
| .valname = "<source-files>", | |||
| @@ -228,6 +241,7 @@ struct setup { | |||
| .valname = "<file-path>", | |||
| .action = debate::put_into(opts.build_deps.cmake_file), | |||
| }); | |||
| build_deps_cmd.add_argument(tweaks_dir_arg.dup()); | |||
| build_deps_cmd.add_argument({ | |||
| .help = "Dependency statement strings", | |||
| .valname = "<dependency>", | |||
| @@ -136,6 +136,7 @@ struct options { | |||
| opt_path lm_index; | |||
| std::vector<string> add_repos; | |||
| bool update_repos = false; | |||
| opt_path tweaks_dir; | |||
| } build; | |||
| /** | |||
| @@ -97,6 +97,13 @@ compile_command_info toolchain::create_compile_command(const compile_file_spec& | |||
| extend(flags, _tty_flags); | |||
| } | |||
| if (knobs.cache_buster) { | |||
| // This is simply a CPP definition that is used to "bust" any caches that rely on inspecting | |||
| // the command-line of the compiler (including our own). | |||
| auto def = replace(_def_template, "[def]", "__dds_cachebust=" + *knobs.cache_buster); | |||
| extend(flags, def); | |||
| } | |||
| dds_log(trace, "#include-search dirs:"); | |||
| for (auto&& inc_dir : spec.include_dirs) { | |||
| dds_log(trace, " - search: {}", inc_dir.string()); | |||
| @@ -111,6 +118,13 @@ compile_command_info toolchain::create_compile_command(const compile_file_spec& | |||
| extend(flags, inc_args); | |||
| } | |||
| if (knobs.tweaks_dir) { | |||
| dds_log(trace, " - search (tweaks): {}", knobs.tweaks_dir->string()); | |||
| auto shortest = shortest_path_from(*knobs.tweaks_dir, cwd); | |||
| auto tweak_inc_args = include_args(shortest); | |||
| extend(flags, tweak_inc_args); | |||
| } | |||
| for (auto&& def : spec.definitions) { | |||
| auto def_args = definition_args(def); | |||
| extend(flags, def_args); | |||
| @@ -18,6 +18,9 @@ enum class language { | |||
| struct toolchain_knobs { | |||
| bool is_tty = false; | |||
| // Directory storing tweaks for the compilation | |||
| std::optional<fs::path> tweaks_dir{}; | |||
| std::optional<std::string> cache_buster{}; | |||
| }; | |||
| struct compile_file_spec { | |||
| @@ -0,0 +1,21 @@ | |||
| #pragma once | |||
| #if __has_include(<tweakable.tweaks.hpp>) | |||
| #include <tweakable.tweaks.hpp> | |||
| #endif | |||
| namespace tweakable { | |||
| namespace config { | |||
| namespace defaults { | |||
| const int value = 99; | |||
| } // namespace defaults | |||
| using namespace defaults; | |||
| } // namespace config | |||
| } // namespace tweakable | |||
| @@ -0,0 +1,7 @@ | |||
| #pragma once | |||
| namespace tweakable { | |||
| extern int get_value(); | |||
| } // namespace tweakable | |||
| @@ -0,0 +1,3 @@ | |||
| { | |||
| "name": "foo" | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| { | |||
| name: 'tweakable', | |||
| version: '1.2.3', | |||
| "namespace": "test", | |||
| } | |||
| @@ -0,0 +1,6 @@ | |||
| #include <tweakable.config.hpp> | |||
| #include <tweakable.hpp> | |||
| #include <iostream> | |||
| int tweakable::get_value() { return tweakable::config::value; } | |||
| @@ -0,0 +1,3 @@ | |||
| #include <tweakable.hpp> | |||
| int main() { return tweakable::get_value(); } | |||
| @@ -0,0 +1,29 @@ | |||
| from dds_ci.testing.fixtures import ProjectOpener | |||
| from dds_ci import paths, proc | |||
| def test_lib_with_tweaks(project_opener: ProjectOpener) -> None: | |||
| pr = project_opener.open('projects/tweaks') | |||
| pr.build() | |||
| app = pr.build_root / ('tweakable' + paths.EXE_SUFFIX) | |||
| res = proc.run([app]) | |||
| # The default value is 99: | |||
| assert res.returncode == 99 | |||
| # Build again, but with an empty/non-existent tweaks directory | |||
| pr.build(tweaks_dir=pr.root / 'conf') | |||
| res = proc.run([app]) | |||
| assert res.returncode == 99 | |||
| # Now write a tweaks header and rebuild: | |||
| pr.write( | |||
| 'conf/tweakable.tweaks.hpp', r''' | |||
| #pragma once | |||
| namespace tweakable { | |||
| namespace config { | |||
| const int value = 41; | |||
| } | |||
| } | |||
| ''') | |||
| pr.build(tweaks_dir=pr.root / 'conf') | |||
| res = proc.run([app]) | |||
| assert res.returncode == 41 | |||
| @@ -94,6 +94,7 @@ class DDSWrapper: | |||
| toolchain: Optional[Path] = None, | |||
| build_root: Optional[Path] = None, | |||
| jobs: Optional[int] = None, | |||
| tweaks_dir: Optional[Path] = None, | |||
| more_args: Optional[proc.CommandLine] = None, | |||
| timeout: Optional[int] = None) -> None: | |||
| """ | |||
| @@ -115,6 +116,7 @@ class DDSWrapper: | |||
| f'--jobs={jobs}', | |||
| f'{self.project_dir_flag}={root}', | |||
| f'--out={build_root}', | |||
| f'--tweaks-dir={tweaks_dir}' if tweaks_dir else (), | |||
| more_args or (), | |||
| ], | |||
| timeout=timeout, | |||
| @@ -75,7 +75,11 @@ class Project: | |||
| """Argument for --project""" | |||
| return f'--project={self.root}' | |||
| def build(self, *, toolchain: Optional[Pathish] = None, timeout: Optional[int] = None) -> None: | |||
| def build(self, | |||
| *, | |||
| toolchain: Optional[Pathish] = None, | |||
| timeout: Optional[int] = None, | |||
| tweaks_dir: Optional[Path] = None) -> None: | |||
| """ | |||
| Execute 'dds build' on the project | |||
| """ | |||
| @@ -84,7 +88,8 @@ class Project: | |||
| build_root=self.build_root, | |||
| toolchain=tc, | |||
| timeout=timeout, | |||
| more_args=['-ldebug']) | |||
| tweaks_dir=tweaks_dir, | |||
| more_args=['-ltrace']) | |||
| def compile_file(self, *paths: Pathish, toolchain: Optional[Pathish] = None) -> None: | |||
| with tc_mod.fixup_toolchain(toolchain or tc_mod.get_default_test_toolchain()) as tc: | |||