aboutsummaryrefslogtreecommitdiff
path: root/libbuild2
diff options
context:
space:
mode:
Diffstat (limited to 'libbuild2')
-rw-r--r--libbuild2/buildfile2
-rw-r--r--libbuild2/cli/buildfile71
-rw-r--r--libbuild2/cli/export.hxx37
-rw-r--r--libbuild2/cli/init.cxx287
-rw-r--r--libbuild2/cli/init.hxx31
-rw-r--r--libbuild2/cli/module.hxx30
-rw-r--r--libbuild2/cli/rule.cxx340
-rw-r--r--libbuild2/cli/rule.hxx46
-rw-r--r--libbuild2/cli/target.cxx75
-rw-r--r--libbuild2/cli/target.hxx61
-rw-r--r--libbuild2/module.cxx1
11 files changed, 980 insertions, 1 deletions
diff --git a/libbuild2/buildfile b/libbuild2/buildfile
index 6707b05..b3a9c52 100644
--- a/libbuild2/buildfile
+++ b/libbuild2/buildfile
@@ -4,7 +4,7 @@
# NOTE: remember to update bundled_modules in libbuild2/module.cxx if adding a
# new module.
#
-bundled_modules = bash/ bin/ c/ cc/ cxx/ in/ version/
+bundled_modules = bash/ bin/ c/ cc/ cli/ cxx/ in/ version/
./: lib{build2} $bundled_modules
diff --git a/libbuild2/cli/buildfile b/libbuild2/cli/buildfile
new file mode 100644
index 0000000..9b6e4eb
--- /dev/null
+++ b/libbuild2/cli/buildfile
@@ -0,0 +1,71 @@
+# file : libbuild2/cli/buildfile
+# license : MIT; see accompanying LICENSE file
+
+# NOTE: shared imports should go into root.build.
+#
+include ../
+impl_libs = ../lib{build2} # Implied interface dependency.
+
+include ../cxx/
+intf_libs = ../cxx/lib{build2-cxx}
+
+./: lib{build2-cli}: libul{build2-cli}: {hxx ixx txx cxx}{** -**.test...} \
+ $intf_libs $impl_libs
+
+# Unit tests.
+#
+exe{*.test}:
+{
+ test = true
+ install = false
+}
+
+for t: cxx{**.test...}
+{
+ d = $directory($t)
+ n = $name($t)...
+
+ ./: $d/exe{$n}: $t $d/{hxx ixx txx}{+$n} $d/testscript{+$n}
+ $d/exe{$n}: libul{build2-cli}: bin.whole = false
+}
+
+# Build options.
+#
+obja{*}: cxx.poptions += -DLIBBUILD2_CLI_STATIC_BUILD
+objs{*}: cxx.poptions += -DLIBBUILD2_CLI_SHARED_BUILD
+
+# Export options.
+#
+lib{build2-cli}:
+{
+ cxx.export.poptions = "-I$out_root" "-I$src_root"
+ cxx.export.libs = $intf_libs
+}
+
+liba{build2-cli}: cxx.export.poptions += -DLIBBUILD2_CLI_STATIC
+libs{build2-cli}: cxx.export.poptions += -DLIBBUILD2_CLI_SHARED
+
+# For pre-releases use the complete version to make sure they cannot be used
+# in place of another pre-release or the final version. See the version module
+# for details on the version.* variable values.
+#
+# And because this is a build system module, we also embed the same value as
+# the interface version (note that we cannot use build.version.interface for
+# bundled modules because we could be built with a different version of the
+# build system).
+#
+ver = ($version.pre_release \
+ ? "$version.project_id" \
+ : "$version.major.$version.minor")
+
+lib{build2-cli}: bin.lib.version = @"-$ver"
+libs{build2-cli}: bin.lib.load_suffix = "-$ver"
+
+# Install into the libbuild2/cli/ subdirectory of, say, /usr/include/
+# recreating subdirectories.
+#
+{hxx ixx txx}{*}:
+{
+ install = include/libbuild2/cli/
+ install.subdirs = true
+}
diff --git a/libbuild2/cli/export.hxx b/libbuild2/cli/export.hxx
new file mode 100644
index 0000000..67c1eb9
--- /dev/null
+++ b/libbuild2/cli/export.hxx
@@ -0,0 +1,37 @@
+// file : libbuild2/cli/export.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#pragma once
+
+// Normally we don't export class templates (but do complete specializations),
+// inline functions, and classes with only inline member functions. Exporting
+// classes that inherit from non-exported/imported bases (e.g., std::string)
+// will end up badly. The only known workarounds are to not inherit or to not
+// export. Also, MinGW GCC doesn't like seeing non-exported functions being
+// used before their inline definition. The workaround is to reorder code. In
+// the end it's all trial and error.
+
+#if defined(LIBBUILD2_CLI_STATIC) // Using static.
+# define LIBBUILD2_CLI_SYMEXPORT
+#elif defined(LIBBUILD2_CLI_STATIC_BUILD) // Building static.
+# define LIBBUILD2_CLI_SYMEXPORT
+#elif defined(LIBBUILD2_CLI_SHARED) // Using shared.
+# ifdef _WIN32
+# define LIBBUILD2_CLI_SYMEXPORT __declspec(dllimport)
+# else
+# define LIBBUILD2_CLI_SYMEXPORT
+# endif
+#elif defined(LIBBUILD2_CLI_SHARED_BUILD) // Building shared.
+# ifdef _WIN32
+# define LIBBUILD2_CLI_SYMEXPORT __declspec(dllexport)
+# else
+# define LIBBUILD2_CLI_SYMEXPORT
+# endif
+#else
+// If none of the above macros are defined, then we assume we are being used
+// by some third-party build system that cannot/doesn't signal the library
+// type. Note that this fallback works for both static and shared but in case
+// of shared will be sub-optimal compared to having dllimport.
+//
+# define LIBBUILD2_CLI_SYMEXPORT // Using static or shared.
+#endif
diff --git a/libbuild2/cli/init.cxx b/libbuild2/cli/init.cxx
new file mode 100644
index 0000000..581fdaf
--- /dev/null
+++ b/libbuild2/cli/init.cxx
@@ -0,0 +1,287 @@
+// file : libbuild2/cli/init.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <libbuild2/cli/init.hxx>
+
+#include <libbuild2/file.hxx>
+#include <libbuild2/scope.hxx>
+#include <libbuild2/target.hxx>
+#include <libbuild2/variable.hxx>
+#include <libbuild2/diagnostics.hxx>
+
+#include <libbuild2/config/utility.hxx>
+
+#include <libbuild2/cxx/target.hxx>
+
+#include <libbuild2/cli/rule.hxx>
+#include <libbuild2/cli/module.hxx>
+#include <libbuild2/cli/target.hxx>
+
+namespace build2
+{
+ namespace cli
+ {
+ // Remaining issues/semantics change:
+ //
+ // @@ Unconfigured caching.
+ //
+ // @@ Default-found cli used to result in config.cli=cli and now it's just
+ // omitted (and default-not-found -- in config.cli.configured=false).
+ //
+ // - Writing any default will take precedence over config.import.cli.
+ // In fact, this duality is a bigger problem: if we have a config
+ // that uses config.cli there is no way to reconfigure it to use
+ // config.import.cli.
+ //
+ // - We could have saved it commented.
+ //
+ // - We could do this at the module level only since we also have
+ // config.cli.options?
+ //
+ // - Note that in the CLI compiler itself we now rely on default cli
+ // being NULL/undefined. So if faving, should probably be commented
+ // out. BUT: it will still be defined, so will need to be defined
+ // NULL. Note also that long term the CLI compiler will not use the
+ // module relying on an ad hoc recipe instead.
+ //
+ // ! Maybe reserving NULL (instead of making it the same as NULL) for
+ // this "configured to default" state and saving commented is not a
+ // bad idea. Feels right to have some marker in config.build that
+ // things are in effect. And I believe if config.import.cli is
+ // specified, it will just be dropped.
+
+ bool
+ guess_init (scope& rs,
+ scope& bs,
+ const location& loc,
+ bool,
+ bool opt,
+ module_init_extra& extra)
+ {
+ tracer trace ("cli::guess_init");
+ l5 ([&]{trace << "for " << rs;});
+
+ // We only support root loading (which means there can only be one).
+ //
+ if (rs != bs)
+ fail (loc) << "cli.guess module must be loaded in project root";
+
+ // Adjust module config.build save priority (code generator).
+ //
+ config::save_module (rs, "cli", 150);
+
+ // Enter metadata variables.
+ //
+ // They are all qualified so go straight for the public variable pool.
+ //
+ auto& vp (rs.var_pool (true /* public */));
+
+ auto& v_ver (vp.insert<string> ("cli.version"));
+ auto& v_sum (vp.insert<string> ("cli.checksum"));
+
+ // Import the CLI compiler target.
+ //
+ // Note that the special config.cli=false value (recognized by the
+ // import machinery) is treated as an explicit request to leave the
+ // module unconfigured.
+ //
+ bool new_cfg (false);
+ import_result<exe> ir (
+ import_direct<exe> (
+ new_cfg,
+ rs,
+ name ("cli", dir_path (), "exe", "cli"), // cli%exe{cli}
+ true /* phase2 */,
+ opt,
+ true /* metadata */,
+ loc,
+ "module load"));
+
+ const exe* tgt (ir.target);
+
+ // Extract metadata.
+ //
+ auto* ver (tgt != nullptr ? &cast<string> (tgt->vars[v_ver]) : nullptr);
+ auto* sum (tgt != nullptr ? &cast<string> (tgt->vars[v_sum]) : nullptr);
+
+ // Print the report.
+ //
+ // If this is a configuration with new values, then print the report
+ // at verbosity level 2 and up (-v).
+ //
+ if (verb >= (new_cfg ? 2 : 3))
+ {
+ diag_record dr (text);
+ dr << "cli " << project (rs) << '@' << rs << '\n';
+
+ if (tgt != nullptr)
+ dr << " cli " << ir << '\n'
+ << " version " << *ver << '\n'
+ << " checksum " << *sum;
+ else
+ dr << " cli " << "not found, leaving unconfigured";
+ }
+
+ if (tgt == nullptr)
+ return false;
+
+ // The cli variable (untyped) is an imported compiler target name.
+ //
+ rs.assign ("cli") = move (ir.name);
+ rs.assign (v_sum) = *sum;
+ rs.assign (v_ver) = *ver;
+
+ {
+ standard_version v (*ver);
+
+ rs.assign<uint64_t> ("cli.version.number") = v.version;
+ rs.assign<uint64_t> ("cli.version.major") = v.major ();
+ rs.assign<uint64_t> ("cli.version.minor") = v.minor ();
+ rs.assign<uint64_t> ("cli.version.patch") = v.patch ();
+ }
+
+ // Cache some values in the module for easier access in the rule.
+ //
+ extra.set_module (new module (data {*tgt, *sum}));
+
+ return true;
+ }
+
+ bool
+ config_init (scope& rs,
+ scope& bs,
+ const location& loc,
+ bool,
+ bool opt,
+ module_init_extra& extra)
+ {
+ tracer trace ("cli::config_init");
+ l5 ([&]{trace << "for " << rs;});
+
+ // We only support root loading (which means there can only be one).
+ //
+ if (rs != bs)
+ fail (loc) << "cli.config module must be loaded in project root";
+
+ // Load cli.guess and share its module instance as ours.
+ //
+ if (optional<shared_ptr<build2::module>> r = load_module (
+ rs, rs, "cli.guess", loc, opt, extra.hints))
+ {
+ extra.module = *r;
+ }
+ else
+ {
+ // This can happen if someone already optionally loaded cli.guess
+ // and it has failed to configure.
+ //
+ if (!opt)
+ fail (loc) << "cli could not be configured" <<
+ info << "re-run with -V for more information";
+
+ return false;
+ }
+
+ // Configuration.
+ //
+ using config::append_config;
+
+ // config.cli.options
+ //
+ // Note that we merge it into the corresponding cli.* variable.
+ //
+ append_config<strings> (rs, rs, "cli.options", nullptr);
+
+ return true;
+ }
+
+ bool
+ init (scope& rs,
+ scope& bs,
+ const location& loc,
+ bool,
+ bool opt,
+ module_init_extra& extra)
+ {
+ tracer trace ("cli::init");
+ l5 ([&]{trace << "for " << rs;});
+
+ // We only support root loading (which means there can only be one).
+ //
+ if (rs != bs)
+ fail (loc) << "cli module must be loaded in project root";
+
+ // Make sure the cxx module has been loaded since we need its targets
+ // types (?xx{}). Note that we don't try to load it ourselves because of
+ // the non-trivial variable merging semantics. So it is better to let
+ // the user load cxx explicitly. @@ Not sure the reason still holds
+ // though it might still make sense to expect the user to load cxx.
+ //
+ if (!cast_false<bool> (rs["cxx.loaded"]))
+ fail (loc) << "cxx module must be loaded before cli";
+
+ // Load cli.config and get its module instance.
+ //
+ if (optional<shared_ptr<build2::module>> r = load_module (
+ rs, rs, "cli.config", loc, opt, extra.hints))
+ {
+ extra.module = *r;
+ }
+ else
+ {
+ // This can happen if someone already optionally loaded cli.config
+ // and it has failed to configure.
+ //
+ if (!opt)
+ fail (loc) << "cli could not be configured" <<
+ info << "re-run with -V for more information";
+
+ return false;
+ }
+
+ auto& m (extra.module_as<module> ());
+
+ // Register target types.
+ //
+ rs.insert_target_type<cli> ();
+ rs.insert_target_type<cli_cxx> ();
+
+ // Register our rules.
+ //
+ // Other rules (e.g., cc::compile) may need to have the group members
+ // resolved/linked up. Looks like a general pattern: groups should
+ // resolve on *(update).
+ {
+ auto reg = [&rs, &m] (meta_operation_id mid, operation_id oid)
+ {
+ rs.insert_rule<cli_cxx> (mid, oid, "cli.compile", m);
+ rs.insert_rule<cxx::hxx> (mid, oid, "cli.compile", m);
+ rs.insert_rule<cxx::cxx> (mid, oid, "cli.compile", m);
+ rs.insert_rule<cxx::ixx> (mid, oid, "cli.compile", m);
+ };
+
+ reg (0 /* wildcard */, update_id);
+ reg (perform_id, clean_id);
+ }
+
+ return true;
+ }
+
+ static const module_functions mod_functions[] =
+ {
+ // NOTE: don't forget to also update the documentation in init.hxx if
+ // changing anything here.
+
+ {"cli.guess", nullptr, guess_init},
+ {"cli.config", nullptr, config_init},
+ {"cli", nullptr, init},
+ {nullptr, nullptr, nullptr}
+ };
+
+ const module_functions*
+ build2_cli_load ()
+ {
+ return mod_functions;
+ }
+ }
+}
diff --git a/libbuild2/cli/init.hxx b/libbuild2/cli/init.hxx
new file mode 100644
index 0000000..6d23795
--- /dev/null
+++ b/libbuild2/cli/init.hxx
@@ -0,0 +1,31 @@
+// file : libbuild2/cli/init.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBUILD2_CLI_INIT_HXX
+#define LIBBUILD2_CLI_INIT_HXX
+
+#include <libbuild2/types.hxx>
+#include <libbuild2/utility.hxx>
+
+#include <libbuild2/module.hxx>
+
+#include <libbuild2/cli/export.hxx>
+
+namespace build2
+{
+ namespace cli
+ {
+ // Module `cli` does not require bootstrapping.
+ //
+ // Submodules:
+ //
+ // `cli.guess` -- set variables describing the compiler.
+ // `cli.config` -- load `cli.guess` and set the rest of the variables.
+ // `cli` -- load `cli.config` and register targets and rules.
+ //
+ extern "C" LIBBUILD2_CLI_SYMEXPORT const module_functions*
+ build2_cli_load ();
+ }
+}
+
+#endif // LIBBUILD2_CLI_INIT_HXX
diff --git a/libbuild2/cli/module.hxx b/libbuild2/cli/module.hxx
new file mode 100644
index 0000000..ba10540
--- /dev/null
+++ b/libbuild2/cli/module.hxx
@@ -0,0 +1,30 @@
+// file : libbuild2/cli/module.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBUILD2_CLI_MODULE_HXX
+#define LIBBUILD2_CLI_MODULE_HXX
+
+#include <libbuild2/types.hxx>
+#include <libbuild2/utility.hxx>
+
+#include <libbuild2/module.hxx>
+
+#include <libbuild2/cli/rule.hxx>
+
+namespace build2
+{
+ namespace cli
+ {
+ class module: public build2::module,
+ public virtual data,
+ public compile_rule
+ {
+ public:
+ explicit
+ module (data&& d)
+ : data (move (d)), compile_rule (move (d)) {}
+ };
+ }
+}
+
+#endif // LIBBUILD2_CLI_MODULE_HXX
diff --git a/libbuild2/cli/rule.cxx b/libbuild2/cli/rule.cxx
new file mode 100644
index 0000000..996ca51
--- /dev/null
+++ b/libbuild2/cli/rule.cxx
@@ -0,0 +1,340 @@
+// file : libbuild2/cli/rule.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <libbuild2/cli/rule.hxx>
+
+#include <libbuild2/depdb.hxx>
+#include <libbuild2/scope.hxx>
+#include <libbuild2/target.hxx>
+#include <libbuild2/context.hxx>
+#include <libbuild2/algorithm.hxx>
+#include <libbuild2/filesystem.hxx>
+#include <libbuild2/diagnostics.hxx>
+
+#include <libbuild2/cli/target.hxx>
+
+namespace build2
+{
+ namespace cli
+ {
+ // Figure out if name contains stem and, optionally, calculate prefix and
+ // suffix.
+ //
+ static bool
+ match_stem (const string& name, const string& stem,
+ string* prefix = nullptr, string* suffix = nullptr)
+ {
+ size_t p (name.find (stem));
+
+ if (p != string::npos)
+ {
+ if (prefix != nullptr)
+ prefix->assign (name, 0, p);
+
+ if (suffix != nullptr)
+ suffix->assign (name, p + stem.size (), string::npos);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ bool compile_rule::
+ match (action a, target& t) const
+ {
+ tracer trace ("cli::compile_rule::match");
+
+ // Find the .cli source file.
+ //
+ auto find = [&trace, a, &t] (auto&& r) -> optional<prerequisite_member>
+ {
+ for (prerequisite_member p: r)
+ {
+ // If excluded or ad hoc, then don't factor it into our tests.
+ //
+ if (include (a, t, p) != include_type::normal)
+ continue;
+
+ if (p.is_a<cli> ())
+ {
+ // Check that the stem match.
+ //
+ if (match_stem (t.name, p.name ()))
+ return p;
+
+ l4 ([&]{trace << ".cli file stem '" << p.name () << "' "
+ << "doesn't match target " << t;});
+ }
+ }
+
+ return nullopt;
+ };
+
+ if (cli_cxx* pt = t.is_a<cli_cxx> ())
+ {
+ // The cli.cxx{} group.
+ //
+ cli_cxx& t (*pt);
+
+ // See if we have a .cli source file.
+ //
+ if (!find (group_prerequisite_members (a, t)))
+ {
+ l4 ([&]{trace << "no .cli source file for target " << t;});
+ return false;
+ }
+
+ // Figure out the member list.
+ //
+ // At this stage, no further changes to cli.options are possible and
+ // we can determine whether the --suppress-inline option is present.
+ //
+ // Passing the group as a "reference target" is a bit iffy,
+ // conceptually.
+ //
+ t.h = &search<cxx::hxx> (t, t.dir, t.out, t.name);
+ t.c = &search<cxx::cxx> (t, t.dir, t.out, t.name);
+ t.i = find_option ("--suppress-inline", t, "cli.options")
+ ? nullptr
+ : &search<cxx::ixx> (t, t.dir, t.out, t.name);
+
+ return true;
+ }
+ else
+ {
+ // One of the ?xx{} members.
+ //
+
+ // Check if there is a corresponding cli.cxx{} group.
+ //
+ const cli_cxx* g (t.ctx.targets.find<cli_cxx> (t.dir, t.out, t.name));
+
+ // If not or if it has no prerequisites (happens when we use it to
+ // set cli.options) and this target has a cli{} prerequisite, then
+ // synthesize the dependency.
+ //
+ if (g == nullptr || !g->has_prerequisites ())
+ {
+ if (optional<prerequisite_member> p = find (
+ prerequisite_members (a, t)))
+ {
+ if (g == nullptr)
+ g = &t.ctx.targets.insert<cli_cxx> (t.dir, t.out, t.name, trace);
+
+ prerequisites ps;
+ ps.push_back (p->as_prerequisite ());
+ g->prerequisites (move (ps));
+ }
+ }
+
+ if (g == nullptr)
+ return false;
+
+ // For ixx{}, verify it is part of the group (i.e., not disabled
+ // via --suppress-inline).
+ //
+ if (t.is_a<cxx::ixx> () &&
+ find_option ("--suppress-inline", *g, "cli.options"))
+ return false;
+
+ t.group = g;
+ return true;
+ }
+ }
+
+ recipe compile_rule::
+ apply (action a, target& xt) const
+ {
+ if (cli_cxx* pt = xt.is_a<cli_cxx> ())
+ {
+ cli_cxx& t (*pt);
+
+ // Derive file names for the members.
+ //
+ t.h->derive_path ();
+ t.c->derive_path ();
+ if (t.i != nullptr)
+ t.i->derive_path ();
+
+ // Inject dependency on the output directory.
+ //
+ inject_fsdir (a, t);
+
+ // Match prerequisites.
+ //
+ match_prerequisite_members (a, t);
+
+ // For update inject dependency on the CLI compiler target.
+ //
+ if (a == perform_update_id)
+ inject (a, t, ctgt);
+
+ switch (a)
+ {
+ case perform_update_id: return [this] (action a, const target& t)
+ {
+ return perform_update (a, t);
+ };
+ case perform_clean_id: return &perform_clean_group_depdb;
+ default: return noop_recipe; // Configure/dist update.
+ }
+ }
+ else
+ {
+ const cli_cxx& g (xt.group->as<cli_cxx> ());
+ match_sync (a, g);
+ return group_recipe; // Execute the group's recipe.
+ }
+ }
+
+ static void
+ append_extension (cstrings& args,
+ const path_target& t,
+ const char* option,
+ const char* default_extension)
+ {
+ const string* e (t.ext ());
+ assert (e != nullptr); // Should have been figured out in apply().
+
+ if (*e != default_extension)
+ {
+ // CLI needs the extension with the leading dot (unless it is empty)
+ // while we store the extension without. But if there is an extension,
+ // then we can get it (with the dot) from the file name.
+ //
+ args.push_back (option);
+ args.push_back (e->empty ()
+ ? e->c_str ()
+ : t.path ().extension_cstring () - 1);
+ }
+ }
+
+ target_state compile_rule::
+ perform_update (action a, const target& xt) const
+ {
+ tracer trace ("cli::compile_rule::perform_update");
+
+ // The rule has been matched which means the members should be resolved
+ // and paths assigned. We use the header file as our "target path" for
+ // timestamp, depdb, etc.
+ //
+ const cli_cxx& t (xt.as<cli_cxx> ());
+ const path& tp (t.h->path ());
+
+ context& ctx (t.ctx);
+
+ // Update prerequisites and determine if any relevant ones render us
+ // out-of-date. Note that currently we treat all the prerequisites as
+ // potentially affecting the result (think prologues/epilogues, CLI
+ // compiler target itself, etc).
+ //
+ timestamp mt (t.load_mtime (tp));
+ auto pr (execute_prerequisites<cli> (a, t, mt));
+
+ bool update (!pr.first);
+ target_state ts (update ? target_state::changed : *pr.first);
+
+ const cli& s (pr.second);
+
+ // We use depdb to track changes to the .cli file name, options,
+ // compiler, etc.
+ //
+ depdb dd (tp + ".d");
+ {
+ // First should come the rule name/version.
+ //
+ if (dd.expect ("cli.compile 1") != nullptr)
+ l4 ([&]{trace << "rule mismatch forcing update of " << t;});
+
+ // Then the compiler checksum.
+ //
+ if (dd.expect (csum) != nullptr)
+ l4 ([&]{trace << "compiler mismatch forcing update of " << t;});
+
+ // Then the options checksum.
+ //
+ sha256 cs;
+ append_options (cs, t, "cli.options");
+
+ if (dd.expect (cs.string ()) != nullptr)
+ l4 ([&]{trace << "options mismatch forcing update of " << t;});
+
+ // Finally the .cli input file.
+ //
+ if (dd.expect (s.path ()) != nullptr)
+ l4 ([&]{trace << "input file mismatch forcing update of " << t;});
+ }
+
+ // Update if depdb mismatch.
+ //
+ if (dd.writing () || dd.mtime > mt)
+ update = true;
+
+ dd.close ();
+
+ // If nothing changed, then we are done.
+ //
+ if (!update)
+ return ts;
+
+ // Translate paths to relative (to working directory). This results in
+ // easier to read diagnostics.
+ //
+ path relo (relative (t.dir));
+ path rels (relative (s.path ()));
+
+ const process_path& pp (ctgt.process_path ());
+ cstrings args {pp.recall_string ()};
+
+ // See if we need to pass --output-{prefix,suffix}
+ //
+ string prefix, suffix;
+ match_stem (t.name, s.name, &prefix, &suffix);
+
+ if (!prefix.empty ())
+ {
+ args.push_back ("--output-prefix");
+ args.push_back (prefix.c_str ());
+ }
+
+ if (!suffix.empty ())
+ {
+ args.push_back ("--output-suffix");
+ args.push_back (suffix.c_str ());
+ }
+
+ // See if we need to pass any --?xx-suffix options.
+ //
+ append_extension (args, *t.h, "--hxx-suffix", "hxx");
+ append_extension (args, *t.c, "--cxx-suffix", "cxx");
+ if (t.i != nullptr)
+ append_extension (args, *t.i, "--ixx-suffix", "ixx");
+
+ append_options (args, t, "cli.options");
+
+ if (!relo.empty ())
+ {
+ args.push_back ("-o");
+ args.push_back (relo.string ().c_str ());
+ }
+
+ args.push_back (rels.string ().c_str ());
+ args.push_back (nullptr);
+
+ if (verb >= 2)
+ print_process (args);
+ else if (verb)
+ print_diag ("cli", s, t);
+
+ if (!ctx.dry_run)
+ {
+ run (ctx, pp, args, 1 /* finish_verbosity */);
+ dd.check_mtime (tp);
+ }
+
+ t.mtime (system_clock::now ());
+ return target_state::changed;
+ }
+ }
+}
diff --git a/libbuild2/cli/rule.hxx b/libbuild2/cli/rule.hxx
new file mode 100644
index 0000000..0132b44
--- /dev/null
+++ b/libbuild2/cli/rule.hxx
@@ -0,0 +1,46 @@
+// file : libbuild2/cli/rule.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBUILD2_CLI_RULE_HXX
+#define LIBBUILD2_CLI_RULE_HXX
+
+#include <libbuild2/types.hxx>
+#include <libbuild2/utility.hxx>
+
+#include <libbuild2/rule.hxx>
+
+#include <libbuild2/cli/export.hxx>
+
+namespace build2
+{
+ namespace cli
+ {
+ // Cached data shared between rules and the module.
+ //
+ struct data
+ {
+ const exe& ctgt; // CLI compiler target.
+ const string& csum; // CLI compiler checksum.
+ };
+
+ // @@ Redo as two separate rules?
+ //
+ class LIBBUILD2_CLI_SYMEXPORT compile_rule: public simple_rule,
+ private virtual data
+ {
+ public:
+ compile_rule (data&& d): data (move (d)) {}
+
+ virtual bool
+ match (action, target&) const override;
+
+ virtual recipe
+ apply (action, target&) const override;
+
+ target_state
+ perform_update (action, const target&) const;
+ };
+ }
+}
+
+#endif // LIBBUILD2_CLI_RULE_HXX
diff --git a/libbuild2/cli/target.cxx b/libbuild2/cli/target.cxx
new file mode 100644
index 0000000..22ae75c
--- /dev/null
+++ b/libbuild2/cli/target.cxx
@@ -0,0 +1,75 @@
+// file : libbuild2/cli/target.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <libbuild2/cli/target.hxx>
+
+#include <libbuild2/context.hxx>
+
+namespace build2
+{
+ namespace cli
+ {
+ // cli
+ //
+ extern const char cli_ext_def[] = "cli";
+
+ const target_type cli::static_type
+ {
+ "cli",
+ &file::static_type,
+ &target_factory<cli>,
+ nullptr, /* fixed_extension */
+ &target_extension_var<cli_ext_def>,
+ &target_pattern_var<cli_ext_def>,
+ nullptr,
+ &file_search,
+ target_type::flag::none
+ };
+
+ // cli.cxx
+ //
+ group_view cli_cxx::
+ group_members (action) const
+ {
+ static_assert (sizeof (cli_cxx_members) == sizeof (const target*) * 3,
+ "member layout incompatible with array");
+
+ return h != nullptr
+ ? group_view {reinterpret_cast<const target* const*> (&h),
+ (i != nullptr ? 3U : 2U)}
+ : group_view {nullptr, 0};
+ }
+
+ static target*
+ cli_cxx_factory (context& ctx,
+ const target_type&, dir_path d, dir_path o, string n)
+ {
+ tracer trace ("cli::cli_cxx_factory");
+
+ // Pre-enter (potential) members as targets. The main purpose of doing
+ // this is to avoid searching for existing files in src_base if the
+ // buildfile mentions some of them explicitly as prerequisites.
+ //
+ // Also required for the src-out remapping logic.
+ //
+ ctx.targets.insert<cxx::hxx> (d, o, n, trace);
+ ctx.targets.insert<cxx::cxx> (d, o, n, trace);
+ ctx.targets.insert<cxx::ixx> (d, o, n, trace);
+
+ return new cli_cxx (ctx, move (d), move (o), move (n));
+ }
+
+ const target_type cli_cxx::static_type
+ {
+ "cli.cxx",
+ &mtime_target::static_type,
+ &cli_cxx_factory,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ &target_search,
+ target_type::flag::see_through // Group with "see through" iteration.
+ };
+ }
+}
diff --git a/libbuild2/cli/target.hxx b/libbuild2/cli/target.hxx
new file mode 100644
index 0000000..8efb837
--- /dev/null
+++ b/libbuild2/cli/target.hxx
@@ -0,0 +1,61 @@
+// file : libbuild2/cli/target.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBUILD2_CLI_TARGET_HXX
+#define LIBBUILD2_CLI_TARGET_HXX
+
+#include <libbuild2/types.hxx>
+#include <libbuild2/utility.hxx>
+
+#include <libbuild2/target.hxx>
+
+#include <libbuild2/cxx/target.hxx>
+
+#include <libbuild2/cli/export.hxx>
+
+namespace build2
+{
+ namespace cli
+ {
+ class LIBBUILD2_CLI_SYMEXPORT cli: public file
+ {
+ public:
+ cli (context& c, dir_path d, dir_path o, string n)
+ : file (c, move (d), move (o), move (n))
+ {
+ dynamic_type = &static_type;
+ }
+
+ public:
+ static const target_type static_type;
+ };
+
+ // Standard layout type compatible with group_view's const target*[3].
+ //
+ struct cli_cxx_members
+ {
+ const cxx::hxx* h = nullptr;
+ const cxx::cxx* c = nullptr;
+ const cxx::ixx* i = nullptr;
+ };
+
+ class LIBBUILD2_CLI_SYMEXPORT cli_cxx: public mtime_target,
+ public cli_cxx_members
+ {
+ public:
+ cli_cxx (context& c, dir_path d, dir_path o, string n)
+ : mtime_target (c, move (d), move (o), move (n))
+ {
+ dynamic_type = &static_type;
+ }
+
+ virtual group_view
+ group_members (action) const override;
+
+ public:
+ static const target_type static_type;
+ };
+ }
+}
+
+#endif // LIBBUILD2_CLI_TARGET_HXX
diff --git a/libbuild2/module.cxx b/libbuild2/module.cxx
index 0928307..4c1271c 100644
--- a/libbuild2/module.cxx
+++ b/libbuild2/module.cxx
@@ -50,6 +50,7 @@ namespace build2
"bin",
"c",
"cc",
+ "cli",
"cxx",
"in",
"version"