aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/install
diff options
context:
space:
mode:
Diffstat (limited to 'libbuild2/install')
-rw-r--r--libbuild2/install/functions.cxx116
-rw-r--r--libbuild2/install/init.cxx340
-rw-r--r--libbuild2/install/operation.cxx361
-rw-r--r--libbuild2/install/operation.hxx64
-rw-r--r--libbuild2/install/rule.cxx857
-rw-r--r--libbuild2/install/rule.hxx189
-rw-r--r--libbuild2/install/utility.cxx261
-rw-r--r--libbuild2/install/utility.hxx51
8 files changed, 1923 insertions, 316 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 25dc845..3df912f 100644
--- a/libbuild2/install/init.cxx
+++ b/libbuild2/install/init.cxx
@@ -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,25 +385,33 @@ 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-[un]install
// operation, similar to update.
//
- rs.global_scope ().insert_rule<mtime_target> (
- perform_install_id, "install.file", fr);
+ // @@ Hm, it's a bit fuzzy why we would be updating-for-install
+ // something outside of any project?
+ //
+ scope& gs (rs.global_scope ());
- rs.global_scope ().insert_rule<mtime_target> (
- perform_uninstall_id, "uninstall.file", fr);
+ gs.insert_rule<mtime_target> (perform_install_id, "install.file", fr);
+ gs.insert_rule<mtime_target> (perform_uninstall_id, "install.file", fr);
}
// Configuration.
@@ -404,9 +425,9 @@ namespace build2
using config::lookup_config;
using config::specified_config;
- // Note: ignore config.install.scope (see below).
+ // Note: ignore config.install.{scope,manifest} (see below).
//
- bool s (specified_config (rs, "install", {"scope"}));
+ 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.
@@ -443,6 +464,123 @@ namespace build2
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;
@@ -480,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..ce5d24a 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,22 +19,356 @@ 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.
const operation_info op_install {
install_id,
@@ -39,8 +380,10 @@ namespace build2
"has nothing to install", // We cannot "be installed".
execution_mode::first,
0 /* concurrency */, // Run serially.
- &install_pre,
+ &pre_install,
nullptr,
+ &install_pre,
+ &install_post,
nullptr,
nullptr
};
@@ -64,7 +407,9 @@ namespace build2
"is not installed",
execution_mode::last,
0 /* concurrency */, // Run serially
- &install_pre,
+ &pre_uninstall,
+ nullptr,
+ nullptr,
nullptr,
nullptr,
nullptr
@@ -82,8 +427,10 @@ namespace build2
op_update.name_done,
op_update.mode,
op_update.concurrency,
- op_update.pre,
- op_update.post,
+ 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 2d81067..873b2e9 100644
--- a/libbuild2/install/rule.cxx
+++ b/libbuild2/install/rule.cxx
@@ -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,27 +71,45 @@ namespace build2
return true;
}
- const target* alias_rule::
+ pair<const target*, uint64_t> alias_rule::
filter (const scope* is,
- action a, const target& t, prerequisite_iterator& i) const
+ action a, const target& t, prerequisite_iterator& i,
+ match_extra& me) const
{
assert (i->member == nullptr);
- return filter (is, a, t, i->prerequisite);
+ return filter (is, a, t, i->prerequisite, me);
}
- const target* alias_rule::
+ pair<const target*, uint64_t> alias_rule::
filter (const scope* is,
- action, const target& t, const prerequisite& p) const
+ action, const target& t, const prerequisite& p,
+ match_extra&) const
{
const target& pt (search (t, p));
- return is == nullptr || pt.in (*is) ? &pt : nullptr;
+ 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)?
@@ -84,6 +120,8 @@ namespace build2
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.
@@ -105,15 +143,19 @@ namespace build2
// iterates over all its members.
//
if (!is)
- is = install_scope (t);
+ 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;
- const target* pt (filter (*is, a, t, i));
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.
//
@@ -125,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
@@ -190,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");
@@ -211,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.
//
@@ -221,22 +333,23 @@ namespace build2
? resolve_members (a, t)
: t.group_members (a));
- if (gv.members != nullptr)
+ if (gv.members != nullptr && gv.count != 0)
{
+ 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;
}
@@ -245,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);
}
@@ -268,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()).
@@ -276,44 +389,73 @@ namespace build2
return true;
}
- const target* file_rule::
+ 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) const
+ action a, const target& t, prerequisite_iterator& i,
+ match_extra& me) const
{
assert (i->member == nullptr);
- return filter (is, a, t, i->prerequisite);
+ return filter (is, a, t, i->prerequisite, me);
}
- const target* file_rule::
+ pair<const target*, uint64_t> file_rule::
filter (const scope* is,
- action, const target& t, const prerequisite& p) const
+ 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> ())
{
- // Feels like one day this should be unified with include (see
- // context::var_include).
+ 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["install"]).string () != "true")
- return nullptr;
+ cast_empty<path> (p.vars[var_install (rs)]).string () != "true")
+ return r;
}
const target& pt (search (t, p));
- return is == nullptr || pt.in (*is) ? &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));
- return r != nullptr ? r : noop_recipe;
+ recipe r (apply_impl (a, t, me));
+ return r != nullptr ? move (r) : noop_recipe;
}
recipe file_rule::
- apply_impl (action a, target& t) const
+ 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.
@@ -333,11 +475,36 @@ 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
optional<const scope*> is; // Installation scope (resolve lazily).
@@ -345,6 +512,8 @@ namespace build2
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.
@@ -366,29 +535,32 @@ namespace build2
// iterates over all its members.
//
if (!is)
- is = install_scope (t);
+ is = a.operation () != update_id ? install_scope (t) : nullptr;
+
+ pair<const target*, uint64_t> fr (filter (*is, a, t, i, me));
- const target* pt (filter (*is, a, t, i));
+ 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
@@ -396,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
@@ -426,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)
{
@@ -510,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 ());
@@ -521,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.
//
@@ -621,24 +885,52 @@ namespace build2
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
@@ -654,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)
@@ -704,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
@@ -740,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));
@@ -767,13 +1051,13 @@ 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 ());
@@ -798,10 +1082,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::
@@ -812,14 +1100,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 ());
@@ -852,23 +1147,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,
@@ -882,7 +1201,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};
@@ -895,11 +1214,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.
//
@@ -911,15 +1238,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)
{
@@ -931,6 +1258,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::
@@ -954,6 +1287,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));
@@ -975,7 +1312,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 ());
@@ -1009,6 +1346,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);
@@ -1019,10 +1358,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;
+ }
}
}
}
@@ -1031,9 +1373,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;
}
@@ -1046,9 +1388,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));
@@ -1095,7 +1441,7 @@ namespace build2
if (verb >= 2)
text << "rmdir " << reld;
else if (verb)
- text << "uninstall " << reld;
+ print_diag ("uninstall -d", chd);
}
try
@@ -1125,11 +1471,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)
@@ -1153,40 +1507,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).
//
@@ -1196,7 +1526,7 @@ namespace build2
if (verb >= verbosity && verb >= 2)
text << "rm " << relf;
- if (!rs.ctx.dry_run)
+ if (!ctx.dry_run)
{
try
{
@@ -1222,13 +1552,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 (!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 (!rs.ctx.dry_run)
- run (pp, args);
+ 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;
}
@@ -1251,6 +1675,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));
@@ -1297,8 +1725,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
@@ -1312,23 +1742,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 53d97d2..b023af5 100644
--- a/libbuild2/install/rule.hxx
+++ b/libbuild2/install/rule.hxx
@@ -22,45 +22,63 @@ 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.
+ // 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.
//
using prerequisite_iterator =
prerequisite_members_range<group_prerequisites>::iterator;
- virtual const target*
+ virtual pair<const target*, uint64_t>
filter (const scope*,
- action, const target&, prerequisite_iterator&) const;
+ action, const target&, prerequisite_iterator&,
+ match_extra&) const;
- virtual const target*
- filter (const scope*, 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
@@ -78,25 +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.
+ // 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;
@@ -105,10 +131,23 @@ 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.
+ // 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
@@ -120,27 +159,47 @@ namespace build2
//
// 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.
//
using prerequisite_iterator =
prerequisite_members_range<group_prerequisites>::iterator;
- virtual const target*
+ virtual pair<const target*, uint64_t>
filter (const scope*,
- action, const target&, prerequisite_iterator&) const;
+ action, const target&, prerequisite_iterator&,
+ match_extra&) const;
- virtual const target*
- filter (const scope*, 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 if the target is
- // not installable.
+ // 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&) const;
+ 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&);
@@ -178,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:
@@ -189,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,
@@ -199,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:
@@ -223,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,
@@ -245,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
index 17b1365..43d97fb 100644
--- a/libbuild2/install/utility.cxx
+++ b/libbuild2/install/utility.cxx
@@ -3,6 +3,9 @@
#include <libbuild2/install/utility.hxx>
+#include <libbuild2/variable.hxx>
+#include <libbuild2/diagnostics.hxx>
+
namespace build2
{
namespace install
@@ -12,6 +15,8 @@ namespace build2
{
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]))
@@ -30,5 +35,261 @@ namespace build2
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 cc5cd53..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);
@@ -61,26 +62,64 @@ namespace build2
// 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);
}
}