diff options
52 files changed, 3349 insertions, 535 deletions
diff --git a/build2/b.cxx b/build2/b.cxx index 6f1452f..0b4ec3a 100644 --- a/build2/b.cxx +++ b/build2/b.cxx @@ -1559,8 +1559,13 @@ main (int argc, char* argv[]) if (dump_match_pre) dump (ctx, a); - if (mif->execute != nullptr && !ctx.match_only) - mif->execute (mparams, a, tgs, diag, true /* progress */); + if (mif->execute != nullptr) + { + if (!ctx.match_only) + mif->execute (mparams, a, tgs, diag, true /* progress */); + else if (mif->execute == &perform_execute) + perform_post_operation_callbacks (ctx, a, tgs, false /*failed*/); + } } if (pre_oif->operation_post != nullptr) @@ -1603,8 +1608,13 @@ main (int argc, char* argv[]) if (dump_match) dump (ctx, a); - if (mif->execute != nullptr && !ctx.match_only) - mif->execute (mparams, a, tgs, diag, true /* progress */); + if (mif->execute != nullptr) + { + if (!ctx.match_only) + mif->execute (mparams, a, tgs, diag, true /* progress */); + else if (mif->execute == &perform_execute) + perform_post_operation_callbacks (ctx, a, tgs, false /*failed*/); + } } if (oif->operation_post != nullptr) @@ -1647,8 +1657,13 @@ main (int argc, char* argv[]) if (dump_match_post) dump (ctx, a); - if (mif->execute != nullptr && !ctx.match_only) - mif->execute (mparams, a, tgs, diag, true /* progress */); + if (mif->execute != nullptr) + { + if (!ctx.match_only) + mif->execute (mparams, a, tgs, diag, true /* progress */); + else if (mif->execute == &perform_execute) + perform_post_operation_callbacks (ctx, a, tgs, false /*failed*/); + } } if (post_oif->operation_post != nullptr) diff --git a/doc/manual.cli b/doc/manual.cli index 07d816a..8beb4ea 100644 --- a/doc/manual.cli +++ b/doc/manual.cli @@ -2093,7 +2093,7 @@ If we forget to adjust the \c{missing-name} test, then this is what we could expect to see when running the tests: \ -b test +$ b test c++ hello/cxx{hello} -> hello/obje{hello} ld hello/exe{hello} test hello/exe{hello} + hello/testscript{testscript} @@ -6128,8 +6128,9 @@ source <functions-target.cli>; The \c{$regex.*()} function family contains function that provide comprehensive regular expression matching and substitution facilities. The -supported regular expression flavor is ECMAScript (more specifically, -ECMA-262-based C++11 regular expressions). +supported regular expression flavor is ECMAScript, more precisely, +ECMA-262-based C++11 regular expressions. Note that the \c{match_not_null} +flag is in effect unless the string being matched is empty. In the \c{$regex.*()} functions the substitution escape sequences in the format string (the \ci{fmt} argument) are extended with a subset of the Perl @@ -6700,8 +6701,8 @@ quickly re-run a previously failed test), it can also be persisted in subset of tests by default. For example: \ -b test config.test=foo/exe{driver} # Only test foo/exe{driver} target. -b test config.test=bar/baz # Only run bar/baz testscript test. +$ b test config.test=foo/exe{driver} # Only test foo/exe{driver} target. +$ b test config.test=bar/baz # Only run bar/baz testscript test. \ The \c{config.test} variable contains a list of \c{@}-separated pairs with the @@ -6712,14 +6713,14 @@ name. Otherwise \- an id path. The targets are resolved relative to the root scope where the \c{config.test} value is set. For example: \ -b test config.test=foo/exe{driver}@bar +$ b test config.test=foo/exe{driver}@bar \ To specify multiple id paths for the same target we can use the pair generation syntax: \ -b test config.test=foo/exe{driver}@{bar baz} +$ b test config.test=foo/exe{driver}@{bar baz} \ If no targets are specified (only id paths), then all the targets are tested @@ -6741,9 +6742,9 @@ and the right hand side \- for individual tests. The zero value clears the previously set timeout. For example: \ -b test config.test.timeout=20 # Test operation. -b test config.test.timeout=20/5 # Test operation and individual tests. -b test config.test.timeout=/5 # Individual tests. +$ b test config.test.timeout=20 # Test operation. +$ b test config.test.timeout=20/5 # Test operation and individual tests. +$ b test config.test.timeout=/5 # Individual tests. \ The test timeout can be specified on multiple nested root scopes. For example, @@ -6759,7 +6760,7 @@ specifying the \c{config.test.runner} variable. Its value has the \c{<path> [<options>]} form. For example: \ -b test config.test.runner=\"valgrind -q\" +$ b test config.test.runner=\"valgrind -q\" \ When the runner program is specified, commands of simple and Testscript tests @@ -7648,6 +7649,12 @@ config.cc.reprocess cc.reprocess config.cc.pkgconfig.sysroot + +config.cc.compiledb +config.cc.compiledb.name +config.cc.compiledb.filter +config.cc.compiledb.filter.input +config.cc.compiledb.filter.output \ Note that the compiler mode options are \"cross-hinted\" between \c{config.c} @@ -8054,6 +8061,340 @@ As a result, it should only be used for dealing with issues in third-party installation} should be used instead.| +\h#cc-compiledb|Compilation Database| + +The \c{cc}-based modules provide support for generating and maintaining the +\l{https://clang.llvm.org/docs/JSONCompilationDatabase.html JSON Compilation +Database} which can be used by other tools (static analyzers, language +servers, IDEs, etc) to understand how a codebase is compiled. \"Maintaining\" +in the previous sentence means that if new source files get added to the +project or old ones removed, or if any compilation options change, then the +corresponding entries in the compilation database will be automatically +updated when you update your project. This helps maintain the database in sync +with the project state. + +The generation of compilation databases and their configuration are controlled +with a number of \c{config.cc.compiledb.*} variables. The +\c{config.cc.compiledb} variable provides a simplified interface that enables +the generation of one database per project with the resulting database +containing entries for all the source and object files. The rest of the +variables provide a more flexible interface that allows you to generate +multiple databases in different locations as well as filter the entries that +end up in each database. + +Let's start with the simplified interface as provided by +\c{config.cc.compiledb}. The value of this configuration variable is a single +\ci{name} or a \ci{name} and \ci{path} pair in the \c{\i{name}[@\i{path}]} +form. + +The \ci{name} part is the compilation database name that can be used to refer +to it in filters (see below). If \ci{path} is absent or is (syntactically) a +directory, then \ci{name} is also used to derive the compilation database file +by appending the \c{.json} extension to it. + +If \ci{path} is absent, then the compilation database is placed into the +top-level amalgamation that loads any \c{cc}-based module. Otherwise, the +database is placed into the specified location. + +The special \c{-} name is interpreted as an instruction to dump the database +to \c{stdout}. + +Let's see some examples of using \c{config.cc.compiledb} to handle a few +common scenarios. Here we will use \l{bdep(1)} to create amalgamations +(configurations) and configure (initialize) one or more projects. We will +assume we have \c{hello} and \c{libhello} as if created like this: + +\ +$ bdep new -t exe hello +$ bdep new -t lib libhello +\ + +The most common scenario is likely having a compilation database per +project: + +\ +$ cd libhello +$ bdep config create ../build-gcc @gcc cc config.cxx=g++ +$ bdep init @gcc config.cc.compiledb=libhello +$ cd .. + +$ cd hello +$ bdep config add ../build-gcc @gcc +$ bdep init @gcc config.cc.compiledb=hello +$ cd .. + +$ b hello/ libhello/ +\ + +\N|Or if you prefer to create/add configuration as part of \c{init} (notice +the \c{--} separator): + +\ +$ bdep init -C ../build-gcc @gcc cc config.cxx=g++ -- \\ + config.cc.compiledb=libhello + +$ bdep init -A ../build-gcc @gcc config.cc.compiledb=hello +\ + +| + +After the update (the last command), we will have \c{hello.json} and +\c{libhello.json} in \c{build-gcc/} which contain the compilation command +lines for each project. + +\N|Only source files that are compiled end up being added to the compilation +database. + +To illustrate this point, let's assume our \c{hello} project imports and links +\c{libhello}. And instead of updating both as in the above example, we will +first update only \c{hello}: + +\ +$ b hello/ +\ + +In this case \c{libhello.json} will still be generated but it will only +contain a subset of the expected entries \- only those that were caused to be +compiled by \c{hello}. The missing entries can be added by updating +\c{libhello}: + +\ +$ b libhello/ +\ + +| + +In the above setup it feels natural to call each database after the project +and place them into the output directory. However, some consumers, such as +IDEs and LSP servers, may not handle this setup well. Specifically, they may +only recognize the canonical \c{compile_commands.json} file as the compilation +database, opening all other files as generic JSON. They may also assume the +directory where this file resides to be the project source directory root. To +accommodate these assumptions we can instead place each database into the +project's source directory and call it \c{compile_commands.json}: + +\ +$ cd libhello +$ bdep init @gcc config.cc.compiledb=libhello@./compile_commands.json + +$ cd hello +$ bdep init @gcc config.cc.compiledb=hello@./compile_commands.json +\ + +To facilitate this use-case, \c{config.cc.compiledb} supports another +shortcut: if we specify just \ci{name} and it contains a directory component, +then it is interpreted as \ci{path} rather than \ci{name}. In this case +\ci{name} is taken to be the name of the last directory component in \ci{path} +(which would typically be a project or package name). And if \ci{path} is a +directory, then the database file name is taken to be +\c{compile_commands.json}. Or, in other words, the following: + +\ +config.cc.compiledb=.../<dir>/ +\ + +Is equivalent to: + +\ +config.cc.compiledb=<dir>@.../<dir>/compile_commands.json +\ + +This shortcut allows us to simplify the above \c{init} commands to read: + +\ +$ cd libhello +$ bdep init @gcc config.cc.compiledb=./ + +$ cd hello +$ bdep init @gcc config.cc.compiledb=./ +\ + +Note also that in this case it will be your responsibility to remove the +database files if and when necessary. \N{\l{bdep-new(1)} adds +\c{compile_commands.json} to \c{.gitignore} it generates.} + +If instead of having a separate database for each project we wanted to place +all the entries into a single database (and in the output directory), then the +relevant commands would change as follows: + +\ +$ bdep init @gcc config.cc.compiledb=compiledb + +$ bdep init @gcc config.cc.compiledb=compiledb +\ + +This would give us a single \c{build-gcc/compiledb.json} that contains the +compilation command lines for both projects. + +In the above example only \c{hello} and \c{libhello} will end up in the +database, but not any of their dependencies. What if we wanted entries for +everything in \c{build-gcc/}? In this case, we should enable the compilation +database for the entire configuration rather than for individual projects: + +\ +$ bdep config create ../build-gcc @gcc cc \\ + config.cxx=g++ \\ + config.cc.compiledb=compiledb +$ bdep init @gcc + +$ bdep config add ../build-gcc @gcc +$ bdep init @gcc +\ + +If multiple linked configurations are involved, then we would often want +projects initialized in different configurations share the compilation +database. The representative scenario here is a tool, such as a source code +generator, which is initialized in the host configuration, and its runtime +library plus tests/examples, which are initialized in the target +configuration. Let's assume that in our example \c{hello} is the tool and +\c{libhello} is the runtime library and both are part of the same project. +This is how we can arrange for them to share the compilation database: + +\ +$ bdep config create @host ../host-gcc --type host cc config.cxx=g++ +$ bdep config create @target ../build-gcc cc config.cxx=g++ + +$ bdep init @host -d hello config.cc.compiledb=hello@../build-gcc/ +$ bdep init @target -d libhello config.cc.compiledb=hello + +$ bdep update @host @target +\ + +With this setup the \c{hello.json} database in \c{build-gcc/} will contain +entries for both \c{hello} and \c{libhello}. + +If instead of configuring and maintaining the compilation database in a file +you want to dump it somewhere once, the recommended approach is to write it +to \c{stdout}. For example: + +\ +$ b -n hello/ libhello/ config.cc.compiledb=- >/tmp/compiledb.json +\ + +Note that writing to \c{stdout} forces recompilation of all the targets that +would be updated in order to make sure their entries end up in the database. +If you don't want the actual recompilation, then you can use the dry run mode +(\c{-n} option above). + +\N|If your projects are spread across multiple linked configurations and you +would like to get compilation command lines for all of them, then use the +global override for \c{config.cc.compiledb}: + +\ +$ b '!config.cc.compiledb=-' ... +\ + +As mentioned earlier, the entries that will end up in such a database are +determined by what gets updated.| + +Let's now turn to the rest of the \c{config.cc.compiledb.*} configuration +variables that provide a lower-level but more flexible interface. The +following listing shows their synopsis: + +\ +config.cc.compiledb.name = <name>[@<path>]... +config.cc.compiledb.filter = [<name>@]<bool>... +config.cc.compiledb.filter.input = [<name>@]<target-type>... +config.cc.compiledb.filter.output = [<name>@]<target-type>... +\ + +The \c{config.cc.compiledb.name} variable specifies the name and location of +one or more compilation databases. The semantics of the +\c{\i{name}[@\i{path}]} pair is the same as in \c{config.cc.compiledb} +discussed above, except that if \ci{path} is absent, then the database is +placed into the project being configured rather than into the top-level +amalgamation. + +Also, unlike \c{config.cc.compiledb}, this variable does not automatically +enable writing to the specified databases. Instead, this is the job of +\c{config.cc.compiledb.filter}. Splitting this logic into two steps allows us +to configure the database name/location in one place, typically an outer +amalgamation, and then enable writing to it in other places, typically +specific subprojects. + +The \c{config.cc.compiledb.filter.{input,output\}} variables allow us to +filter the entries that end up in the databases based on the input (\c{c{\}}, +\c{cxx{\}}, etc) and output (\c{obja{\}}, \c{objs{\}}, etc) target types. + +Note that in all three \c{.filter} variables the values are examined in the +reverse order and the first entry that matches determines the outcome. +Entries without \ci{name} apply to all databases and the target types are +matched taking into account inheritance (so \c{target{\}} will match any type) +and groups (so \c{obj{\}} will match any \c{obj[eas]{\}}). If no target type +filter (input or output) is specified, then no corresponding target filtering +is performed. + +\N|The \c{config.cc.compiledb=<name>} semantics can be expressed as the +following set of lower-level variables: + +\ +config.cc.compiledb.name = <name>@../path/to/amalgamation/ +config.cc.compiledb.filter += <name>@true +config.cc.compiledb.filter.input += <name>@target +config.cc.compiledb.filter.output += <name>@target +\ + +The last three assignments only apply if the corresponding variable is not set +to a custom value for this project.| + +Let's look at a few examples of using these lower-level configuration +variables. The common use for the output target filtering is getting rid of +\c{obja{\}} or \c{objs{\}} entries in libraries. Unless configured otherwise, +when we build a library we end up with both static and shared variants. And +this means that each source file for the library is compiled twice, once to +produce \c{obja{\}} that goes to the static library and once -- \c{objs{\}}. +And that, in turn, means that we will end up with two compilation database +entries for each such source file. If we don't want that for some reason (for +instance, because the consumer of the database does not handle this well), +then we can filter one of them out. For example, below is how we can +initialize \c{libhello} to achieve this (notice that we also include +\c{obje{\}} to keep object files for executables, such as tests): + +\ +$ bdep init @gcc \\ + config.cc.compiledb=libhello \\ + config.cc.compiledb.filter.output='obje objs' +\ + +As an example of the input target type filtering, below is how we can keep +entries only for the C and C++ source files, filtering out everything else +(assembler, Objective-C/C++), for instance, because the consumer of our +database does not recognize them: + +\ +$ bdep init @gcc \\ + config.cc.compiledb=libhello \\ + config.cc.compiledb.filter.input='c cxx' +\ + +As an example of a more advanced configuration, consider a compilation +database for a project that use C++ modules. To know how such a project is +compiled we not only need to know how its own source files are compiled, but +also how to compile all the module interfaces that it consumes, including from +other projects, transitively. One way to set this up would be to enable +writing entries of the \c{bmi{\}} output target type to any database in the +amalgamation: + +\ +$ bdep config create ../build-gcc @gcc cc \\ + config.cxx=g++ \\ + config.cc.compiledb.filter=true \\ + config.cc.compiledb.filter.output=bmi \\ + + +$ bdep init @gcc config.cc.compiledb=libhello + +$ bdep init @gcc config.cc.compiledb=hello +\ + +With this setup \c{libhello.json} and \c{hello.json} will contain module +interface entries from all the dependencies. + +\N|When debugging complex compilation database setups it can be helpful to +increase diagnostics verbosity to level 6 in order to get a trace of filtering +decisions (the relevant lines will contain the \c{compiledb} keyword).| + + \h#cc-gcc|GCC Compiler Toolchain| The GCC compiler id is \c{gcc}. diff --git a/doc/testscript.cli b/doc/testscript.cli index 18dac41..7f7522d 100644 --- a/doc/testscript.cli +++ b/doc/testscript.cli @@ -2372,8 +2372,8 @@ continuations. \h#syntax-regex|Output Regex| Instead of literal text the expected result in output here-strings and -here-documents can be specified as ECMAScript regular expressions (more -specifically, ECMA-262-based C++11 regular expressions). To signal the use of +here-documents can be specified as ECMAScript regular expressions, more +precisely, ECMA-262-based C++11 regular expressions. To signal the use of regular expressions the redirect must end with the \c{~} modifier, for example: @@ -3082,7 +3082,9 @@ illegal. Note that this builtin implementation deviates substantially from POSIX \c{sed} (as described next). Most significantly, the regular expression flavor -is ECMAScript (more specifically, ECMA-262-based C++11 regular expressions). +is ECMAScript, more precisely, ECMA-262-based C++11 regular expressions. Note +that the \c{match_not_null} flag is in effect unless the line being matched +is empty. \dl| diff --git a/libbuild2/adhoc-rule-buildscript.cxx b/libbuild2/adhoc-rule-buildscript.cxx index 3e868a6..b125ac5 100644 --- a/libbuild2/adhoc-rule-buildscript.cxx +++ b/libbuild2/adhoc-rule-buildscript.cxx @@ -223,6 +223,19 @@ namespace build2 using dynamic_target = build::script::parser::dynamic_target; using dynamic_targets = build::script::parser::dynamic_targets; + // Return true if the path exist and is a symlink. + // + static inline bool + path_symlink (const path& p) + { + pair<bool, butl::entry_stat> r ( + butl::path_entry (p, + false /* follow_symlinks */, + true /* ignore_errors */)); + + return r.first && r.second.type == butl::entry_type::symlink; + }; + struct adhoc_buildscript_rule::match_data { match_data (action a, const target& t, const scope& bs, bool temp_dir) @@ -236,6 +249,7 @@ namespace build2 const scope* bs; timestamp mt; + bool symlink; bool deferred_failure; }; @@ -257,6 +271,7 @@ namespace build2 const scope* bs; timestamp mt; + bool symlink; }; bool adhoc_buildscript_rule:: @@ -1015,8 +1030,12 @@ namespace build2 bool update (false); timestamp mt; + // Support creating file symlinks using ad hoc recipes. + // + bool symlink (false); + if (dd.writing ()) - update = true; + update = true; // Will re-query symlink. else { if (g == nullptr) @@ -1025,6 +1044,8 @@ namespace build2 if ((mt = ft.mtime ()) == timestamp_unknown) ft.mtime (mt = mtime (tp)); // Cache. + + symlink = mt != timestamp_nonexistent && path_symlink (tp); } else { @@ -1038,13 +1059,21 @@ namespace build2 : nullptr)); if (p != nullptr) + { mt = g->load_mtime (*p); + symlink = mt != timestamp_nonexistent && path_symlink (*p); + } else - update = true; + update = true; // Will re-query symlink. } if (!update) - update = dd.mtime > mt; + { + // If this is a symlink, depdb mtime could be greater than the symlink + // target. + // + update = dd.mtime > mt && !symlink; + } } if (update) @@ -1189,6 +1218,7 @@ namespace build2 // mdb->bs = &bs; mdb->mt = update ? timestamp_nonexistent : mt; + mdb->symlink = symlink; return [this, md = move (mdb)] (action a, const target& t) { @@ -1265,6 +1295,7 @@ namespace build2 md->bs = &bs; md->dd = move (dd.path); md->mt = update ? timestamp_nonexistent : mt; + md->symlink = symlink; return [this, md = move (md)] (action a, const target& t) { @@ -1592,18 +1623,32 @@ namespace build2 timestamp now (system_clock::now ()); + const path* tp (nullptr); if (!ctx.dry_run) { // Only now we know for sure there must be a member in the group. // const file& ft ((g == nullptr ? t : *g->members.front ()).as<file> ()); + tp = &ft.path (); + + md.symlink = path_symlink (*tp); // Re-query. - depdb::check_mtime (start, md.dd.path, ft.path (), now); + // Again, if this is a symlink, depdb mtime will be greater than + // the symlink target. + // + if (!md.symlink) + depdb::check_mtime (start, md.dd.path, *tp, now); } + // Symlinks don't play well with dry-run (see full description in + // perform_update_file_or_group()). + // (g == nullptr ? static_cast<const mtime_target&> (t.as<file> ()) - : static_cast<const mtime_target&> (*g)).mtime (now); + : static_cast<const mtime_target&> (*g)).mtime ( + md.symlink && tp != nullptr + ? build2::mtime (*tp) + : now); return target_state::changed; } @@ -1662,17 +1707,32 @@ namespace build2 timestamp now (system_clock::now ()); + const path* tp (nullptr); if (!ctx.dry_run) { // Note: in case of deferred failure we may not have any members. // const file& ft ((g == nullptr ? t : *g->members.front ()).as<file> ()); - depdb::check_mtime (start, md.dd, ft.path (), now); + tp = &ft.path (); + + md.symlink = path_symlink (*tp); // Re-query. + + // Again, if this is a symlink, depdb mtime will be greater than the + // symlink target. + // + if (!md.symlink) + depdb::check_mtime (start, md.dd, *tp, now); } + // Symlinks don't play well with dry-run (see full description in + // perform_update_file_or_group()). + // (g == nullptr ? static_cast<const mtime_target&> (t) - : static_cast<const mtime_target&> (*g)).mtime (now); + : static_cast<const mtime_target&> (*g)).mtime ( + md.symlink && tp != nullptr + ? build2::mtime (*tp) + : now); return target_state::changed; } @@ -1695,35 +1755,14 @@ namespace build2 const file& ft ((g == nullptr ? t : *g->members.front ()).as<file> ()); const path& tp (ft.path ()); - // Support creating file symlinks using ad hoc recipes. - // - auto path_symlink = [&tp] () - { - pair<bool, butl::entry_stat> r ( - butl::path_entry (tp, - false /* follow_symlinks */, - true /* ignore_errors */)); - - return r.first && r.second.type == butl::entry_type::symlink; - }; - // Update prerequisites and determine if any of them render this target // out-of-date. // - // If the file entry exists, check if its a symlink. - // - bool symlink (false); - timestamp mt; - - if (g == nullptr) - { - mt = ft.load_mtime (); + timestamp mt (g == nullptr ? ft.load_mtime () : g->load_mtime (tp)); - if (mt != timestamp_nonexistent) - symlink = path_symlink (); - } - else - mt = g->load_mtime (tp); + // Support creating file symlinks using ad hoc recipes. + // + bool symlink (mt != timestamp_nonexistent && path_symlink (tp)); // This is essentially ps=execute_prerequisites(a, t, mt) which we // cannot use because we need to see ad hoc prerequisites. @@ -1923,8 +1962,7 @@ namespace build2 { if (!ctx.dry_run) { - if (g == nullptr) - symlink = path_symlink (); + symlink = path_symlink (tp); // Re-query. // Again, if this is a symlink, depdb mtime will be greater than // the symlink target. diff --git a/libbuild2/algorithm.cxx b/libbuild2/algorithm.cxx index 16f1503..d2d1eb6 100644 --- a/libbuild2/algorithm.cxx +++ b/libbuild2/algorithm.cxx @@ -2077,7 +2077,11 @@ namespace build2 case target_state::unchanged: break; case target_state::postponed: - ts = t[a].state = target_state::unchanged; + // Keep the target state postponed (see group_action() for details) + // but translate the result from postponed to unchanged, similar to + // executed_state_impl(). + // + ts = target_state::unchanged; break; case target_state::group: ts = (*t.group)[a].state; diff --git a/libbuild2/bin/init.cxx b/libbuild2/bin/init.cxx index 610082e..f01adfb 100644 --- a/libbuild2/bin/init.cxx +++ b/libbuild2/bin/init.cxx @@ -12,7 +12,6 @@ #include <libbuild2/test/module.hxx> -#include <libbuild2/install/rule.hxx> #include <libbuild2/install/utility.hxx> #include <libbuild2/bin/rule.hxx> @@ -31,6 +30,7 @@ namespace build2 static const obj_rule obj_; static const libul_rule libul_; static const lib_rule lib_; + static const install_lib_rule install_lib_; static const def_rule def_; // Default config.bin.*.lib values. @@ -631,10 +631,8 @@ namespace build2 // if (install_loaded) { - auto& gr (install::group_rule::instance); - - r.insert<lib> (perform_install_id, "bin.lib", gr); - r.insert<lib> (perform_uninstall_id, "bin.lib", gr); + r.insert<lib> (perform_install_id, "bin.lib", install_lib_); + r.insert<lib> (perform_uninstall_id, "bin.lib", install_lib_); } if (const test::module* m = rs.find_module<test::module> ("test")) diff --git a/libbuild2/bin/rule.cxx b/libbuild2/bin/rule.cxx index c7147bf..1fea558 100644 --- a/libbuild2/bin/rule.cxx +++ b/libbuild2/bin/rule.cxx @@ -218,5 +218,18 @@ namespace build2 const target* m[] = {t.a, t.s}; return execute_members (a, t, m); } + + // install_lib_rule + // + pair<const target*, uint64_t> install_lib_rule:: + filter (const scope* is, + action a, const target& t, const prerequisite& p, + match_extra& me) const + { + if (p.is_a<lib> ()) + return pair<const target*, uint64_t> (nullptr, 0); + + return install::group_rule::filter (is, a, t, p, me); + } } } diff --git a/libbuild2/bin/rule.hxx b/libbuild2/bin/rule.hxx index 9dd1d14..74f4301 100644 --- a/libbuild2/bin/rule.hxx +++ b/libbuild2/bin/rule.hxx @@ -10,6 +10,7 @@ #include <libbuild2/rule.hxx> #include <libbuild2/dist/rule.hxx> +#include <libbuild2/install/rule.hxx> #include <libbuild2/bin/export.hxx> @@ -78,6 +79,26 @@ namespace build2 static target_state perform (action, const target&); }; + + // Install rule for lib{} group. + // + // The only difference compared to the standard install::group_rule is + // that it ignores the lib{} prerequisites, instead expecting the correct + // things to be installed via the liba{}/libs{} members. This is important + // due to the presence of match options (see lib{} target for details). + // + class LIBBUILD2_BIN_SYMEXPORT install_lib_rule: public install::group_rule + { + public: + install_lib_rule () {} + + virtual pair<const target*, uint64_t> + filter (const scope*, + action, const target&, const prerequisite&, + match_extra&) const override; + + using install::group_rule::filter; // "Unhide" to make Clang happy. + }; } } diff --git a/libbuild2/build/script/builtin.cli b/libbuild2/build/script/builtin.cli index 5aea034..9639477 100644 --- a/libbuild2/build/script/builtin.cli +++ b/libbuild2/build/script/builtin.cli @@ -103,9 +103,10 @@ namespace build2 // Dynamic target extraction options. // // This functionality is enabled with the --dyn-target option. Only - // the make format is supported, where the listed targets are added as - // ad hoc group members (unless already specified as static members). - // This functionality is not available in the byproduct mode. + // the `make` and `lines` formats are supported (see above), where the + // listed targets are added as ad hoc group members (unless already + // specified as static members). This functionality is not available + // in the byproduct mode. // string --target-what; // Target kind, e.g., "source". diff --git a/libbuild2/cc/compile-rule.cxx b/libbuild2/cc/compile-rule.cxx index cebd244..29a26b5 100644 --- a/libbuild2/cc/compile-rule.cxx +++ b/libbuild2/cc/compile-rule.cxx @@ -25,6 +25,7 @@ #include <libbuild2/cc/target.hxx> // h #include <libbuild2/cc/module.hxx> #include <libbuild2/cc/utility.hxx> +#include <libbuild2/cc/compiledb.hxx> using std::exit; using std::strlen; @@ -1181,6 +1182,11 @@ namespace build2 fsdir_rule::perform_update_direct (a, *dir); } + // Use the subset of the depdb checks to detect changes to the + // compilation database entry. + // + bool compiledb_changed (false); + // Note: the leading '@' is reserved for the module map prefix (see // extract_modules()) and no other line must start with it. // @@ -1198,8 +1204,14 @@ namespace build2 // but only in what it targets, then the checksum will still change. // if (dd.expect (cast<string> (rs[x_checksum])) != nullptr) + { l4 ([&]{trace << "compiler mismatch forcing update of " << t;}); + // The checksum includes the absolute compiler path. + // + compiledb_changed = true; + } + // Then the compiler environment checksum. // if (dd.expect (env_checksum) != nullptr) @@ -1263,7 +1275,17 @@ namespace build2 append_sys_hdr_options (cs); // Extra system header dirs (last). if (dd.expect (cs.string ()) != nullptr) + { l4 ([&]{trace << "options mismatch forcing update of " << t;}); + + // Note that this doesn't include any of the "plumbing" options + // like -x, -c, -o, etc. In the unlikely event that there are + // changes in this area that also affect the semantics of the + // compilation database (options reordering doesn't, for example), + // then we can resort to incrementing the rule version. + // + compiledb_changed = true; + } } // Finally the source file. @@ -1273,7 +1295,10 @@ namespace build2 assert (!p.empty ()); // Sanity check. if (dd.expect (p) != nullptr) + { l4 ([&]{trace << "source file mismatch forcing update of " << t;}); + compiledb_changed = true; + } } // If any of the above checks resulted in a mismatch (different @@ -1296,6 +1321,14 @@ namespace build2 u = dd.mtime > mt; } + // Confirm the entry in the compilation database, if any. + // + if (compiledb::match (bs, t, tp, src, compiledb_changed) && !u) + { + l4 ([&]{trace << "compilation database forcing update of " << t;}); + u = true; + } + // If updating for any of the above reasons, treat it as if doesn't // exist. // @@ -3148,7 +3181,7 @@ namespace build2 } hk.file = move (fp); - hk.hash = hash<path> () (hk.file); + hk.hash = hash<string> () (hk.file.string ()); slock l (hc.header_map_mutex); auto i (hc.header_map.find (hk)); @@ -3201,7 +3234,7 @@ namespace build2 // path has changed (header has been remapped). // if (!e || r.second) - hk.hash = hash<path> () (hk.file); + hk.hash = hash<string> () (hk.file.string ()); const file* f; { @@ -3214,6 +3247,8 @@ namespace build2 { //cache_cls.fetch_add (1, memory_order_relaxed); + // @@ TMP cleanup. + // #if 0 assert (r.first == f); #else @@ -7352,8 +7387,11 @@ namespace build2 // apply()). For named modules there may be no obj*{} if this is a // sidebuild (obj*{} is already in the library binary). // - path relm; + const path* abso (nullptr); + const path* absm (nullptr); path relo; + path relm; + switch (ut) { case unit_type::module_header: @@ -7363,12 +7401,18 @@ namespace build2 case unit_type::module_impl_part: { if (const file* o = find_adhoc_member<file> (t, tts.obj)) - relo = relative (o->path ()); + { + abso = &o->path (); + relo = relative (*abso); + } break; } default: - relo = relative (tp); + { + abso = &tp; + relo = relative (tp); + } } // Build the command line. @@ -7398,6 +7442,9 @@ namespace build2 small_vector<string, 2> header_args; // Header unit options storage. small_vector<string, 2> module_args; // Module options storage. + // NOTE: see a note in apply() on the compilation database implications + // if changing anything below. + // switch (cclass) { case compiler_class::msvc: @@ -7532,6 +7579,7 @@ namespace build2 { assert (ut != unit_type::module_header); // @@ MODHDR + absm = &tp; relm = relative (tp); args.push_back ("/ifcOutput"); @@ -7745,6 +7793,9 @@ namespace build2 // Output module file is specified in the mapping file, the // same as input. // + // We set neither relm nor absm since they are not on the + // command line. + // if (ut == unit_type::module_header) // No obj, -c implied. break; @@ -7773,6 +7824,7 @@ namespace build2 { assert (ut != unit_type::module_header); // @@ MODHDR + absm = &tp; relm = relative (tp); // Without this option Clang's .pcm will reference source @@ -7884,6 +7936,15 @@ namespace build2 else if (verb == 2) print_process (args); + // Insert or update the entry in the compilation database, if any. + // + compiledb::execute ( + bs, + t, tp, s, *sp, + cpath, args, + relo, abso != nullptr ? *abso : empty_path, + relm, absm != nullptr ? *absm : empty_path); + // If we have the (partially) preprocessed output, switch to that. // // But we remember the original source/position to restore later. diff --git a/libbuild2/cc/compiledb.cxx b/libbuild2/cc/compiledb.cxx new file mode 100644 index 0000000..ccf08a9 --- /dev/null +++ b/libbuild2/cc/compiledb.cxx @@ -0,0 +1,1100 @@ +// file : libbuild2/cc/compiledb.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/cc/compiledb.hxx> + +#include <cstring> // strlen() +#include <iostream> // cout + +#ifndef BUILD2_BOOTSTRAP +# include <libbutl/json/parser.hxx> +#endif + +#include <libbuild2/filesystem.hxx> +#include <libbuild2/diagnostics.hxx> + +#include <libbuild2/cc/module.hxx> + +#include <libbuild2/cc/target.hxx> +#include <libbuild2/bin/target.hxx> + +using namespace std; + +namespace build2 +{ + namespace cc + { + compiledb_set compiledbs; + + // compiledb + // + compiledb:: + ~compiledb () + { + } + + // Return true if this entry should be written to the database with the + // specified name. + // + static bool + filter (const scope& rs, + const core_module& m, + const string& name, + const file& ot, const file& it) + { + tracer trace ("cc::compiledb_filter"); + + bool r (true); + const char* w (nullptr); // Why r is false. + + // First check if writing to this database is enabled. + // + // No filter means not enabled. + // + if (m.cdb_filter_ == nullptr) + { + r = false; + w = "no database name filter"; + } + else + { + // Iterate in reverse (so that later values override earlier) and take + // the first name match. + // + r = false; + for (const pair<optional<string>, bool>& p: + reverse_iterate (*m.cdb_filter_)) + { + if (!p.first || *p.first == name) + { + r = p.second; + break; + } + } + + if (!r) + w = "no match in database name filter"; + } + + // Verify the name is known in this amalgamation. Note that without + // this check we may end up writing to unrelated databases in other + // amalgamations (think linked configurations). + // + if (r) + { + r = false; + for (const core_module* pm (&m); + pm != nullptr; + pm = pm->outer_module_) + { + const strings& ns (pm->cdb_names_); + + if (find (ns.begin (), ns.end (), name) != ns.end ()) + { + r = true; + break; + } + } + + if (!r) + w = "database name unknown in amalgamation"; + } + + // Filter based on the output target. + // + // If there is no filter specified, then accept all targets. + // + if (r && m.cdb_filter_output_ != nullptr) + { + // If the filter is empty, then there is no match. + // + if (m.cdb_filter_output_->empty ()) + { + r = false; + w = "empty output target type filter"; + } + else + { + const target_type& ott (ot.type ()); + + // Iterate in reverse (so that later values override earlier) and + // take the first name match. + // + r = false; + for (const pair<optional<string>, string>& p: + reverse_iterate (*m.cdb_filter_output_)) + { + if (p.first && *p.first != name) + continue; + + using namespace bin; + + const string& n (p.second); + + if (ott.name == n || n == "target") + { + r = true; + } + // + // Handle obj/bmi/hbmi{} groups ad hoc. + // + else if (n == "obj") + { + r = ott.is_a<obje> () || ott.is_a<objs> () || ott.is_a<obja> (); + } + else if (n == "bmi") + { + r = ott.is_a<bmie> () || ott.is_a<bmis> () || ott.is_a<bmia> (); + } + else if (n == "hbmi") + { + r = ott.is_a<hbmie> () || ott.is_a<hbmis> () || ott.is_a<hbmia> (); + } + else + { + // Handle the commonly-used, well-known targets directly (see + // note in core_config_init() for why we cannot pre-lookup + // them). + // + const target_type* tt ( + n == "obje" ? &obje::static_type : + n == "objs" ? &objs::static_type : + n == "obja" ? &obja::static_type : + n == "bmie" ? &bmie::static_type : + n == "bmis" ? &bmis::static_type : + n == "bmia" ? &bmia::static_type : + n == "hbmie" ? &hbmie::static_type : + n == "hbmis" ? &hbmis::static_type : + n == "hbmia" ? &hbmia::static_type : + rs.find_target_type (n)); + + if (tt == nullptr) + fail << "unknown target type '" << n << "' in " + << "config.cc.compiledb.filter.output value"; + + r = ott.is_a (*tt); + } + + if (r) + break; + } + + if (!r) + w = "no match in output target type filter"; + } + } + + // Filter based on the input target. + // + // If there is no filter specified, then accept all targets. + // + if (r && m.cdb_filter_input_ != nullptr) + { + // If the filter is empty, then there is no match. + // + if (m.cdb_filter_input_->empty ()) + { + r = false; + w = "empty input target type filter"; + } + else + { + const target_type& itt (it.type ()); + + // Iterate in reverse (so that later values override earlier) and + // take the first name match. + // + r = false; + for (const pair<optional<string>, string>& p: + reverse_iterate (*m.cdb_filter_input_)) + { + if (p.first && *p.first != name) + continue; + + const string& n (p.second); + + if (itt.name == n || n == "target") + r = true; + else + { + // The same optimization as above. Note: cxx{}, etc., are in the + // cxx module so we have to look them up. + // + const target_type* tt ( + n == "c" ? &c::static_type : + n == "m" ? &m::static_type : + n == "S" ? &m::static_type : + rs.find_target_type (n)); + + if (tt == nullptr) + fail << "unknown target type '" << n << "' in " + << "config.cc.compiledb.filter.input value"; + + r = itt.is_a (*tt); + } + + if (r) + break; + } + + if (!r) + w = "no match in input target type filter"; + } + } + + l6 ([&] + { + if (r) + trace << "keep " << ot << " in " << name; + else + trace << "omit " << ot << " from " << name << ": " << w; + }); + + return r; + } + + bool compiledb:: + match (const scope& bs, + const file& ot, const path_type& op, + const file& it, + bool changed) + { + if (compiledbs.empty ()) + return false; + + const scope& rs (*bs.root_scope ()); + const auto* m (rs.find_module<core_module> (core_module::name)); + + assert (m != nullptr); + + bool u (false); + + for (const unique_ptr<compiledb>& db: compiledbs) + { + if (filter (rs, *m, db->name, ot, it)) + u = db->match (ot, op, changed) || u; + } + + return u; + } + + void compiledb:: + execute (const scope& bs, + const file& ot, const path_type& op, + const file& it, const path_type& ip, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) + { + if (compiledbs.empty ()) + return; + + const scope& rs (*bs.root_scope ()); + const auto* m (rs.find_module<core_module> (core_module::name)); + + assert (m != nullptr); + + assert (relo.empty () == abso.empty () && + relm.empty () == absm.empty ()); + + for (const unique_ptr<compiledb>& db: compiledbs) + { + if (filter (rs, *m, db->name, ot, it)) + db->execute (ot, op, it, ip, cpath, args, relo, abso, relm, absm); + } + } + + void + compiledb_pre (context& ctx, action a, const action_targets&) + { + // Note: won't be registered if compiledbs is empty. + + // Note: may be called directly with empty action_targets. + + assert (a.inner_action () == perform_update_id); + + tracer trace ("cc::compiledb_pre"); + + bool mctx (ctx.module_context == &ctx); + + l6 ([&]{trace << (mctx ? "module" : "normal") << " context " << &ctx;}); + + for (const unique_ptr<compiledb>& db: compiledbs) + db->pre (ctx); + } + + void + compiledb_post (context& ctx, + action a, + const action_targets& ts, + bool failed) + { + // Note: won't be registered if compiledbs is empty. + + assert (a.inner_action () == perform_update_id); + + tracer trace ("cc::compiledb_post"); + + bool mctx (ctx.module_context == &ctx); + + l6 ([&]{trace << (mctx ? "module" : "normal") << " context " << &ctx + << ", failed: " << failed;}); + + for (const unique_ptr<compiledb>& db: compiledbs) + db->post (ctx, ts, failed); + } + +#ifndef BUILD2_BOOTSTRAP + + namespace json = butl::json; + + // compiledb_stdout + // + compiledb_stdout:: + compiledb_stdout (string n) + : compiledb (move (n), path_type ()), + state_ (state::init), + nesting_ (0), + js_ (cout, 0 /* indentation */, "" /* multi_value_separator */) + { + } + + void compiledb_stdout:: + pre (context&) + { + // If the previous operation batch failed, then we shouldn't be here. + // + assert (state_ != state::failed); + + // The module context (used to build build system modules) poses a + // problem: we can receive its callbacks before the main context's or + // nested in the pre/post calls of the main context (or both, in + // fact). Plus there may be multiple pre/post sequences corresponding to + // the module context of both kinds. The three distinct cases are: + // + // 1. Module is loaded as part of the initial buildfile load (e.g., from + // root.build) -- in this case we will observe module pre/post before + // the main context's pre/post. + // + // In fact, to be precise, we will only observe them if cc is loaded + // before such a module. + // + // 2. Module is loaded via the interrupting load (e.g., from a directory + // buildfile that is loaded implicitly during match) -- in this case + // we will observe pre/post calls nested into the main context's + // pre/post. + // + // 3. The module context is used to build an ad hoc C++ recipe -- in + // this case we also get nested calls like in (2) since this happens + // during the recipe's match(). + // + // One thing to keep in mind (and which we rely upon quite a bit below) + // is that the main context's post will always be last (within any given + // operation; there could be another for the subsequent operation in a + // batch). + // + // Handling the nested case is relatively straightforward: we can keep + // track and ignore all the nested calls. + // + // The before case is where things get complicated. We could "take" the + // first module pre call and then wait until the main post, unless we + // see a module post call with failed=true, in which case there will be + // no further pre/post calls. There is, however, a nuance: the module is + // loaded and build for any operation, not just update, which means that + // if the main operation is not update (say, it's clean), we won't see + // any of the main context's pre/post calls. + // + // The way we are going to resolve this problem is different for the + // stdout and file implementations: + // + // For stdout we will just say that it should only be used with the + // update operation. There is really no good reason to use it with + // anything else anyway. See compiledb_stdout::post() for additional + // details. + // + // For file we will rely on its persistence and simply close and reopen + // the database for each pre/post sequence, the same way as if they were + // separate operations in a batch. + // + if (nesting_++ != 0) // Nested pre() call. + return; + + if (state_ == state::init) // First pre() call. + { + state_ = state::empty; + cout << "[\n"; + } + } + + bool compiledb_stdout:: + match (const file&, const path_type&, bool) + { + return true; + } + + static inline const char* + rel_to_abs (const char* a, + const string& rs, const string& as, + string& buf) + { + if (size_t rn = rs.size ()) + { + size_t an (strlen (a)); + + if (an >= rn && rs.compare (0, rn, a, rn) == 0) + { + if (an == rn) + return as.c_str (); + + buf = as; + buf.append (a + rn, an - rn); + + return buf.c_str (); + } + } + + return nullptr; + } + + void compiledb_stdout:: + execute (const file&, const path_type& op, + const file&, const path_type& ip, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) + { + const string& ro (relo.string ()); + const string& ao (abso.string ()); + + const string& rm (relm.string ()); + const string& am (absm.string ()); + + mlock l (mutex_); + + switch (state_) + { + case state::full: + { + cout << ",\n"; + break; + } + case state::empty: + { + state_ = state::full; + break; + } + case state::failed: + return; + case state::init: + assert (false); + return; + } + + try + { + // Duplicate what we have in the file implementation (instead of + // factoring it out to something common) in case here we need to + // adjust things (change order, omit some values; for example to + // accommodate broken consumers). We have this freedom here but not + // there. + // + js_.begin_object (); + { + js_.member ("output", op.string ()); + js_.member ("file", ip.string ()); + + js_.member_begin_array ("arguments"); + { + string buf; // Reuse. + for (auto b (args.begin ()), i (b), e (args.end ()); + i != e && *i != nullptr; + ++i) + { + const char* r; + + if (i == b) + r = cpath.effect_string (); + else + { + // Untranslate relative paths back to absolute. + // + const char* a (*i); + + if ((r = rel_to_abs (a, ro, ao, buf)) == nullptr && + (r = rel_to_abs (a, rm, am, buf)) == nullptr) + r = a; + } + + js_.value (r); + } + } + js_.end_array (); + + js_.member ("directory", work.string ()); + } + js_.end_object (); + } + catch (const json::invalid_json_output& e) + { + // There is no way (nor reason; the output will most likely be invalid + // anyway) to reuse the failed json serializer so make sure we ignore + // all the subsequent callbacks. + // + state_ = state::failed; + + l.unlock (); + + fail << "invalid compilation database json output: " << e; + } + } + + void compiledb_stdout:: + post (context& ctx, const action_targets&, bool failed) + { + assert (nesting_ != 0); + if (--nesting_ != 0) // Nested post() call. + return; + + bool mctx (ctx.module_context == &ctx); + + switch (state_) + { + case state::empty: + case state::full: + { + // If this is a module context's post, wait for the main context's + // post (last) unless the module load failed (in which case there + // will be no main pre/post). + // + // Note that there is no easy way to diagnose the case where we + // won't get the main pre/post calls. Instead, we will just produce + // invalid JSON (array won't be closed). In a somewhat hackish way, + // this actually makes the `b [-n] clean update` sequence work: we + // will take the pre() call from clean and the main post() from + // update. + // + if (mctx && !failed) + return; + + if (state_ == state::full) + cout << '\n'; + + cout << "]\n"; + break; + } + case state::failed: + return; + case state::init: + assert (false); + } + + state_ = state::init; + } + + // compiledb_file + // + compiledb_file:: + compiledb_file (string n, path_type p) + : compiledb (move (n), move (p)), + state_ (state::closed), + nesting_ (0) + { + } + + void compiledb_file:: + pre (context&) + { + // If the previous operation batch failed, then we shouldn't be here. + // + assert (state_ != state::failed); + + // See compiledb_stdout::pre() for background on dealing with the module + // context. Here are some file-specific nuances: + // + // We are going to load the database on the first pre call and flush + // (but not close) it on the matching post. Flushing means that we will + // update the file but still keep the in-memory state, in case there is + // another pre/post session coming. This is both a performance + // optimization but also the way we handle prunning no longer present + // entries, which gets tricky across multiple pre/post sessions (see + // post() for details). + // + if (nesting_++ != 0) // Nested pre() call. + return; + + if (state_ == state::closed) // First pre() call. + { + // Load the contents of the file if it exists, marking all the entries + // as (presumed) absent. + // + if (exists (path)) + { + uint64_t line (1); + try + { + ifdstream ifs (path, ifdstream::badbit); + + // Parse the top-level array manually (see post() for the expected + // format). + // + auto throw_invalid_input = [] (const string& d) + { + throw json::invalid_json_input ("", 0, 1, 0, d); + }; + + enum {first, second, next, last, end} s (first); + + for (string l; !eof (getline (ifs, l)); line++) + { + switch (s) + { + case first: + { + if (l != "[") + throw_invalid_input ("beginning of array expected"); + + s = second; + continue; + } + case second: + { + if (l == "]") + { + s = end; + continue; + } + + s = next; + } + // Fall through. + case next: + { + if (!l.empty () && l.back () == ',') + l.pop_back (); + else + s = last; + + break; + } + case last: + { + if (l != "]") + throw_invalid_input ("end of array expected"); + + s = end; + continue; + } + case end: + { + throw_invalid_input ("junk after end of array"); + } + } + + // Parse just the output target path, which must come first. + // + json::parser jp (l, "" /* name */); + + jp.next_expect (json::event::begin_object); + string op (move (jp.next_expect_member_string ("output"))); + + auto r (db_.emplace (move (op), entry {entry_status::absent, l})); + if (!r.second) + throw_invalid_input ( + "duplicate output value '" + r.first->first + '\''); + } + + if (s != end) + throw_invalid_input ("corrupt input text"); + } + catch (const json::invalid_json_input& e) + { + state_ = state::failed; + + location l (path, line, e.column); + fail (l) << "invalid compilation database json input: " << e << + info << "remove this file if it was produced by a different tool"; + } + catch (const io_error& e) + { + state_ = state::failed; + fail << "unable to read " << path << ": " << e; + } + } + + absent_ = db_.size (); + changed_ = false; + + state_ = state::open; + } + } + + bool compiledb_file:: + match (const file&, const path_type& op, bool changed) + { + mlock l (mutex_); + + switch (state_) + { + case state::open: + break; + case state::failed: + return false; + case state::closed: + assert (false); + return false; + } + + // Mark an existing entry as present or changed. And if one does not + // exist, then (for now) as missing. + // + auto i (db_.find (op.string ())); + + if (i != db_.end ()) + { + entry& e (i->second); + + // Note: we can end up with present entries via the module context + // (see post() below). And we can see changed entries in a subsequent + // nested module context. + // + switch (e.status) + { + case entry_status::present: + case entry_status::changed: + assert (!changed); + break; + case entry_status::absent: + { + e.status = changed ? entry_status::changed : entry_status::present; + + absent_--; + changed_ = changed_ || (e.status == entry_status::changed); + break; + } + case entry_status::missing: + assert (false); + } + + return false; + } + else + { + db_.emplace (op.string (), entry {entry_status::missing, string ()}); + + changed_ = true; + + return true; + } + } + + void compiledb_file:: + execute (const file&, const path_type& op, + const file&, const path_type& ip, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) + { + const string& ro (relo.string ()); + const string& ao (abso.string ()); + + const string& rm (relm.string ()); + const string& am (absm.string ()); + + mlock l (mutex_); + + switch (state_) + { + case state::open: + break; + case state::failed: + return; + case state::closed: + assert (false); + return; + } + + auto i (db_.find (op.string ())); + + // We should have had the match() call before execute(). + // + assert (i != db_.end () && i->second.status != entry_status::absent); + + entry& e (i->second); + + if (e.status == entry_status::present) // Present and unchanged. + return; + + // The entry is either missing or changed. + // + try + { + e.json.clear (); + json::buffer_serializer js (e.json, 0 /* indentation */); + + js.begin_object (); + { + js.member ("output", op.string ()); // Note: must come first. + js.member ("file", ip.string ()); + + js.member_begin_array ("arguments"); + { + string buf; // Reuse. + for (auto b (args.begin ()), i (b), e (args.end ()); + i != e && *i != nullptr; + ++i) + { + const char* r; + + if (i == b) + r = cpath.effect_string (); + else + { + // Untranslate relative paths back to absolute. + // + const char* a (*i); + + if ((r = rel_to_abs (a, ro, ao, buf)) == nullptr && + (r = rel_to_abs (a, rm, am, buf)) == nullptr) + r = a; + } + + js.value (r); + } + } + js.end_array (); + + js.member ("directory", work.string ()); + } + js.end_object (); + } + catch (const json::invalid_json_output& e) + { + // There is no way (nor reason; the output will most likely be invalid + // anyway) to reuse the failed json serializer so make sure we ignore + // all the subsequent callbacks. + // + state_ = state::failed; + + l.unlock (); + + fail << "invalid compilation database json output: " << e; + } + + e.status = entry_status::changed; + } + + void compiledb_file:: + post (context& ctx, const action_targets& ts, bool failed) + { + assert (nesting_ != 0); + if (--nesting_ != 0) // Nested post() call. + return; + + switch (state_) + { + case state::open: + break; + case state::failed: + return; + case state::closed: + assert (false); + return; + } + + bool mctx (ctx.module_context == &ctx); + + tracer trace ("cc::compiledb_file::post"); + + // See if we need to update the file. + // + if (changed_) + l6 ([&]{trace << "updating due to missing/changed entries: " << path;}); + + // Don't prune the stale entries if the operation failed since we may + // not have gotten to execute some of them. + // + // And if this is a module context's post, then also don't prune the + // stale entries, instead waiting for the main context's post (if there + // will be one; this means we will only prune on update). + // + // Actually, this pruning business is even trickier than that: if we + // are not updating the entire project (say, rather only a subdirectory + // or even a specific target), then we will naturally not get any + // match/execute calls for targets of this project that don't get pulled + // into this build. Which means that we cannot just prune entries that + // we did not match/execute. It feels the correct semantics is to only + // prune the entries if they are in a subdirectory of the dir{} targets + // which we are building. + // + // What do we do about the module context, where we always update a + // specific libs{}? We could use its directory instead but that may lead + // to undesirable results. For example, if there are unit tests in the + // same directory, we will end up dropping their entries. It feels like + // the correct approach is to just ignore module context's entries + // entirely. If someone wants to prune the compilation database of a + // module, they will just need to update it directly (i.e., via the main + // context). Note that we cannot apply the same "simplification" to the + // changed entries since we will only observe the change once. + // + bool absent (false); + + if (!failed && !mctx && absent_ != 0) + { + // Pre-scan the entries and drop the appropriate absent ones. + // + for (auto i (db_.begin ()); i != db_.end (); ) + { + const entry& e (i->second); + + if (e.status == entry_status::absent) + { + // Absent entries should be rare enough during the normal + // development that we don't need to bother with caching the + // directories. + // + bool a (false); + for (const action_target& at: ts) + { + const target& t (at.as<target> ()); + if (t.is_a<dir> ()) + { + const string& p (i->first); + const string& d (t.out_dir ().string ()); + + if (path_traits::sub (p.c_str (), p.size (), + d.c_str (), d.size ())) + { + // Remove this entry from the in-memory state so that it + // matches the file state. + // + i = db_.erase (i); + --absent_; + a = absent = true; + break; + } + } + } + + if (a) + continue; + } + + ++i; + } + } + + if (absent) + l6 ([&]{trace << "updating due to absent entries: " << path;}); + + try + { + auto_rmfile rm; + ofdstream ofs; + + bool u (changed_ || absent); // Update the file. + + if (u) + { + rm = auto_rmfile (path); + ofs.open (path); + + // We parse the top-level array manually (see pre() above) and the + // expected format is as follows: + // + // [ + // {"output":...}, + // ... + // {"output":...} + // ] + // + ofs.write ("[\n", 2); + } + + // Iterate over the entries resetting their status and writing them to + // the file if necessary. + // + bool first (true); + for (auto& p: db_) + { + entry& e (p.second); + + // First sort out the status also skipping appropriate entries. + // + switch (e.status) + { + case entry_status::absent: + { + // This is an absent entry that we should keep (see pre-scan + // above). + // + break; + } + case entry_status::missing: + { + // This should only happen if this operation has failed (see + // also below) or we are in the match-only mode. + // + assert (failed || ctx.match_only); + continue; + } + case entry_status::present: + case entry_status::changed: + { + // This is tricky: if this is a module context, then we don't + // want to mark the entries as absent since they will then get + // dropped by the main operation context. + // + if (mctx) + e.status = entry_status::present; + else + { + // Note: this is necessary for things to work across multiple + // operations in a batch. + // + e.status = entry_status::absent; + absent_++; + } + } + } + + if (u) + { + if (first) + first = false; + else + ofs.write (",\n", 2); + + ofs.write (e.json.c_str (), e.json.size ()); + } + } + + if (u) + { + ofs.write (first ? "]\n" : "\n]\n", first ? 2 : 3); + + ofs.close (); + rm.cancel (); + } + } + catch (const io_error& e) + { + state_ = state::failed; + fail << "unable to write to " << path << ": " << e; + } + + // If this operation has failed, then our state may not be accurate + // (e.g., entries with missing status) but we also don't expect any + // further pre calls. Let's change out state to failed as a sanity + // check. + // + if (failed) + state_ = state::failed; + else + changed_ = false; + + // Note: keep in the open state (see pre() for details). + } + +#endif // BUILD2_BOOTSTRAP + } +} diff --git a/libbuild2/cc/compiledb.hxx b/libbuild2/cc/compiledb.hxx new file mode 100644 index 0000000..8288cf5 --- /dev/null +++ b/libbuild2/cc/compiledb.hxx @@ -0,0 +1,236 @@ +// file : libbuild2/cc/compiledb.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_CC_COMPILEDB_HXX +#define LIBBUILD2_CC_COMPILEDB_HXX + +#include <unordered_map> + +#ifndef BUILD2_BOOTSTRAP +# include <libbutl/json/serializer.hxx> +#endif + +#include <libbuild2/types.hxx> +#include <libbuild2/utility.hxx> + +#include <libbuild2/target.hxx> +#include <libbuild2/action.hxx> +#include <libbuild2/context.hxx> + +namespace build2 +{ + namespace cc + { + using compiledb_name_filter = vector<pair<optional<string>, bool>>; + using compiledb_type_filter = vector<pair<optional<string>, string>>; + + class compiledb + { + public: + // Match callback where we confirm an entry in the database and also + // signal whether it has changes (based on change tracking in depdb). + // Return true to force compilation of this target and thus make sure + // the below execute() is called (unless something before that failed). + // + // Besides noticing changes, this callback is also necessary to notice + // and delete entries that should no longer be in the database (e.g., a + // source file was removed from the project). + // + // Note that output is either obj*{}, bmi*{}, of hbmi*{}. + // + static bool + match (const scope& bs, + const file& output, const path& output_path, + const file& input, + bool changed); + + // Execute callback where we insert or update an entry in the database. + // + // The {relo, abso}, and {relm, absm} pairs are used to "untranslate" + // relative paths to absolute. Specifically, any argument that has rel? + // as a prefix has this prefix replaced with the corresponding abs?. + // Note that this means we won't be able to handle old MSVC and + // clang-cl, which don't support the `/F?: <path>` form, only + // `/F?<path>`. Oh, well. Note also that either relo or relm (but not + // both) could be empty if unused. + // + // Note that we assume the source file is always absolute and is the + // last argument. + // + // Why do we want absolute paths? That's a good question. Our initial + // plan was to compare command lines in order to detect when we need to + // update the database. And if those changed with every change of CWD, + // that would be of little use. But then we realized we could do better + // by using depdb to detect changes. So now we actually don't have a + // need to get rid of the relative paths in the command line. But seeing + // that we already have it, let's keep it for now in case it makes a + // different to some broken/legacy consumers. Note also that C++ module + // name-to-BMI mapping is not untranslated (see append_module_options()). + // + static void + execute (const scope& bs, + const file& output, const path& output_path, + const file& input, const path& input_path, + const process_path& cpath, const cstrings& args, + const path& relo, const path& abso, + const path& relm, const path& absm); + + public: + using path_type = build2::path; + + string name; + path_type path; + + // The path is expected to be absolute and normalized or empty if the + // name is `-` (stdout). + // + compiledb (string n, path_type p) + : name (move (n)), path (move (p)) + { + } + + virtual void + pre (context&) = 0; + + virtual bool + match (const file& output, const path_type& output_path, + bool changed) = 0; + + virtual void + execute (const file& output, const path_type& output_path, + const file& input, const path_type& input_path, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) = 0; + + virtual void + post (context&, const action_targets&, bool failed) = 0; + + virtual + ~compiledb (); + }; + + using compiledb_set = vector<unique_ptr<compiledb>>; + + // Populated by core_config_init() during serial load. + // + extern compiledb_set compiledbs; + + // Context operation callbacks. + // + void + compiledb_pre (context&, action, const action_targets&); + + void + compiledb_post (context&, action, const action_targets&, bool failed); + +#ifndef BUILD2_BOOTSTRAP + + // Implementation that writes to stdout. + // + // Note that this implementation forces compilation of all the targets for + // which it is called to make sure their entries are in the database. So + // typically used in the dry run mode. + // + class compiledb_stdout: public compiledb + { + public: + // The path is expected to be empty. + // + explicit + compiledb_stdout (string name); + + virtual void + pre (context&) override; + + virtual bool + match (const file& output, const path_type& output_path, + bool changed) override; + + virtual void + execute (const file& output, const path_type& output_path, + const file& input, const path_type& input_path, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) override; + + virtual void + post (context&, const action_targets&, bool failed) override; + + private: + mutex mutex_; + enum class state {init, empty, full, failed} state_; + size_t nesting_; + butl::json::stream_serializer js_; + }; + + // Implementation that maintains a file. + // + class compiledb_file: public compiledb + { + public: + compiledb_file (string name, path_type path); + + virtual void + pre (context&) override; + + virtual bool + match (const file& output, const path_type& output_path, + bool changed) override; + + virtual void + execute (const file& output, const path_type& output_path, + const file& input, const path_type& input_path, + const process_path& cpath, const cstrings& args, + const path_type& relo, const path_type& abso, + const path_type& relm, const path_type& absm) override; + + virtual void + post (context&, const action_targets&, bool failed) override; + + private: + mutex mutex_; + enum class state {closed, open, failed} state_; + size_t nesting_; + + // We want to optimize the performance for the incremental update case + // where only a few files will be recompiled and most of the time there + // will be no change in the command line, which means we won't need to + // rewrite the file. + // + // As a result, our in-memory representation is a hashmap (we could have + // thousands of entries) of absolute and normalized output file paths + // (stored as strings for lookup efficiency) to their serialized JSON + // text lines plus the status: absent, present, changed, or missing + // (entry should be there but is not). This way we don't waste + // (completely) parsing (and re-serializing) each line knowing that we + // won't need to touch most of them. + // + // In fact, we could have gone even further and used a sorted vector + // since insertions will be rare in this case. But we will need to + // lookup every entry on each update, so it's unclear this is a win. + // + enum class entry_status {absent, present, changed, missing}; + + struct entry + { + entry_status status; + string json; + }; + + using map_type = std::unordered_map<string, entry>; + map_type db_; + + // Number/presence of various entries in the database (used to determine + // whether we need to update the file without iterating over all the + // entries). + // + size_t absent_; // Number of absent entries. + bool changed_; // Presence of changed or missing entries. + }; + +#endif // BUILD2_BOOTSTRAP + } +} + +#endif // LIBBUILD2_CC_COMPILEDB_HXX diff --git a/libbuild2/cc/init.cxx b/libbuild2/cc/init.cxx index e124450..d691bc5 100644 --- a/libbuild2/cc/init.cxx +++ b/libbuild2/cc/init.cxx @@ -10,8 +10,10 @@ #include <libbuild2/config/utility.hxx> +#include <libbuild2/cc/module.hxx> #include <libbuild2/cc/target.hxx> #include <libbuild2/cc/utility.hxx> +#include <libbuild2/cc/compiledb.hxx> using namespace std; using namespace butl; @@ -23,7 +25,7 @@ namespace build2 // Scope operation callback that cleans up module sidebuilds. // static target_state - clean_module_sidebuilds (action, const scope& rs, const dir&) + clean_module_sidebuilds (const scope& rs) { context& ctx (rs.ctx); @@ -67,6 +69,131 @@ namespace build2 return target_state::unchanged; } + // Scope operation callback that cleans up compilation databases. + // + static target_state + clean_compiledb (const scope& rs) + { + context& ctx (rs.ctx); + + target_state r (target_state::unchanged); + + for (const unique_ptr<compiledb>& db: compiledbs) + { + const path& p (db->path); + + if (p.empty () || + ctx.scopes.find_out (p.directory ()).root_scope () != &rs) + continue; + + if (rmfile (ctx, p)) + r = target_state::changed; + } + + return r; + } + + // Scope operation callback for cleaning module sidebuilds and compilation + // databases. + // + static target_state + clean_callback (action, const scope& rs, const dir&) + { + target_state r (clean_module_sidebuilds (rs)); + + if (!compiledbs.empty ()) + r |= clean_compiledb (rs); + + return r; + } + + // Detect if just <name> in the <name>[@<path>] form is actually <path>. + // We assume it is <path> and not <name> if it contains a directory + // component or is the special directory name (`.`/`..`) . If that's the + // case, return canonicalized name representing <path>. See the call site + // in core_config_init() below for background. + // + static optional<name> + compiledb_name_to_path (const name& n) + { + if (n.directory ()) + return n; + + if (n.file ()) + { + if (!n.dir.empty () || + path_traits::find_separator (n.value) != string::npos) + { + name r (n); + r.canonicalize (); + return r; + } + else if (n.value == "." || n.value == "..") + { + return name (dir_path (n.value)); + } + } + + return nullopt; + } + + // Custom save function that completes relative paths in the + // config.cc.compiledb and config.cc.compiledb.name values. + // + static pair<names_view, const char*> + save_compiledb_name (const scope&, + const value& v, + const value*, + names& storage) + { + const names& ns (v.as<names> ()); // Value is untyped. + + // Detect and handle the case where just <name> is actually <path>. + // + if (ns.size () == 1) + { + const name& n (ns.back ()); + + if (optional<name> otn = compiledb_name_to_path (n)) + { + name& tn (*otn); + + if (tn.dir.relative ()) + tn.dir.complete (); + + tn.dir.normalize (); + + storage.push_back (move (tn)); + return make_pair (names_view (storage), "="); + } + } + + if (find_if (ns.begin (), ns.end (), + [] (const name& n) {return n.pair;}) == ns.end ()) + { + return make_pair (names_view (ns), "="); + } + + storage = ns; + for (auto i (storage.begin ()); i != storage.end (); ++i) + { + if (i->pair) + { + name& n (*++i); + + if (!n.directory ()) + n.canonicalize (); + + if (n.dir.relative ()) + n.dir.complete (); + + n.dir.normalize (); + } + } + + return make_pair (names_view (storage), "="); + } + bool core_vars_init (scope& rs, scope&, @@ -107,6 +234,22 @@ namespace build2 vp.insert<abs_dir_path> ("config.cc.pkgconfig.sysroot"); + // Compilation database. + // + // See the manual for the semantics. + // + // config.cc.compiledb -- <name>[@<path>]|<path> (untyped) + // config.cc.compiledb.name -- <name>[@<path>]... (untyped) + // config.cc.compiledb.filter -- [<name>@]<bool>... + // config.cc.compiledb.filter.input -- [<name>@]<target-type>... + // config.cc.compiledb.filter.output -- [<name>@]<target-type>... + // + vp.insert ("config.cc.compiledb"); + vp.insert ("config.cc.compiledb.name"); + vp.insert<compiledb_name_filter> ("config.cc.compiledb.filter"); + vp.insert<compiledb_type_filter> ("config.cc.compiledb.filter.input"); + vp.insert<compiledb_type_filter> ("config.cc.compiledb.filter.output"); + vp.insert<strings> ("cc.poptions"); vp.insert<strings> ("cc.coptions"); vp.insert<strings> ("cc.loptions"); @@ -192,16 +335,6 @@ namespace build2 // vp.insert<bool> ("cc.serialize"); - // Register scope operation callback. - // - // It feels natural to clean up sidebuilds as a post operation but that - // prevents the (otherwise-empty) out root directory to be cleaned up - // (via the standard fsdir{} chain). - // - rs.operation_callbacks.emplace ( - perform_clean_id, - scope::operation_callback {&clean_module_sidebuilds, nullptr /*post*/}); - return true; } @@ -292,6 +425,8 @@ namespace build2 assert (first); + context& ctx (rs.ctx); + // Load cc.core.guess. // load_module (rs, rs, "cc.core.guess", loc); @@ -312,7 +447,6 @@ namespace build2 // // @@ Same nonsense as in module. // - // rs.assign ("cc.poptions") += cast_null<strings> ( lookup_config (rs, "config.cc.poptions", nullptr)); @@ -363,21 +497,16 @@ namespace build2 if (!cast_false<bool> (rs["bin.config.loaded"])) { // Prepare configuration hints (pretend it belongs to root scope). - // They are only used on the first load of bin.config so we only - // populate them on our first load. // variable_map h (rs); - if (first) - { - // Note that all these variables have already been registered. - // - h.assign ("config.bin.target") = - cast<target_triplet> (rs["cc.target"]).representation (); + // Note that all these variables have already been registered. + // + h.assign ("config.bin.target") = + cast<target_triplet> (rs["cc.target"]).representation (); - if (auto l = extra.hints["config.bin.pattern"]) - h.assign ("config.bin.pattern") = cast<string> (l); - } + if (auto l = extra.hints["config.bin.pattern"]) + h.assign ("config.bin.pattern") = cast<string> (l); init_module (rs, rs, "bin.config", loc, false /* optional */, h); } @@ -386,7 +515,6 @@ namespace build2 // ourselves since the target can come from the configuration and not // our hint). // - if (first) { const auto& ct (cast<target_triplet> (rs["cc.target"])); const auto& bt (cast<target_triplet> (rs["bin.target"])); @@ -416,6 +544,447 @@ namespace build2 if (tsys == "mingw32") load_module (rs, rs, "bin.rc.config", loc); + // Find the innermost outer core_module, if any. + // + const core_module* om (nullptr); + for (const scope* s (&rs); + (s = s->parent_scope ()->root_scope ()) != nullptr; ) + { + if ((om = s->find_module<core_module> (core_module::name)) != nullptr) + break; + } + + auto& m (extra.set_module (new core_module (om))); + + // config.cc.compiledb.* + // + { + // For config.cc.compiledb and config.cc.compiledb.name we only + // consider a value in this root scope (if it's inherited from the + // outer scope, then that's where it will be handled). One special + // case is when it's specified on a scope that doesn't load the cc + // module (including, ultimately, the global scope for a global + // override). We handle it by assuming the value belongs to the + // outermost amalgamation that loads the cc module. + // + // Note: cache the result. + // + auto find_outermost = + [&rs, o = optional<pair<scope*, core_module*>> ()] () mutable + { + if (!o) + { + o = pair<scope*, core_module*> (&rs, nullptr); + for (scope* s (&rs); + (s = s->parent_scope ()->root_scope ()) != nullptr; ) + { + if (auto* m = s->find_module<core_module> (core_module::name)) + { + o->first = s; + o->second = m; + } + } + } + + return *o; + }; + + auto belongs = [&rs, &find_outermost] (const lookup& l) + { + return l.belongs (rs) || find_outermost ().first == &rs; + }; + + // Add compilation databases specified in ns as <name>[@<path>] pairs, + // appending their names to cdb_names. If <path> is absent, then place + // the database into the base directory. Return the last added name. + // + auto add_cdbs = [&ctx, + &loc, + &trace] (strings& cdb_names, + const names& ns, + const dir_path& base) -> const string& + { + // Check that names and paths match. Return false if this entry + // already exist. + // + // Note that before we also checked that the same paths are not used + // across contexts. But, actually, there doesn't seem to be anything + // wrong with that and this can actually be useful, for example, + // when developing build system modules. + // + auto check = [&loc] (const string& n, const path& p) + { + for (const unique_ptr<compiledb>& db: compiledbs) + { + bool nm (db->name == n); + bool pm (db->path == p); + + if (nm != pm) + fail (loc) << "inconsistent compilation database names/paths" << + info << p << " is called " << n << + info << db->path << " is called " << db->name; + + if (nm) + return false; + } + + return true; + }; + + const string* r (&empty_string); + + bool reg (false); + size_t j (compiledbs.size ()); // First newly added database. + for (auto i (ns.begin ()); i != ns.end (); ++i) + { + // Each element has the <name>[@<path>] form. + // + // The special `-` <name> signifies stdout. + // + // If <path> is absent, then the file is called <name>.json and + // placed into the output directory of the amalgamation or project + // root scope (passed as the base argument). + // + // If <path> is (syntactically) a directory, then the file path is + // <path>/<name>.json. + // + if (!i->simple () || i->empty ()) + fail (loc) << "invalid compilation database name '" << *i << "'"; + + // Don't allow names that have (or are) directory components. + // + if (compiledb_name_to_path (*i)) + fail (loc) << "directory component in compilation database name '" + << *i << "'"; + + string n (i->value); + + path p; + if (i->pair) + { + ++i; + + if (n == "-") + fail (loc) << "compilation database path specified for stdout " + << "name"; + try + { + if (i->directory ()) + p = i->dir / n + ".json"; + else if (i->file ()) + { + if (i->dir.empty ()) + p = path (i->value); + else + p = i->dir / i->value; + } + else + throw invalid_path (""); + + if (p.relative ()) + p.complete (); + + p.normalize (); + } + catch (const invalid_path&) + { + fail (loc) << "invalid compilation database path '" << *i + << "'"; + } + } + else if (n != "-") + { + p = base / n + ".json"; + } + + if (check (n, p)) + { + reg = compiledbs.empty (); // First time. + +#ifdef BUILD2_BOOTSTRAP + fail (loc) << "compilation database requested during bootstrap"; +#else + if (n == "-") + compiledbs.push_back ( + unique_ptr<compiledb> ( + new compiledb_stdout (n))); + else + compiledbs.push_back ( + unique_ptr<compiledb> ( + new compiledb_file (n, move (p)))); +#endif + } + + // We may end up with duplicates via the config.cc.compiledb + // logic. + // + auto k (find (cdb_names.begin (), cdb_names.end (), n)); + + if (k == cdb_names.end ()) + { + cdb_names.push_back (move (n)); + r = &cdb_names.back (); + } + else + r = &*k; + } + + // Register context operation callback for compiledb generation. + // + // We have two complications here: + // + // 1. We could be performing all this from the load phase that + // interrupted the match phase, which means the point where the + // pre callback would have been called is already gone (but the + // post callback will still be called). This will happen if we, + // say, import a project that has a compilation database from a + // project that doesn't. + // + // (Note that if you think that this can be solved by simply + // always registering the callbacks, regardless of whether we + // have any databases or not, consider a slightly different + // scenario where we import a project that loads the cc module + // from a project that does not). + // + // What we are going to do in this case is simply call the pre + // callback manually. + // + // 2. We could again be performing all this from the load phase that + // interrupted the match phase, but this time the pre callback + // has already been called, which means there will be no pre() + // call for the newly added database(s). This will happen if we, + // say, import a project that has a compilation database from a + // project that also has one. + // + // Again, what we are going to do in this case is simply call the + // pre callback for the new database(s) manually. + // + if (reg) + ctx.operation_callbacks.emplace ( + perform_update_id, + context::operation_callback {&compiledb_pre, &compiledb_post}); + + if (ctx.load_generation > 1) + { + action a (ctx.current_action ()); + + if (a.inner_action () == perform_update_id) + { + if (reg) // Case #1. + { + l6 ([&]{trace << "direct compiledb_pre for context " << &ctx;}); + compiledb_pre (ctx, a, action_targets {}); + } + else // Case #2. + { + size_t n (compiledbs.size ()); + + if (j != n) + { + l6 ([&]{trace << "additional compiledb for context " << &ctx;}); + + for (; j != n; ++j) + compiledbs[j]->pre (ctx); + } + } + } + } + + return *r; + }; + + lookup l; + + // config.cc.compiledb + // + // The semantics of this value is as follows: + // + // Location: outermost amalgamation that loads the cc module. + // Name filter: enable from this scope unless specified explicitly. + // Type filter: enable from this scope unless specified explicitly. + // + // Note: save omitted. + // + optional<string> enable_filter; + + l = lookup_config (rs, "config.cc.compiledb", 0, &save_compiledb_name); + if (l && belongs (l)) + { + l6 ([&]{trace << "config.cc.compiledb specified on " << rs;}); + + const names& ns (cast<names> (l)); + + // Make sure it's one name/path. + // + size_t n (ns.size ()); + if (n == 0 || n != (ns.front ().pair ? 2 : 1)) + fail (loc) << "invalid compilation database name '" << ns << "'"; + + // Detect and translate just <name> which is actually <path> to the + // <name>@<path> form: + // + // - The <name> part is the name of the directory where the database + // file will reside (typically project/repository or package + // name). + // + // - If <path> is a directory, then the database name is + // compile_commands.json. + // + names tns; + if (n == 1) + { + const name& n (ns.front ()); + + if (optional<name> otn = compiledb_name_to_path (n)) + { + name& tn (*otn); + + // Note: the add_cdbs() call below completes and normalizes the + // path but we need to do it earlier in order to be able to + // derive the name (the last component can be `.`/`..`). + // + if (tn.dir.relative ()) + tn.dir.complete (); + + tn.dir.normalize (); + + if (!exists (tn.dir)) + fail (loc) << "compilation database directory " << tn.dir + << " does not exist"; + + if (tn.value.empty ()) + tn.value = "compile_commands.json"; + + tns.push_back (name (tn.dir.leaf ().string ())); + tns.back ().pair = '@'; + tns.push_back (move (tn)); + } + } + + // We inject the database directly into the outer amalgamation's + // module, as-if config.cc.compiledb.name was specified in its + // scope. Unless there isn't one, in which case it's us. + // + pair<scope*, core_module*> p (find_outermost ()); + + // Save the name for the name filter below. + // + enable_filter = add_cdbs ( + (p.second != nullptr ? *p.second : m).cdb_names_, + tns.empty () ? ns : tns, + p.first->out_path ()); + } + + // config.cc.compiledb.name + // + // Note: save omitted. + // + l = lookup_config (rs, + "config.cc.compiledb.name", + 0, + &save_compiledb_name); + if (l && belongs (l)) + { + l6 ([&]{trace << "config.cc.compiledb.name specified on " << rs;}); + + add_cdbs (m.cdb_names_, cast<names> (l), rs.out_path ()); + } + + // config.cc.compiledb.filter + // + // Note: save omitted. + // + l = lookup_config (rs, "config.cc.compiledb.filter"); + if (l && belongs (l)) // Custom. + { + m.cdb_filter_ = &cast<compiledb_name_filter> (l); + } + else if (enable_filter) // Override. + { + // Inherit outer filter. + // + if (om != nullptr && om->cdb_filter_ != nullptr) + m.cdb_filter_storage_ = *om->cdb_filter_; + + m.cdb_filter_storage_.emplace_back (*enable_filter, true); + m.cdb_filter_ = &m.cdb_filter_storage_; + } + else if (om != nullptr) // Inherit. + { + m.cdb_filter_ = om->cdb_filter_; + } + + // config.cc.compiledb.filter.input + // config.cc.compiledb.filter.output + // + // Note that filtering happens before we take into account the change + // status, which means for larger projects there would be a lot of + // targets to filter even during the incremental update. So it feels + // it would have been better to pre-lookup the target types. However, + // the targets that would normally be used are registered by other + // modules (bin, c/cxx) and which haven't been loaded yet. So instead + // we try to optimize the lookup for the commonly used targets. + // + // Note: save omitted. + // + l = lookup_config (rs, "config.cc.compiledb.filter.input"); + if (l && belongs (l)) // Custom. + { + m.cdb_filter_input_ = &cast<compiledb_type_filter> (l); + } + else if (enable_filter) // Override. + { + // Inherit outer filter. + // + if (om != nullptr && om->cdb_filter_input_ != nullptr) + { + m.cdb_filter_input_storage_ = *om->cdb_filter_input_; + m.cdb_filter_input_storage_.emplace_back (*enable_filter, "target"); + m.cdb_filter_input_ = &m.cdb_filter_input_storage_; + } + else + m.cdb_filter_input_ = nullptr; // Enable all. + } + else if (om != nullptr) // Inherit. + { + m.cdb_filter_input_ = om->cdb_filter_input_; + } + + l = lookup_config (rs, "config.cc.compiledb.filter.output"); + if (l && belongs (l)) // Custom. + { + m.cdb_filter_output_ = &cast<compiledb_type_filter> (l); + } + else if (enable_filter) // Override. + { + // Inherit outer filter. + // + if (om != nullptr && om->cdb_filter_output_ != nullptr) + { + m.cdb_filter_output_storage_ = *om->cdb_filter_output_; + m.cdb_filter_output_storage_.emplace_back (*enable_filter, "target"); + m.cdb_filter_output_ = &m.cdb_filter_output_storage_; + } + else + m.cdb_filter_output_ = nullptr; // Enable all. + } + else if (om != nullptr) // Inherit. + { + m.cdb_filter_output_ = om->cdb_filter_output_; + } + } + + // Register scope operation callback for cleaning module sidebuilds and + // compilation databases. + // + // It feels natural to clean this stuff up as a post operation but that + // prevents the (otherwise-empty) out root directory to be cleaned up + // (via the standard fsdir{} chain). + // + rs.operation_callbacks.emplace ( + perform_clean_id, + scope::operation_callback {&clean_callback, nullptr /*post*/}); + return true; } diff --git a/libbuild2/cc/install-rule.cxx b/libbuild2/cc/install-rule.cxx index 6758e03..46764a6 100644 --- a/libbuild2/cc/install-rule.cxx +++ b/libbuild2/cc/install-rule.cxx @@ -76,86 +76,6 @@ namespace build2 otype ot (link_type (t).type); - // @@ TMP: drop eventually. - // -#if 0 - // If this is a shared library prerequisite, install it as long as it is - // in the installation scope. - // - // Less obvious: we also want to install a static library prerequisite - // of a library (since it could be referenced from its .pc file, etc). - // - // Note: for now we assume these prerequisites never come from see- - // through groups. - // - // Note: we install ad hoc prerequisites by default. - // - - // Note: at least one must be true since we only register this rule for - // exe{}, and lib[as]{} (this makes sure the following if-condition will - // always be true for libx{}). - // - bool st (t.is_a<exe> () || t.is_a<libs> ()); // Target needs shared. - bool at (t.is_a<liba> () || t.is_a<libs> ()); // Target needs static. - assert (st || at); - - if ((st && (p.is_a<libx> () || p.is_a<libs> ())) || - (at && (p.is_a<libx> () || p.is_a<liba> ()))) - { - const target* pt (&search (t, p)); - - // If this is the lib{}/libu*{} group, pick a member which we would - // link. For libu*{} we want the "see through" logic. - // - if (const libx* l = pt->is_a<libx> ()) - pt = link_member (*l, a, link_info (t.base_scope (), ot)); - - // Note: not redundant since we could be returning a member. - // - if ((st && pt->is_a<libs> ()) || (at && pt->is_a<liba> ())) - { - // Adjust match options. - // - if (a.operation () != update_id) - { - if (t.is_a<exe> ()) - options = lib::option_install_runtime; - else - { - // This is a library prerequisite of a library target and - // runtime-only begets runtime-only. - // - if (me.cur_options == lib::option_install_runtime) - options = lib::option_install_runtime; - } - } - - return make_pair (is == nullptr || pt->in (*is) ? pt : nullptr, - options); - } - - // See through to libu*{} members. Note that we are always in the same - // project (and thus amalgamation). - // - if (pt->is_a<libux> ()) - { - // Adjust match options (similar to above). - // - if (a.operation () != update_id && !pt->is_a<libue> ()) - { - if (t.is_a<exe> ()) - options = lib::option_install_runtime; - else - { - if (me.cur_options == lib::option_install_runtime) - options = lib::option_install_runtime; - } - } - - return make_pair (pt, options); - } - } -#else // Note that at first it may seem like we don't need to install static // library prerequisites of executables. But such libraries may still // have prerequisites that are needed at runtime (say, some data files). @@ -189,7 +109,21 @@ namespace build2 // This is a library prerequisite of a library target and // runtime-only begets runtime-only. // - if (me.cur_options == lib::option_install_runtime) + // @@ But it goes further: while an interface prerequisite should + // match the target's options, it feels an implementation can be + // runtime-only, at least for shared library targets (for static + // the consumer would still need to link the prerequisite + // explicitly, which means it is more like buildtime). We could + // probably distinguish between interface/implementation by + // examining the *.export.libs variable and looking for the + // prerequisite (or its group). Feels hairy, though. So for now we + // only do this for target-shared/prerequisite-static case where + // we can assume the prerequisite is always implementation. See GH + // issue #448. See also apply_posthoc() as well as + // libux_install_rule below. + // + if (me.cur_options == lib::option_install_runtime || + (t.is_a<libs> () && pt->is_a<liba> ())) options = lib::option_install_runtime; } } @@ -209,7 +143,6 @@ namespace build2 return make_pair (pt, options); } } -#endif // The rest of the tests only succeed if the base filter() succeeds. // @@ -396,6 +329,9 @@ namespace build2 p.match_options = lib::option_install_runtime; else { + // @@ Hm, maybe runtime should be unconditional here since a + // plugin is always an implementation dependency? + // if (me.cur_options == lib::option_install_runtime) p.match_options = lib::option_install_runtime; } @@ -561,56 +497,6 @@ namespace build2 // above. In particular, here we use libue/libua/libus{} as proxies for // exe/liba/libs{} there. // - - // @@ TMP: drop eventually. - // -#if 0 - bool st (t.is_a<libue> () || t.is_a<libus> ()); // Target needs shared. - bool at (t.is_a<libua> () || t.is_a<libus> ()); // Target needs static. - assert (st || at); - - if ((st && (p.is_a<libx> () || p.is_a<libs> ())) || - (at && (p.is_a<libx> () || p.is_a<liba> ()))) - { - const target* pt (&search (t, p)); - - if (const libx* l = pt->is_a<libx> ()) - pt = link_member (*l, a, link_info (t.base_scope (), ot)); - - if ((st && pt->is_a<libs> ()) || (at && pt->is_a<liba> ())) - { - if (a.operation () != update_id) - { - if (t.is_a<libue> ()) - options = lib::option_install_runtime; - else - { - if (me.cur_options == lib::option_install_runtime) - options = lib::option_install_runtime; - } - } - - return make_pair (is == nullptr || pt->in (*is) ? pt : nullptr, - options); - } - - if (pt->is_a<libux> ()) - { - if (a.operation () != update_id && !pt->is_a<libue> ()) - { - if (t.is_a<libue> ()) - options = lib::option_install_runtime; - else - { - if (me.cur_options == lib::option_install_runtime) - options = lib::option_install_runtime; - } - } - - return make_pair (pt, options); - } - } -#else if (p.is_a<libx> () || p.is_a<libs> () || p.is_a<liba> ()) { const target* pt (&search (t, p)); @@ -624,7 +510,8 @@ namespace build2 options = lib::option_install_runtime; else { - if (me.cur_options == lib::option_install_runtime) + if (me.cur_options == lib::option_install_runtime || + (t.is_a<libus> () && pt->is_a<liba> ())) options = lib::option_install_runtime; } } @@ -637,7 +524,6 @@ namespace build2 else return make_pair (pt, options); } -#endif const target* pt (file_rule::instance.filter (is, a, t, p, me).first); if (pt == nullptr) diff --git a/libbuild2/cc/link-rule.cxx b/libbuild2/cc/link-rule.cxx index d9fbbea..a669f37 100644 --- a/libbuild2/cc/link-rule.cxx +++ b/libbuild2/cc/link-rule.cxx @@ -3522,10 +3522,6 @@ namespace build2 // args.push_back ("/NOLOGO"); - // Add /MACHINE. - // - args.push_back (msvc_machine (cast<string> (rs[x_target_cpu]))); - // For utility libraries use thin archives if possible. // // LLVM's lib replacement had the /LLVMLIBTHIN option at least from @@ -3592,7 +3588,16 @@ namespace build2 { // Are we using the compiler or the linker (e.g., link.exe) directly? // - bool ldc (tsys != "win32-msvc"); + bool ldc; + + if (tsys == "win32-msvc") + { + args.push_back ("/NOLOGO"); + ldc = false; + } + else + ldc = true; + if (ldc) { @@ -4002,6 +4007,20 @@ namespace build2 if (lt.shared_library () && (tsys == "win32-msvc" || tsys == "mingw32")) reli = relative (find_adhoc_member<libi> (t)->path ()); + if (tsys == "win32-msvc") + { + // Add /MACHINE unless there is a custom value (/MACHINE:ARM64EC). + // + // Note that we don't bother hashing it since to change its value one + // would have to use a different MSVC toolchain (which means things + // would be rebuilt from scratch anyway). + // + if (!find_option_prefix ("/MACHINE:", args, true)) + { + args.push_back (msvc_machine (cast<string> (rs[x_target_cpu]))); + } + } + const process_path* ld (nullptr); if (lt.static_library ()) { @@ -4025,15 +4044,10 @@ namespace build2 // Using link.exe directly. // ld = &cast<process_path> (rs["bin.ld.path"]); - args.push_back ("/NOLOGO"); if (ot == otype::s) args.push_back ("/DLL"); - // Add /MACHINE. - // - args.push_back (msvc_machine (cast<string> (rs[x_target_cpu]))); - // Unless explicitly enabled with /INCREMENTAL, disable incremental // linking (it is implicitly enabled if /DEBUG is specified). The // reason is the .ilk file: its name cannot be changed and if we diff --git a/libbuild2/cc/module.cxx b/libbuild2/cc/module.cxx index cf6c6e4..a3c64d9 100644 --- a/libbuild2/cc/module.cxx +++ b/libbuild2/cc/module.cxx @@ -22,6 +22,12 @@ namespace build2 { namespace cc { + // cc.core_module + // + const string core_module::name ("cc.core.config"); + + // x.config_module + // void config_module:: guess (scope& rs, const location& loc, const variable_map&) { @@ -891,6 +897,9 @@ namespace build2 config::save_environment (rs, xi.platform_environment); } + // x module + // + // Global cache of ad hoc importable headers. // // The key is a hash of the system header search directories diff --git a/libbuild2/cc/module.hxx b/libbuild2/cc/module.hxx index 4213516..d4e9a67 100644 --- a/libbuild2/cc/module.hxx +++ b/libbuild2/cc/module.hxx @@ -14,6 +14,8 @@ #include <libbuild2/cc/common.hxx> +#include <libbuild2/cc/compiledb.hxx> + #include <libbuild2/cc/compile-rule.hxx> #include <libbuild2/cc/link-rule.hxx> #include <libbuild2/cc/install-rule.hxx> @@ -27,6 +29,35 @@ namespace build2 { struct compiler_info; + // cc.core module + // + class core_module: public build2::module + { + public: + static const string name; + + explicit + core_module (const core_module* om) + : outer_module_ (om) + { + } + + public: + const core_module* outer_module_; + + strings cdb_names_; + + const compiledb_name_filter* cdb_filter_ = nullptr; + const compiledb_type_filter* cdb_filter_input_ = nullptr; + const compiledb_type_filter* cdb_filter_output_ = nullptr; + + compiledb_name_filter cdb_filter_storage_; + compiledb_type_filter cdb_filter_input_storage_; + compiledb_type_filter cdb_filter_output_storage_; + }; + + // x.config module + // class LIBBUILD2_CC_SYMEXPORT config_module: public build2::module, public config_data { @@ -92,13 +123,36 @@ namespace build2 // struct header_key { - path file; + // We used to use path comparison/hash which are case-insensitive on + // Windows. While this sounds right on the surface, the catch is that + // our target names are always case-sensitive, even on Windows. So + // what can happen is that the same header spelled in different case + // ends up with distinct targets but trying to occupy the same cache + // entry. See GH issue #390 for details. + // + // The conceptually correct way to fix this would be to actualize the + // header name. But that would be prohibitively expensive, especially + // on Windows. Plus the header may be generated and thus not yet + // exist. + // + // On the other hand, we can already end up with different targets + // mapped to the same filesystem entry in other situations (h{} vs + // hxx{} is the canonical example). And we are ok with that provided + // they have noop recipes. So it feels like the simplest solution here + // is to go along by having case-sensitive cache entries. This means + // that auto-generated headers will have to be included using the same + // spelling, but that seems like a sensible restriction (doing + // otherwise won't be portable). + // + path file; size_t hash; friend bool operator== (const header_key& x, const header_key& y) { - return x.file == y.file; // Note: hash was already compared. + // Note: hash was already compared. + // + return x.file.string () == y.file.string (); } }; @@ -130,6 +184,8 @@ namespace build2 msvc_library_search_dirs (const compiler_info&, scope&) const; }; + // x module + // class LIBBUILD2_CC_SYMEXPORT module: public build2::module, public virtual common, public link_rule, @@ -139,7 +195,6 @@ namespace build2 public predefs_rule { public: - explicit module (data&& d, const scope& rs) : common (move (d)), link_rule (move (d)), diff --git a/libbuild2/cc/pkgconfig.cxx b/libbuild2/cc/pkgconfig.cxx index 7e47534..79a38ea 100644 --- a/libbuild2/cc/pkgconfig.cxx +++ b/libbuild2/cc/pkgconfig.cxx @@ -1,6 +1,8 @@ // file : libbuild2/cc/pkgconfig.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file +#include <libbuild2/cc/pkgconfig.hxx> + #include <libbuild2/scope.hxx> #include <libbuild2/target.hxx> #include <libbuild2/context.hxx> @@ -18,7 +20,6 @@ #include <libbuild2/cc/utility.hxx> #include <libbuild2/cc/common.hxx> -#include <libbuild2/cc/pkgconfig.hxx> #include <libbuild2/cc/compile-rule.hxx> #include <libbuild2/cc/link-rule.hxx> @@ -707,6 +708,7 @@ namespace build2 cmp ("user32") || cmp ("userenv") || cmp ("uuid") || + cmp ("uxtheme") || cmp ("version") || cmp ("windowscodecs") || cmp ("winhttp") || diff --git a/libbuild2/config/init.cxx b/libbuild2/config/init.cxx index 2f134c4..776299c 100644 --- a/libbuild2/config/init.cxx +++ b/libbuild2/config/init.cxx @@ -27,6 +27,7 @@ namespace build2 namespace config { static const file_rule file_rule_ (true /* check_type */); + static const noop_rule noop_rule_ (true /* exclude_group */); void functions (function_map&); // functions.cxx @@ -38,7 +39,10 @@ namespace build2 // the entire values. // static pair<names_view, const char*> - save_environment (const value& d, const value* b, names& storage) + save_environment (const scope&, + const value& d, + const value* b, + names& storage) { if (b == nullptr) return make_pair (reverse (d, storage, true /* reduce */), "="); @@ -730,7 +734,7 @@ namespace build2 // This allows a custom configure rule while doing nothing by default. // - rs.insert_rule<target> (configure_id, 0, "config.noop", noop_rule::instance); + rs.insert_rule<target> (configure_id, 0, "config.noop", noop_rule_); // We need this rule for out-of-any-project dependencies (for example, // libraries imported from /usr/lib). We are registering it on the diff --git a/libbuild2/config/module.cxx b/libbuild2/config/module.cxx index 713d30c..faae865 100644 --- a/libbuild2/config/module.cxx +++ b/libbuild2/config/module.cxx @@ -14,7 +14,7 @@ namespace build2 bool module:: save_variable (const variable& var, optional<uint64_t> flags, - save_variable_function* save) + save_variable_function* func) { const string& n (var.name); @@ -45,15 +45,18 @@ namespace build2 return false; } - sv.push_back (saved_variable {var, flags, save}); + sv.push_back (saved_variable {var, flags, func}); return true; } void module:: - save_variable (scope& rs, const variable& var, optional<uint64_t> flags) + save_variable (scope& rs, + const variable& var, + optional<uint64_t> flags, + save_variable_function* func) { if (module* m = rs.find_module<module> (module::name)) - m->save_variable (var, flags); + m->save_variable (var, flags, func); } void module:: diff --git a/libbuild2/config/module.hxx b/libbuild2/config/module.hxx index 8d3ff67..77109ce 100644 --- a/libbuild2/config/module.hxx +++ b/libbuild2/config/module.hxx @@ -22,20 +22,12 @@ namespace build2 namespace config { // An ordered list of build system modules each with an ordered list of - // config.* variables and their "save flags" (see save_variable()) that - // are used (as opposed to just being specified) in this configuration. - // Populated by the config utility functions (required(), optional()) and - // saved in the order populated. If flags are absent, then this variable - // was marked as "unsaved" (always transient). + // config.* variables and their save flags/function (see save_variable()) + // that are used (as opposed to just being specified) in this + // configuration. Populated by the config utility functions (required(), + // optional()) and saved in the order populated. If flags are absent, then + // this variable was marked as "unsaved" (always transient). // - // The optional save function can be used to implement custom variable - // saving, for example, as a difference appended to the base value. The - // second half of the result is the assignment operator to use. - // - using save_variable_function = - pair<names_view, const char*> (const value&, - const value* base, - names& storage); struct saved_variable { reference_wrapper<const variable> var; @@ -151,7 +143,10 @@ namespace build2 save_variable_function* = nullptr); static void - save_variable (scope&, const variable&, optional<uint64_t>); + save_variable (scope&, + const variable&, + optional<uint64_t>, + save_variable_function*); bool save_module (const char* name, int prio = 0); diff --git a/libbuild2/config/operation.cxx b/libbuild2/config/operation.cxx index b1716cf..6e7ef18 100644 --- a/libbuild2/config/operation.cxx +++ b/libbuild2/config/operation.cxx @@ -558,7 +558,7 @@ namespace build2 storage.clear (); pair<names_view, const char*> p ( sv.save != nullptr - ? sv.save (v, base, storage) + ? sv.save (rs, v, base, storage) : make_pair (reverse (v, storage, true /* reduce */), "=")); // Might becomes empty after a custom save function had at it. diff --git a/libbuild2/config/utility.cxx b/libbuild2/config/utility.cxx index 6574367..aa0d5af 100644 --- a/libbuild2/config/utility.cxx +++ b/libbuild2/config/utility.cxx @@ -7,7 +7,14 @@ using namespace std; namespace build2 { - void (*config_save_variable) (scope&, const variable&, optional<uint64_t>); + void + (*config_save_variable) (scope&, + const variable&, + optional<uint64_t>, + pair<names_view, const char*> (*)(const scope&, + const value&, + const value*, + names&)); void (*config_save_environment) (scope&, const char*); void (*config_save_module) (scope&, const char*, int); const string& (*config_preprocess_create) (context&, @@ -21,7 +28,10 @@ namespace build2 namespace config { pair<lookup, bool> - lookup_config_impl (scope& rs, const variable& var, uint64_t sflags) + lookup_config_impl (scope& rs, + const variable& var, + uint64_t sflags, + save_variable_function* sfunc) { // This is a stripped-down version of the default value case. @@ -71,7 +81,7 @@ namespace build2 } if (l.defined ()) - save_variable (rs, var, sflags); + save_variable (rs, var, sflags, sfunc); return pair<lookup, bool> (l, n); } diff --git a/libbuild2/config/utility.hxx b/libbuild2/config/utility.hxx index 1e2ff53..3b67c6c 100644 --- a/libbuild2/config/utility.hxx +++ b/libbuild2/config/utility.hxx @@ -29,13 +29,22 @@ namespace build2 // disfigure hooks (for example, for second-level configuration). These are // accessed through the config module entry points (which are NULL for // transient configurations). Note also that the exact interpretation of the - // save flags and module order depends on the config module implementation - // (which may ignore them as not applicable). An implementation may also - // define custom save flags (for example, accessible through the config.save - // attribute). Such flags should start from 0x100000000. + // save flags/function and module order depends on the config module + // implementation (which may ignore them as not applicable). An + // implementation may also define custom save flags (for example, accessible + // through the config.save attribute). Such flags should start from + // 0x100000000. + // + // See below for the save function (last argument) semantics. // LIBBUILD2_SYMEXPORT extern void - (*config_save_variable) (scope&, const variable&, optional<uint64_t>); + (*config_save_variable) (scope&, + const variable&, + optional<uint64_t>, + pair<names_view, const char*> (*)(const scope&, + const value&, + const value*, + names&)); LIBBUILD2_SYMEXPORT extern void (*config_save_environment) (scope&, const char*); @@ -75,11 +84,27 @@ namespace build2 const uint64_t save_false_omitted = 0x08; // Treat false as undefined. const uint64_t save_base = 0x10; // Custom save with base. + // The optional save function can be used to implement custom variable + // saving, for example, as a difference appended to the base value or to + // complete relative paths (if abs_dir_path does not fit). The base + // argument is the value of this variable from the outer scope (if any) + // and is only calculated if the save_base flag is specified. The second + // half of the result is the assignment operator to use. + // + using save_variable_function = + pair<names_view, const char*> (const scope& rs, + const value&, + const value* base, + names& storage); + inline void - save_variable (scope& rs, const variable& var, uint64_t flags = 0) + save_variable (scope& rs, + const variable& var, + uint64_t flags = 0, + save_variable_function* func = nullptr) { if (config_save_variable != nullptr) - config_save_variable (rs, var, flags); + config_save_variable (rs, var, flags, func); } // Mark a variable as "unsaved" (always transient). @@ -91,7 +116,7 @@ namespace build2 unsave_variable (scope& rs, const variable& var) { if (config_save_variable != nullptr) - config_save_variable (rs, var, nullopt); + config_save_variable (rs, var, nullopt, nullptr); } // Mark an environment variable to be saved during hermetic configuration. @@ -256,35 +281,40 @@ namespace build2 lookup lookup_config (scope& rs, const variable&, - uint64_t save_flags = 0); + uint64_t save_flags = 0, + save_variable_function* = nullptr); lookup lookup_config (bool& new_value, scope& rs, const variable&, - uint64_t save_flags = 0); + uint64_t save_flags = 0, + save_variable_function* = nullptr); // Note that the variable is expected to have already been entered. // inline lookup lookup_config (scope& rs, const string& var, - uint64_t save_flags = 0) + uint64_t save_flags = 0, + save_variable_function* func = nullptr) { // Note: go straight for the public variable pool. // - return lookup_config (rs, rs.ctx.var_pool[var], save_flags); + return lookup_config (rs, rs.ctx.var_pool[var], save_flags, func); } inline lookup lookup_config (bool& new_value, scope& rs, const string& var, - uint64_t save_flags = 0) + uint64_t save_flags = 0, + save_variable_function* func = nullptr) { // Note: go straight for the public variable pool. // - return lookup_config (new_value, rs, rs.ctx.var_pool[var], save_flags); + return lookup_config ( + new_value, rs, rs.ctx.var_pool[var], save_flags, func); } // Lookup a config.* variable value and, if the value is undefined, set it diff --git a/libbuild2/config/utility.ixx b/libbuild2/config/utility.ixx index d8348bd..87f628c 100644 --- a/libbuild2/config/utility.ixx +++ b/libbuild2/config/utility.ixx @@ -6,25 +6,32 @@ namespace build2 namespace config { LIBBUILD2_SYMEXPORT pair<lookup, bool> - lookup_config_impl (scope&, const variable&, uint64_t); + lookup_config_impl (scope&, + const variable&, + uint64_t, + save_variable_function*); template <typename T> pair<lookup, bool> lookup_config_impl (scope&, const variable&, T&&, uint64_t, bool); inline lookup - lookup_config (scope& rs, const variable& var, uint64_t sflags) + lookup_config (scope& rs, + const variable& var, + uint64_t sflags, + save_variable_function* func) { - return lookup_config_impl (rs, var, sflags).first; + return lookup_config_impl (rs, var, sflags, func).first; } inline lookup lookup_config (bool& new_value, scope& rs, const variable& var, - uint64_t sflags) + uint64_t sflags, + save_variable_function* func) { - auto r (lookup_config_impl (rs, var, sflags)); + auto r (lookup_config_impl (rs, var, sflags, func)); new_value = new_value || r.second; return r.first; } diff --git a/libbuild2/context.hxx b/libbuild2/context.hxx index 828c41e..81ac970 100644 --- a/libbuild2/context.hxx +++ b/libbuild2/context.hxx @@ -178,34 +178,35 @@ namespace build2 // match - search prerequisites and match rules // execute - execute the matched rule // - // The build system starts with a "serial load" phase and then continues - // with parallel match and execute. Match, however, can be interrupted - // both with load and execute. + // The build system starts with a serial "initial load" phase and then + // continues with parallel match and execute. Match, however, can be + // interrupted both with load and execute. // - // Match can be interrupted with "exclusive load" in order to load - // additional buildfiles. Similarly, it can be interrupted with (parallel) - // execute in order to build targetd required to complete the match (for - // example, generated source code or source code generators themselves). + // Match can be interrupted with a (serial) "interrupting load" in order + // to load additional buildfiles. Similarly, it can be interrupted with + // (parallel) execute in order to build targetd required to complete the + // match (for example, generated source code or source code generators + // themselves). // // Such interruptions are performed by phase change that is protected by // phase_mutex (which is also used to synchronize the state changes // between phases). // - // Serial load can perform arbitrary changes to the build state. Exclusive - // load, however, can only perform "island appends". That is, it can - // create new "nodes" (variables, scopes, etc) but not (semantically) - // change already existing nodes or invalidate any references to such (the - // idea here is that one should be able to load additional buildfiles as - // long as they don't interfere with the existing build state). The - // "islands" are identified by the load_generation number (1 for the - // initial/serial load). It is incremented in case of a phase switch and - // can be stored in various "nodes" to verify modifications are only done - // "within the islands". Another example of invalidation would be - // insertion of a new scope "under" an existing target thus changing its - // scope hierarchy (and potentially even its base scope). This would be - // bad because we may have made decisions based on the original hierarchy, - // for example, we may have queried a variable which in the new hierarchy - // would "see" a new value from the newly inserted scope. + // Initial load can perform arbitrary changes to the build state. + // Interrupting load, however, can only perform what we call "island + // appends". That is, it can create new "nodes" (variables, scopes, etc) + // but not (semantically) change already existing nodes or invalidate any + // references to such (the idea here is that one should be able to load + // additional buildfiles as long as they don't interfere with the existing + // build state). The "islands" are identified by the load_generation + // number (1 for the initial load). It is incremented in case of a phase + // switch and can be stored in various "nodes" to verify modifications are + // only done "within the islands". Another example of invalidation would + // be insertion of a new scope "under" an existing target thus changing + // its scope hierarchy (and potentially even its base scope). This would + // be bad because we may have made decisions based on the original + // hierarchy, for example, we may have queried a variable which in the new + // hierarchy would "see" a new value from the newly inserted scope. // // The special load_generation value 0 indicates initialization before // anything has been loaded. Currently, it is changed to 1 at the end @@ -351,10 +352,52 @@ namespace build2 (current_mname.empty () && current_oname == mo)); }; + // Operation callbacks. + // + // An entity (module, core) can register a function that will be called + // when an action is executed on a set of targets. The pre callback is + // called before any recipes for the action are matched and the post -- + // after all have been executed. The post callback is called even if + // execution has failed. + // + // The callback should only be registered during the load phase. Note + // that it's registered for the inner action, meaning that it will be + // called for any outer action (which is discernible from the first + // argument of the callback). Note also that meta-operations other than + // perform never actually execute any recipes and it probably only makes + // sense to register these callbacks for the perform_* actions. + // + // Note that the callbacks will also be called when building a build + // system module or an ad hoc C++ recipe. See create_module_context() for + // details. + // + // Note also that if the callbacks are registered from a module load + // function, then there are nuances with interrupted load phases. See the + // compilation database handling in the cc module for details. + // + // See also scope::operation_callback. + // + struct operation_callback + { + using pre_callback = + void (context&, action, const action_targets&); + + using post_callback = + void (context&, action, const action_targets&, bool failed); + + function<pre_callback> pre; + function<post_callback> post; + }; + + using operation_callback_map = multimap<action_id, operation_callback>; + + operation_callback_map operation_callbacks; + // Meta/operation-specific context-global auxiliary data storage. // - // Note: cleared by current_[meta_]operation() below. Normally set by - // meta/operation-specific callbacks from [mate_]operation_info. + // Normally set by meta/operation-specific callbacks from + // [mata_]operation_info. The operation data is cleared by + // current_operation() below. // // Note also: watch out for MT-safety in the data itself. // @@ -759,6 +802,9 @@ namespace build2 // Set current meta-operation and operation. // + // Note that the context instance is not to be re-used between different + // meta-operations. + // void current_meta_operation (const meta_operation_info&); diff --git a/libbuild2/dist/operation.cxx b/libbuild2/dist/operation.cxx index 6dc830b..5b00980 100644 --- a/libbuild2/dist/operation.cxx +++ b/libbuild2/dist/operation.cxx @@ -282,120 +282,134 @@ namespace build2 const location loc (pn); // Dummy location. action_targets ts {tgt}; - auto process_postponed = [&ctx, &mod] () { - if (!mod.postponed.list.empty ()) + auto mog = make_guard ([&ctx] () {ctx.match_only = nullopt;}); + ctx.match_only = match_only_level::all; + + auto process_postponed = [&ctx, &mod, &ts] (action a) { - // Re-grab the phase lock similar to perform_match(). - // - phase_lock l (ctx, run_phase::match); + if (!mod.postponed.list.empty ()) + { + auto eg ( + make_exception_guard ( + [&ctx, a, &ts] () + { + perform_post_operation_callbacks ( + ctx, a, ts, true /* failed */); + })); - // Note that we don't need to bother with the mutex since we do - // all of this serially. But we can end up with new elements at - // the end. - // - // Strictly speaking, to handle this correctly we would need to do - // multiple passes over this list and only give up when we cannot - // make any progress since earlier entries that we cannot resolve - // could be "fixed" by later entries. But this feels far-fetched - // and so let's wait for a real example before complicating this. - // - for (auto i (mod.postponed.list.begin ()); - i != mod.postponed.list.end (); - ++i) - rule::match_postponed (*i); - } - }; + // Re-grab the phase lock similar to perform_match(). + // + phase_lock l (ctx, run_phase::match); - auto mog = make_guard ([&ctx] () {ctx.match_only = nullopt;}); - ctx.match_only = match_only_level::all; + // Note that we don't need to bother with the mutex since we do + // all of this serially. But we can end up with new elements at + // the end. + // + // Strictly speaking, to handle this correctly we would need to + // do multiple passes over this list and only give up when we + // cannot make any progress since earlier entries that we cannot + // resolve could be "fixed" by later entries. But this feels + // far-fetched and so let's wait for a real example before + // complicating this. + // + for (auto i (mod.postponed.list.begin ()); + i != mod.postponed.list.end (); + ++i) + rule::match_postponed (*i); + } + }; - const operations& ops (rs.root_extra->operations); - for (operations::size_type id (default_id + 1); // Skip default_id. - id < ops.size (); - ++id) - { - if (const operation_info* oif = ops[id]) + const operations& ops (rs.root_extra->operations); + for (operations::size_type id (default_id + 1); // Skip default_id. + id < ops.size (); + ++id) { - // Skip aliases (e.g., update-for-install). In fact, one can argue - // the default update should be sufficient since it is assumed to - // update all prerequisites and we no longer support ad hoc stuff - // like test.input. Though here we are using the dist - // meta-operation, not perform. - // - if (oif->id != id) - continue; - - // Use standard (perform) match. - // - if (auto pp = oif->pre_operation) + if (const operation_info* oif = ops[id]) { - if (operation_id pid = pp (ctx, {}, dist_id, loc)) + // Skip aliases (e.g., update-for-install). In fact, one can + // argue the default update should be sufficient since it is + // assumed to update all prerequisites and we no longer support + // ad hoc stuff like test.input. Though here we are using the + // dist meta-operation, not perform. + // + if (oif->id != id) + continue; + + // Use standard (perform) match. + // + if (auto pp = oif->pre_operation) { - const operation_info* poif (ops[pid]); - ctx.current_operation (*poif, oif, false /* diag_noise */); + if (operation_id pid = pp (ctx, {}, dist_id, loc)) + { + const operation_info* poif (ops[pid]); + ctx.current_operation (*poif, oif, false /* diag_noise */); - if (oif->operation_pre != nullptr) - oif->operation_pre (ctx, {}, false /* inner */, loc); + if (oif->operation_pre != nullptr) + oif->operation_pre (ctx, {}, false /* inner */, loc); - if (poif->operation_pre != nullptr) - poif->operation_pre (ctx, {}, true /* inner */, loc); + if (poif->operation_pre != nullptr) + poif->operation_pre (ctx, {}, true /* inner */, loc); - action a (dist_id, poif->id, oif->id); - mod.postponed.list.clear (); - perform_match ({}, a, ts, - 1 /* diag (failures only) */, - false /* progress */); - process_postponed (); + action a (dist_id, poif->id, oif->id); + mod.postponed.list.clear (); + perform_match ({}, a, ts, + 1 /* diag (failures only) */, + false /* progress */); + process_postponed (a); + perform_post_operation_callbacks (ctx, a, ts, false /*failed*/); - if (poif->operation_post != nullptr) - poif->operation_post (ctx, {}, true /* inner */); + if (poif->operation_post != nullptr) + poif->operation_post (ctx, {}, true /* inner */); - if (oif->operation_post != nullptr) - oif->operation_post (ctx, {}, false /* inner */); + if (oif->operation_post != nullptr) + oif->operation_post (ctx, {}, false /* inner */); + } } - } - ctx.current_operation (*oif, nullptr, false /* diag_noise */); + ctx.current_operation (*oif, nullptr, false /* diag_noise */); - if (oif->operation_pre != nullptr) - oif->operation_pre (ctx, {}, true /* inner */, loc); + if (oif->operation_pre != nullptr) + oif->operation_pre (ctx, {}, true /* inner */, loc); - action a (dist_id, oif->id); - mod.postponed.list.clear (); - perform_match ({}, a, ts, - 1 /* diag (failures only) */, - false /* progress */); - process_postponed (); + action a (dist_id, oif->id); + mod.postponed.list.clear (); + perform_match ({}, a, ts, + 1 /* diag (failures only) */, + false /* progress */); + process_postponed (a); + perform_post_operation_callbacks (ctx, a, ts, false /*failed*/); - if (oif->operation_post != nullptr) - oif->operation_post (ctx, {}, true /* inner */); + if (oif->operation_post != nullptr) + oif->operation_post (ctx, {}, true /* inner */); - if (auto po = oif->post_operation) - { - if (operation_id pid = po (ctx, {}, dist_id)) + if (auto po = oif->post_operation) { - const operation_info* poif (ops[pid]); - ctx.current_operation (*poif, oif, false /* diag_noise */); + if (operation_id pid = po (ctx, {}, dist_id)) + { + const operation_info* poif (ops[pid]); + ctx.current_operation (*poif, oif, false /* diag_noise */); - if (oif->operation_pre != nullptr) - oif->operation_pre (ctx, {}, false /* inner */, loc); + if (oif->operation_pre != nullptr) + oif->operation_pre (ctx, {}, false /* inner */, loc); - if (poif->operation_pre != nullptr) - poif->operation_pre (ctx, {}, true /* inner */, loc); + if (poif->operation_pre != nullptr) + poif->operation_pre (ctx, {}, true /* inner */, loc); - action a (dist_id, poif->id, oif->id); - mod.postponed.list.clear (); - perform_match ({}, a, ts, - 1 /* diag (failures only) */, - false /* progress */); - process_postponed (); + action a (dist_id, poif->id, oif->id); + mod.postponed.list.clear (); + perform_match ({}, a, ts, + 1 /* diag (failures only) */, + false /* progress */); + process_postponed (a); + perform_post_operation_callbacks (ctx, a, ts, false /*failed*/); - if (poif->operation_post != nullptr) - poif->operation_post (ctx, {}, true /* inner */); + if (poif->operation_post != nullptr) + poif->operation_post (ctx, {}, true /* inner */); - if (oif->operation_post != nullptr) - oif->operation_post (ctx, {}, false /* inner */); + if (oif->operation_post != nullptr) + oif->operation_post (ctx, {}, false /* inner */); + } } } } diff --git a/libbuild2/dump.cxx b/libbuild2/dump.cxx index 9b7f5b1..9fcfca8 100644 --- a/libbuild2/dump.cxx +++ b/libbuild2/dump.cxx @@ -242,7 +242,8 @@ namespace build2 h_pair = true; } else if (t.is_a<map<optional<string>, string>> () || - t.is_a<vector<pair<optional<string>, string>>> ()) + t.is_a<vector<pair<optional<string>, string>>> () || + t.is_a<vector<pair<optional<string>, bool>>> ()) { h_array = true; h_pair = false; diff --git a/libbuild2/functions-regex.cxx b/libbuild2/functions-regex.cxx index c46f6f5..de34d63 100644 --- a/libbuild2/functions-regex.cxx +++ b/libbuild2/functions-regex.cxx @@ -138,12 +138,24 @@ namespace build2 // string s (to_string (move (v))); + // Match flags. + // + // Note that by default std::regex_search() matches the empty substrings + // in non-empty strings for all the major implementations. We suppress + // such a counter-intuitive behavior with the match_not_null flag (see the + // butl::regex_replace_search() function implementation for details). + // + regex_constants::match_flag_type mf (regex_constants::match_default); + + if (!s.empty ()) + mf |= regex_constants::match_not_null; + if (!match && !subs) - return value (regex_search (s, rge)); // Return boolean value. + return value (regex_search (s, rge, mf)); // Return boolean value. match_results<string::const_iterator> m; - if (regex_search (s, m, rge)) + if (regex_search (s, m, rge, mf)) { assert (!m.empty ()); @@ -483,7 +495,19 @@ namespace build2 for (auto& n: ns) { - if (regex_search (convert<string> (move (n)), rge)) + string s (convert<string> (move (n))); + + // Match flags. + // + // Suppress matching of empty substrings in non-empty strings (see above + // for details). + // + regex_constants::match_flag_type mf (regex_constants::match_default); + + if (!s.empty ()) + mf |= regex_constants::match_not_null; + + if (regex_search (s, rge, mf)) return true; } @@ -516,7 +540,17 @@ namespace build2 bool s (n.simple ()); string v (convert<string> (s ? move (n) : name (n))); - if (regex_search (v, rge) == matching) + // Match flags. + // + // Suppress matching of empty substrings in non-empty strings (see above + // for details). + // + regex_constants::match_flag_type mf (regex_constants::match_default); + + if (!v.empty ()) + mf |= regex_constants::match_not_null; + + if (regex_search (v, rge, mf) == matching) r.emplace_back (s ? name (move (v)) : move (n)); } diff --git a/libbuild2/functions-target.cxx b/libbuild2/functions-target.cxx index d564aa2..c7cb50e 100644 --- a/libbuild2/functions-target.cxx +++ b/libbuild2/functions-target.cxx @@ -23,10 +23,10 @@ namespace build2 // // Return the path of a target (or a list of paths for a list of // targets). The path must be assigned, which normally happens during - // match. As a result, this function is normally called form a recipe. + // match. As a result, this function is normally called from a recipe. // // Note that while this function is technically not pure, we don't mark it - // as such since it can only be called (normally form a recipe) after the + // as such since it can only be called (normally from a recipe) after the // target has been matched, meaning that this target is a prerequisite and // therefore this impurity has been accounted for. // @@ -53,6 +53,10 @@ namespace build2 else fail << "target " << t << " path is not assigned"; } + else if (t.is_a<dir> () || t.is_a<fsdir> ()) + { + r.push_back (t.out_dir ()); + } else fail << "target " << t << " is not path-based"; } diff --git a/libbuild2/install/operation.cxx b/libbuild2/install/operation.cxx index ce5d24a..029a5f6 100644 --- a/libbuild2/install/operation.cxx +++ b/libbuild2/install/operation.cxx @@ -369,7 +369,10 @@ namespace build2 // files, there is not going to be much speedup from doing it in parallel. // There is also now the installation manifest, which relies on us // installing all the filesystem entries of a target serially. - + // + // Additionally, we stop on first error since there is no sense in + // continuing. + // const operation_info op_install { install_id, 0, @@ -379,7 +382,8 @@ namespace build2 "installed", "has nothing to install", // We cannot "be installed". execution_mode::first, - 0 /* concurrency */, // Run serially. + 0 /* concurrency */, // Run serially. + false /* keep_going */, // Stop on first error. &pre_install, nullptr, &install_pre, @@ -406,7 +410,8 @@ namespace build2 "uninstalled", "is not installed", execution_mode::last, - 0 /* concurrency */, // Run serially + 0 /* concurrency */, // Run serially. + false /* keep_going */, // Stop on first error. &pre_uninstall, nullptr, nullptr, @@ -427,6 +432,7 @@ namespace build2 op_update.name_done, op_update.mode, op_update.concurrency, + op_update.keep_going, op_update.pre_operation, op_update.post_operation, op_update.operation_pre, diff --git a/libbuild2/install/rule.hxx b/libbuild2/install/rule.hxx index b023af5..3dbb68d 100644 --- a/libbuild2/install/rule.hxx +++ b/libbuild2/install/rule.hxx @@ -119,7 +119,7 @@ namespace build2 virtual recipe apply (action, target&, match_extra&) const override; - group_rule (bool sto): see_through_only (sto) {} + group_rule (bool sto = false): see_through_only (sto) {} static const group_rule instance; bool see_through_only; diff --git a/libbuild2/module.cxx b/libbuild2/module.cxx index 1aaa38d..36a7ce5 100644 --- a/libbuild2/module.cxx +++ b/libbuild2/module.cxx @@ -96,18 +96,28 @@ namespace build2 nullopt)); /* module_context */ // We use the same context for building any nested modules that might be - // required while building modules. + // required while building modules. Note: this is also used to detect + // module building context. @@ Maybe we should invent special build.mode? // context& mctx (*(ctx.module_context = ctx.module_context_storage->get ())); mctx.module_context = &mctx; + // Copy over any operation callbacks. If a callback implementation does + // not wish to see module context's calls, it can filter them out based on + // the passed context. + // + // Note also that only the callbacks registered before we need to build + // the first module will be in effect. Probably good enough for now. + // + mctx.operation_callbacks = ctx.operation_callbacks; + // Setup the context to perform update. In a sense we have a long-running // perform meta-operation batch (indefinite, in fact, since we never call // the meta-operation's *_post() callbacks) in which we periodically // execute update operations. // // Note that we perform each build in a separate update operation. Failed - // that, if the same target is update twice (which may happen with ad hoc + // that, if the same target is updated twice (which may happen with ad hoc // recipes) we will see the old state. // if (mo_perform.meta_operation_pre != nullptr) @@ -470,7 +480,7 @@ namespace build2 // if (nested) { - // This could be initial or exclusive load. + // This could be initial or interrupting load. // // @@ TODO: see the ad hoc recipe case as a reference. // diff --git a/libbuild2/name.hxx b/libbuild2/name.hxx index f5cb2c5..c6aac45 100644 --- a/libbuild2/name.hxx +++ b/libbuild2/name.hxx @@ -136,7 +136,7 @@ namespace build2 // value to dir. Throw invalid_argument if value would become empty. May // also throw invalid_path. // - void + LIBBUILD2_SYMEXPORT void canonicalize (); }; diff --git a/libbuild2/operation.cxx b/libbuild2/operation.cxx index 6f88e38..76118ea 100644 --- a/libbuild2/operation.cxx +++ b/libbuild2/operation.cxx @@ -271,9 +271,12 @@ namespace build2 struct monitor_data { size_t incr; - string what; - atomic<timestamp::rep> time {timestamp_nonexistent_rep}; + string what1; + string what2; + size_t exec = 0; // Number of targets executed during match. + timestamp time = timestamp_nonexistent; } md; // Note: must outlive monitor_guard. + scheduler::monitor_guard mg; if (prog && show_progress (2 /* max_verb */)) @@ -282,39 +285,66 @@ namespace build2 // the up-to-date check on some projects (e.g., Boost). So we jump // through a few hoops to make sure we don't overindulge. // + // Note also that the higher the increment, the less accurate our + // executed during match number will be. + // md.incr = stderr_term // Scale depending on output type. - ? (ctx.sched->serial () ? 1 : 5) + ? (ctx.sched->serial () ? 1 : 2) : 100; - md.what = " targets to " + diag_do (ctx, a); + md.what1 = " targets to " + diag_do (ctx, a); + md.what2 = ' ' + diag_did (ctx, a) + " during match)"; mg = ctx.sched->monitor ( ctx.target_count, md.incr, - [&md] (size_t c) -> size_t + [&md, &ctx] (size_t p, size_t c) -> size_t { - size_t r (c + md.incr); + if (p > c) + md.exec += p - c; if (stderr_term) { - timestamp o (duration (md.time.load (memory_order_consume))); timestamp n (system_clock::now ()); - if (n - o < chrono::milliseconds (80)) - return r; + if (n - md.time < chrono::milliseconds (80)) + return md.incr; - md.time.store (n.time_since_epoch ().count (), - memory_order_release); + md.time = n; } diag_progress_lock pl; diag_progress = ' '; diag_progress += to_string (c); - diag_progress += md.what; + diag_progress += md.what1; + + if (md.exec != 0) + { + // Offset by the number of targets skipped. + // + size_t s (ctx.skip_count.load (memory_order_relaxed)); + + if (md.exec > s) + { + diag_progress += " ("; + diag_progress += to_string (md.exec - s); + diag_progress += md.what2; + } + } - return r; + return md.incr; }); } + // Call the pre operation callbacks. + // + // See a comment in perform_execute() for why we are doing it here + // (short answer: phase switches). + // + auto cs (ctx.operation_callbacks.equal_range (a)); + for (auto i (cs.first); i != cs.second; ++i) + if (const auto& f = i->second.pre) + f (ctx, a, ts); + // Start asynchronous matching of prerequisites keeping track of how // many we have started. Wait with unlocked phase to allow phase // switching. @@ -417,7 +447,11 @@ namespace build2 diag_progress.clear (); } - // We are now running serially. Re-examine targets that we have matched. + // We are now running serially. + // + + // Re-examine targets that we have matched and determine whether we have + // failed. // for (size_t j (0); j != n; ++j) { @@ -443,11 +477,8 @@ namespace build2 case target_state::postponed: { // We bailed before matching it (leave state in action_target as - // unknown). + // unknown for the structured result printing). // - if (verb != 0 && diag >= 1) - info << "not " << diag_did (a, t); - break; } case target_state::unknown: @@ -460,9 +491,6 @@ namespace build2 { // Things didn't go well for this target. // - if (verb != 0 && diag >= 1) - info << "failed to " << diag_do (a, t); - at.state = s; fail = true; break; @@ -472,6 +500,36 @@ namespace build2 } } + // Call the post operation callbacks if perform_execute() won't be + // called. + // + if (fail) + perform_post_operation_callbacks (ctx, a, ts, fail); + + // Re-examine targets that we have matched and print diagnostics. + // + if (verb != 0 && diag >= 1) + { + for (size_t j (0); j != n; ++j) + { + action_target& at (ts[j]); + const target& t (at.as<target> ()); + + if (at.state == target_state::failed) + { + // Things didn't go well for this target. + // + info << "failed to " << diag_do (a, t); + } + else if (j >= i || t.matched_state (a) == target_state::postponed) + { + // We bailed before matching it. + // + info << "not " << diag_did (a, t); + } + } + } + if (fail) throw failed (); @@ -602,40 +660,55 @@ namespace build2 switch (ctx.current_inner_oif->concurrency) { case 0: sched_tune = tune_guard (*ctx.sched, 1); break; // Run serially. - case 1: break; // Run as is. - default: assert (false); // Not supported. + case 1: break; // Run as is. + default: assert (false); // Not supported. } + // Override the keep_going flag if requested by the operation. + // + auto kgg = make_guard ([&ctx, o = ctx.keep_going] () + { + ctx.keep_going = o; + }); + if (!ctx.current_inner_oif->keep_going) + ctx.keep_going = false; + // Set the dry-run flag. // ctx.dry_run = ctx.dry_run_option; // Setup progress reporting if requested. // - string what; // Note: must outlive monitor_guard. + struct monitor_data + { + size_t init; + size_t incr; + string what; + } md; // Note: must outlive monitor_guard. + scheduler::monitor_guard mg; if (prog && show_progress (1 /* max_verb */)) { - size_t init (ctx.target_count.load (memory_order_relaxed)); - size_t incr (init > 100 ? init / 100 : 1); // 1%. + md.init = ctx.target_count.load (memory_order_relaxed); + md.incr = md.init > 100 ? md.init / 100 : 1; // 1%. - if (init != incr) + if (md.init != md.incr) { - what = "% of targets " + diag_did (ctx, a); + md.what = "% of targets " + diag_did (ctx, a); mg = ctx.sched->monitor ( ctx.target_count, - init - incr, - [init, incr, &what, &ctx] (size_t c) -> size_t + md.incr, + [&md, &ctx] (size_t, size_t c) -> size_t { - size_t p ((init - c) * 100 / init); + size_t p ((md.init - c) * 100 / md.init); size_t s (ctx.skip_count.load (memory_order_relaxed)); diag_progress_lock pl; diag_progress = ' '; diag_progress += to_string (p); - diag_progress += what; + diag_progress += md.what; if (s != 0) { @@ -644,11 +717,18 @@ namespace build2 diag_progress += " skipped)"; } - return c - incr; + return md.incr; }); } } + // Note that while this would seem like the natural place to call the + // pre operation callbacks, it is actually too late since during match + // we may switch to the execute phase and execute some recipes (think + // building a tool to generate some code). So we have to do this in + // perform_match() and then carefully make sure the post callbacks are + // called for all the exit paths (match failed, match_only, etc). + // In the 'last' execution mode run post hoc first. // if (ctx.current_mode == execution_mode::last) @@ -697,9 +777,44 @@ namespace build2 // We are now running serially. // - // Clear the dry-run flag. + // Re-examine all the targets and determine whether we have failed. // - ctx.dry_run = false; + for (action_target& at: ts) + { + const target& t (at.as<target> ()); + + // Similar to match we cannot attribute post hoc failures to specific + // targets so it seems the best we can do is just fail them all. + // + if (!posthoc_fail) + { + // Note that here we call executed_state() directly instead of + // execute_complete() since we know there is no need to wait. + // + at.state = t.executed_state (a, false /* fail */); + } + else + at.state = /*t.state[a].state =*/ target_state::failed; + + switch (at.state) + { + case target_state::unknown: + case target_state::unchanged: + case target_state::changed: + break; + case target_state::failed: + { + fail = true; + break; + } + default: + assert (false); + } + } + + // Call the post operation callbacks. + // + perform_post_operation_callbacks (ctx, a, ts, fail); // Clear the progress if present. // @@ -709,7 +824,11 @@ namespace build2 diag_progress.clear (); } - // Restore original scheduler settings. + // Clear the dry-run flag. + // + ctx.dry_run = false; + + // Restore original scheduler and keep_going settings. } // Print skip count if not zero. Note that we print it regardless of the @@ -733,19 +852,6 @@ namespace build2 { const target& t (at.as<target> ()); - // Similar to match we cannot attribute post hoc failures to specific - // targets so it seems the best we can do is just fail them all. - // - if (!posthoc_fail) - { - // Note that here we call executed_state() directly instead of - // execute_complete() since we know there is no need to wait. - // - at.state = t.executed_state (a, false /* fail */); - } - else - at.state = /*t.state[a].state =*/ target_state::failed; - switch (at.state) { case target_state::unknown: @@ -780,7 +886,6 @@ namespace build2 if (verb != 0 && diag >= 1) info << "failed to " << diag_do (a, t); - fail = true; break; } default: @@ -978,6 +1083,19 @@ namespace build2 #endif } + void + perform_post_operation_callbacks (context& ctx, + action a, + const action_targets& ts, + bool failed) + { + auto cs (ctx.operation_callbacks.equal_range (a)); + + for (auto i (cs.first); i != cs.second; ++i) + if (const auto& f = i->second.post) + f (ctx, a, ts, failed); + } + const meta_operation_info mo_perform { perform_id, "perform", @@ -1391,7 +1509,8 @@ namespace build2 "", "", execution_mode::first, - 1 /* concurrency */, + 1 /* concurrency */, + true /* keep_going */, nullptr, nullptr, nullptr, @@ -1419,7 +1538,8 @@ namespace build2 "updated", "is up to date", execution_mode::first, - 1 /* concurrency */, + 1 /* concurrency */, + true /* keep_going */, nullptr, nullptr, nullptr, @@ -1437,7 +1557,8 @@ namespace build2 "cleaned", "is clean", execution_mode::last, - 1 /* concurrency */, + 1 /* concurrency */, + true /* keep_going */, nullptr, nullptr, nullptr, diff --git a/libbuild2/operation.hxx b/libbuild2/operation.hxx index e8ff38a..4ca1305 100644 --- a/libbuild2/operation.hxx +++ b/libbuild2/operation.hxx @@ -177,9 +177,19 @@ namespace build2 // diagnostics (unless quiet). // LIBBUILD2_SYMEXPORT void - perform_execute (const values&, action, const action_targets&, + perform_execute (const values&, action, action_targets&, uint16_t diag, bool prog); + // Call the context-wide post operation callbacks. Should be called after + // perfrom_match() if perform_execute() will not be called. Note that + // perform_match() handles its own failures but not the match_only case. + // + LIBBUILD2_SYMEXPORT void + perform_post_operation_callbacks (context&, + action, + const action_targets&, + bool failed); + LIBBUILD2_SYMEXPORT extern const meta_operation_info mo_noop; LIBBUILD2_SYMEXPORT extern const meta_operation_info mo_perform; LIBBUILD2_SYMEXPORT extern const meta_operation_info mo_info; @@ -225,6 +235,12 @@ namespace build2 // const size_t concurrency; + // Whether to keep going in case of errors for this operation. If the + // value is false, then the context's keep_going flag is overridden for + // the duration of the operation. + // + const bool keep_going; + // The values argument in the callbacks is the operation parameters. If // the operation expects parameters, then it should have a non-NULL // operation_pre() callback. Failed that, any parameters will be diagnosed diff --git a/libbuild2/prerequisite.cxx b/libbuild2/prerequisite.cxx index bb77c9e..ec18665 100644 --- a/libbuild2/prerequisite.cxx +++ b/libbuild2/prerequisite.cxx @@ -91,4 +91,8 @@ namespace build2 return r; } + + // prerequisites + // + const prerequisites empty_prerequisites; } diff --git a/libbuild2/prerequisite.hxx b/libbuild2/prerequisite.hxx index 9b9cccf..008fc11 100644 --- a/libbuild2/prerequisite.hxx +++ b/libbuild2/prerequisite.hxx @@ -190,6 +190,8 @@ namespace build2 } using prerequisites = vector<prerequisite>; + + LIBBUILD2_SYMEXPORT extern const prerequisites empty_prerequisites; } #endif // LIBBUILD2_PREREQUISITE_HXX diff --git a/libbuild2/rule.cxx b/libbuild2/rule.cxx index dc1c96c..b504dc7 100644 --- a/libbuild2/rule.cxx +++ b/libbuild2/rule.cxx @@ -430,9 +430,9 @@ namespace build2 // noop_rule // bool noop_rule:: - match (action, target&) const + match (action, target& t) const { - return true; + return !exclude_group_ || !t.is_a<group> (); } recipe noop_rule:: diff --git a/libbuild2/rule.hxx b/libbuild2/rule.hxx index eceb6ad..71782c0 100644 --- a/libbuild2/rule.hxx +++ b/libbuild2/rule.hxx @@ -209,8 +209,17 @@ namespace build2 virtual recipe apply (action, target&) const override; - noop_rule () {} - static const noop_rule instance; + // If exclude_group is true then exclude the group-based targets (since + // their membership can only be accurately determined by the ad hoc + // recipe). + // + explicit + noop_rule (bool exclude_group = false): exclude_group_ (exclude_group) {} + + static const noop_rule instance; // Note: does not exclude group. + + private: + bool exclude_group_; }; // Ad hoc rule. diff --git a/libbuild2/scheduler.cxx b/libbuild2/scheduler.cxx index 69673e6..ebb38e4 100644 --- a/libbuild2/scheduler.cxx +++ b/libbuild2/scheduler.cxx @@ -701,7 +701,7 @@ namespace build2 } scheduler::monitor_guard scheduler:: - monitor (atomic_count& c, size_t t, function<size_t (size_t)> f) + monitor (atomic_count& c, size_t t, function<size_t (size_t, size_t)> f) { assert (monitor_count_ == nullptr && t != 0); @@ -713,7 +713,7 @@ namespace build2 monitor_count_ = &c; monitor_tshold_.store (t, memory_order_relaxed); - monitor_init_ = c.load (memory_order_relaxed); + monitor_prev_ = c.load (memory_order_relaxed); monitor_func_ = move (f); return monitor_guard (this); diff --git a/libbuild2/scheduler.hxx b/libbuild2/scheduler.hxx index 3cc206e..2d4d189 100644 --- a/libbuild2/scheduler.hxx +++ b/libbuild2/scheduler.hxx @@ -452,9 +452,11 @@ namespace build2 // should be set before any tasks are queued and cleared after all of // them have completed. // - // The counter must go in one direction, either increasing or decreasing, - // and should contain the initial value during the call. Zero threshold - // value is reserved. + // The counter can go in either direction and should contain the initial + // value during the call. The callback function is called when the current + // value differs form the previous/initial by at least the specified + // threshold. The callback returns the new threshold. Zero threshold value + // is reserved. The callback calls are serialized and synchronized. // struct monitor_guard { @@ -488,7 +490,9 @@ namespace build2 }; monitor_guard - monitor (atomic_count&, size_t threshold, function<size_t (size_t)>); + monitor (atomic_count&, + size_t threshold, + function<size_t (size_t previous, size_t current)>); // If initially active thread(s) (besides the one that calls startup()) // exist before the call to startup(), then they must call join() before @@ -598,10 +602,10 @@ namespace build2 // Monitor. // - atomic_count* monitor_count_ = nullptr; // NULL if not used. - atomic_count monitor_tshold_; // 0 means locked. - size_t monitor_init_; // Initial count. - function<size_t (size_t)> monitor_func_; + atomic_count* monitor_count_ = nullptr; // NULL if not used. + atomic_count monitor_tshold_; // 0 means locked. + size_t monitor_prev_; // Previous values. + function<size_t (size_t, size_t)> monitor_func_; build2::mutex mutex_; bool shutdown_ = true; // Shutdown flag. @@ -914,19 +918,19 @@ namespace build2 if (monitor_tshold_.compare_exchange_strong ( t, 0, - memory_order_release, + memory_order_acq_rel, // Synchronize on success. memory_order_relaxed)) { - // Now we are the only ones messing with this. + // Now we are the only ones messing with this and everything + // is synchronized. // + size_t p (monitor_prev_); size_t v (monitor_count_->load (memory_order_relaxed)); - if (v != monitor_init_) + if ((p > v ? p - v : p < v ? v - p : 0) >= t) { - // See which direction we are going. - // - if (v > monitor_init_ ? (v >= t) : (v <= t)) - t = monitor_func_ (v); + t = monitor_func_ (p, v); + monitor_prev_= v; } monitor_tshold_.store (t, memory_order_release); diff --git a/libbuild2/scheduler.txx b/libbuild2/scheduler.txx index 87c9384..15fefcc 100644 --- a/libbuild2/scheduler.txx +++ b/libbuild2/scheduler.txx @@ -29,12 +29,13 @@ namespace build2 // if (monitor_count_ != nullptr) { + size_t t (monitor_tshold_.load (memory_order_relaxed)); + size_t p (monitor_prev_); size_t v (monitor_count_->load (memory_order_relaxed)); - if (v != monitor_init_) + if ((p > v ? p - v : p < v ? v - p : 0) >= t) { - size_t t (monitor_tshold_.load (memory_order_relaxed)); - if (v > monitor_init_ ? (v >= t) : (v <= t)) - monitor_tshold_.store (monitor_func_ (v), memory_order_relaxed); + monitor_tshold_.store (monitor_func_ (p, v), memory_order_relaxed); + monitor_prev_ = v; } } diff --git a/libbuild2/scope.hxx b/libbuild2/scope.hxx index ece78b7..d792ed2 100644 --- a/libbuild2/scope.hxx +++ b/libbuild2/scope.hxx @@ -459,14 +459,20 @@ namespace build2 // when an action is executed on the dir{} target that corresponds to this // scope. The pre callback is called just before the recipe and the post // -- immediately after. The callbacks are only called if the recipe - // (including noop recipe) is executed for the corresponding target. The - // callbacks should only be registered during the load phase. + // (including noop recipe) is executed for the corresponding target. + // + // The callback should only be registered during the load phase. Note that + // it's registered for the inner action, meaning that it will be called + // for any outer action (which is discernible from the first argument of + // the callback). // // It only makes sense for callbacks to return target_state changed or // unchanged and to throw failed in case of an error. These pre/post // states will be merged with the recipe state and become the target // state. See execute_recipe() for details. // + // See also context::operation_callback. + // public: struct operation_callback { diff --git a/libbuild2/target-state.hxx b/libbuild2/target-state.hxx index a6106f7..df54876 100644 --- a/libbuild2/target-state.hxx +++ b/libbuild2/target-state.hxx @@ -25,7 +25,8 @@ namespace build2 // enum class target_state: uint8_t { - unknown = 1, + uninitialized = 0, + unknown, unchanged, postponed, busy, diff --git a/libbuild2/target.cxx b/libbuild2/target.cxx index 65e18d3..b915b25 100644 --- a/libbuild2/target.cxx +++ b/libbuild2/target.cxx @@ -76,8 +76,6 @@ namespace build2 // target // - const target::prerequisites_type target::empty_prerequisites_; - target:: ~target () { diff --git a/libbuild2/target.hxx b/libbuild2/target.hxx index b008347..1b7b755 100644 --- a/libbuild2/target.hxx +++ b/libbuild2/target.hxx @@ -733,8 +733,6 @@ namespace build2 atomic<uint8_t> prerequisites_state_ {0}; prerequisites_type prerequisites_; - static const prerequisites_type empty_prerequisites_; - // Target-specific variables. // // See also rule-specific variables below. @@ -953,10 +951,13 @@ namespace build2 mutable bool recipe_keep; // Keep after execution. bool recipe_group_action; // Recipe is group_action. - // Target state for this operation. Note that it is undetermined until - // a rule is matched and recipe applied (see set_recipe()). + // Target state for this operation. + // + // Note that it is undetermined until a rule is matched and recipe + // applied (see set_recipe()). However, we need it to be not postponed + // for ad hoc members that are not matched (see group_state()). // - target_state state; + target_state state = target_state::uninitialized; // Set to true (only for the inner action) if this target has been // matched but not executed as a result of the resolve_members() call. @@ -1485,9 +1486,9 @@ namespace build2 { public: explicit - group_prerequisites (const target& t); + group_prerequisites (const target&); - group_prerequisites (const target& t, const target* g); + group_prerequisites (const target&, const target* group); using prerequisites_type = target::prerequisites_type; using base_iterator = prerequisites_type::const_iterator; @@ -1501,8 +1502,8 @@ namespace build2 using iterator_category = std::bidirectional_iterator_tag; iterator () {} - iterator (const target* t, - const target* g, + iterator (const prerequisites_type* t, + const prerequisites_type* g, const prerequisites_type* c, base_iterator i): t_ (t), g_ (g), c_ (c), i_ (i) {} @@ -1531,8 +1532,8 @@ namespace build2 operator!= (const iterator& x, const iterator& y) {return !(x == y);} private: - const target* t_ = nullptr; - const target* g_ = nullptr; + const prerequisites_type* t_ = nullptr; + const prerequisites_type* g_ = nullptr; const prerequisites_type* c_ = nullptr; base_iterator i_; }; @@ -1555,8 +1556,8 @@ namespace build2 size () const; private: - const target& t_; - const target* g_; + const prerequisites_type* t_; // NULL if empty. + const prerequisites_type* g_; // NULL if no group or empty. }; // A member of a prerequisite. If 'member' is NULL, then this is the diff --git a/libbuild2/target.ixx b/libbuild2/target.ixx index 39b81e7..1dc667d 100644 --- a/libbuild2/target.ixx +++ b/libbuild2/target.ixx @@ -207,7 +207,7 @@ namespace build2 { return prerequisites_state_.load (memory_order_acquire) == 2 ? prerequisites_ - : empty_prerequisites_; + : empty_prerequisites; } inline bool target:: @@ -286,6 +286,7 @@ namespace build2 // @@ Hm, I wonder why not just return s.recipe_group_action now that we // cache it. // + const opstate& s (state[a]); // This special hack allows us to do things like query an ad hoc member's // state or mtime without matching/executing the member, only the group. @@ -294,12 +295,15 @@ namespace build2 // execute phase). // // Note: this test must come first since the member may not be matched and - // thus its state uninitialized. + // thus its state set (but it won't be postponed; see opstate::state). // if (ctx.phase == run_phase::execute && adhoc_group_member ()) - return true; - - const opstate& s (state[a]); + { + // Note: if the member state is postponed, then the group state may not + // be yet known (see group_action() for details). + // + return s.state != target_state::postponed; + } if (s.state == target_state::group) return true; @@ -342,7 +346,14 @@ namespace build2 inline target_state target:: executed_state_impl (action a) const { - return (group_state (a) ? group->state : state)[a].state; + target_state ts ((group_state (a) ? group->state : state)[a].state); + + // Translate postponed to unchanged, similar to execute_recipe(). + // + if (ts == target_state::postponed) + ts = target_state::unchanged; + + return ts; } inline target_state target:: @@ -480,42 +491,64 @@ namespace build2 // inline group_prerequisites:: group_prerequisites (const target& t) - : t_ (t), - g_ (t_.group == nullptr || - t_.group->adhoc_member != nullptr || // Ad hoc group member. - t_.group->prerequisites ().empty () - ? nullptr : t_.group) + : t_ (nullptr), g_ (nullptr) { + // Take "snapshot" of prerequisites, both for target and group. + // + const auto& ps (t.prerequisites ()); + if (!ps.empty ()) + t_ = &ps; + + if (const target* g = t.group) + { + if (g->adhoc_member == nullptr) // Not ad hoc group member. + { + const auto& ps (g->prerequisites ()); + if (!ps.empty ()) + g_ = &ps; + } + } } inline group_prerequisites:: group_prerequisites (const target& t, const target* g) - : t_ (t), - g_ (g == nullptr || - g->prerequisites ().empty () - ? nullptr : g) + : t_ (nullptr), g_ (nullptr) { + const auto& ps (t.prerequisites ()); + if (!ps.empty ()) + t_ = &ps; + + if (g != nullptr) + { + const auto& ps (g->prerequisites ()); + if (!ps.empty ()) + g_ = &ps; + } } inline auto group_prerequisites:: begin () const -> iterator { - auto& c ((g_ != nullptr ? *g_ : t_).prerequisites ()); - return iterator (&t_, g_, &c, c.begin ()); + auto* c (g_ != nullptr ? g_ : + t_ != nullptr ? t_ : + &empty_prerequisites); + return iterator (t_, g_, c, c->begin ()); } inline auto group_prerequisites:: end () const -> iterator { - auto& c (t_.prerequisites ()); - return iterator (&t_, g_, &c, c.end ()); + auto* c (t_ != nullptr ? t_ : + g_ != nullptr ? g_ : + &empty_prerequisites); + return iterator (t_, g_, c, c->end ()); } inline size_t group_prerequisites:: size () const { - return t_.prerequisites ().size () + - (g_ != nullptr ? g_->prerequisites ().size () : 0); + return ((t_ != nullptr ? t_->size () : 0) + + (g_ != nullptr ? g_->size () : 0)); } // group_prerequisites::iterator @@ -523,9 +556,9 @@ namespace build2 inline auto group_prerequisites::iterator:: operator++ () -> iterator& { - if (++i_ == c_->end () && c_ != &t_->prerequisites ()) + if (++i_ == c_->end () && c_ == g_ && t_ != nullptr) { - c_ = &t_->prerequisites (); + c_ = t_; i_ = c_->begin (); } return *this; @@ -535,9 +568,9 @@ namespace build2 inline auto group_prerequisites::iterator:: operator-- () -> iterator& { - if (i_ == c_->begin () && c_ == &t_->prerequisites ()) + if (i_ == c_->begin () && c_ == t_) { - c_ = &g_->prerequisites (); + c_ = g_; i_ = c_->end (); } diff --git a/libbuild2/test/operation.cxx b/libbuild2/test/operation.cxx index 2535adb..640d2fe 100644 --- a/libbuild2/test/operation.cxx +++ b/libbuild2/test/operation.cxx @@ -63,7 +63,8 @@ namespace build2 "tested", "has nothing to test", // We cannot "be tested". execution_mode::first, - 1 /* concurrency */, + 1 /* concurrency */, + true /* keep_going */, &pre_test, nullptr, nullptr, @@ -84,6 +85,7 @@ namespace build2 op_update.name_done, op_update.mode, op_update.concurrency, + op_update.keep_going, op_update.pre_operation, op_update.post_operation, op_update.operation_pre, diff --git a/libbuild2/variable.cxx b/libbuild2/variable.cxx index 0ec23d3..0abc360 100644 --- a/libbuild2/variable.cxx +++ b/libbuild2/variable.cxx @@ -3320,10 +3320,13 @@ namespace build2 value_traits<vector<pair<string, optional<string>>>>; template struct LIBBUILD2_DEFEXPORT + value_traits<vector<pair<string, optional<bool>>>>; + + template struct LIBBUILD2_DEFEXPORT value_traits<vector<pair<optional<string>, string>>>; template struct LIBBUILD2_DEFEXPORT - value_traits<vector<pair<string, optional<bool>>>>; + value_traits<vector<pair<optional<string>, bool>>>; template struct LIBBUILD2_DEFEXPORT value_traits<set<string>>; template struct LIBBUILD2_DEFEXPORT value_traits<set<json_value>>; diff --git a/libbuild2/variable.hxx b/libbuild2/variable.hxx index a14c52b..e55a121 100644 --- a/libbuild2/variable.hxx +++ b/libbuild2/variable.hxx @@ -1380,6 +1380,9 @@ namespace build2 extern template struct LIBBUILD2_DECEXPORT value_traits<vector<pair<string, optional<bool>>>>; + extern template struct LIBBUILD2_DECEXPORT + value_traits<vector<pair<optional<string>, bool>>>; + extern template struct LIBBUILD2_DECEXPORT value_traits<set<string>>; extern template struct LIBBUILD2_DECEXPORT value_traits<set<json_value>>; diff --git a/tests/function/regex/testscript b/tests/function/regex/testscript index 538bdab..7fbcc8e 100644 --- a/tests/function/regex/testscript +++ b/tests/function/regex/testscript @@ -366,6 +366,33 @@ EOI } } + + : empty-substring + : + : Note that regex_search() ignores the match_not_null flag for older + : versions of libstdc++ and libc++. + : + if (($cxx.id != 'gcc' || $cxx.version.major >= 7) && \ + ($cxx.id != 'clang' || $cxx.version.major >= 6)) + { + : empty + : + $* <<EOI >'true' + print $regex.search('', '.*') + EOI + + : match + : + $* <<EOI >'true' + print $regex.search('a', 'a*') + EOI + + : no-match + : + $* <<EOI >'false' + print $regex.search('aa', 'b*') + EOI + } } : split @@ -576,6 +603,33 @@ print $regex.find_search(Foo.cxx, 'f', icase) EOI } + + : empty-substring + : + : Note that regex_search() ignores the match_not_null flag for older + : versions of libstdc++ and libc++. + : + if (($cxx.id != 'gcc' || $cxx.version.major >= 7) && \ + ($cxx.id != 'clang' || $cxx.version.major >= 6)) + { + : empty + : + $* <<EOI >'true' + print $regex.find_search('', '.*') + EOI + + : match + : + $* <<EOI >'true' + print $regex.find_search('a', 'a*') + EOI + + : no-match + : + $* <<EOI >'false' + print $regex.find_search('aa', 'b*') + EOI + } } : filter-search @@ -607,6 +661,33 @@ $* <<EOI >'' print $regex.filter_search(-g, '-O') EOI + + : empty-substring + : + : Note that regex_search() ignores the match_not_null flag for older + : versions of libstdc++ and libc++. + : + if (($cxx.id != 'gcc' || $cxx.version.major >= 7) && \ + ($cxx.id != 'clang' || $cxx.version.major >= 6)) + { + : empty + : + $* <<EOI >'{}' + print $regex.filter_search('', '.*') + EOI + + : match + : + $* <<EOI >'a' + print $regex.filter_search('a', 'a*') + EOI + + : no-match + : + $* <<EOI >'' + print $regex.filter_search('aa', 'b*') + EOI + } } : filter-out |