diff options
Diffstat (limited to 'libbuild2/install')
-rw-r--r-- | libbuild2/install/functions.cxx | 116 | ||||
-rw-r--r-- | libbuild2/install/init.cxx | 374 | ||||
-rw-r--r-- | libbuild2/install/operation.cxx | 373 | ||||
-rw-r--r-- | libbuild2/install/operation.hxx | 64 | ||||
-rw-r--r-- | libbuild2/install/rule.cxx | 899 | ||||
-rw-r--r-- | libbuild2/install/rule.hxx | 209 | ||||
-rw-r--r-- | libbuild2/install/utility.cxx | 295 | ||||
-rw-r--r-- | libbuild2/install/utility.hxx | 59 |
8 files changed, 2068 insertions, 321 deletions
diff --git a/libbuild2/install/functions.cxx b/libbuild2/install/functions.cxx index 5668efe..1de4d3e 100644 --- a/libbuild2/install/functions.cxx +++ b/libbuild2/install/functions.cxx @@ -15,17 +15,125 @@ namespace build2 { function_family f (m, "install"); - // Resolve potentially relative install.* value to an absolute directory - // based on (other) install.* values visible from the calling scope. + // $install.resolve(<dir>[, <rel_base>]) + // + // @@ TODO: add overload to call resolve_file(). + // + // Resolve potentially relative install.* value to an absolute and + // normalized directory based on (other) install.* values visible from + // the calling scope. + // + // If rel_base is specified and is not empty, then make the resulting + // directory relative to it. If rel_base itself is relative, first + // resolve it to an absolute and normalized directory based on install.* + // values. Note that this argument is mandatory if this function is + // called during relocatable installation (install.relocatable is true). + // While you can pass empty directory to suppress this functionality, + // make sure this does not render the result non-relocatable. + // + // As an example, consider an executable that supports loading plugins + // and requires the plugin installation directory to be embedded into + // the executable during the build. The common way to support + // relocatable installations for such cases is to embed a path relative + // to the executable and complete it at runtime. If you would like to + // always use the relative path, regardless of whether the installation + // is relocatable of not, then you can simply always pass rel_base, for + // example: + // + // plugin_dir = $install.resolve($install.lib, $install.bin) + // + // Alternatively, if you would like to continue using absolute paths for + // non-relocatable installations, then you can use something like this: + // + // plugin_dir = $install.resolve($install.lib, ($install.relocatable ? $install.bin : [dir_path] )) + // + // Finally, if you are unable to support relocatable installations, the + // correct way to handle this is NOT to always pass an empty path for + // rel_base but rather assert in root.build that your project does not + // support relocatable installations, for example: + // + // assert (!$install.relocatable) 'relocatable installation not supported' // // Note that this function is not pure. // - f.insert (".resolve", false) += [] (const scope* s, dir_path d) + f.insert (".resolve", false) += [] (const scope* s, + dir_path dir, + optional<dir_path> rel_base) { if (s == nullptr) fail << "install.resolve() called out of scope" << endf; - return resolve_dir (*s, move (d)); + if (!rel_base) + { + const scope& rs (*s->root_scope ()); + + if (cast_false<bool> (rs["install.relocatable"])) + { + fail << "relocatable installation requires relative base " + << "directory" << + info << "pass empty relative base directory if this call does " + << "not affect installation relocatability" << + info << "or add `assert (!$install.relocatable) 'relocatable " + << "installation not supported'` before the call"; + } + } + + return resolve_dir (*s, + move (dir), + rel_base ? move (*rel_base) : dir_path ()); + }; + + // @@ TODO: add $install.chroot(). + + // $install.filter(<path>[, <type>]) + // + // Apply filters from config.install.filter and return true if the + // specified filesystem entry should be installed/uninstalled. Note that + // the entry is specified as an absolute and normalized installation + // path (so not $path($>) but $install.resolve($>)). + // + // The type argument can be one of `regular`, `directory`, or `symlink`. + // If unspecified, either `directory` or `regular` is assumed, based on + // whether path is syntactially a directory (ends with a directory + // separator). + // + // Note that this function is not pure. + // + f.insert (".filter", false) += [] (const scope* s, + path p, + optional<names> ot) + { + if (s == nullptr) + fail << "install.filter() called out of scope" << endf; + + entry_type t; + if (ot) + { + string v (convert<string> (move (*ot))); + + if (v == "regular") t = entry_type::regular; + else if (v == "directory") t = entry_type::directory; + else if (v == "symlink") t = entry_type::symlink; + else throw invalid_argument ("unknown type '" + v + '\''); + } + else + t = p.to_directory () ? entry_type::directory : entry_type::regular; + + // Split into directory and leaf. + // + dir_path d; + if (t == entry_type::directory) + { + d = path_cast<dir_path> (move (p)); + p = path (); // No leaf. + } + else + { + d = p.directory (); + p.make_leaf (); + } + + return filter_entry (*s->root_scope (), d, p, t); }; } } diff --git a/libbuild2/install/init.cxx b/libbuild2/install/init.cxx index 677ee07..3df912f 100644 --- a/libbuild2/install/init.cxx +++ b/libbuild2/install/init.cxx @@ -3,7 +3,7 @@ #include <libbuild2/install/init.hxx> -#include <libbutl/command.mxx> // command_substitute() +#include <libbutl/command.hxx> // command_substitute() #include <libbuild2/scope.hxx> #include <libbuild2/target.hxx> @@ -166,6 +166,8 @@ namespace build2 bool global (*name == '\0'); + auto& vp (rs.var_pool (true /* default */)); // All qualified. + if (spec) { vn = "config.install"; @@ -175,7 +177,7 @@ namespace build2 vn += name; } vn += var; - const variable& vr (rs.var_pool ().insert<CT> (move (vn))); + const variable& vr (vp.insert<CT> (move (vn))); using config::lookup_config; @@ -192,7 +194,7 @@ namespace build2 vn = "install."; vn += name; vn += var; - const variable& vr (rs.var_pool ().insert<T> (move (vn))); + const variable& vr (vp.insert<T> (move (vn))); value& v (rs.assign (vr)); @@ -236,7 +238,7 @@ namespace build2 // This one doesn't have config.* value (only set in a buildfile). // if (!global) - rs.var_pool ().insert<bool> (string ("install.") + n + ".subdirs"); + rs.var_pool (true).insert<bool> (string ("install.") + n + ".subdirs"); } void @@ -250,6 +252,20 @@ namespace build2 context& ctx (rs.ctx); + // Enter module variables (note that init() below enters some more). + // + // The install variable is a path, not dir_path, since it can be used + // to both specify the target directory (to install with the same file + // name) or target file (to install with a different name). And the + // 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 an + // operation variable. + // + auto& ovar (rs.var_pool ().insert<path> ("install", + variable_visibility::target)); + // Register the install function family if this is the first instance of // the install modules. // @@ -258,9 +274,9 @@ namespace build2 // Register our operations. // - rs.insert_operation (install_id, op_install); - rs.insert_operation (uninstall_id, op_uninstall); - rs.insert_operation (update_for_install_id, op_update_for_install); + rs.insert_operation (install_id, op_install, &ovar); + rs.insert_operation (uninstall_id, op_uninstall, &ovar); + rs.insert_operation (update_for_install_id, op_update_for_install, &ovar); } static const path cmd ("install"); @@ -269,24 +285,26 @@ namespace build2 // #define DIR(N, V) static const dir_path dir_##N (V) - DIR (data_root, dir_path ("root")); - DIR (exec_root, dir_path ("root")); + DIR (data_root, dir_path ("root")); + DIR (exec_root, dir_path ("root")); - DIR (sbin, dir_path ("exec_root") /= "sbin"); - DIR (bin, dir_path ("exec_root") /= "bin"); - DIR (lib, (dir_path ("exec_root") /= "lib") /= "<private>"); - DIR (libexec, ((dir_path ("exec_root") /= "libexec") /= "<private>") /= "<project>"); - DIR (pkgconfig, dir_path ("lib") /= "pkgconfig"); + DIR (sbin, dir_path ("exec_root") /= "sbin"); + DIR (bin, dir_path ("exec_root") /= "bin"); + DIR (lib, (dir_path ("exec_root") /= "lib") /= "<private>"); + DIR (libexec, ((dir_path ("exec_root") /= "libexec") /= "<private>") /= "<project>"); + DIR (pkgconfig, dir_path ("lib") /= "pkgconfig"); - DIR (etc, dir_path ("data_root") /= "etc"); - DIR (include, (dir_path ("data_root") /= "include") /= "<private>"); - DIR (share, dir_path ("data_root") /= "share"); - DIR (data, (dir_path ("share") /= "<private>") /= "<project>"); + DIR (etc, dir_path ("data_root") /= "etc"); + DIR (include, (dir_path ("data_root") /= "include") /= "<private>"); + DIR (include_arch, dir_path ("include")); + DIR (share, dir_path ("data_root") /= "share"); + DIR (data, (dir_path ("share") /= "<private>") /= "<project>"); + DIR (buildfile, ((dir_path ("share") /= "build2") /= "export") /= "<project>"); - DIR (doc, ((dir_path ("share") /= "doc") /= "<private>") /= "<project>"); - DIR (legal, dir_path ("doc")); - DIR (man, dir_path ("share") /= "man"); - DIR (man1, dir_path ("man") /= "man1"); + DIR (doc, ((dir_path ("share") /= "doc") /= "<private>") /= "<project>"); + DIR (legal, dir_path ("doc")); + DIR (man, dir_path ("share") /= "man"); + DIR (man1, dir_path ("man") /= "man1"); #undef DIR @@ -312,22 +330,17 @@ namespace build2 // Enter module variables. // - auto& vp (rs.var_pool ()); + rs.var_pool ().insert<bool> ("for_install", variable_visibility::prereq); + + // The rest of the variables we enter are qualified so go straight + // for the public variable pool. + // + auto& vp (rs.var_pool (true /* public */)); // Note that the set_dir() calls below enter some more. // - { - // The install variable is a path, not dir_path, since it can be used - // to both specify the target directory (to install with the same file - // name) or target file (to install with a different name). And the - // way we distinguish between the two is via the presence/absence of - // the trailing directory separator. - // - vp.insert<path> ("install", variable_visibility::target); - vp.insert<bool> ("for_install", variable_visibility::prereq); - vp.insert<string> ("install.mode"); - vp.insert<bool> ("install.subdirs"); - } + vp.insert<string> ("install.mode"); + vp.insert<bool> ("install.subdirs"); // Environment. // @@ -372,23 +385,34 @@ namespace build2 const auto& gr (group_rule_); bs.insert_rule<alias> (perform_install_id, "install.alias", ar); - bs.insert_rule<alias> (perform_uninstall_id, "uninstall.alias", ar); + bs.insert_rule<alias> (perform_uninstall_id, "install.alias", ar); bs.insert_rule<fsdir> (perform_install_id, "install.fsdir", dr); bs.insert_rule<fsdir> (perform_uninstall_id, "install.fsdir", dr); bs.insert_rule<file> (perform_install_id, "install.file", fr); - bs.insert_rule<file> (perform_uninstall_id, "uninstall.file", fr); + bs.insert_rule<file> (perform_uninstall_id, "install.file", fr); - bs.insert_rule<target> (perform_install_id, "install.file", gr); - bs.insert_rule<target> (perform_uninstall_id, "uninstall.file", gr); + // Note: use mtime_target (instead of target) to take precedence over + // the fallback file rules below. + // + // @@ We could fix this by checking the target type in file_rule, + // similar to build2::file_rule. + // + bs.insert_rule<mtime_target> (perform_install_id, "install.group", gr); + bs.insert_rule<mtime_target> (perform_uninstall_id, "install.group", gr); - // Register the fallback file rule for the update-for-install + // Register the fallback file rule for the update-for-[un]install // operation, similar to update. // - rs.global_scope ().insert_rule<mtime_target> ( - perform_install_id, "install.file", file_rule::instance); - } + // @@ Hm, it's a bit fuzzy why we would be updating-for-install + // something outside of any project? + // + scope& gs (rs.global_scope ()); + + gs.insert_rule<mtime_target> (perform_install_id, "install.file", fr); + gs.insert_rule<mtime_target> (perform_uninstall_id, "install.file", fr); + } // Configuration. // @@ -401,7 +425,9 @@ namespace build2 using config::lookup_config; using config::specified_config; - bool s (specified_config (rs, "install")); + // Note: ignore config.install.{scope,manifest} (see below). + // + bool s (specified_config (rs, "install", {"scope", "manifest"})); // Adjust module priority so that the (numerous) config.install.* // values are saved at the end of config.build. @@ -409,6 +435,152 @@ namespace build2 if (s) config::save_module (rs, "install", INT32_MAX); + // config.install.scope + // + // We do not install prerequisites (for example, shared libraries) of + // targets (for example, executables) that belong to projects outside + // of this scope. Valid values are: + // + // project -- project scope + // bundle -- bundle amalgamation + // strong -- strong amalgamation + // weak -- weak amalgamation + // global -- all projects (default) + // + // Note: can only be specified as a global override. + // + { + auto& v (vp.insert<string> ("config.install.scope")); + + // If specified, verify it is a global override. + // + if (lookup l = rs[v]) + { + if (!l.belongs (rs.global_scope ())) + fail << "config.install.scope must be a global override" << + info << "specify !config.install.scope=..."; + } + + config::unsave_variable (rs, v); + } + + // config.install.manifest + // + // Installation manifest. Valid values are a file path or `-` to dump + // the manifest to stdout. + // + // If specified during the install operation, then write the + // information about all the filesystem entries being installed into + // the manifest. If specified during uninstall, then remove the + // filesystem entries according to the manifest as opposed to the + // current build state. In particular, this functionality can be used + // to avoid surprising (and potentially lengthy) updates during + // uninstall that may happen because of changes to system-installed + // dependencies (for example, the compiler or standard library). + // + // @@ TODO: manifest uninstall is still TODO. + // + // Note: there is a single manifest per operation and thus this + // variable can only be specified as a global override. (While it + // could be handy to save this varible in config.build in some + // situations, supporting this will complicate the global override + // case). Note that as a result the manifest file path may not be + // specified in terms of the config.install.* values. + // + // Note also that the manifest is produced even in the dry-run mode. + // However, in this case no directory creation is tracked. + // + // The format of the installation manifest is "JSON lines", that is, + // each line is a JSON text (this makes it possible to reverse the + // order of lines without loading the entire file into memory). For + // example (indented lines indicate line continuations): + // + // {"type":"directory","path":"/tmp/install","mode":"755"} + // {"type":"target","name":"/tmp/libhello/libs{hello}", + // "entries":[ + // {"type":"file","path":"/tmp/install/lib/libhello-1.0.so","mode":"755"}, + // {"type":"symlink","path":"/tmp/install/lib/libhello.so","target":"libhello-1.0.so"}]} + // + // Each line is a serialization of one of the following non-abstract + // C++ structs: + // + // struct entry // abstract + // { + // enum {directory, file, symlink, target} type; + // }; + // + // struct filesystem_entry: entry // abstract + // { + // path path; + // }; + // + // struct directory_entry: filesystem_entry + // { + // string mode; + // }; + // + // struct file_entry: filesystem_entry + // { + // string mode; + // }; + // + // struct symlink_entry: filesystem_entry + // { + // path target; + // }; + // + // struct target_entry: entry + // { + // string name; + // vector<filesystem_entry*> entries; + // }; + // + // New entry types may be added later. Additional entry members may be + // added later to existing entries after the existing members. + // + // If installation is relocatable (see config.install.relocatable) and + // the installation manifest file path is inside config.install.root + // (including chroot), then absolute filesystem_entry::path's are + // saved as relative to the manifest file's directory (note that + // symlink_entry::target cannot be absolute in relocatable + // installation). + // + { + auto& v (vp.insert<path> ("config.install.manifest")); + + // If specified, verify it is a global override. + // + if (lookup l = rs[v]) + { + if (!l.belongs (rs.global_scope ())) + fail << "config.install.manifest must be a global override" << + info << "specify !config.install.manifest=..."; + } + + config::unsave_variable (rs, v); + } + + // Support for relocatable install. + // + // Note that it is false by default since supporting relocatable + // installation may require extra effort and not all projects may + // support it. A project that is known not to support it should assert + // this fact in its root.build, for example: + // + // assert (!$install.relocatable) 'relocatable installation not supported' + // + { + auto& var (vp.insert<bool> ( "install.relocatable")); + auto& cvar (vp.insert<bool> ("config.install.relocatable")); + + value& v (rs.assign (var)); + + // Note: unlike other variables, for ease of assertion set it to + // false if no config.install.* is specified. + // + v = s && cast_false<bool> (lookup_config (rs, cvar, false)); + } + // Support for private install (aka poor man's Flatpack). // const dir_path* p; @@ -446,35 +618,109 @@ namespace build2 } } - // Global config.install.* values. + // config.install.filter // - set_dir (s, p, rs, "", abs_dir_path (), false, "644", "755", cmd); - - set_dir (s, p, rs, "root", abs_dir_path ()); - - set_dir (s, p, rs, "data_root", dir_data_root); - set_dir (s, p, rs, "exec_root", dir_exec_root, false, "755"); + // Installation filterting. The value of this variable is a list of + // key-value pairs that specify the filesystem entries to include or + // exclude from the installation. For example, the following filters + // will omit installing headers and static libraries (notice the + // quoting of the wildcard). + // + // config.install.filter='include/@false "*.a"@false' + // + // The key in each pair is a file or directory path or a path wildcard + // pattern. If a key is relative and contains a directory component or + // is a directory, then it is treated relative to the corresponding + // config.install.* location. Otherwise (simple path, normally a + // pattern), it is matched against the leaf of any path. Note that if + // an absolute path is specified, it should be without the + // config.install.chroot prefix. + // + // The value in each pair is either true (include) or false (exclude). + // The filters are evaluated in the order specified and the first + // match that is found determines the outcome. If no match is found, + // the default is to include. For a directory, while false means + // exclude all the sub-paths inside this directory, true does not mean + // that all the sub-paths will be included wholesale. Rather, the + // matched component of the sub-path is treated as included with the + // rest of the components matched against the following + // sub-filters. For example: + // + // config.install.filter=' + // include/x86_64-linux-gnu/@true + // include/x86_64-linux-gnu/details/@false + // include/@false' + // + // The true or false value may be followed by comma and the `symlink` + // modifier to only apply to symlink filesystem entries. For example: + // + // config.install.filter='"*.so"@false,symlink' + // + // Note that this mechanism only affects what gets physically copied + // to the installation directory without affecting what gets built for + // install or the view of what gets installed at the buildfile level. + // For example, given the `include/@false *.a@false` filters, static + // libraries will still be built (unless arranged not to with + // config.bin.lib) and the pkg-config files will still end up with -I + // options pointing to the header installation directory. Note also + // that this mechanism applies to both install and uninstall + // operations. + // + // If you are familiar with the Debian or Fedora packaging, this + // mechanism is somewhat similar to (and can be used for a similar + // purpose as) the Debian's .install files and Fedora's %files spec + // file sections that are used to split the installation into multiple + // binary packages. + // + { + auto& var (vp.insert<filters> ( "install.filter")); + auto& cvar (vp.insert<filters> ("config.install.filter")); - set_dir (s, p, rs, "sbin", dir_sbin); - set_dir (s, p, rs, "bin", dir_bin); - set_dir (s, p, rs, "lib", dir_lib); - set_dir (s, p, rs, "libexec", dir_libexec); - set_dir (s, p, rs, "pkgconfig", dir_pkgconfig, false, "644"); + value& v (rs.assign (var)); - set_dir (s, p, rs, "etc", dir_etc); - set_dir (s, p, rs, "include", dir_include); - set_dir (s, p, rs, "share", dir_share); - set_dir (s, p, rs, "data", dir_data); + if (s) + { + if (lookup l = lookup_config (rs, cvar, nullptr)) + v = cast<filters> (l); + } + } - set_dir (s, p, rs, "doc", dir_doc); - set_dir (s, p, rs, "legal", dir_legal); - set_dir (s, p, rs, "man", dir_man); - set_dir (s, p, rs, "man1", dir_man1); + // Global config.install.* values. + // + set_dir (s, p, rs, "", abs_dir_path (), false, "644", "755", cmd); + + set_dir (s, p, rs, "root", abs_dir_path ()); + + set_dir (s, p, rs, "data_root", dir_data_root); + set_dir (s, p, rs, "exec_root", dir_exec_root, false, "755"); + + set_dir (s, p, rs, "sbin", dir_sbin); + set_dir (s, p, rs, "bin", dir_bin); + set_dir (s, p, rs, "lib", dir_lib); + set_dir (s, p, rs, "libexec", dir_libexec); + set_dir (s, p, rs, "pkgconfig", dir_pkgconfig, false, "644"); + + set_dir (s, p, rs, "etc", dir_etc); + set_dir (s, p, rs, "include", dir_include); + 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); + set_dir (s, p, rs, "man", dir_man); + set_dir (s, p, rs, "man1", dir_man1); } // Configure "installability" for built-in target types. // + // Note that for exe{} we also set explicit 755 mode in case it gets + // installed somewhere else where the default is not 755 (for example to + // libexec/, which on Debian has the 644 mode). + // install_path<exe> (bs, dir_path ("bin")); + install_mode<exe> (bs, "755"); install_path<doc> (bs, dir_path ("doc")); install_path<legal> (bs, dir_path ("legal")); install_path<man> (bs, dir_path ("man")); diff --git a/libbuild2/install/operation.cxx b/libbuild2/install/operation.cxx index 54d5b9a..029a5f6 100644 --- a/libbuild2/install/operation.cxx +++ b/libbuild2/install/operation.cxx @@ -3,8 +3,15 @@ #include <libbuild2/install/operation.hxx> +#include <sstream> + +#include <libbuild2/scope.hxx> +#include <libbuild2/target.hxx> +#include <libbuild2/context.hxx> #include <libbuild2/variable.hxx> +#include <libbuild2/install/utility.hxx> + using namespace std; using namespace butl; @@ -12,23 +19,360 @@ namespace build2 { namespace install { +#ifndef BUILD2_BOOTSTRAP + context_data:: + context_data (const path* mf) + : manifest_name (mf), + manifest_os (mf != nullptr + ? open_file_or_stdout (manifest_name, manifest_ofs) + : manifest_ofs), + manifest_autorm (manifest_ofs.is_open () ? *mf : path ()), + manifest_json (manifest_os, 0 /* indentation */) + { + if (manifest_ofs.is_open ()) + { + manifest_file = *mf; + manifest_file.complete (); + manifest_file.normalize (); + } + } + + static path + relocatable_path (context_data& d, const target& t, path p) + { + // This is both inefficient (re-detecting relocatable manifest for every + // path) and a bit dirty (if multiple projects are being installed with + // different install.{relocatable,root} values, we may end up producing + // some paths relative and some absolute). But doing either of these + // properly is probably not worth the extra complexity. + // + if (!d.manifest_file.empty ()) // Not stdout. + { + const scope& rs (t.root_scope ()); + + if (cast_false<bool> (rs["install.relocatable"])) + { + // Note: install.root is abs_dir_path so absolute and normalized. + // + const dir_path* root (cast_null<dir_path> (rs["install.root"])); + if (root == nullptr) + fail << "unknown installation root directory in " << rs << + info << "did you forget to specify config.install.root?"; + + // The manifest path would include chroot so if used, we need to add + // it to root and the file path (we could also strip it, but then + // making it absolute gets tricky on Windows). + // + dir_path md (d.manifest_file.directory ()); + + if (md.sub (chroot_path (rs, *root))) // Inside installation root. + { + p = chroot_path (rs, p); + try + { + p = p.relative (md); + } + catch (const invalid_path&) + { + fail << "unable to make filesystem entry path " << p + << " relative to " << md << + info << "required for relocatable installation manifest"; + } + } + } + } + + return p; + } + + // Serialize current target and, if tgt is not NULL, start the new target. + // + // Note that we always serialize directories as top-level entries. And + // theoretically we can end up "splitting" a target with a directory + // creation. For example, if some files that belong to the target are + // installed into subdirectories that have not yet been created. So we + // have to cache the information for the current target in memory and only + // flush it once we see the next target (or the end). + // + // You may be wondering why not just serialize directories as target + // entries. While we could do that, it's not quite correct conceptually, + // since this would be the first of potentially many targets that caused + // the directory's creation. To put it another way, while files and + // symlinks belong to tragets, directories do not. + // + static void + manifest_flush_target (context_data& d, const target* tgt) + { + if (d.manifest_target != nullptr) + { + assert (!d.manifest_target_entries.empty ()); + + // Target name format is the same as in the structured result output. + // + ostringstream os; + stream_verb (os, stream_verbosity (1, 0)); + os << *d.manifest_target; + + try + { + auto& s (d.manifest_json); + + s.begin_object (); + s.member ("type", "target"); + s.member ("name", os.str ()); + s.member_name ("entries"); + s.begin_array (); + + for (const auto& e: d.manifest_target_entries) + { + path p (relocatable_path (d, *d.manifest_target, move (e.path))); + + s.begin_object (); + + if (e.target.empty ()) + { + s.member ("type", "file"); + s.member ("path", p.string ()); + s.member ("mode", e.mode); + } + else + { + s.member ("type", "symlink"); + s.member ("path", p.string ()); + s.member ("target", e.target.string ()); + } + + s.end_object (); + } + + s.end_array (); // entries member + s.end_object (); // target object + } + catch (const json::invalid_json_output& e) + { + fail << "invalid " << d.manifest_name << " json output: " << e; + } + catch (const io_error& e) + { + fail << "unable to write to " << d.manifest_name << ": " << e; + } + + d.manifest_target_entries.clear (); + } + + d.manifest_target = tgt; + } + + void context_data:: + manifest_install_d (context& ctx, + const target& tgt, + const dir_path& dir, + const string& mode) + { + auto& d (*static_cast<context_data*> (ctx.current_inner_odata.get ())); + + if (d.manifest_name.path != nullptr) + { + try + { + auto& s (d.manifest_json); + + // If we moved to the next target, flush the current one. + // + if (d.manifest_target != &tgt) + manifest_flush_target (d, nullptr); + + s.begin_object (); + s.member ("type", "directory"); + s.member ("path", relocatable_path (d, tgt, dir).string ()); + s.member ("mode", mode); + s.end_object (); + } + catch (const json::invalid_json_output& e) + { + fail << "invalid " << d.manifest_name << " json output: " << e; + } + catch (const io_error& e) + { + fail << "unable to write to " << d.manifest_name << ": " << e; + } + } + } + + void context_data:: + manifest_install_f (context& ctx, + const target& tgt, + const dir_path& dir, + const path& name, + const string& mode) + { + auto& d (*static_cast<context_data*> (ctx.current_inner_odata.get ())); + + if (d.manifest_name.path != nullptr) + { + if (d.manifest_target != &tgt) + manifest_flush_target (d, &tgt); + + d.manifest_target_entries.push_back ( + manifest_target_entry {dir / name, mode, path ()}); + } + } + + void context_data:: + manifest_install_l (context& ctx, + const target& tgt, + const path& link_target, + const dir_path& dir, + const path& link) + { + auto& d (*static_cast<context_data*> (ctx.current_inner_odata.get ())); + + if (d.manifest_name.path != nullptr) + { + if (d.manifest_target != &tgt) + manifest_flush_target (d, &tgt); + + d.manifest_target_entries.push_back ( + manifest_target_entry {dir / link, "", link_target}); + } + } + + static void + manifest_close (context& ctx) + { + auto& d (*static_cast<context_data*> (ctx.current_inner_odata.get ())); + + if (d.manifest_name.path != nullptr) + { + try + { + manifest_flush_target (d, nullptr); + + d.manifest_os << '\n'; // Final newline. + + if (d.manifest_ofs.is_open ()) + { + d.manifest_ofs.close (); + d.manifest_autorm.cancel (); + } + } + catch (const json::invalid_json_output& e) + { + fail << "invalid " << d.manifest_name << " json output: " << e; + } + catch (const io_error& e) + { + fail << "unable to write to " << d.manifest_name << ": " << e; + } + } + } +#else + context_data:: + context_data (const path*) + { + } + + void context_data:: + manifest_install_d (context&, + const target&, + const dir_path&, + const string&) + { + } + + void context_data:: + manifest_install_f (context&, + const target&, + const dir_path&, + const path&, + const string&) + { + } + + void context_data:: + manifest_install_l (context&, + const target&, + const path&, + const dir_path&, + const path&) + { + } + + static void + manifest_close (context&) + { + } +#endif + static operation_id - install_pre (const values& params, meta_operation_id mo, const location& l) + pre_install (context&, + const values&, + meta_operation_id mo, + const location&) { - if (!params.empty ()) - fail (l) << "unexpected parameters for operation install"; + // Run update as a pre-operation, unless we are disfiguring. + // + return mo != disfigure_id ? update_id : 0; + } + static operation_id + pre_uninstall (context&, + const values&, + meta_operation_id mo, + const location&) + { // Run update as a pre-operation, unless we are disfiguring. // return mo != disfigure_id ? update_id : 0; } + static void + install_pre (context& ctx, + const values& params, + bool inner, + const location& l) + { + if (!params.empty ()) + fail (l) << "unexpected parameters for operation install"; + + if (inner) + { + // See if we need to write the installation manifest. + // + // Note: go straight for the public variable pool. + // + const path* mf ( + cast_null<path> ( + ctx.global_scope[*ctx.var_pool.find ("config.install.manifest")])); + + // Note that we cannot calculate whether the manifest should use + // relocatable (relative) paths once here since we don't know the + // value of config.install.root. + + ctx.current_inner_odata = context::current_data_ptr ( + new context_data (mf), + [] (void* p) {delete static_cast<context_data*> (p);}); + } + } + + static void + install_post (context& ctx, const values&, bool inner) + { + if (inner) + manifest_close (ctx); + } + // Note that we run both install and uninstall serially. The reason for // this is all the fuzzy things we are trying to do like removing empty // outer directories if they are empty. If we do this in parallel, then // those things get racy. Also, since all we do here is creating/removing // 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, @@ -38,9 +382,12 @@ namespace build2 "installed", "has nothing to install", // We cannot "be installed". execution_mode::first, - 0 /* concurrency */, // Run serially. - &install_pre, + 0 /* concurrency */, // Run serially. + false /* keep_going */, // Stop on first error. + &pre_install, nullptr, + &install_pre, + &install_post, nullptr, nullptr }; @@ -63,8 +410,11 @@ namespace build2 "uninstalled", "is not installed", execution_mode::last, - 0 /* concurrency */, // Run serially - &install_pre, + 0 /* concurrency */, // Run serially. + false /* keep_going */, // Stop on first error. + &pre_uninstall, + nullptr, + nullptr, nullptr, nullptr, nullptr @@ -82,8 +432,11 @@ namespace build2 op_update.name_done, op_update.mode, op_update.concurrency, - op_update.pre, - op_update.post, + op_update.keep_going, + op_update.pre_operation, + op_update.post_operation, + op_update.operation_pre, + op_update.operation_post, op_update.adhoc_match, op_update.adhoc_apply }; diff --git a/libbuild2/install/operation.hxx b/libbuild2/install/operation.hxx index c1f5416..bd818b4 100644 --- a/libbuild2/install/operation.hxx +++ b/libbuild2/install/operation.hxx @@ -4,10 +4,15 @@ #ifndef LIBBUILD2_INSTALL_OPERATION_HXX #define LIBBUILD2_INSTALL_OPERATION_HXX +#ifndef BUILD2_BOOTSTRAP +# include <libbutl/json/serializer.hxx> +#endif + #include <libbuild2/types.hxx> #include <libbuild2/utility.hxx> #include <libbuild2/operation.hxx> +#include <libbuild2/filesystem.hxx> // auto_rmfile namespace build2 { @@ -16,6 +21,65 @@ namespace build2 extern const operation_info op_install; extern const operation_info op_uninstall; extern const operation_info op_update_for_install; + + // Set as context::current_inner_odata during the install/uninstall inner + // operations. + // + struct context_data + { + // Manifest. + // +#ifndef BUILD2_BOOTSTRAP + path manifest_file; // Absolute and normalized, empty if `-`. + path_name manifest_name; // Original path/name. + ofdstream manifest_ofs; + ostream& manifest_os; + auto_rmfile manifest_autorm; + butl::json::stream_serializer manifest_json; + const target* manifest_target = nullptr; // Target being installed. + struct manifest_target_entry + { + build2::path path; + string mode; + build2::path target; + }; + vector<manifest_target_entry> manifest_target_entries; +#endif + + // The following manifest_install_[dfl]() functions correspond to (and + // are called from) file_rule::install_[dfl](). + + // install -d -m <mode> <dir> + // + static void + manifest_install_d (context&, + const target&, + const dir_path& dir, + const string& mode); + + // install -m <mode> <file> <dir>/<name> + // + static void + manifest_install_f (context&, + const target& file, + const dir_path& dir, + const path& name, + const string& mode); + + // install -l <link_target> <dir>/<link> + // + static void + manifest_install_l (context&, + const target&, + const path& link_target, + const dir_path& dir, + const path& link); + + // Constructor. + // + explicit + context_data (const path* manifest); + }; } } diff --git a/libbuild2/install/rule.cxx b/libbuild2/install/rule.cxx index 388400a..1aa21d0 100644 --- a/libbuild2/install/rule.cxx +++ b/libbuild2/install/rule.cxx @@ -4,7 +4,7 @@ #include <libbuild2/install/rule.hxx> #include <libbuild2/install/utility.hxx> // resolve_dir() declaration -#include <libbutl/filesystem.mxx> // dir_exists(), file_exists() +#include <libbutl/filesystem.hxx> // dir_exists(), file_exists() #include <libbuild2/scope.hxx> #include <libbuild2/target.hxx> @@ -13,6 +13,8 @@ #include <libbuild2/filesystem.hxx> #include <libbuild2/diagnostics.hxx> +#include <libbuild2/install/operation.hxx> + using namespace std; using namespace butl; @@ -37,12 +39,28 @@ namespace build2 return r.simple () && r.string () == "false" ? nullptr : &r; } + // Note that the below rules are called for both install and + // update-for-install. + // + // @@ TODO: we clearly need a module class. + // + static inline const variable& + var_install (const scope& rs) + { + context& ctx (rs.ctx); + + return *rs.root_extra->operations[ + (ctx.current_outer_oif != nullptr + ? ctx.current_outer_oif + : ctx.current_inner_oif)->id].ovar; + } + // alias_rule // const alias_rule alias_rule::instance; bool alias_rule:: - match (action, target&, const string&) const + match (action, target&) const { // We always match. // @@ -53,33 +71,57 @@ namespace build2 return true; } - const target* alias_rule:: - filter (action a, const target& t, prerequisite_iterator& i) const + pair<const target*, uint64_t> alias_rule:: + filter (const scope* is, + action a, const target& t, prerequisite_iterator& i, + match_extra& me) const { assert (i->member == nullptr); - return filter (a, t, i->prerequisite); + return filter (is, a, t, i->prerequisite, me); } - const target* alias_rule:: - filter (action, const target& t, const prerequisite& p) const + pair<const target*, uint64_t> alias_rule:: + filter (const scope* is, + action, const target& t, const prerequisite& p, + match_extra&) const { - return &search (t, p); + const target& pt (search (t, p)); + const uint64_t options (match_extra::all_options); // No definition. + return make_pair (is == nullptr || pt.in (*is) ? &pt : nullptr, options); } recipe alias_rule:: - apply (action a, target& t) const + apply (action a, target& t, match_extra& me) const + { + return apply_impl (a, t, me); + } + + recipe alias_rule:: + apply (action, target&) const + { + assert (false); // Never called. + return nullptr; + } + + recipe alias_rule:: + apply_impl (action a, target& t, match_extra& me, bool reapply) const { tracer trace ("install::alias_rule::apply"); + assert (!reapply || a.operation () != update_id); + // Pass-through to our installable prerequisites. // // @@ Shouldn't we do match in parallel (here and below)? // - auto& pts (t.prerequisite_targets[a]); + optional<const scope*> is; // Installation scope (resolve lazily). + auto& pts (t.prerequisite_targets[a]); auto pms (group_prerequisite_members (a, t, members_mode::never)); for (auto i (pms.begin ()), e (pms.end ()); i != e; ++i) { + // NOTE: see essentially the same logic in reapply_impl() below. + // const prerequisite& p (i->prerequisite); // Ignore excluded. @@ -100,13 +142,20 @@ namespace build2 // Note: we assume that if the filter enters the group, then it // iterates over all its members. // - const target* pt (filter (a, t, i)); + if (!is) + is = a.operation () != update_id ? install_scope (t) : nullptr; + + pair<const target*, uint64_t> fr (filter (*is, a, t, i, me)); + + const target* pt (fr.first); + uint64_t options (fr.second); + + lookup l; + if (pt == nullptr) { l5 ([&]{trace << "ignoring " << p << " (filtered out)";}); - continue; } - // Check if this prerequisite is explicitly "not installable", that // is, there is the 'install' variable and its value is false. // @@ -118,64 +167,108 @@ namespace build2 // // Note: not the same as lookup_install() above. // - auto l ((*pt)["install"]); - if (l && cast<path> (l).string () == "false") + else if ((l = (*pt)[var_install (*p.scope.root_scope ())]) && + cast<path> (l).string () == "false") { l5 ([&]{trace << "ignoring " << *pt << " (not installable)";}); - continue; + pt = nullptr; } - // If this is not a file-based target (e.g., a target group such as // libu{}) then ignore it if there is no rule to install. // - if (pt->is_a<file> ()) - build2::match (a, *pt); - else if (!try_match (a, *pt).first) + else if (pt->is_a<file> ()) + { + match_sync (a, *pt, options); + } + else if (!try_match_sync (a, *pt, options).first) { l5 ([&]{trace << "ignoring " << *pt << " (no rule)";}); pt = nullptr; } - if (pt != nullptr) - pts.push_back (prerequisite_target (pt, pi)); + if (pt != nullptr || reapply) + { + // Use auxiliary data for a NULL entry to distinguish between + // filtered out (1) and ignored for other reasons (0). + // + pts.push_back ( + prerequisite_target (pt, pi, fr.first == nullptr ? 1 : 0)); + } } return default_recipe; } - // fsdir_rule - // - const fsdir_rule fsdir_rule::instance; - - bool fsdir_rule:: - match (action, target&, const string&) const + void alias_rule:: + reapply_impl (action a, target& t, match_extra& me) const { - // We always match. - // - // Note that we are called both as the outer part during the update-for- - // un/install pre-operation and as the inner part during the un/install - // operation itself. - // - return true; - } + tracer trace ("install::alias_rule::reapply"); - recipe fsdir_rule:: - apply (action a, target& t) const - { - // If this is outer part of the update-for-un/install, delegate to the - // default fsdir rule. Otherwise, this is a noop (we don't install - // fsdir{}). - // - // For now we also assume we don't need to do anything for prerequisites - // (the only sensible prerequisite of fsdir{} is another fsdir{}). + assert (a.operation () != update_id); + + optional<const scope*> is; + + // Iterate over prerequisites and prerequisite targets in parallel. // - if (a.operation () == update_id) + auto& pts (t.prerequisite_targets[a]); + size_t j (0), n (pts.size ()), en (0); + + auto pms (group_prerequisite_members (a, t, members_mode::never)); + for (auto i (pms.begin ()), e (pms.end ()); + i != e && j != n; + ++i, ++j, ++en) { - match_inner (a, t); - return &execute_inner; + // The same logic as in apply() above except that we skip + // prerequisites that were not filtered out. + // + const prerequisite& p (i->prerequisite); + + include_type pi (include (a, t, p)); + if (!pi) + continue; + + if (p.proj) + continue; + + prerequisite_target& pto (pts[j]); + + if (pto.target != nullptr || pto.data == 0) + continue; + + if (!is) + is = a.operation () != update_id ? install_scope (t) : nullptr; + + pair<const target*, uint64_t> fr (filter (*is, a, t, i, me)); + + const target* pt (fr.first); + uint64_t options (fr.second); + + lookup l; + + if (pt == nullptr) + { + l5 ([&]{trace << "ignoring " << p << " (filtered out)";}); + } + else if ((l = (*pt)[var_install (*p.scope.root_scope ())]) && + cast<path> (l).string () == "false") + { + l5 ([&]{trace << "ignoring " << *pt << " (not installable)";}); + pt = nullptr; + } + else if (pt->is_a<file> ()) + { + match_sync (a, *pt, options); + } + else if (!try_match_sync (a, *pt, options).first) + { + l5 ([&]{trace << "ignoring " << *pt << " (no rule)";}); + pt = nullptr; + } + + pto = prerequisite_target (pt, pi, fr.first == nullptr ? 1 : 0); } - else - return noop_recipe; + + assert (en == n); // Did not call apply() with true for reapply? } // group_rule @@ -183,20 +276,46 @@ namespace build2 const group_rule group_rule::instance (false /* see_through_only */); bool group_rule:: - match (action a, target& t, const string& h) const + match (action a, target& t) const { - return (!see_through || t.type ().see_through) && - alias_rule::match (a, t, h); + return (!see_through_only || t.type ().see_through ()) && + alias_rule::match (a, t); } - const target* group_rule:: - filter (action, const target&, const target& m) const + bool group_rule:: + filter (action, const target&, const target&) const { - return &m; + return true; + } + + pair<const target*, uint64_t> group_rule:: + filter (const scope* is, + action, const target& t, const prerequisite& p, + match_extra&) const + { + const uint64_t options (match_extra::all_options); // No definition. + pair<const target*, uint64_t> r (nullptr, options); + + // The same logic as in file_rule::filter() below. + // + if (p.is_a<exe> ()) + { + const scope& rs (*p.scope.root_scope ()); + + if (p.vars.empty () || + cast_empty<path> (p.vars[var_install (rs)]).string () != "true") + return r; + } + + const target& pt (search (t, p)); + if (is == nullptr || pt.in (*is)) + r.first = &pt; + + return r; } recipe group_rule:: - apply (action a, target& t) const + apply (action a, target& t, match_extra& me) const { tracer trace ("install::group_rule::apply"); @@ -204,7 +323,7 @@ namespace build2 // // Remember that we are called twice: first during update for install // (pre-operation) and then during install. During the former, we rely - // on the normall update rule to resolve the group members. During the + // on the normal update rule to resolve the group members. During the // latter, there will be no rule to do this but the group will already // have been resolved by the pre-operation. // @@ -214,23 +333,23 @@ namespace build2 ? resolve_members (a, t) : t.group_members (a)); - if (gv.members != nullptr) + if (gv.members != nullptr && gv.count != 0) { - auto& pts (t.prerequisite_targets[a]); + const scope& rs (t.root_scope ()); + auto& pts (t.prerequisite_targets[a]); for (size_t i (0); i != gv.count; ++i) { - const target* m (gv.members[i]); + const target* mt (gv.members[i]); - if (m == nullptr) + if (mt == nullptr) continue; // Let a customized rule have its say. // - const target* mt (filter (a, t, *m)); - if (mt == nullptr) + if (!filter (a, t, *mt)) { - l5 ([&]{trace << "ignoring " << *m << " (filtered out)";}); + l5 ([&]{trace << "ignoring " << *mt << " (filtered out)";}); continue; } @@ -239,21 +358,21 @@ namespace build2 // // Note: not the same as lookup_install() above. // - auto l ((*mt)["install"]); + auto l ((*mt)[var_install (rs)]); if (l && cast<path> (l).string () == "false") { l5 ([&]{trace << "ignoring " << *mt << " (not installable)";}); continue; } - build2::match (a, *mt); + match_sync (a, *mt); pts.push_back (mt); // Never ad hoc. } } // Delegate to the base rule. // - return alias_rule::apply (a, t); + return alias_rule::apply (a, t, me); } @@ -262,7 +381,7 @@ namespace build2 const file_rule file_rule::instance; bool file_rule:: - match (action, target&, const string&) const + match (action, target&) const { // We always match, even if this target is not installable (so that we // can ignore it; see apply()). @@ -270,25 +389,73 @@ namespace build2 return true; } - const target* file_rule:: - filter (action a, const target& t, prerequisite_iterator& i) const + bool file_rule:: + filter (action, const target&, const target&) const + { + return true; + } + + pair<const target*, uint64_t> file_rule:: + filter (const scope* is, + action a, const target& t, prerequisite_iterator& i, + match_extra& me) const { assert (i->member == nullptr); - return filter (a, t, i->prerequisite); + return filter (is, a, t, i->prerequisite, me); } - const target* file_rule:: - filter (action, const target& t, const prerequisite& p) const + pair<const target*, uint64_t> file_rule:: + filter (const scope* is, + action, const target& t, const prerequisite& p, + match_extra&) const { + const uint64_t options (match_extra::all_options); // No definition. + pair<const target*, uint64_t> r (nullptr, options); + + // See also group_rule::filter() with identical semantics. + // + if (p.is_a<exe> ()) + { + const scope& rs (*p.scope.root_scope ()); + + // Note that while include() checks for install=false, here we need to + // check for explicit install=true. We could have re-used the lookup + // performed by include(), but then we would have had to drag it + // through and also diagnose any invalid values. + // + if (p.vars.empty () || + cast_empty<path> (p.vars[var_install (rs)]).string () != "true") + return r; + } + const target& pt (search (t, p)); - return pt.in (t.root_scope ()) ? &pt : nullptr; + if (is == nullptr || pt.in (*is)) + r.first = &pt; + + return r; } recipe file_rule:: - apply (action a, target& t) const + apply (action a, target& t, match_extra& me) const + { + recipe r (apply_impl (a, t, me)); + return r != nullptr ? move (r) : noop_recipe; + } + + recipe file_rule:: + apply (action, target&) const + { + assert (false); // Never called. + return nullptr; + } + + recipe file_rule:: + apply_impl (action a, target& t, match_extra& me, bool reapply) const { tracer trace ("install::file_rule::apply"); + assert (!reapply || a.operation () != update_id); + // Note that we are called both as the outer part during the update-for- // un/install pre-operation and as the inner part during the un/install // operation itself. @@ -296,10 +463,10 @@ namespace build2 // In both cases we first determine if the target is installable and // return noop if it's not. Otherwise, in the first case (update-for- // un/install) we delegate to the normal update and in the second - // (un/install) -- perform the test. + // (un/install) -- perform the install. // if (!lookup_install<path> (t, "install")) - return noop_recipe; + return empty_recipe; // In both cases, the next step is to search, match, and collect all the // installable prerequisites. @@ -308,17 +475,45 @@ namespace build2 // (actual update). We used to do this after matching the prerequisites // but the inner rule may provide some rule-specific information (like // the target extension for exe{}) that may be required during the - // prerequisite search (like the base name for in{}). + // prerequisite search (like the base name for in{}; this no longer + // reproduces likely due to the changes to exe{} extension derivation + // but a contrived arrangement can still be made to trigger this). + // + // But then we discovered that doing this before the prerequisites messes + // up with the for-install signaling. Specifically, matching the + // prerequisites may signal that they are being updated for install, + // for example, for a library via a metadata library used in a moc + // recipe. While matching the inner rule may trigger updating during + // match of such prerequisites, for example, a source file generated by + // that moc recipe that depends on this metadata library. If we match + // prerequisites before, then the library that is pulled by the metadata + // library will be updated before we had a chance to signal that it + // should be updated for install. // + // To try to accommodate both cases (as best as we can) we now split the + // inner rule match into two steps: we do the match before and apply + // after. This allows rules that deal with tricky prerequisites like + // in{} to assign the target path in match() instead of apply() (see + // in::rule, for example). + // +#if 0 optional<bool> unchanged; if (a.operation () == update_id) unchanged = match_inner (a, t, unmatch::unchanged).first; +#else + action ia (a.inner_action ()); + if (a.operation () == update_id) + match_only_sync (ia, t); +#endif - auto& pts (t.prerequisite_targets[a]); + optional<const scope*> is; // Installation scope (resolve lazily). + auto& pts (t.prerequisite_targets[a]); auto pms (group_prerequisite_members (a, t, members_mode::never)); for (auto i (pms.begin ()), e (pms.end ()); i != e; ++i) { + // NOTE: see essentially the same logic in reapply_impl() below. + // const prerequisite& p (i->prerequisite); // Ignore excluded. @@ -339,26 +534,33 @@ namespace build2 // Note: we assume that if the filter enters the group, then it // iterates over all its members. // - const target* pt (filter (a, t, i)); + if (!is) + is = a.operation () != update_id ? install_scope (t) : nullptr; + + pair<const target*, uint64_t> fr (filter (*is, a, t, i, me)); + + const target* pt (fr.first); + uint64_t options (fr.second); + + lookup l; + if (pt == nullptr) { l5 ([&]{trace << "ignoring " << p << " (filtered out)";}); - continue; } - + // // See if we were explicitly instructed not to touch this target (the // same semantics as in alias_rule). // // Note: not the same as lookup_install() above. // - auto l ((*pt)["install"]); - if (l && cast<path> (l).string () == "false") + else if ((l = (*pt)[var_install (*p.scope.root_scope ())]) && + cast<path> (l).string () == "false") { l5 ([&]{trace << "ignoring " << *pt << " (not installable)";}); - continue; + pt = nullptr; } - - if (pt->is_a<file> ()) + else if (pt->is_a<file> ()) { // If the matched rule returned noop_recipe, then the target state // is set to unchanged as an optimization. Use this knowledge to @@ -366,19 +568,36 @@ namespace build2 // when updating static installable content (headers, documentation, // etc). // - if (build2::match (a, *pt, unmatch::unchanged).first) + // Regarding options, the expectation here is that they are not used + // for the update operation. And for install/uninstall, if they are + // used, then they don't effect whether the target is unchanged. All + // feels reasonable. + // + if (match_sync (a, *pt, unmatch::unchanged, options).first) pt = nullptr; } - else if (!try_match (a, *pt).first) + else if (!try_match_sync (a, *pt, options).first) { l5 ([&]{trace << "ignoring " << *pt << " (no rule)";}); pt = nullptr; } - if (pt != nullptr) - pts.push_back (prerequisite_target (pt, pi)); + if (pt != nullptr || reapply) + { + // Use auxiliary data for a NULL entry to distinguish between + // filtered out (1) and ignored for other reasons (0). + // + pts.push_back ( + prerequisite_target (pt, pi, fr.first == nullptr ? 1 : 0)); + } } +#if 1 + optional<bool> unchanged; + if (a.operation () == update_id) + unchanged = match_sync (ia, t, unmatch::unchanged).first; +#endif + if (a.operation () == update_id) { return *unchanged @@ -396,6 +615,79 @@ namespace build2 } } + void file_rule:: + reapply_impl (action a, target& t, match_extra& me) const + { + tracer trace ("install::file_rule::reapply"); + + assert (a.operation () != update_id); + + optional<const scope*> is; + + // Iterate over prerequisites and prerequisite targets in parallel. + // + auto& pts (t.prerequisite_targets[a]); + size_t j (0), n (pts.size ()), en (0); + + auto pms (group_prerequisite_members (a, t, members_mode::never)); + for (auto i (pms.begin ()), e (pms.end ()); + i != e && j != n; + ++i, ++j, ++en) + { + // The same logic as in apply() above except that we skip + // prerequisites that were not filtered out. + // + const prerequisite& p (i->prerequisite); + + include_type pi (include (a, t, p)); + if (!pi) + continue; + + if (p.proj) + continue; + + prerequisite_target& pto (pts[j]); + + if (pto.target != nullptr || pto.data == 0) + continue; + + if (!is) + is = a.operation () != update_id ? install_scope (t) : nullptr; + + pair<const target*, uint64_t> fr (filter (*is, a, t, i, me)); + + const target* pt (fr.first); + uint64_t options (fr.second); + + lookup l; + + if (pt == nullptr) + { + l5 ([&]{trace << "ignoring " << p << " (filtered out)";}); + } + else if ((l = (*pt)[var_install (*p.scope.root_scope ())]) && + cast<path> (l).string () == "false") + { + l5 ([&]{trace << "ignoring " << *pt << " (not installable)";}); + pt = nullptr; + } + else if (pt->is_a<file> ()) + { + if (match_sync (a, *pt, unmatch::unchanged, options).first) + pt = nullptr; + } + else if (!try_match_sync (a, *pt, options).first) + { + l5 ([&]{trace << "ignoring " << *pt << " (no rule)";}); + pt = nullptr; + } + + pto = prerequisite_target (pt, pi, fr.first == nullptr ? 1 : 0); + } + + assert (en == n); // Did not call apply() with true for reapply? + } + target_state file_rule:: perform_update (action a, const target& t) { @@ -480,7 +772,8 @@ namespace build2 const dir_path& d (t.out_dir ().leaf (p->out_path ())); // Add it as another leading directory rather than modifying - // the last one directly; somehow, it feels right. + // the last one directly; somehow, it feels right. Note: the + // result is normalized. // if (!d.empty ()) rs.emplace_back (rs.back ().dir / d, rs.back ()); @@ -491,8 +784,9 @@ namespace build2 return rs.back (); } - // Resolve installation directory name to absolute directory path. Return - // all the super-directories leading up to the destination (last). + // Resolve installation directory name to absolute and normalized + // directory path. Return all the super-directories leading up to the + // destination (last). // // If target is not NULL, then also handle the subdirs logic. // @@ -541,7 +835,9 @@ namespace build2 { if (fail_unknown) fail << "unknown installation directory name '" << sn << "'" << - info << "did you forget to specify config." << var << "?"; + info << "did you forget to specify config." << var << "?" << + info << "specify !config." << var << "=... if installing " + << "from multiple projects"; return rs; // Empty. } @@ -575,38 +871,66 @@ namespace build2 r->sudo = cast_null<string> (s["config.install.sudo"]); if (r->cmd == nullptr) - r->cmd = &cast<path> (s["config.install.cmd"]); + r->cmd = cast_null<path> (s["config.install.cmd"]); if (r->options == nullptr) r->options = cast_null<strings> (s["config.install.options"]); if (r->mode == nullptr) - r->mode = &cast<string> (s["config.install.mode"]); + r->mode = cast_null<string> (s["config.install.mode"]); if (r->dir_mode == nullptr) - r->dir_mode = &cast<string> (s["config.install.dir_mode"]); + r->dir_mode = cast_null<string> (s["config.install.dir_mode"]); return rs; } - static inline install_dirs - resolve (const target& t, dir_path d, bool fail_unknown = true) + static dir_path + resolve_dir (const scope& s, const target* t, + dir_path d, dir_path rb, + bool fail_unknown) { - return resolve (t.base_scope (), &t, move (d), fail_unknown); + install_dirs rs (resolve (s, t, move (d), fail_unknown)); + + if (rs.empty ()) + return dir_path (); + + dir_path r (move (rs.back ().dir)); + + if (!rb.empty ()) + { + dir_path b (resolve (s, t, move (rb), false).back ().dir); + + try + { + r = r.relative (b); + } + catch (const invalid_path&) + { + fail << "unable to make installation directory " << r + << " relative to " << b; + } + } + + return r; } dir_path - resolve_dir (const target& t, dir_path d, bool fail_unknown) + resolve_dir (const target& t, dir_path d, dir_path rb, bool fail_unknown) { - install_dirs r (resolve (t, move (d), fail_unknown)); - return r.empty () ? dir_path () : move (r.back ().dir); + return resolve_dir (t.base_scope (), &t, move (d), move (rb), fail_unknown); } dir_path - resolve_dir (const scope& s, dir_path d, bool fail_unknown) + resolve_dir (const scope& s, dir_path d, dir_path rb, bool fail_unknown) + { + return resolve_dir (s, nullptr, move (d), move (rb), fail_unknown); + } + + static inline install_dirs + resolve (const target& t, dir_path d, bool fail_unknown = true) { - install_dirs r (resolve (s, nullptr, move (d), fail_unknown)); - return r.empty () ? dir_path () : move (r.back ().dir); + return resolve (t.base_scope (), &t, move (d), fail_unknown); } path @@ -622,6 +946,10 @@ namespace build2 bool n (!p->to_directory ()); dir_path d (n ? p->directory () : path_cast<dir_path> (*p)); + if (n && d.empty ()) + fail << "relative installation file path '" << p + << "' has no directory component"; + install_dirs ids (resolve (f, d)); if (!n) @@ -672,30 +1000,15 @@ namespace build2 return s; } - // Given an abolute path return its chroot'ed version, if any, accoring to - // install.chroot. - // - template <typename P> - static inline P - chroot_path (const scope& rs, const P& p) - { - if (const dir_path* d = cast_null<dir_path> (rs["install.chroot"])) - { - dir_path r (p.root_directory ()); - assert (!r.empty ()); // Must be absolute. - - return *d / p.leaf (r); - } - - return p; - } - void file_rule:: install_d (const scope& rs, const install_dir& base, const dir_path& d, + const file& t, uint16_t verbosity) { + assert (d.absolute ()); + context& ctx (rs.ctx); // Here is the problem: if this is a dry-run, then we will keep showing @@ -708,7 +1021,10 @@ namespace build2 // with uninstall since the directories won't be empty (because we don't // actually uninstall any files). // - if (ctx.dry_run) + // Note that this also means we won't have the directory entries in the + // manifest created with dry-run. Probably not a big deal. + // + if (ctx.dry_run || !filter_entry (rs, d, path (), entry_type::directory)) return; dir_path chd (chroot_path (rs, d)); @@ -735,19 +1051,23 @@ namespace build2 dir_path pd (d.directory ()); if (pd != base.dir) - install_d (rs, base, pd, verbosity); + install_d (rs, base, pd, t, verbosity); } cstrings args; string reld ( - cast<string> (ctx.global_scope["build.host.class"]) == "windows" + ctx.build_host->class_ == "windows" ? msys_path (chd) : relative (chd).string ()); if (base.sudo != nullptr) args.push_back (base.sudo->c_str ()); + // Wouldn't be here otherwise. + // + assert (base.cmd != nullptr && base.dir_mode != nullptr); + args.push_back (base.cmd->string ().c_str ()); args.push_back ("-d"); @@ -766,10 +1086,14 @@ namespace build2 if (verb >= 2) print_process (args); else if (verb) - text << "install " << chd; + print_diag ("install -d", chd); // See also `install -l` below. } - run (pp, args); + run (ctx, + pp, args, + verb >= verbosity ? 1 : verb_never /* finish_verbosity */); + + context_data::manifest_install_d (ctx, t, d, *base.dir_mode); } void file_rule:: @@ -780,14 +1104,21 @@ namespace build2 const path& f, uint16_t verbosity) { + assert (name.empty () || name.simple ()); + context& ctx (rs.ctx); + const path& leaf (name.empty () ? f.leaf () : name); + + if (!filter_entry (rs, base.dir, leaf, entry_type::regular)) + return; + path relf (relative (f)); dir_path chd (chroot_path (rs, base.dir)); string reld ( - cast<string> (ctx.global_scope["build.host.class"]) == "windows" + ctx.build_host->class_ == "windows" ? msys_path (chd) : relative (chd).string ()); @@ -802,6 +1133,10 @@ namespace build2 if (base.sudo != nullptr) args.push_back (base.sudo->c_str ()); + // Wouldn't be here otherwise. + // + assert (base.cmd != nullptr && base.mode != nullptr); + args.push_back (base.cmd->string ().c_str ()); if (base.options != nullptr) @@ -820,23 +1155,47 @@ namespace build2 if (verb >= 2) print_process (args); else if (verb) - text << "install " << t; + { + if (name.empty ()) + print_diag ("install", t, chd); + else + print_diag ("install", t, chd / name); + } } if (!ctx.dry_run) - run (pp, args); + run (ctx, + pp, args, + verb >= verbosity ? 1 : verb_never /* finish_verbosity */); + + context_data::manifest_install_f (ctx, t, base.dir, leaf, *base.mode); } void file_rule:: install_l (const scope& rs, const install_dir& base, - const path& target, const path& link, + const file& target, + const path& link_target, uint16_t verbosity) { + assert (link.simple () && !link.empty ()); + context& ctx (rs.ctx); - path rell (relative (chroot_path (rs, base.dir))); + if (!filter_entry (rs, base.dir, link, entry_type::symlink)) + return; + + if (link_target.absolute () && + cast_false<bool> (rs["install.relocatable"])) + { + fail << "absolute symlink target " << link_target.string () + << " in relocatable installation"; + } + + dir_path chd (chroot_path (rs, base.dir)); + + path rell (relative (chd)); rell /= link; // We can create a symlink directly without calling ln. This, however, @@ -850,7 +1209,7 @@ namespace build2 base.sudo != nullptr ? base.sudo->c_str () : nullptr, "ln", "-sf", - target.string ().c_str (), + link_target.string ().c_str (), rell.string ().c_str (), nullptr}; @@ -863,11 +1222,19 @@ namespace build2 if (verb >= 2) print_process (args); else if (verb) - text << "install " << rell << " -> " << target; + { + // Without a flag it's unclear (unlike with ln) that we are creating + // a link. FreeBSD install(1) has the -l flag with the appropriate + // semantics. For consistency, we also pass -d above. + // + print_diag ("install -l", link_target, chd / link); + } } if (!ctx.dry_run) - run (pp, args); + run (ctx, + pp, args, + verb >= verbosity ? 1 : verb_never /* finish_verbosity */); #else // The -f part. // @@ -879,15 +1246,15 @@ namespace build2 if (verb >= verbosity) { if (verb >= 2) - text << "ln -sf " << target.string () << ' ' << rell.string (); + text << "ln -sf " << link_target.string () << ' ' << rell.string (); else if (verb) - text << "install " << rell << " -> " << target; + print_diag ("install -l", link_target, chd / link); } if (!ctx.dry_run) try { - mkanylink (target, rell, true /* copy */); + mkanylink (link_target, rell, true /* copy */); } catch (const pair<entry_type, system_error>& e) { @@ -899,6 +1266,12 @@ namespace build2 fail << "unable to make " << w << ' ' << rell << ": " << e.second; } #endif + + context_data::manifest_install_l (ctx, + target, + link_target, + base.dir, + link); } target_state file_rule:: @@ -922,6 +1295,10 @@ namespace build2 bool n (!p.to_directory ()); dir_path d (n ? p.directory () : path_cast<dir_path> (p)); + if (n && d.empty ()) + fail << "relative installation file path '" << p + << "' has no directory component"; + // Resolve target directory. // install_dirs ids (resolve (t, d)); @@ -943,7 +1320,7 @@ namespace build2 // sudo, etc). // for (auto i (ids.begin ()), j (i); i != ids.end (); j = i++) - install_d (rs, *j, i->dir, verbosity); // install -d + install_d (rs, *j, i->dir, t, verbosity); // install -d install_dir& id (ids.back ()); @@ -977,6 +1354,8 @@ namespace build2 // target_state r (straight_execute_prerequisites (a, t)); + bool fr (filter (a, t, t)); + // Then installable ad hoc group members, if any. // for (const target* m (t.adhoc_member); @@ -987,10 +1366,13 @@ namespace build2 { if (!mf->path ().empty () && mf->mtime () != timestamp_nonexistent) { - if (const path* p = lookup_install<path> (*mf, "install")) + if (filter (a, t, *mf)) { - install_target (*mf, *p, tp.empty () ? 1 : 2); - r |= target_state::changed; + if (const path* p = lookup_install<path> (*mf, "install")) + { + install_target (*mf, *p, !fr || tp.empty () ? 1 : 2); + r |= target_state::changed; + } } } } @@ -999,9 +1381,9 @@ namespace build2 // Finally install the target itself (since we got here we know the // install variable is there). // - if (!tp.empty ()) + if (fr && !tp.empty ()) { - install_target (t, cast<path> (t["install"]), 1); + install_target (t, cast<path> (t[var_install (rs)]), 1); r |= target_state::changed; } @@ -1014,9 +1396,13 @@ namespace build2 const dir_path& d, uint16_t verbosity) { + assert (d.absolute ()); + + context& ctx (rs.ctx); + // See install_d() for the rationale. // - if (rs.ctx.dry_run) + if (ctx.dry_run || !filter_entry (rs, d, path (), entry_type::directory)) return false; dir_path chd (chroot_path (rs, d)); @@ -1063,7 +1449,7 @@ namespace build2 if (verb >= 2) text << "rmdir " << reld; else if (verb) - text << "uninstall " << reld; + print_diag ("uninstall -d", chd); } try @@ -1093,11 +1479,19 @@ namespace build2 if (verb >= 2) print_process (args); else if (verb) - text << "uninstall " << reld; + print_diag ("uninstall -d", chd); } - process pr (run_start (pp, args)); - r = run_finish_code (args, pr); + process pr (run_start (pp, args, + 0 /* stdin */, + 1 /* stdout */, + diag_buffer::pipe (ctx) /* stderr */)); + diag_buffer dbuf (ctx, args[0], pr); + dbuf.read (); + r = run_finish_code ( + dbuf, + args, pr, + verb >= verbosity ? 1 : verb_never /* verbosity */); } if (!r) @@ -1121,40 +1515,16 @@ namespace build2 return r; } - bool file_rule:: - uninstall_f (const scope& rs, - const install_dir& base, - const file* t, - const path& name, - uint16_t verbosity) + static void + uninstall_f_impl (const scope& rs, + const install_dir& base, + const path& f, + uint16_t verbosity) { - assert (t != nullptr || !name.empty ()); - path f (chroot_path (rs, base.dir) / - (name.empty () ? t->path ().leaf () : name)); - - try - { - // Note: don't follow symlinks so if the target is a dangling symlinks - // we will proceed to removing it. - // - if (!file_exists (f, false)) // May throw (e.g., EACCES). - return false; - } - catch (const system_error& e) - { - fail << "invalid installation path " << f << ": " << e; - } + context& ctx (rs.ctx); path relf (relative (f)); - if (verb >= verbosity && verb == 1) - { - if (t != nullptr) - text << "uninstall " << *t; - else - text << "uninstall " << relf; - } - // The same story as with uninstall -d (on Windows rm is also from // MSYS2/Cygwin). // @@ -1164,7 +1534,7 @@ namespace build2 if (verb >= verbosity && verb >= 2) text << "rm " << relf; - if (!rs.ctx.dry_run) + if (!ctx.dry_run) { try { @@ -1190,13 +1560,107 @@ namespace build2 process_path pp (run_search (args[0])); - if (verb >= verbosity && verb >= 2) - print_process (args); + if (verb >= verbosity) + { + if (verb >= 2) + print_process (args); + } - if (!rs.ctx.dry_run) - run (pp, args); + if (!ctx.dry_run) + run (ctx, + pp, args, + verb >= verbosity ? 1 : verb_never /* finish_verbosity */); + } + } + + bool file_rule:: + uninstall_f (const scope& rs, + const install_dir& base, + const file* t, + const path& name, + uint16_t verbosity) + { + assert (name.empty () ? t != nullptr : name.simple ()); + + const path& leaf (name.empty () ? t->path ().leaf () : name); + + if (!filter_entry (rs, base.dir, leaf, entry_type::regular)) + return false; + + dir_path chd (chroot_path (rs, base.dir)); + path f (chd / leaf); + + try + { + // Note: don't follow symlinks so if the target is a dangling symlinks + // we will proceed to removing it. + // + if (!file_exists (f, false)) // May throw (e.g., EACCES). + return false; + } + catch (const system_error& e) + { + fail << "invalid installation path " << f << ": " << e; } + if (verb >= verbosity && verb == 1) + { + if (t != nullptr) + { + if (name.empty ()) + print_diag ("uninstall", *t, chd, "<-"); + else + print_diag ("uninstall", *t, f, "<-"); + } + else + print_diag ("uninstall", f); + } + + uninstall_f_impl (rs, base, f, verbosity); + return true; + } + + bool file_rule:: + uninstall_l (const scope& rs, + const install_dir& base, + const path& link, + const path& /*link_target*/, + uint16_t verbosity) + { + assert (link.simple () && !link.empty ()); + + if (!filter_entry (rs, base.dir, link, entry_type::symlink)) + return false; + + dir_path chd (chroot_path (rs, base.dir)); + path f (chd / link); + + try + { + // Note: don't follow symlinks so if the target is a dangling symlinks + // we will proceed to removing it. + // + if (!file_exists (f, false)) // May throw (e.g., EACCES). + return false; + } + catch (const system_error& e) + { + fail << "invalid installation path " << f << ": " << e; + } + + if (verb >= verbosity && verb == 1) + { + // It's dubious showing the link target path adds anything useful + // here. + // +#if 0 + print_diag ("uninstall -l", target, f, "<-"); +#else + print_diag ("uninstall -l", f); +#endif + } + + uninstall_f_impl (rs, base, f, verbosity); return true; } @@ -1219,6 +1683,10 @@ namespace build2 bool n (!p.to_directory ()); dir_path d (n ? p.directory () : path_cast<dir_path> (p)); + if (n && d.empty ()) + fail << "relative installation file path '" << p + << "' has no directory component"; + // Resolve target directory. // install_dirs ids (resolve (t, d)); @@ -1265,8 +1733,10 @@ namespace build2 // target_state r (target_state::unchanged); - if (!tp.empty ()) - r |= uninstall_target (t, cast<path> (t["install"]), 1); + bool fr (filter (a, t, t)); + + if (fr && !tp.empty ()) + r |= uninstall_target (t, cast<path> (t[var_install (rs)]), 1); // Then installable ad hoc group members, if any. To be anally precise, // we would have to do it in reverse, but that's not easy (it's a @@ -1280,23 +1750,60 @@ namespace build2 { if (!mf->path ().empty () && mf->mtime () != timestamp_nonexistent) { - if (const path* p = lookup_install<path> (*m, "install")) + if (filter (a, t, *mf)) { - r |= uninstall_target ( - *mf, - *p, - tp.empty () || r != target_state::changed ? 1 : 2); + if (const path* p = lookup_install<path> (*m, "install")) + { + r |= uninstall_target ( + *mf, + *p, + !fr || tp.empty () || r != target_state::changed ? 1 : 2); + } } } } } - // Finally handle installable prerequisites. // r |= reverse_execute_prerequisites (a, t); return r; } + + // fsdir_rule + // + const fsdir_rule fsdir_rule::instance; + + bool fsdir_rule:: + match (action, target&) const + { + // We always match. + // + // Note that we are called both as the outer part during the update-for- + // un/install pre-operation and as the inner part during the un/install + // operation itself. + // + return true; + } + + recipe fsdir_rule:: + apply (action a, target& t) const + { + // If this is outer part of the update-for-un/install, delegate to the + // default fsdir rule. Otherwise, this is a noop (we don't install + // fsdir{}). + // + // For now we also assume we don't need to do anything for prerequisites + // (the only sensible prerequisite of fsdir{} is another fsdir{}). + // + if (a.operation () == update_id) + { + match_inner (a, t); + return inner_recipe; + } + else + return noop_recipe; + } } } diff --git a/libbuild2/install/rule.hxx b/libbuild2/install/rule.hxx index 73f2486..b023af5 100644 --- a/libbuild2/install/rule.hxx +++ b/libbuild2/install/rule.hxx @@ -22,10 +22,18 @@ namespace build2 { public: virtual bool - match (action, target&, const string&) const override; + match (action, target&) const override; // Return NULL if this prerequisite should be ignored and pointer to its - // target otherwise. The default implementation allows all prerequsites. + // target otherwise. In the latter case, return the match options that + // should be used for this prerequisite (use match_extra::all_options + // and not 0 if no match options are needed). + // + // The default implementation ignores prerequsites that are outside of + // the installation scope (see install_scope() for details). + // + // The default implementation always returns match_extra::all_options. + // The match_extra argument is not used by the default implementation. // // The prerequisite is passed as an iterator allowing the filter to // "see" inside groups. @@ -33,30 +41,44 @@ namespace build2 using prerequisite_iterator = prerequisite_members_range<group_prerequisites>::iterator; - virtual const target* - filter (action, const target&, prerequisite_iterator&) const; + virtual pair<const target*, uint64_t> + filter (const scope*, + action, const target&, prerequisite_iterator&, + match_extra&) const; - virtual const target* - filter (action, const target&, const prerequisite&) const; + virtual pair<const target*, uint64_t> + filter (const scope*, + action, const target&, const prerequisite&, + match_extra&) const; + // Note: rule::apply() override (with match_extra). + // virtual recipe - apply (action, target&) const override; + apply (action, target&, match_extra&) const override; + + // Implementation of apply(). + // + // If the implementation may call reapply_impl(), then the reapply + // argument to apply_impl() must be true. Note that in this case, the + // *_impl() functions use the prerequisite_target::data member for own + // housekeeping. + // + recipe + apply_impl (action, target&, match_extra&, bool reapply = false) const; + + // Implementation of reapply() that re-tries prerequisites that have + // been filtered out during the reapply() call. Note that currently not + // supported for update, only for install/uninstall. + // + void + reapply_impl (action, target&, match_extra&) const; alias_rule () {} static const alias_rule instance; - }; - - class fsdir_rule: public simple_rule - { - public: - virtual bool - match (action, target&, const string&) const override; + private: virtual recipe - apply (action, target&) const override; - - fsdir_rule () {} - static const fsdir_rule instance; + apply (action, target&) const override; // Dummy simple_rule override. }; // In addition to the alias rule's semantics, this rule sees through to @@ -74,23 +96,33 @@ namespace build2 { public: virtual bool - match (action, target&, const string&) const override; + match (action, target&) const override; - // Return NULL if this group member should be ignored and pointer to its - // target otherwise. The default implementation accepts all members. + // Return false if this group member should be ignored and true + // otherwise. Note that this filter is called during apply(). + // + // The default implementation accepts all members. // - virtual const target* + virtual bool filter (action, const target&, const target& group_member) const; + // Return NULL if this prerequisite should be ignored and pointer to its + // target otherwise. The same semantics as in file_rule below. + // + virtual pair<const target*, uint64_t> + filter (const scope*, + action, const target&, const prerequisite&, + match_extra&) const override; + using alias_rule::filter; // "Unhide" to make Clang happy. virtual recipe - apply (action, target&) const override; + apply (action, target&, match_extra&) const override; - group_rule (bool see_through_only): see_through (see_through_only) {} + group_rule (bool sto): see_through_only (sto) {} static const group_rule instance; - bool see_through; + bool see_through_only; }; struct install_dir; @@ -99,15 +131,36 @@ namespace build2 { public: virtual bool - match (action, target&, const string&) const override; + match (action, target&) const override; + + // Return false if this ad hoc group member should be ignored and true + // otherwise. Note that this filter is called during execute and only + // for install/uninstall (and not update). For generality, it is also + // (first) called on the target itself (can be detected by comparing + // the second and third arguments). + // + // The default implementation accepts all members. + // + virtual bool + filter (action, const target&, const target& adhoc_group_member) const; // Return NULL if this prerequisite should be ignored and pointer to its - // target otherwise. The default implementation ignores prerequsites - // that are outside of this target's project. + // target otherwise. In the latter case, return the match options that + // should be used for this prerequisite (use match_extra::all_options + // and not 0 if no match options are needed). + // + // The default implementation ignores prerequsites that are outside of + // the installation scope (see install_scope() for details). It also + // ignores exe{} prerequisites assuming an exe{} listed for a file + // target is there to execute (e.g., to generate that target) and + // normally should not be installed (an exe{} would typically be + // installed via the dir{./} alias). But this can be overridden with a + // prerequisite-specific install=true, for example: // - // @@ I wonder why we do weak amalgamation for alias but project for - // file? And then override this for prerequisite libraries/modules - // in cc::install_rule and bash::install_rule... + // exe{foo}: exe{bar}: install = true # foo runs bar + // + // The default implementation always returns match_extra::all_options. + // The match_extra argument is not used by the default implementation. // // The prerequisite is passed as an iterator allowing the filter to // "see" inside groups. @@ -115,14 +168,38 @@ namespace build2 using prerequisite_iterator = prerequisite_members_range<group_prerequisites>::iterator; - virtual const target* - filter (action, const target&, prerequisite_iterator&) const; + virtual pair<const target*, uint64_t> + filter (const scope*, + action, const target&, prerequisite_iterator&, + match_extra&) const; - virtual const target* - filter (action, const target&, const prerequisite&) const; + virtual pair<const target*, uint64_t> + filter (const scope*, + action, const target&, const prerequisite&, + match_extra&) const; + // Note: rule::apply() override (with match_extra). + // virtual recipe - apply (action, target&) const override; + apply (action, target&, match_extra&) const override; + + // Implementation of apply() that returns empty_recipe (i.e., NULL) if + // the target is not installable. + // + // If the implementation may call reapply_impl(), then the reapply + // argument to apply_impl() must be true. Note that in this case, the + // *_impl() functions use the prerequisite_target::data member for own + // housekeeping. + // + recipe + apply_impl (action, target&, match_extra&, bool reapply = false) const; + + // Implementation of reapply() that re-tries prerequisites that have + // been filtered out during the reapply() call. Note that currently not + // supported for update, only for install/uninstall. + // + void + reapply_impl (action, target&, match_extra&) const; static target_state perform_update (action, const target&); @@ -160,10 +237,16 @@ namespace build2 // // install -d <dir> // + // Note: <dir> is expected to be absolute. + // + // Note that the target argument only specifies which target caused + // this directory to be created. + // static void install_d (const scope& rs, const install_dir& base, const dir_path& dir, + const file& target, uint16_t verbosity = 1); // Install a file: @@ -171,6 +254,8 @@ namespace build2 // install <file> <base>/ # if <name> is empty // install <file> <base>/<name> # if <name> is not empty // + // Note that <name> should be a simple path. + // static void install_f (const scope& rs, const install_dir& base, @@ -181,13 +266,25 @@ namespace build2 // Install (make) a symlink: // - // ln -s <target> <base>/<link> + // install -l <link_target> <base>/<link> + // + // Which is essentially: + // + // ln -s <link_target> <base>/<link> + // + // Note that <link> should be a simple path. Note that <link_target> + // must not be absolute if relocatable installation is requested + // (config.install.relocatable). + // + // Note that the target argument only specifies which target this + // symlink "belongs" to. // static void install_l (const scope& rs, const install_dir& base, - const path& target, const path& link, + const file& target, + const path& link_target, uint16_t verbosity = 1); // Uninstall (remove) a file or symlink: @@ -205,13 +302,26 @@ namespace build2 const path& name, uint16_t verbosity = 1); + // Uninstall (remove) a symlink. + // + // This is essentially unistall_f() but with better low-verbosity + // diagnostics. + // + static bool + uninstall_l (const scope& rs, + const install_dir& base, + const path& link, + const path& link_target, + uint16_t verbosity = 1); + + // Uninstall (remove) an empty directory. // // uninstall -d <dir> // - // We try to remove all the directories between base and dir but not base - // itself unless base == dir. Return false if nothing has been removed - // (i.e., the directories do not exist or are not empty). + // We try to remove all the directories between base and dir but not + // base itself unless base == dir. Return false if nothing has been + // removed (i.e., the directories do not exist or are not empty). // static bool uninstall_d (const scope& rs, @@ -227,6 +337,23 @@ namespace build2 static const file_rule instance; file_rule () {} + + private: + virtual recipe + apply (action, target&) const override; // Dummy simple_rule override. + }; + + class fsdir_rule: public simple_rule + { + public: + virtual bool + match (action, target&) const override; + + virtual recipe + apply (action, target&) const override; + + fsdir_rule () {} + static const fsdir_rule instance; }; } } diff --git a/libbuild2/install/utility.cxx b/libbuild2/install/utility.cxx new file mode 100644 index 0000000..43d97fb --- /dev/null +++ b/libbuild2/install/utility.cxx @@ -0,0 +1,295 @@ +// file : libbuild2/install/utility.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/install/utility.hxx> + +#include <libbuild2/variable.hxx> +#include <libbuild2/diagnostics.hxx> + +namespace build2 +{ + namespace install + { + const scope* + install_scope (const target& t) + { + context& ctx (t.ctx); + + // Note: go straight for the public variable pool. + // + const variable& var (*ctx.var_pool.find ("config.install.scope")); + + if (const string* s = cast_null<string> (ctx.global_scope[var])) + { + if (*s == "project") + return &t.root_scope (); + else if (*s == "bundle") + return &t.bundle_scope (); + else if (*s == "strong") + return &t.strong_scope (); + else if (*s == "weak") + return &t.weak_scope (); + else if (*s != "global") + fail << "invalid " << var << " value '" << *s << "'"; + } + + return nullptr; + } + + bool + filter_entry (const scope& rs, + const dir_path& base, + const path& leaf, + entry_type type) + { + assert (type != entry_type::unknown && + (type == entry_type::directory) == leaf.empty ()); + + const filters* fs (cast_null<filters> (rs["install.filter"])); + + if (fs == nullptr || fs->empty ()) + return true; + + tracer trace ("install::filter"); + + // Parse, resolve, and apply each filter in order. + // + // If redoing all this work for every entry proves too slow, we can + // consider some form of caching (e.g., on the per-project basis). + // + auto i (fs->begin ()); + + bool negate (false); + if (i->first == "!") + { + negate = true; + ++i; + } + + size_t limit (0); // See below. + + for (auto e (fs->end ()); i != e; ++i) + { + const pair<string, optional<string>>& kv (*i); + + path k; + try + { + k = path (kv.first); + + if (k.absolute ()) + k.normalize (); + } + catch (const invalid_path&) + { + fail << "invalid path '" << kv.first << "' in config.install.filter " + << "value"; + } + + bool v; + { + const string& s (kv.second ? *kv.second : string ()); + + size_t p (s.find (',')); + + if (s.compare (0, p, "true") == 0) + v = true; + else if (s.compare (0, p, "false") == 0) + v = false; + else + fail << "expected true or false instead of '" << string (s, 0, p) + << "' in config.install.filter value" << endf; + + if (p != string::npos) + { + if (s.compare (p + 1, string::npos, "symlink") == 0) + { + if (type != entry_type::symlink) + continue; + } + else + fail << "unknown modifier '" << string (s, p + 1) << "' in " + << "config.install.filter value"; + } + } + + // @@ TODO (see below for all the corner cases). Note that in a sense + // we already have the match file in any subdirectory support via + // simple patterns so perhaps this is not worth the trouble. Or we + // could support some limited form (e.g., `**` should be in the + // last component). But it may still be tricky to determine if + // it is a sub-filter. + // + if (path_pattern_recursive (k)) + fail << "recursive wildcard pattern '" << kv.first << "' in " + << "config.install.filter value"; + + if (k.simple () && !k.to_directory ()) + { + // Simple name/pattern matched against the leaf. + // + // @@ What if it is `**`? + // + if (path_pattern (k)) + { + if (!path_match (leaf, k)) + continue; + } + else + { + if (k != leaf) + continue; + } + } + else + { + // Split into directory and leaf. + // + // @@ What if leaf is `**`? + // + dir_path d; + if (k.to_directory ()) + { + d = path_cast<dir_path> (move (k)); + k = path (); // No leaf. + } + else + { + d = k.directory (); + k.make_leaf (); + } + + // Resolve relative directory. + // + // Note that this resolution is potentially project-specific (that + // is, different projects may have different install.* locaitons). + // + // Note that if the first component is/contains a wildcard (e.g., + // `*/`), then the resulution will fail, which feels correct (what + // does */ mean?). + // + if (d.relative ()) + { + // @@ Strictly speaking, this should be base, not root scope. + // + d = resolve_dir (rs, move (d)); + } + + // Return the number of path components in the path. + // + auto path_comp = [] (const path& p) + { + size_t n (0); + for (auto i (p.begin ()); i != p.end (); ++i) + ++n; + return n; + }; + + // We need the sub() semantics but which uses pattern match instead + // of equality for the prefix. Looks like chopping off the path and + // calling path_match() on that is the best we can do. + // + // @@ Assumes no `**` components. + // + auto path_sub = [&path_comp] (const dir_path& ent, + const dir_path& pat, + size_t n = 0) + { + if (n == 0) + n = path_comp (pat); + + dir_path p; + for (auto i (ent.begin ()); n != 0 && i != ent.end (); --n, ++i) + p.combine (*i, i.separator ()); + + return path_match (p, pat); + }; + + // The following checks should continue on no match and fall through + // to return. + // + if (k.empty ()) // Directory. + { + // Directories have special semantics. + // + // Consider this sequence of filters: + // + // include/x86_64-linux-gnu/@true + // include/x86_64-linux-gnu/details/@false + // include/@false + // + // It seems the semantics we want is that only subcomponent + // filters should apply. Maybe remember the latest matched + // directory as a current limit? But perhaps we don't need to + // remember the directory itself but the number of path + // components? + // + // I guess for patterns we will use the actual matched directory, + // not the pattern, to calculate the limit? @@ Because we + // currently don't support `**`, we for now can count components + // in the pattern. + + // Check if this is a sub-filter. + // + size_t n (path_comp (d)); + if (n <= limit) + continue; + + if (path_pattern (d)) + { + if (!path_sub (base, d, n)) + continue; + } + else + { + if (!base.sub (d)) + continue; + } + + if (v) + { + limit = n; + continue; // Continue looking for sub-filters. + } + } + else + { + if (path_pattern (d)) + { + if (!path_sub (base, d)) + continue; + } + else + { + if (!base.sub (d)) + continue; + } + + if (path_pattern (k)) + { + // @@ Does not handle `**`. + // + if (!path_match (leaf, k)) + continue; + } + else + { + if (k != leaf) + continue; + } + } + } + + if (negate) + v = !v; + + l4 ([&]{trace << (base / leaf) + << (v ? " included by " : " excluded by ") + << kv.first << '@' << *kv.second;}); + return v; + } + + return !negate; + } + } +} diff --git a/libbuild2/install/utility.hxx b/libbuild2/install/utility.hxx index 2c0ca56..fc40ebe 100644 --- a/libbuild2/install/utility.hxx +++ b/libbuild2/install/utility.hxx @@ -9,6 +9,7 @@ #include <libbuild2/scope.hxx> #include <libbuild2/target.hxx> +#include <libbuild2/filesystem.hxx> // entry_type #include <libbuild2/export.hxx> @@ -43,7 +44,7 @@ namespace build2 { auto r ( s.target_vars[tt]["*"].insert ( - *s.var_pool ().find ("install.mode"))); + *s.ctx.var_pool.find ("install.mode"))); if (r.second) // Already set by the user? r.first = move (m); @@ -56,23 +57,69 @@ namespace build2 return install_mode (s, T::static_type, move (m)); } + // Return the "installation scope". We do not install prerequisites (for + // example, shared libraries) of targets (for example, executables) that + // belong to projects outside of this scope. If it's NULL, install + // prerequisites from all projects. See also config.install.scope. + // + // Note that this should not apply to update-for-install. Failed that we + // may end up using incompatibly-built prerequisites (e.g., a library) in + // a target built for install (e.g., an executable). + // + LIBBUILD2_SYMEXPORT const scope* + install_scope (const target&); + // Resolve relative installation directory path (e.g., include/libfoo) to - // its absolute directory path (e.g., /usr/include/libfoo). If the - // resolution encountered an unknown directory, issue diagnostics and fail - // unless fail_unknown is false, in which case return empty directory. + // its absolute and normalized directory path (e.g., /usr/include/libfoo). + // If the resolution encountered an unknown directory, issue diagnostics + // and fail unless fail_unknown is false, in which case return empty + // directory. + // + // For rel_base semantics, see the $install.resolve() documentation. Note + // that fail_unknown does not apply to the rel_base resolution. // // Note: implemented in rule.cxx. // LIBBUILD2_SYMEXPORT dir_path - resolve_dir (const target&, dir_path, bool fail_unknown = true); + resolve_dir (const target&, + dir_path, + dir_path rel_base = {}, + bool fail_unknown = true); LIBBUILD2_SYMEXPORT dir_path - resolve_dir (const scope&, dir_path, bool fail_unknown = true); + resolve_dir (const scope&, + dir_path, + dir_path rel_base = {}, + bool fail_unknown = true); // Resolve file installation path returning empty path if not installable. // LIBBUILD2_SYMEXPORT path resolve_file (const file&); // rule.cxx + + // Given an abolute path return its chroot'ed version, if any, accoring to + // install.chroot. + // + template <typename P> + inline P + chroot_path (const scope& rs, const P& p) + { + assert (p.absolute ()); + const dir_path* d (cast_null<dir_path> (rs["install.chroot"])); + return d != nullptr ? *d / p.leaf (p.root_directory ()) : p; + } + + // Installation filtering (config.install.filter). + // + // If entry type is a directory, then leaf must be empty. + // + using filters = vector<pair<string, optional<string>>>; + + LIBBUILD2_SYMEXPORT bool + filter_entry (const scope& rs, + const dir_path& base, + const path& leaf, + entry_type); } } |