From 1c30f9e9c4fca846f05e881638920a9beb082fd1 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Thu, 8 Jun 2023 13:31:08 +0200 Subject: Add support for buildfile importation --- doc/manual.cli | 46 ++-- libbuild2/algorithm.cxx | 4 +- libbuild2/buildfile | 35 ++- libbuild2/file.cxx | 242 ++++++++++++++++--- libbuild2/file.hxx | 52 +++- libbuild2/file.ixx | 20 +- libbuild2/install/init.cxx | 4 +- libbuild2/operation.cxx | 3 +- libbuild2/parser.cxx | 482 +++++++++++++++++++++++++++++--------- libbuild2/parser.hxx | 18 +- libbuild2/utility-installed.cxx | 4 + libbuild2/utility-uninstalled.cxx | 6 + libbuild2/utility.hxx | 6 +- 13 files changed, 728 insertions(+), 194 deletions(-) diff --git a/doc/manual.cli b/doc/manual.cli index 28f8e0c..85a6613 100644 --- a/doc/manual.cli +++ b/doc/manual.cli @@ -2312,29 +2312,30 @@ If the value of the \c{install} variable is not \c{false}, then it is normally a relative path with the first path component being one of these names: \ -name default override ----- ------- -------- -root config.install.root +name default override +---- ------- -------- +root config.install.root -data_root root/ config.install.data_root -exec_root root/ config.install.exec_root +data_root root/ config.install.data_root +exec_root root/ config.install.exec_root -bin exec_root/bin/ config.install.bin -sbin exec_root/sbin/ config.install.sbin -lib exec_root/lib/ config.install.lib -libexec exec_root/libexec// config.install.libexec -pkgconfig lib/pkgconfig/ config.install.pkgconfig +bin exec_root/bin/ config.install.bin +sbin exec_root/sbin/ config.install.sbin +lib exec_root/lib/ config.install.lib +libexec exec_root/libexec// config.install.libexec +pkgconfig lib/pkgconfig/ config.install.pkgconfig -etc data_root/etc/ config.install.etc -include data_root/include/ config.install.include -include_arch include/ config.install.include_arch -share data_root/share/ config.install.share -data share// config.install.data +etc data_root/etc/ config.install.etc +include data_root/include/ config.install.include +include_arch include/ config.install.include_arch +share data_root/share/ config.install.share +data share// config.install.data +buildfile share/build2/export// config.install.buildfile -doc share/doc// config.install.doc -legal doc/ config.install.legal -man share/man/ config.install.man -man man/man/ config.install.man +doc share/doc// config.install.doc +legal doc/ config.install.legal +man share/man/ config.install.man +man man/man/ config.install.man \ Let's see what's going on here: The default install directory tree is derived @@ -6159,6 +6160,7 @@ include data_root/include// c.i.include include_arch include/ c.i.include_arch share data_root/share/ c.i.share data share/// c.i.data +buildfile share/build2/export// c.i.buildfile doc share/doc/// c.i.doc legal doc/ c.i.legal @@ -6181,6 +6183,12 @@ h{*}: install = include/libhello/ h{config}: install = include_arch/libhello/ \ +The \c{buildfile} location is meant for exported buildfiles that can be +imported by other projects. If a project contains any \c{**.build} buildfiles +in its \c{build/export/} directory (or \c{**.build2} and \c{build2/export/} in +the alternative naming scheme), then they are automatically installed into +this location (recreating subdirectories). + The \c{}, \c{}, and \c{} substitutions in these \c{config.install.*} values are replaced with the project name, version, and private subdirectory, respectively. If either is empty, then the corresponding diff --git a/libbuild2/algorithm.cxx b/libbuild2/algorithm.cxx index a2e1b57..3c0da3b 100644 --- a/libbuild2/algorithm.cxx +++ b/libbuild2/algorithm.cxx @@ -57,10 +57,10 @@ namespace build2 assert (t.ctx.phase == run_phase::match); // If this is a project-qualified prerequisite, then this is import's - // business. + // business (phase 2). // if (pk.proj) - return import (t.ctx, pk); + return import2 (t.ctx, pk); if (const target* pt = pk.tk.type->search (t, pk)) return *pt; diff --git a/libbuild2/buildfile b/libbuild2/buildfile index b3a9c52..6d7c597 100644 --- a/libbuild2/buildfile +++ b/libbuild2/buildfile @@ -191,14 +191,37 @@ if! $cross { {obja objs}{context}: cxx.poptions += \ -DBUILD2_IMPORT_PATH=\"$regex.replace($out_root, '\\', '\\\\')\" +} + +# Note that while the -installed object file should only be linked when we +# are installing, it will be compiled even in the uninstalled case. +# +if ($install.root != [null]) +{ + # Only if installed. + # + {obja objs}{utility-installed}: cxx.poptions += \ + -DBUILD2_INSTALL_LIB=\"$regex.replace(\ + $install.resolve($install.lib), '\\', '\\\\')\" - # While this object file should only be linked when we are installing, it - # will be compiled even in the uninstalled case. + # Only if configured. + # + # Note: strip the last directory component (). # - if ($install.root != [null]) - {obja objs}{utility-installed}: cxx.poptions += \ - -DBUILD2_INSTALL_LIB=\"$regex.replace(\ - $install.resolve($install.lib), '\\', '\\\\')\" + # @@ TMP drop after 0.16.0 release. + # + install_buildfile = ($install.buildfile != [null] \ + ? $directory($install.resolve($install.buildfile)) \ + :) + {obja objs}{utility-installed utility-uninstalled}: cxx.poptions += \ + -DBUILD2_INSTALL_BUILDFILE=\"$regex.replace($install_buildfile, '\\', '\\\\')\" + + #\ + {obja objs}{utility-installed utility-uninstalled}: cxx.poptions += \ + -DBUILD2_INSTALL_BUILDFILE=\"$regex.replace(\ + $directory($install.resolve($install.buildfile)), '\\', '\\\\')\" + #\ + } if ($cxx.target.class != 'windows') diff --git a/libbuild2/file.cxx b/libbuild2/file.cxx index 7a48b2e..64a92a4 100644 --- a/libbuild2/file.cxx +++ b/libbuild2/file.cxx @@ -29,6 +29,8 @@ namespace build2 { // Standard and alternative build file/directory naming schemes. // + extern const dir_path std_export_dir; + extern const dir_path alt_export_dir; // build: @@ -36,6 +38,7 @@ namespace build2 const dir_path std_root_dir (dir_path (std_build_dir) /= "root"); const dir_path std_bootstrap_dir (dir_path (std_build_dir) /= "bootstrap"); const dir_path std_build_build_dir (dir_path (std_build_dir) /= "build"); + const dir_path std_export_dir (dir_path (std_build_dir) /= "export"); const path std_root_file (std_build_dir / "root.build"); const path std_bootstrap_file (std_build_dir / "bootstrap.build"); @@ -53,6 +56,7 @@ namespace build2 const dir_path alt_root_dir (dir_path (alt_build_dir) /= "root"); const dir_path alt_bootstrap_dir (dir_path (alt_build_dir) /= "bootstrap"); const dir_path alt_build_build_dir (dir_path (alt_build_dir) /= "build"); + const dir_path alt_export_dir (dir_path (alt_build_dir) /= "export"); const path alt_root_file (alt_build_dir / "root.build2"); const path alt_bootstrap_file (alt_build_dir / "bootstrap.build2"); @@ -2043,7 +2047,7 @@ namespace build2 static void import_suggest (const diag_record& dr, const project_name& pn, - const target_type& tt, + const target_type* tt, const string& tn, bool rule_hint, const char* qual = nullptr) @@ -2058,11 +2062,11 @@ namespace build2 // Suggest ad hoc import but only if it's a path-based target (doing it // for lib{} is very confusing). // - if (tt.is_a ()) + if (tt != nullptr && tt->is_a ()) { - string v (tt.is_a () && (pv == tn || pn == tn) + string v (tt->is_a () && (pv == tn || pn == tn) ? "config." + pv - : "config.import." + pv + '.' + tn + '.' + tt.name); + : "config.import." + pv + '.' + tn + '.' + tt->name); dr << info << "or use " << v << " configuration variable to specify " << "its " << (qual != nullptr ? qual : "") << "path"; @@ -2119,6 +2123,9 @@ namespace build2 // // 4. Normal import. // + // @@ PERF: in quite a few places (local, subproject) we could have + // returned the scope and save on bootstrap in import_load(). + // if (tgt.unqualified ()) { if (tgt.directory () && tgt.relative ()) @@ -2126,6 +2133,8 @@ namespace build2 if (tgt.absolute ()) { + // Ad hoc import. + // // Actualize the directory to be analogous to the config.import. // case (which is of abs_dir_path type). // @@ -2142,7 +2151,7 @@ namespace build2 fail (loc) << "project-local importation of target " << tgt << " from an unnamed project"; - tgt.proj = pn; + tgt.proj = pn; // Reduce to normal import. return make_pair (move (tgt), optional (iroot.out_path ())); } @@ -2347,7 +2356,7 @@ namespace build2 [&proj, tt, &on] (const diag_record& dr) { import_suggest ( - dr, proj, *tt, on, false, "alternative "); + dr, proj, tt, on, false, "alternative "); }); md = extract_metadata (e->process_path (), @@ -2670,6 +2679,65 @@ namespace build2 fail (loc) << out_root << " is not out_root for " << *proj; } + // Buildfile importation is quite different so handle it separately. + // + // Note that we don't need to load the project in this case. + // + // @@ For now we don't out-qualify the resulting target to be able to + // re-import it ad hoc (there is currently no support for out-qualified + // ad hoc import). Feels like this should be harmless since it's just a + // glorified path to a static file that nobody is actually going to use + // as a target (e.g., to depend upon). + // + if (tgt.type == "buildfile") + { + auto add_ext = [&altn] (string& n) + { + if (path_traits::find_extension (n) == string::npos) + { + if (n != (*altn ? alt_buildfile_file : std_buildfile_file).string ()) + { + n += "."; + n += *altn ? alt_build_ext : std_build_ext; + } + } + }; + + if (proj) + { + name n; + + n.dir = move (src_root); + n.dir /= *altn ? alt_export_dir : std_export_dir; + if (!tgt.dir.empty ()) + { + n.dir /= tgt.dir; + n.dir.normalize (); + } + + n.type = tgt.type; + n.value = tgt.value; + add_ext (n.value); + + pair r (names {move (n)}, *root); + + // Cache. + // + if (cache_out_root.empty ()) + cache_out_root = move (out_root); + + ctx.import_cache.emplace ( + import_key {move (cache_out_root), move (tgt), metav}, r); + + return r; + } + else + { + add_ext (tgt.value); + return pair (names {move (tgt)}, *root); + } + } + // Load the imported root scope. // if (!root->root_extra->loaded) @@ -2806,6 +2874,9 @@ namespace build2 } } + static names + import2_buildfile (context&, names&&, bool, const location&); + pair import (scope& base, name tgt, @@ -2867,18 +2938,23 @@ namespace build2 // if (ns.back ().qualified ()) { - if (ph2) + if (ns.back ().type == "buildfile") + { + assert (ph2); + ns = import2_buildfile (ctx, move (ns), opt && !r.second, loc); + } + else if (ph2) { // This is tricky: we only want the optional semantics for the // fallback case. // - if (const target* t = import (ctx, - base.find_prerequisite_key (ns, loc), - *ph2, - opt && !r.second /* optional */, - nullopt /* metadata */, - false /* existing */, - loc)) + if (const target* t = import2 (ctx, + base.find_prerequisite_key (ns, loc), + *ph2, + opt && !r.second /* optional */, + nullopt /* metadata */, + false /* existing */, + loc)) { // Note that here r.first was still project-qualified and we // have no choice but to call as_name(). This shouldn't cause @@ -2909,15 +2985,15 @@ namespace build2 } const target* - import (context& ctx, - const prerequisite_key& pk, - const string& hint, - bool opt, - const optional& meta, - bool exist, - const location& loc) + import2 (context& ctx, + const prerequisite_key& pk, + const string& hint, + bool opt, + const optional& meta, + bool exist, + const location& loc) { - tracer trace ("import"); + tracer trace ("import2"); // Neither hint nor metadata can be requested for existing. // @@ -3037,7 +3113,7 @@ namespace build2 auto df = make_diag_frame ( [&proj, &tt, &tk] (const diag_record& dr) { - import_suggest (dr, proj, tt, *tk.name, false, "alternative "); + import_suggest (dr, proj, &tt, *tk.name, false, "alternative "); }); if (!(md = extract_metadata (pp, *meta, opt, loc))) @@ -3082,7 +3158,84 @@ namespace build2 else // Use metadata as proxy for immediate import. // - import_suggest (dr, proj, tt, *tk.name, meta && hint.empty ()); + import_suggest (dr, proj, &tt, *tk.name, meta && hint.empty ()); + + dr << endf; + } + + static names + import2_buildfile (context&, names&& ns, bool opt, const location& loc) + { + tracer trace ("import2_buildfile"); + + assert (ns.size () == 1); + name n (move (ns.front ())); + + // Our approach doesn't work for targets without a project so let's fail + // hard, even if optional. + // + if (!n.proj || n.proj->empty ()) + fail (loc) << "unable to import target " << n << " without project name"; + + while (!build_install_buildfile.empty ()) // Breakout loop. + { + path f (build_install_buildfile / + dir_path (n.proj->string ()) / + n.dir / + n.value); + + // See if we need to try with extensions. + // + bool ext (path_traits::find_extension (n.value) == string::npos && + n.value != std_buildfile_file.string () && + n.value != alt_buildfile_file.string ()); + + if (ext) + { + f += '.'; + f += std_build_ext; + } + + if (!exists (f)) + { + l6 ([&]{trace << "tried " << f;}); + + if (ext) + { + f.make_base (); + f += '.'; + f += alt_build_ext; + + if (!exists (f)) + { + l6 ([&]{trace << "tried " << f;}); + break; + } + } + else + break; + } + + // Split the path into the target. + // + ns = {name (f.directory (), move (n.type), f.leaf ().string ())}; + return ns; + } + + if (opt) + return names {}; + + diag_record dr; + dr << fail (loc) << "unable to import target " << n; + + import_suggest (dr, *n.proj, nullptr /* tt */, n.value, false); + + if (build_install_buildfile.empty ()) + dr << info << "no exported buildfile installation location is " + << "configured in build2"; + else + dr << info << "exported buildfile installation location is " + << build_install_buildfile; dr << endf; } @@ -3104,7 +3257,7 @@ namespace build2 l5 ([&]{trace << tgt << " from " << base << " for " << what;}); - assert ((!opt || ph2) && (!metadata || ph2)); + assert ((!opt || ph2) && (!metadata || ph2) && tgt.type != "buildfile"); context& ctx (base.ctx); assert (ctx.phase == run_phase::load); @@ -3147,13 +3300,13 @@ namespace build2 // This is tricky: we only want the optional semantics for the // fallback case. // - pt = import (ctx, - base.find_prerequisite_key (ns, loc), - *ph2, - opt && !r.second, - meta, - false /* existing */, - loc); + pt = import2 (ctx, + base.find_prerequisite_key (ns, loc), + *ph2, + opt && !r.second, + meta, + false /* existing */, + loc); } if (pt == nullptr) @@ -3274,6 +3427,31 @@ namespace build2 return import_result {pt, move (rns), k}; } + path + import_buildfile (scope& bs, name n, bool opt, const location& loc) + { + names r (import (bs, + move (n), + string () /* phase2 */, + opt, + false /* metadata */, + loc).first); + + path p; + if (!r.empty ()) // Optional not found. + { + // Note: see also parse_import(). + // + assert (r.size () == 1); // See import_load() for details. + name& n (r.front ()); + p = n.dir / n.value; // Should already include extension. + } + else + assert (opt); + + return p; + } + ostream& operator<< (ostream& o, const import_result& r) { diff --git a/libbuild2/file.hxx b/libbuild2/file.hxx index 5847498..6c5097d 100644 --- a/libbuild2/file.hxx +++ b/libbuild2/file.hxx @@ -19,6 +19,28 @@ namespace build2 class lexer; class parser; + // The following filesystem entries in the build/ subdirectory are reserved + // by the build2 core: + // + // build/ -- build2 core-internal build state (e.g., recipes) + // bootstrap/ -- bootstrap state and hooks + // bootstrap.build -- bootstrap buildfile + // root/ -- root load hooks + // root.build -- root buildfile + // export.build -- export stub + // export/ -- exported buildfiles + // + // The build/, bootstrap/, root/, and config.build entries are in .gitignore + // as generated by bdep-new. + // + // The rest of the filesystem entries are shared between the project and the + // modules that it loads. In particular, if a project loads module named + // , then the .build, /, *. entries (spelled in any + // case) are reserved to this module and should not be used by the project + // unless explicitly allowed by the module. By convention, /build/ is + // for module-internal build state (e.g., C++ modules side-build) and is + // .gitignore'ed. + // LIBBUILD2_SYMEXPORT extern const dir_path std_build_dir; // build/ // build/root.build @@ -339,7 +361,9 @@ namespace build2 // Note also that we return names rather than a single name: while normally // it will be a single target name, it can be an out-qualified pair (if // someone wants to return a source target) but it can also be a non-target - // since we don't restrict what users can import/export. + // since we don't restrict what users can import/export. If name has + // buildfile type, then the result is an absolute buildfile target to be + // included (once) at the point of importation. // // Finally, note that import is (and should be kept) idempotent or, more // precisely, "accumulatively idempotent" in that additional steps may be @@ -358,7 +382,7 @@ namespace build2 // Import phase 2. // const target& - import (context&, const prerequisite_key&); + import2 (context&, const prerequisite_key&); // As above but import the target "here and now" without waiting for phase 2 // (and thus omitting any rule-specific logic). This version of import is, @@ -383,6 +407,9 @@ namespace build2 // target::as_name() for details) as well as the kind of import that was // performed. // + // Note: cannot be used to import buildfile targets (use import_buildfile() + // instead). + // template struct import_result { @@ -391,6 +418,13 @@ namespace build2 import_kind kind; }; + // Print import_direct() result either as a target for a normal import + // or as a process path for ad hoc and fallback imports. Normally used in + // build system modules to print the configuration report. + // + LIBBUILD2_SYMEXPORT ostream& + operator<< (ostream&, const import_result&); + import_result import_direct (scope& base, name, @@ -433,12 +467,16 @@ namespace build2 bool, bool, bool, const location&, const char* = "import"); - // Print import_direct() result either as a target for a normal import - // or as a process path for ad hoc and fallback imports. Normally used in - // build system modules to print the configuration report. + // The import_direct() equivalent for importing buildfile targets. Return + // empty name if optional and not found. Note that the returned file path is + // not necessarily checked for existence so sourcing it may still fail. // - LIBBUILD2_SYMEXPORT ostream& - operator<< (ostream&, const import_result&); + // Note also that this function can be used for an ad hoc import by passing + // an absolute target name as would be returned by the normal import (can be + // useful for importing own buildfiles). + // + LIBBUILD2_SYMEXPORT path + import_buildfile (scope& base, name, bool optional, const location&); // As import phase 2 but only imports as an already existing target. But // unlike it, this function can be called during the load and execute diff --git a/libbuild2/file.ixx b/libbuild2/file.ixx index 43c46c9..dc39bcb 100644 --- a/libbuild2/file.ixx +++ b/libbuild2/file.ixx @@ -22,16 +22,16 @@ namespace build2 } LIBBUILD2_SYMEXPORT const target* - import (context&, - const prerequisite_key&, - const string& hint, - bool optional_, - const optional& metadata, // False or metadata key. - bool existing, - const location&); + import2 (context&, + const prerequisite_key&, + const string& hint, + bool optional_, + const optional& metadata, // False or metadata key. + bool existing, + const location&); inline const target& - import (context& ctx, const prerequisite_key& pk) + import2 (context& ctx, const prerequisite_key& pk) { assert (ctx.phase == run_phase::match); @@ -40,7 +40,7 @@ namespace build2 // Looks like the only way to do this is to keep location in name and // then in prerequisite. Perhaps one day... // - return *import (ctx, pk, string (), false, nullopt, false, location ()); + return *import2 (ctx, pk, string (), false, nullopt, false, location ()); } inline import_result @@ -98,6 +98,6 @@ namespace build2 inline const target* import_existing (context& ctx, const prerequisite_key& pk) { - return import (ctx, pk, string (), false, nullopt, true, location ()); + return import2 (ctx, pk, string (), false, nullopt, true, location ()); } } diff --git a/libbuild2/install/init.cxx b/libbuild2/install/init.cxx index dfd78df..0b33475 100644 --- a/libbuild2/install/init.cxx +++ b/libbuild2/install/init.cxx @@ -260,7 +260,7 @@ namespace build2 // way we distinguish between the two is via the presence/absence of // the trailing directory separator. // - // Plus it can have the special true/false values when acting as a + // Plus it can have the special true/false values when acting as an // operation variable. // auto& ovar (rs.var_pool ().insert ("install", @@ -299,6 +299,7 @@ namespace build2 DIR (include_arch, dir_path ("include")); DIR (share, dir_path ("data_root") /= "share"); DIR (data, (dir_path ("share") /= "") /= ""); + DIR (buildfile, ((dir_path ("share") /= "build2") /= "export") /= ""); DIR (doc, ((dir_path ("share") /= "doc") /= "") /= ""); DIR (legal, dir_path ("doc")); @@ -701,6 +702,7 @@ namespace build2 set_dir (s, p, rs, "include_arch", dir_include_arch); set_dir (s, p, rs, "share", dir_share); set_dir (s, p, rs, "data", dir_data); + set_dir (s, p, rs, "buildfile", dir_buildfile); set_dir (s, p, rs, "doc", dir_doc); set_dir (s, p, rs, "legal", dir_legal); diff --git a/libbuild2/operation.cxx b/libbuild2/operation.cxx index e9b9211..bafc263 100644 --- a/libbuild2/operation.cxx +++ b/libbuild2/operation.cxx @@ -85,7 +85,8 @@ namespace build2 { // Load project's root.build. // - load_root (root); + if (!root.root_extra->loaded) + load_root (root); // Create the base scope. Note that its existence doesn't mean it was // already setup as a base scope; it can be the same as root. diff --git a/libbuild2/parser.cxx b/libbuild2/parser.cxx index b79b65c..6214648 100644 --- a/libbuild2/parser.cxx +++ b/libbuild2/parser.cxx @@ -302,9 +302,9 @@ namespace build2 ? auto_project_env (*root_) : auto_project_env ()); - if (enter && path_->path != nullptr) - enter_buildfile (*path_->path); - + const buildfile* bf (enter && path_->path != nullptr + ? &enter_buildfile (*path_->path) + : nullptr); token t; type tt; next (t, tt); @@ -316,7 +316,7 @@ namespace build2 else { parse_clause (t, tt); - process_default_target (t); + process_default_target (t, bf); } if (tt != type::eos) @@ -1601,7 +1601,8 @@ namespace build2 if (!start_names (tt)) fail (t) << "unexpected " << t; - // @@ PAT: currently we pattern-expand target-specific var names. + // @@ PAT: currently we pattern-expand target-specific var names (see + // also parse_import()). // const location ploc (get_location (t)); names pns (parse_names (t, tt, pattern_mode::expand)); @@ -3140,14 +3141,18 @@ namespace build2 } void parser:: - source (istream& is, const path_name& in, const location& loc, bool deft) + source_buildfile (istream& is, + const path_name& in, + const location& loc, + bool deft) { - tracer trace ("parser::source", &path_); + tracer trace ("parser::source_buildfile", &path_); l5 ([&]{trace (loc) << "entering " << in;}); - if (in.path != nullptr) - enter_buildfile (*in.path); + const buildfile* bf (in.path != nullptr + ? &enter_buildfile (*in.path) + : nullptr); const path_name* op (path_); path_ = ∈ @@ -3173,7 +3178,7 @@ namespace build2 if (deft) { - process_default_target (t); + process_default_target (t, bf); default_target_ = odt; } @@ -3217,10 +3222,10 @@ namespace build2 try { ifdstream ifs (p); - source (ifs, - path_name (p), - get_location (t), - false /* default_target */); + source_buildfile (ifs, + path_name (p), + get_location (t), + false /* default_target */); } catch (const io_error& e) { @@ -3350,6 +3355,8 @@ namespace build2 continue; } + // Note: see a variant of this in parse_import(). + // // Clear/restore if/switch location. // // We do it here but not in parse_source since the included buildfile is @@ -3365,10 +3372,10 @@ namespace build2 try { ifdstream ifs (p); - source (ifs, - path_name (p), - get_location (t), - true /* default_target */); + source_buildfile (ifs, + path_name (p), + get_location (t), + true /* default_target */); } catch (const io_error& e) { @@ -3451,10 +3458,10 @@ namespace build2 dr << info (l) << "while parsing " << args[0] << " output"; }); - source (is, - path_name (""), - l, - false /* default_target */); + source_buildfile (is, + path_name (""), + l, + false /* default_target */); } is.close (); // Detect errors. @@ -3867,10 +3874,14 @@ namespace build2 if (stage_ == stage::boot) fail (t) << "import during bootstrap"; - // General import format: + // General import form: // // import[?!] [] = [] (|%])+ // + // Special form for importing buildfiles: + // + // import[?!] [] (|%])+ + // bool opt (t.value.back () == '?'); optional ph2 (opt || t.value.back () == '!' ? optional (string ()) @@ -3880,13 +3891,15 @@ namespace build2 // next_with_attributes (t, tt); - // Get variable attributes, if any, and deal with the special metadata and - // rule_hint attributes. Since currently they can only appear in the - // import directive, we handle them in an ad hoc manner. + // Get variable (or value, in the second form) attributes, if any, and + // deal with the special metadata and rule_hint attributes. Since + // currently they can only appear in the import directive, we handle them + // in an ad hoc manner. // attributes_push (t, tt); - bool meta (false); + bool meta (false); // Import with metadata. + bool once (false); // Import buildfile once. { attributes& as (attributes_top ()); const location& l (as.loc); @@ -3904,6 +3917,10 @@ namespace build2 meta = true; } + else if (n == "once") + { + once = true; + } else if (n == "rule_hint") { if (!ph2) @@ -3934,80 +3951,197 @@ namespace build2 } } - if (tt != type::word) - fail (t) << "expected variable name instead of " << t; - - const variable& var ( - parse_variable_name (move (t.value), get_location (t))); - apply_variable_attributes (var); + // Note that before supporting the second form (without ) we used to + // parse the value after assignment in the value mode. However, we don't + // really need to since what we should have is a bunch of target names. + // In other words, whatever the value mode does not treat as special + // compared to the normal mode (like `:`) would be illegal here. + // + // Note that we expant patterns for the ad hoc import case: + // + // import sub = */ + // + // @@ PAT: the only issue here is that we currently pattern-expand var + // name (same assue as with target-specific var names). + // + if (!start_names (tt)) + fail (t) << "expected variable name or buildfile target instead of " << t; - if (var.visibility > variable_visibility::scope) - { - fail (t) << "variable " << var << " has " << var.visibility - << " visibility but is assigned in import"; - } + location loc (get_location (t)); + names ns (parse_names (t, tt, pattern_mode::expand)); - // Next should come the assignment operator. Note that we don't support + // Next could come the assignment operator. Note that we don't support // default assignment (?=) yet (could make sense when attempting to import // alternatives or some such). // - next (t, tt); + type atype; + const variable* var (nullptr); + if (tt == type::assign || tt == type::append || tt == type::prepend) + { + var = &parse_variable_name (move (ns), loc); + apply_variable_attributes (*var); - if (tt != type::assign && tt != type::append && tt != type::prepend) - fail (t) << "expected variable assignment instead of " << t; + if (var->visibility > variable_visibility::scope) + { + fail (loc) << "variable " << *var << " has " << var->visibility + << " visibility but is assigned in import"; + } - type atype (tt); - value& val (atype == type::assign - ? scope_->assign (var) - : scope_->append (var)); + atype = tt; + next_with_attributes (t, tt); + attributes_push (t, tt, true /* standalone */); - // The rest should be a list of targets. Parse them similar to a value on - // the RHS of an assignment (attributes, etc). - // - // Note that we expant patterns for the ad hoc import case: - // - // import sub = */ + if (!start_names (tt)) + fail (t) << "expected target to import instead of " << t; + + loc = get_location (t); + ns = parse_names (t, tt, pattern_mode::expand); + } + else if (tt == type::default_assign) + fail (t) << "default assignment not yet supported"; + + + // If there are any value attributes, roundtrip the names through the + // value applying the attributes. // - mode (lexer_mode::value, '@'); - next_with_attributes (t, tt); + if (!attributes_top ().empty ()) + { + value lhs, rhs (move (ns)); + apply_value_attributes (nullptr, lhs, move (rhs), type::assign); - if (tt == type::newline || tt == type::eos) - fail (t) << "expected target to import instead of " << t; + if (!lhs) + fail (loc) << "expected target to import instead of null value"; - const location loc (get_location (t)); + untypify (lhs, true /* reduce */); + ns = move (lhs.as ()); + } + else + attributes_pop (); - if (value v = parse_value_with_attributes (t, tt, pattern_mode::expand)) + value* val (var != nullptr ? + &(atype == type::assign + ? scope_->assign (*var) + : scope_->append (*var)) + : nullptr); + + for (name& n: ns) { - names storage; - for (name& n: reverse (v, storage, true /* reduce */)) + // @@ Could this be an out-qualified ad hoc import? Yes, see comment + // about buildfile import in import_load(). + // + if (n.pair) + fail (loc) << "unexpected pair in import"; + + // See if we are importing a buildfile target. Such an import is always + // immediate. + // + bool bf (n.type == "buildfile"); + if (bf) { - // @@ Could this be an out-qualified ad hoc import? - // - if (n.pair) - fail (loc) << "unexpected pair in import"; + if (meta) + fail (loc) << "metadata requested for buildfile target " << n; - // import() will check the name, if required. - // - names r (import (*scope_, move (n), ph2, opt, meta, loc).first); + if (once && var != nullptr) + fail (loc) << "once importation requested with variable assignment"; + if (ph2 && !ph2->empty ()) + fail (loc) << "rule hint specified for buildfile target " << n; + } + else + { + if (once) + fail (loc) << "once importation requested for target " << n; + + if (var == nullptr) + fail (loc) << "variable assignment required to import target " << n; + } + + // import() will check the name, if required. + // + names r (import (*scope_, + move (n), + ph2 ? ph2 : bf ? optional (string ()) : nullopt, + opt, + meta, + loc).first); + + if (val != nullptr) + { if (r.empty ()) // Optional not found. { if (atype == type::assign) - val = nullptr; + *val = nullptr; } else { - if (atype == type::assign) - val.assign (move (r), &var); - else if (atype == type::prepend) - val.prepend (move (r), &var); - else - val.append (move (r), &var); + if (atype == type::assign) val->assign (move (r), var); + else if (atype == type::prepend) val->prepend (move (r), var); + else val->append (move (r), var); } if (atype == type::assign) atype = type::append; // Append subsequent values. } + else + { + assert (bf); + + if (r.empty ()) // Optional not found. + { + assert (opt); + continue; + } + + // Note: see also import_buildfile(). + // + assert (r.size () == 1); // See import_load() for details. + name& n (r.front ()); + path p (n.dir / n.value); // Should already include extension. + + // Note: similar to parse_include(). + // + // Nuance: we insert this buildfile even with once=false in case it + // gets imported with once=true from another place. + // + if (!root_->root_extra->insert_buildfile (p) && once) + { + l5 ([&]{trace (loc) << "skipping already imported " << p;}); + continue; + } + + // Clear/restore if/switch location. + // + auto g = make_guard ([this, old = condition_] () mutable + { + condition_ = old; + }); + condition_ = nullopt; + + try + { + ifdstream ifs (p); + + auto df = make_diag_frame ( + [this, &loc] (const diag_record& dr) + { + dr << info (loc) << "imported from here"; + }); + + // @@ Do we want to enter this buildfile? What's the harm (one + // benefit is that it will be in dump). But, we currently don't + // out-qualify them, though feels like there is nothing fatal + // in that, just inaccurate. + // + source_buildfile (ifs, + path_name (p), + loc, + false /* default_target */); + } + catch (const io_error& e) + { + fail (loc) << "unable to read imported buildfile " << p << ": " << e; + } + } } next_after_newline (t, tt); @@ -8910,23 +9044,27 @@ namespace build2 return r; } + // file.cxx + // + extern const dir_path std_export_dir; + extern const dir_path alt_export_dir; + void parser:: - process_default_target (token& t) + process_default_target (token& t, const buildfile* bf) { tracer trace ("parser::process_default_target", &path_); // The logic is as follows: if we have an explicit current directory - // target, then that's the default target. Otherwise, we take the - // first target and use it as a prerequisite to create an implicit - // current directory target, effectively making it the default - // target via an alias. If there are no targets in this buildfile, - // then we don't do anything. + // target, then that's the default target. Otherwise, we take the first + // target and use it as a prerequisite to create an implicit current + // directory target, effectively making it the default target via an + // alias. If this is a project root buildfile, then also add exported + // buildfiles. And if there are no targets in this buildfile, then we + // don't do anything (reasonably assuming it's not root). // if (default_target_ == nullptr) // No targets in this buildfile. return; - target& dt (*default_target_); - target* ct ( const_cast ( // Ok (serial execution). ctx->targets.find (dir::static_type, // Explicit current dir target. @@ -8936,35 +9074,169 @@ namespace build2 nullopt, trace))); - if (ct == nullptr) + if (ct != nullptr && ct->decl == target_decl::real) + ; // Existing and not implied. + else { - l5 ([&]{trace (t) << "creating current directory alias for " << dt;}); + target& dt (*default_target_); - // While this target is not explicitly mentioned in the buildfile, we - // say that we behave as if it were. Thus not implied. - // - ct = &ctx->targets.insert (dir::static_type, - scope_->out_path (), - dir_path (), - string (), - nullopt, - target_decl::real, - trace).first; - // Fall through. + if (ct == nullptr) + { + l5 ([&]{trace (t) << "creating current directory alias for " << dt;}); + + // While this target is not explicitly mentioned in the buildfile, we + // say that we behave as if it were. Thus not implied. + // + ct = &ctx->targets.insert (dir::static_type, + scope_->out_path (), + dir_path (), + string (), + nullopt, + target_decl::real, + trace).first; + } + else + ct->decl = target_decl::real; + + ct->prerequisites_state_.store (2, memory_order_relaxed); + ct->prerequisites_.push_back (prerequisite (dt)); } - else if (ct->decl != target_decl::real) + + // See if this is a root buildfile and not in a simple project. + // + if (bf != nullptr && + root_ != nullptr && + root_->root_extra != nullptr && + root_->root_extra->loaded && + *root_->root_extra->project != nullptr && + bf->dir == root_->src_path () && + bf->name == root_->root_extra->buildfile_file.string ()) { - ct->decl = target_decl::real; - // Fall through. - } - else - return; // Existing and not implied. + // See if we have any exported buildfiles. + // + const dir_path& export_dir ( + root_->root_extra->altn ? alt_export_dir : std_export_dir); + + dir_path d (root_->src_path () / export_dir); + if (exists (d)) + { + // Make sure prerequisites are set. + // + ct->prerequisites_state_.store (2, memory_order_relaxed); + + const string& build_ext (root_->root_extra->build_ext); + + // Return true if entered any exported buildfiles. + // + // Note: recursive lambda. + // + auto iterate = [this, &trace, + ct, &build_ext] (const dir_path& d, + const auto& iterate) -> bool + { + bool r (false); + + try + { + for (const dir_entry& e: + dir_iterator (d, dir_iterator::detect_dangling)) + { + switch (e.type ()) + { + case entry_type::directory: + { + r = iterate (d / path_cast (e.path ()), iterate) || r; + break; + } + case entry_type::regular: + { + const path& n (e.path ()); + + if (n.extension () == build_ext) + { + // Similar to above, enter as real. + // + // Note that these targets may already be entered (for + // example, if already imported). + // + const target& bf ( + ctx->targets.insert (buildfile::static_type, + d, + (root_->out_eq_src () + ? dir_path () + : out_src (d, *root_)), + n.base ().string (), + build_ext, + target_decl::real, + trace).first); + + ct->prerequisites_.push_back (prerequisite (bf)); + r = true; + } + + break; + } + case entry_type::unknown: + { + bool sl (e.ltype () == entry_type::symlink); - ct->prerequisites_state_.store (2, memory_order_relaxed); - ct->prerequisites_.emplace_back (prerequisite (dt)); + fail << (sl ? "dangling symlink" : "inaccessible entry") + << ' ' << d / e.path (); + + break; + } + default: + break; + } + } + } + catch (const system_error& e) + { + fail << "unable to iterate over " << d << ": " << e; + } + + return r; + }; + + if (iterate (d, iterate)) + { + // Arrange for the exported buildfiles to be installed, recreating + // subdirectories inside export/. Essentially, we are arranging for + // this: + // + // build/export/buildfile{*}: + // { + // install = buildfile/ + // install.subdirs = true + // } + // + if (cast_false (root_->vars["install.loaded"])) + { + enter_scope es (*this, dir_path (export_dir)); + auto& vars (scope_->target_vars[buildfile::static_type]["*"]); + + // @@ TODO: get cached variables from the module once we have one. + // + { + auto r (vars.insert (*root_->var_pool ().find ("install"))); + + if (r.second) // Already set by the user? + r.first = path_cast (dir_path ("buildfile")); + } + + { + auto r (vars.insert ( + *root_->var_pool (true).find ("install.subdirs"))); + if (r.second) + r.first = true; + } + } + } + } + } } - void parser:: + const buildfile& parser:: enter_buildfile (const path& p, optional out) { tracer trace ("parser::enter_buildfile", &path_); @@ -8984,7 +9256,7 @@ namespace build2 o = out_src (d, *root_); } - ctx->targets.insert ( + return ctx->targets.insert ( move (d), move (o), p.leaf ().base ().string (), diff --git a/libbuild2/parser.hxx b/libbuild2/parser.hxx index d97cd7b..54735d5 100644 --- a/libbuild2/parser.hxx +++ b/libbuild2/parser.hxx @@ -379,15 +379,15 @@ namespace build2 attributes& attributes_top () {return attributes_.back ();} - // Source a stream optionnaly performing the default target processing. - // If the specified path name has a real path, then also enter it as a - // buildfile. + // Source a buildfile as a stream optionally performing the default target + // processing. If the specified path name has a real path, then also enter + // it as a buildfile. // void - source (istream&, - const path_name&, - const location&, - bool default_target); + source_buildfile (istream&, + const path_name&, + const location&, + bool default_target); // The what argument is used in diagnostics (e.g., "expected // instead of ...". @@ -604,11 +604,11 @@ namespace build2 switch_scope (const dir_path& out_base); void - process_default_target (token&); + process_default_target (token&, const buildfile*); // Enter buildfile as a target. // - void + const buildfile& enter_buildfile (const path&, optional out = nullopt); // Lexer. diff --git a/libbuild2/utility-installed.cxx b/libbuild2/utility-installed.cxx index 441e31b..decc71d 100644 --- a/libbuild2/utility-installed.cxx +++ b/libbuild2/utility-installed.cxx @@ -14,6 +14,10 @@ namespace build2 #ifdef BUILD2_INSTALL_LIB const dir_path build_install_lib (BUILD2_INSTALL_LIB); #endif + +#ifdef BUILD2_INSTALL_BUILDFILE + const dir_path build_install_buildfile (BUILD2_INSTALL_BUILDFILE); +#endif } #endif diff --git a/libbuild2/utility-uninstalled.cxx b/libbuild2/utility-uninstalled.cxx index a6bad55..69908f9 100644 --- a/libbuild2/utility-uninstalled.cxx +++ b/libbuild2/utility-uninstalled.cxx @@ -7,4 +7,10 @@ namespace build2 { const bool build_installed = false; const dir_path build_install_lib; // Empty. + +#ifdef BUILD2_INSTALL_BUILDFILE + const dir_path build_install_buildfile (BUILD2_INSTALL_BUILDFILE); +#else + const dir_path build_install_buildfile; // Empty. +#endif } diff --git a/libbuild2/utility.hxx b/libbuild2/utility.hxx index 1e22e7e..43cb904 100644 --- a/libbuild2/utility.hxx +++ b/libbuild2/utility.hxx @@ -186,11 +186,13 @@ namespace build2 LIBBUILD2_SYMEXPORT extern const standard_version build_version; LIBBUILD2_SYMEXPORT extern const string build_version_interface; - // Whether running installed build and, if so, the library installation - // directory (empty otherwise). + // Whether running installed build as well as the library installation + // directory (only if installed, empty otherwise) and the exported buildfile + // installation directory (only if configured, empty otherwise). // LIBBUILD2_SYMEXPORT extern const bool build_installed; LIBBUILD2_SYMEXPORT extern const dir_path build_install_lib; // $install.lib + LIBBUILD2_SYMEXPORT extern const dir_path build_install_buildfile; // $install.buildfile // --[no-]mtime-check // -- cgit v1.1