aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2021-03-16 20:21:59 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2021-03-26 20:00:31 +0300
commit2af2c4f092aa7efffe839ec615c06d22cf43cc3b (patch)
treee84676dbf273602fdf1f3541171df9dad7060daf
parent392c6003321047421467e07eac31e12875377ead (diff)
Add support for interactive builds
-rw-r--r--bbot/agent/agent.cli18
-rw-r--r--bbot/agent/agent.cxx80
-rw-r--r--bbot/agent/agent.hxx1
-rw-r--r--bbot/agent/machine.cxx39
-rw-r--r--bbot/agent/machine.hxx9
-rw-r--r--bbot/bbot-agent@.service6
-rw-r--r--bbot/types-parsers.cxx21
-rw-r--r--bbot/types-parsers.hxx9
-rw-r--r--bbot/worker/worker.cli8
-rw-r--r--bbot/worker/worker.cxx519
-rw-r--r--doc/manual.cli60
-rw-r--r--tests/integration/testscript27
12 files changed, 609 insertions, 188 deletions
diff --git a/bbot/agent/agent.cli b/bbot/agent/agent.cli
index 5d6cc9d..b50a43a 100644
--- a/bbot/agent/agent.cli
+++ b/bbot/agent/agent.cli
@@ -1,6 +1,8 @@
// file : bbot/agent.cli
// license : TBC; see accompanying LICENSE file
+include <libbbot/manifest.hxx>;
+
include <bbot/common.cli>;
"\section=1"
@@ -78,6 +80,15 @@ namespace bbot
testing)."
}
+ interactive_mode --interactive = interactive_mode::false_
+ {
+ "<mode>",
+ "Interactive build support. Valid values for this option are \cb{false}
+ (only non-interactive), \cb{true} (only interactive), and \cb{both}.
+ If this option is not specified, then only non-interactive builds
+ are supported."
+ }
+
// We reserve 0 in case in the future we want to distinguish a single-
// instance mode or some such.
//
@@ -182,6 +193,13 @@ namespace bbot
default."
}
+ size_t --intactive-timeout = 10800
+ {
+ "<sec>",
+ "Maximum number of seconds to wait for interactive build completion,
+ 10800 (3 hours) by default."
+ }
+
size_t --connect-timeout = 60
{
"<sec>",
diff --git a/bbot/agent/agent.cxx b/bbot/agent/agent.cxx
index 6c2e0d9..d85ecf5 100644
--- a/bbot/agent/agent.cxx
+++ b/bbot/agent/agent.cxx
@@ -8,6 +8,7 @@
#include <signal.h> // signal()
#include <stdlib.h> // rand_r()
#include <unistd.h> // sleep(), getuid(), fsync(), [f]stat()
+#include <ifaddrs.h> // getifaddrs(), freeifaddrs()
#include <sys/types.h> // stat
#include <sys/stat.h> // [f]stat()
#include <sys/file.h> // flock()
@@ -62,6 +63,7 @@ namespace bbot
uint16_t offset;
string hname;
+ string hip;
uid_t uid;
string uname;
}
@@ -179,7 +181,8 @@ bootstrap_machine (const dir_path& md,
mm,
obmm ? obmm->machine.mac : nullopt,
ops.bridge (),
- tftpd.port ()));
+ tftpd.port (),
+ false /* pub_vnc */));
{
// If we are terminating with an exception then force the machine down.
@@ -888,7 +891,8 @@ try
mm.machine,
mm.machine.mac,
ops.bridge (),
- tftpd.port ()));
+ tftpd.port (),
+ tm.interactive.has_value ()));
// Note: the machine handling logic is similar to bootstrap.
//
@@ -947,7 +951,9 @@ try
//
size_t to;
const size_t startup_to (120);
- const size_t build_to (ops.build_timeout ());
+ const size_t build_to (tm.interactive
+ ? ops.intactive_timeout ()
+ : ops.build_timeout ());
// Wait periodically making sure the machine is still alive.
//
@@ -1093,6 +1099,8 @@ try
uid = getuid ();
uname = getpwuid (uid)->pw_name;
+ // Obtain our hostname.
+ //
{
char buf[HOST_NAME_MAX + 1];
@@ -1103,6 +1111,44 @@ try
hname = buf;
}
+ // Obtain our IP address as a first discovered non-loopback IPv4 address.
+ //
+ // Note: Linux-specific implementation.
+ //
+ {
+ ifaddrs* i;
+ if (getifaddrs (&i) == -1)
+ fail << "unable to obtain IP addresses: "
+ << system_error (errno, std::generic_category ()); // Sanitize.
+
+ unique_ptr<ifaddrs, void (*)(ifaddrs*)> deleter (i, freeifaddrs);
+
+ for (; i != nullptr; i = i->ifa_next)
+ {
+ sockaddr* sa (i->ifa_addr);
+
+ if (sa != nullptr && // Configured.
+ (i->ifa_flags & IFF_LOOPBACK) == 0 && // Not a loopback interface.
+ (i->ifa_flags & IFF_UP) != 0 && // Up.
+ sa->sa_family == AF_INET) // Ignore IPv6 for now.
+ {
+ char buf[INET_ADDRSTRLEN]; // IPv4 address.
+ if (inet_ntop (AF_INET,
+ &reinterpret_cast<sockaddr_in*> (sa)->sin_addr,
+ buf,
+ sizeof (buf)) == nullptr)
+ fail << "unable to obtain IPv4 address: "
+ << system_error (errno, std::generic_category ()); // Sanitize.
+
+ hip = buf;
+ break;
+ }
+ }
+
+ if (hip.empty ())
+ fail << "no IPv4 address configured";
+ }
+
// On POSIX ignore SIGPIPE which is signaled to a pipe-writing process if
// the pipe reading end is closed. Note that by default this signal
// terminates a process. Also note that there is no way to disable this
@@ -1246,6 +1292,15 @@ try
return std::uniform_int_distribution<unsigned int> (50, 60) (g);
};
+ optional<interactive_mode> imode;
+ optional<string> ilogin;
+
+ if (ops.interactive () != interactive_mode::false_)
+ {
+ imode = ops.interactive ();
+ ilogin = machine_vnc (true /* public */);
+ }
+
for (unsigned int sleep (0);; ::sleep (sleep), sleep = 0)
{
bootstrapped_machines ms (enumerate_machines (ops.machines ()));
@@ -1256,6 +1311,8 @@ try
hname,
tc_name,
tc_ver,
+ imode,
+ ilogin,
fingerprint,
machine_header_manifests {}
};
@@ -1379,6 +1436,19 @@ try
continue;
}
+ // Make sure that the task interactivity matches the requested mode.
+ //
+ if (( t.interactive && !imode) ||
+ (!t.interactive && imode && *imode == interactive_mode::true_))
+ {
+ if (t.interactive)
+ error << "interactive task from " << u << ": " << *t.interactive;
+ else
+ error << "non-interactive task from " << u;
+
+ continue;
+ }
+
l2 ([&]{trace << "task for " << t.name << '/' << t.version << " "
<< "on " << t.machine << " "
<< "from " << u;});
@@ -1558,7 +1628,7 @@ namespace bbot
iface_addr (const string& i)
{
if (i.size () >= IFNAMSIZ)
- throw invalid_argument ("interface nama too long");
+ throw invalid_argument ("interface name too long");
auto_fd fd (socket (AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0));
@@ -1572,7 +1642,7 @@ namespace bbot
if (ioctl (fd.get (), SIOCGIFADDR, &ifr) == -1)
throw_system_error (errno);
- char buf[3 * 4 + 3 + 1]; // IPv4 address.
+ char buf[INET_ADDRSTRLEN]; // IPv4 address.
if (inet_ntop (AF_INET,
&reinterpret_cast<sockaddr_in*> (&ifr.ifr_addr)->sin_addr,
buf,
diff --git a/bbot/agent/agent.hxx b/bbot/agent/agent.hxx
index 93c4b56..ba3719e 100644
--- a/bbot/agent/agent.hxx
+++ b/bbot/agent/agent.hxx
@@ -25,6 +25,7 @@ namespace bbot
extern uint16_t inst; // Instance number.
extern string hname; // Our host name.
+ extern string hip; // Our IP address.
extern uid_t uid; // Our effective user id.
extern string uname; // Our effective user name.
diff --git a/bbot/agent/machine.cxx b/bbot/agent/machine.cxx
index c884f8c..3768971 100644
--- a/bbot/agent/machine.cxx
+++ b/bbot/agent/machine.cxx
@@ -171,7 +171,8 @@ namespace bbot
const machine_manifest&,
const optional<string>& mac,
const string& br_iface,
- uint16_t tftp_port);
+ uint16_t tftp_port,
+ bool pub_vnc);
virtual bool
shutdown (size_t& seconds) override;
@@ -214,18 +215,14 @@ namespace bbot
const machine_manifest& mm,
const optional<string>& omac,
const string& br,
- uint16_t port)
+ uint16_t port,
+ bool pub_vnc)
: machine (mm.mac ? *mm.mac : // Fixed mac from machine manifest.
omac ? *omac : // Generated mac from previous bootstrap.
generate_mac ()),
kvm ("kvm"),
net (br, port),
- //
- // QEMU's -vnc option (see below) expects the port offset from 5900
- // rather than the absolute value. The low 5901+, 6001+, and 6101+
- // ports all look good collision-wise with anything useful.
- //
- vnc ("127.0.0.1:" + to_string (5900 + offset)),
+ vnc (machine_vnc (pub_vnc)),
monitor ("/tmp/monitor-" + tc_name + '-' + to_string (inst))
{
tracer trace ("kvm_machine", md.string ().c_str ());
@@ -402,7 +399,15 @@ namespace bbot
// VNC.
//
- "-vnc", "127.0.0.1:" + to_string (offset), // 5900 + offset
+ // We listen on all IPs for a public VNC session and only on localhost
+ // for private.
+ //
+ // QEMU's -vnc option expects the port offset from 5900 rather than the
+ // absolute value. The low 5901+, 6001+, and 6101+ ports all look good
+ // collision-wise with anything useful.
+ //
+ "-vnc",
+ (pub_vnc ? ":" : "127.0.0.1:") + to_string (offset), // 5900 + offset
// QMP.
//
@@ -636,16 +641,28 @@ namespace bbot
const machine_manifest& mm,
const optional<string>& mac,
const string& br_iface,
- uint16_t tftp_port)
+ uint16_t tftp_port,
+ bool pub_vnc)
{
switch (mm.type)
{
case machine_type::kvm:
- return make_unique<kvm_machine> (md, mm, mac, br_iface, tftp_port);
+ return make_unique<kvm_machine> (
+ md, mm, mac, br_iface, tftp_port, pub_vnc);
+
case machine_type::nspawn:
assert (false); //@@ TODO
}
return nullptr;
}
+
+ string
+ machine_vnc (bool pub)
+ {
+ string r (pub ? hip : "127.0.0.1");
+ r += ':';
+ r += to_string (5900 + offset);
+ return r;
+ }
}
diff --git a/bbot/agent/machine.hxx b/bbot/agent/machine.hxx
index 44c7480..9a47d12 100644
--- a/bbot/agent/machine.hxx
+++ b/bbot/agent/machine.hxx
@@ -83,7 +83,14 @@ namespace bbot
const machine_manifest&,
const optional<string>& mac,
const string& br_iface,
- uint16_t tftp_port);
+ uint16_t tftp_port,
+ bool pub_vnc);
+
+ // Return the machine's public or private VNC session endpoint in the
+ // '<ip>:<port>' form.
+ //
+ string
+ machine_vnc (bool pub_vnc);
}
#endif // BBOT_AGENT_MACHINE_HXX
diff --git a/bbot/bbot-agent@.service b/bbot/bbot-agent@.service
index f610af2..294fde7 100644
--- a/bbot/bbot-agent@.service
+++ b/bbot/bbot-agent@.service
@@ -19,12 +19,16 @@ Environment=BRIDGE=br1
Environment=AUTH_KEY=
+Environment=INTERACTIVE=false
+
Environment=BOOTSTRAP_TIMEOUT=3600
Environment=BOOTSTRAP_RETRIES=2
Environment=BUILD_TIMEOUT=5400
Environment=BUILD_RETRIES=2
+Environment=INTERACTIVE_TIMEOUT=10800
+
Environment=CONNECT_TIMEOUT=60
Environment=REQUEST_TIMEOUT=300
Environment=REQUEST_RETRIES=4
@@ -46,10 +50,12 @@ ExecStart=/build/bots/default/bin/bbot-agent \
--ram ${RAM} \
--bridge ${BRIDGE} \
--auth-key ${AUTH_KEY} \
+ --interactive ${INTERACTIVE} \
--bootstrap-timeout ${BOOTSTRAP_TIMEOUT} \
--bootstrap-retries ${BOOTSTRAP_RETRIES} \
--build-timeout ${BUILD_TIMEOUT} \
--build-retries ${BUILD_RETRIES} \
+ --intactive-timeout ${INTERACTIVE_TIMEOUT} \
--connect-timeout ${CONNECT_TIMEOUT} \
--request-timeout ${REQUEST_TIMEOUT} \
--request-retries ${REQUEST_RETRIES} \
diff --git a/bbot/types-parsers.cxx b/bbot/types-parsers.cxx
index 7e82914..c4eff70 100644
--- a/bbot/types-parsers.cxx
+++ b/bbot/types-parsers.cxx
@@ -67,5 +67,26 @@ namespace bbot
throw invalid_value (o, v, e.what ());
}
}
+
+ void parser<interactive_mode>::
+ parse (interactive_mode& x, bool& xs, scanner& s)
+ {
+ xs = true;
+ const char* o (s.next ());
+
+ if (!s.more ())
+ throw missing_value (o);
+
+ const char* v (s.next ());
+
+ try
+ {
+ x = to_interactive_mode (v);
+ }
+ catch (const invalid_argument&)
+ {
+ throw invalid_value (o, v);
+ }
+ }
}
}
diff --git a/bbot/types-parsers.hxx b/bbot/types-parsers.hxx
index 1657086..23fc95b 100644
--- a/bbot/types-parsers.hxx
+++ b/bbot/types-parsers.hxx
@@ -7,6 +7,8 @@
#ifndef BBOT_TYPES_PARSERS_HXX
#define BBOT_TYPES_PARSERS_HXX
+#include <libbbot/manifest.hxx>
+
#include <bbot/types.hxx>
namespace bbot
@@ -38,6 +40,13 @@ namespace bbot
static void
parse (standard_version&, bool&, scanner&);
};
+
+ template <>
+ struct parser<interactive_mode>
+ {
+ static void
+ parse (interactive_mode&, bool&, scanner&);
+ };
}
}
diff --git a/bbot/worker/worker.cli b/bbot/worker/worker.cli
index e84d147..e3e37ce 100644
--- a/bbot/worker/worker.cli
+++ b/bbot/worker/worker.cli
@@ -82,6 +82,14 @@ namespace bbot
specified, then the user's home directory is used."
}
+ path --environment
+ {
+ "<path>",
+ "The environment setup executable path. This option is normally passed
+ by the worker running in the startup mode to the worker executed in the
+ build mode."
+ }
+
// Testing options.
//
string --tftp-host = "196.254.111.222"
diff --git a/bbot/worker/worker.cxx b/bbot/worker/worker.cxx
index bd880ae..3499ac4 100644
--- a/bbot/worker/worker.cxx
+++ b/bbot/worker/worker.cxx
@@ -16,6 +16,7 @@
#include <libbutl/b.mxx>
#include <libbutl/pager.mxx>
+#include <libbutl/prompt.mxx>
#include <libbutl/utility.mxx> // to_utf8()
#include <libbutl/filesystem.mxx>
#include <libbutl/string-parser.mxx>
@@ -147,33 +148,180 @@ catch (const system_error& e)
fail << "unable to remove directory " << d << ": " << e << endf;
}
+// Step IDs.
+//
+enum class step_id
+{
+ bpkg_module_create,
+ bpkg_module_configure_add,
+ bpkg_module_configure_fetch,
+ bpkg_module_configure_build,
+ bpkg_module_update,
+ bpkg_module_test,
+ bpkg_create,
+ bpkg_configure_add,
+ bpkg_configure_fetch,
+ bpkg_configure_build,
+ bpkg_update,
+ bpkg_test,
+ bpkg_test_separate_configure_build,
+ bpkg_test_separate_update,
+ bpkg_test_separate_test,
+ bpkg_install,
+ b_test_installed_create,
+ b_test_installed_configure,
+ b_test_installed_test,
+ bpkg_test_installed_create,
+ bpkg_test_installed_configure_add,
+ bpkg_test_installed_configure_fetch,
+ bpkg_test_separate_installed_configure_build,
+ bpkg_test_separate_installed_update,
+ bpkg_test_separate_installed_test,
+ bpkg_uninstall
+};
+
+// @@ Feel a bit heavy-weight. Should we move to const char*[]?
+//
+static const strings step_id_str {
+ "bpkg.module.create",
+ "bpkg.module.configure.add",
+ "bpkg.module.configure.fetch",
+ "bpkg.module.configure.build",
+ "bpkg.module.update",
+ "bpkg.module.test",
+ "bpkg.create",
+ "bpkg.configure.add",
+ "bpkg.configure.fetch",
+ "bpkg.configure.build",
+ "bpkg.update",
+ "bpkg.test",
+ "bpkg.test-separate.configure.build",
+ "bpkg.test-separate.update",
+ "bpkg.test-separate.test",
+ "bpkg.install",
+ "b.test-installed.create",
+ "b.test-installed.configure",
+ "b.test-installed.test",
+ "bpkg.test-installed.create",
+ "bpkg.test-installed.configure.add",
+ "bpkg.test-installed.configure.fetch",
+ "bpkg.test-separate-installed.configure.build",
+ "bpkg.test-separate-installed.update",
+ "bpkg.test-separate-installed.test",
+ "bpkg.uninstall"};
+
using std::regex;
namespace regex_constants = std::regex_constants;
using regexes = vector<regex>;
-// Run a named command. Name is used for logging and diagnostics only. Match
-// lines read from the command's stderr against the regular expressions and
-// return the warning result status (instead of success) in case of a match.
+// Run the worker script command. Name is used for logging and diagnostics
+// only. Match lines read from the command's stderr against the regular
+// expressions and return the warning result status (instead of success) in
+// case of a match. Save the executed command into last_cmd.
+//
+// If bkp_step is present and is equal to the command step, then prior to
+// running this command ask the user if to continue or abort the task
+// execution. If bkp_status is present, then ask for that if the command
+// execution results with the specified or more critical status.
//
template <typename... A>
static result_status
-run_cmd (tracer& t,
+run_cmd (step_id step,
+ tracer& t,
string& log, const regexes& warn_detect,
const string& name,
+ const optional<step_id>& bkp_step,
+ const optional<result_status>& bkp_status,
+ string& last_cmd,
const process_env& pe,
A&&... a)
{
+ // UTF-8-sanitize and log the diagnostics. Also print the raw diagnostics
+ // to stderr at verbosity level 3 or higher.
+ //
+ auto add = [&log, &t] (string&& s, bool trace = true)
+ {
+ if (verb >= 3)
+ {
+ if (trace)
+ t << s;
+ else
+ text << s;
+ }
+
+ to_utf8 (s, '?', codepoint_types::graphic, U"\n\r\t");
+
+ log += s;
+ log += '\n';
+ };
+
+ string next_cmd;
+
+ // Prompt the user if to continue the task execution and, if they refuse,
+ // log this and throw abort.
+ //
+ struct abort {};
+
+ auto prompt = [&last_cmd, &next_cmd, &add] (const string& what)
+ {
+ diag_record dr (text);
+
+ dr << '\n'
+ << what << '\n'
+ << " current dir: " << current_directory () << '\n'
+ << " environment: " << ops.environment ();
+
+ if (!last_cmd.empty ())
+ dr << '\n'
+ << " last command: " << last_cmd;
+
+ if (!next_cmd.empty ())
+ dr << '\n'
+ << " next command: " << next_cmd;
+
+ dr.flush ();
+
+ if (!yn_prompt (
+ "continue execution (or you may shutdown the machine)? [y/n]"))
+ {
+ add ("execution aborted by interactive user");
+ throw abort ();
+ }
+ };
+
try
{
- // Trace and log the command line.
+ // Trace, log, and save the command line.
//
- auto cmdc = [&t, &log] (const char* c[], size_t n)
+ auto cmdc = [step, &t, &log, &bkp_step, &next_cmd, &prompt]
+ (const char* c[], size_t n)
{
- t (c, n);
+ const string& sid (step_id_str[static_cast<size_t> (step)]);
std::ostringstream os;
process::print (os, c, n);
- log += os.str ();
+ next_cmd = os.str ();
+
+ // Prompt the user if the breakpoint is reached.
+ //
+ if (bkp_step && *bkp_step == step)
+ prompt (sid + " step is reached");
+
+ // Log the step id and the command to be executed.
+ //
+ l3 ([&]{t << "step id: " << sid;});
+
+#ifndef _WIN32
+ log += "# step id: ";
+#else
+ log += "rem step id: ";
+#endif
+ log += sid;
+ log += '\n';
+
+ t (c, n);
+
+ log += next_cmd;
log += '\n';
};
@@ -191,25 +339,6 @@ run_cmd (tracer& t,
result_status r (result_status::success);
- // UTF-8-sanitize and log the diagnostics. Also print the raw diagnostics
- // to stderr at verbosity level 3 or higher.
- //
- auto add = [&log, &t] (string&& s, bool trace = true)
- {
- if (verb >= 3)
- {
- if (trace)
- t << s;
- else
- text << s;
- }
-
- to_utf8 (s, '?', codepoint_types::graphic, U"\n\r\t");
-
- log += s;
- log += '\n';
- };
-
{
ifdstream is (move (pipe.in), fdstream_mode::skip); // Skip on exception.
@@ -242,14 +371,26 @@ run_cmd (tracer& t,
}
}
- if (pr.wait ())
- return r;
+ if (!pr.wait ())
+ {
+ const process_exit& e (*pr.exit);
+ add (name + " " + to_string (e));
+ r = e.normal () ? result_status::error : result_status::abnormal;
+ }
- const process_exit& e (*pr.exit);
+ last_cmd = move (next_cmd);
- add (name + " " + to_string (e));
+ if (bkp_status && r >= *bkp_status)
+ {
+ next_cmd.clear (); // Note: used by prompt().
+ prompt (!r ? "error occured" : "warning is issued");
+ }
- return e.normal () ? result_status::error : result_status::abnormal;
+ return r;
+ }
+ catch (const abort&)
+ {
+ return result_status::abort;
}
catch (const process_error& e)
{
@@ -263,39 +404,55 @@ run_cmd (tracer& t,
template <typename V, typename... A>
static result_status
-run_bpkg (const V& envvars,
+run_bpkg (step_id step,
+ const V& envvars,
tracer& t,
string& log, const regexes& warn_detect,
+ const optional<step_id>& bkp_step,
+ const optional<result_status>& bkp_status,
+ string& last_cmd,
const char* verbosity,
const string& cmd, A&&... a)
{
- return run_cmd (t,
+ return run_cmd (step,
+ t,
log, warn_detect,
"bpkg " + cmd,
+ bkp_step, bkp_status, last_cmd,
process_env ("bpkg", envvars),
verbosity, cmd, forward<A> (a)...);
}
template <typename... A>
static result_status
-run_bpkg (tracer& t,
+run_bpkg (step_id step,
+ tracer& t,
string& log, const regexes& warn_detect,
+ const optional<step_id>& bkp_step,
+ const optional<result_status>& bkp_status,
+ string& last_cmd,
const char* verbosity,
const string& cmd, A&&... a)
{
const char* const* envvars (nullptr);
- return run_bpkg (envvars,
+ return run_bpkg (step,
+ envvars,
t,
log, warn_detect,
+ bkp_step, bkp_status, last_cmd,
verbosity, cmd, forward<A> (a)...);
}
template <typename V, typename... A>
static result_status
-run_b (const V& envvars,
+run_b (step_id step,
+ const V& envvars,
tracer& t,
string& log, const regexes& warn_detect,
+ const optional<step_id>& bkp_step,
+ const optional<result_status>& bkp_status,
+ string& last_cmd,
const char* verbosity,
const strings& buildspecs, A&&... a)
{
@@ -308,39 +465,53 @@ run_b (const V& envvars,
name += s;
}
- return run_cmd (t,
+ return run_cmd (step,
+ t,
log, warn_detect,
name,
+ bkp_step, bkp_status, last_cmd,
process_env ("b", envvars),
verbosity, buildspecs, forward<A> (a)...);
}
template <typename V, typename... A>
static result_status
-run_b (const V& envvars,
+run_b (step_id step,
+ const V& envvars,
tracer& t,
string& log, const regexes& warn_detect,
+ const optional<step_id>& bkp_step,
+ const optional<result_status>& bkp_status,
+ string& last_cmd,
const char* verbosity,
const string& buildspec, A&&... a)
{
- return run_cmd (t,
+ return run_cmd (step,
+ t,
log, warn_detect,
"b " + buildspec,
+ bkp_step, bkp_status, last_cmd,
process_env ("b", envvars),
verbosity, buildspec, forward<A> (a)...);
}
template <typename... A>
static result_status
-run_b (tracer& t,
+run_b (step_id step,
+ tracer& t,
string& log, const regexes& warn_detect,
+ const optional<step_id>& bkp_step,
+ const optional<result_status>& bkp_status,
+ string& last_cmd,
const char* verbosity,
const string& buildspec, A&&... a)
{
const char* const* envvars (nullptr);
- return run_b (envvars,
+ return run_b (step,
+ envvars,
t,
log, warn_detect,
+ bkp_step, bkp_status, last_cmd,
verbosity, buildspec, forward<A> (a)...);
}
@@ -507,71 +678,54 @@ build (size_t argc, const char* argv[])
for (const string& re: tm.unquoted_warning_regex ())
wre.emplace_back (re, f);
- // Step IDs.
+ // Resolve the breakpoint specified by the interactive manifest value into
+ // the step id or the result status breakpoint. If the breakpoint is
+ // invalid, then log the error and abort the build. Note that we reuse the
+ // configure operation log here not to complicate things.
//
- enum class step_id
+ optional<step_id> bkp_step;
+ optional<result_status> bkp_status;
+ string last_cmd; // Used in the user prompt.
+
+ if (tm.interactive)
{
- bpkg_module_create,
- bpkg_module_configure_add,
- bpkg_module_configure_fetch,
- bpkg_module_configure_build,
- bpkg_module_update,
- bpkg_module_test,
- bpkg_create,
- bpkg_configure_add,
- bpkg_configure_fetch,
- bpkg_configure_build,
- bpkg_update,
- bpkg_test,
- bpkg_test_separate_configure_build,
- bpkg_test_separate_update,
- bpkg_test_separate_test,
- bpkg_install,
- b_test_installed_create,
- b_test_installed_configure,
- b_test_installed_test,
- bpkg_test_installed_create,
- bpkg_test_installed_configure_add,
- bpkg_test_installed_configure_fetch,
- bpkg_test_separate_installed_configure_build,
- bpkg_test_separate_installed_update,
- bpkg_test_separate_installed_test,
- bpkg_uninstall
- };
+ const string& b (*tm.interactive);
+
+ if (b == "error")
+ bkp_status = result_status::error;
+ else if (b == "warning")
+ bkp_status = result_status::warning;
+ else
+ {
+ for (size_t i (0); i < step_id_str.size (); ++i)
+ {
+ if (b == step_id_str[i])
+ {
+ bkp_step = static_cast<step_id> (i);
+ break;
+ }
+ }
+ }
- const strings step_id_str {
- "bpkg.module.create",
- "bpkg.module.configure.add",
- "bpkg.module.configure.fetch",
- "bpkg.module.configure.build",
- "bpkg.module.update",
- "bpkg.module.test",
- "bpkg.create",
- "bpkg.configure.add",
- "bpkg.configure.fetch",
- "bpkg.configure.build",
- "bpkg.update",
- "bpkg.test",
- "bpkg.test-separate.configure.build",
- "bpkg.test-separate.update",
- "bpkg.test-separate.test",
- "bpkg.install",
- "b.test-installed.create",
- "b.test-installed.configure",
- "b.test-installed.test",
- "bpkg.test-installed.create",
- "bpkg.test-installed.configure.add",
- "bpkg.test-installed.configure.fetch",
- "bpkg.test-separate-installed.configure.build",
- "bpkg.test-separate-installed.update",
- "bpkg.test-separate-installed.test",
- "bpkg.uninstall"};
+ if (!bkp_step && !bkp_status)
+ {
+ string e ("invalid interactive build breakpoint '" + b + "'");
+
+ l3 ([&]{trace << e;});
+
+ operation_result& r (add_result ("configure"));
+
+ r.log = "error: " + e + '\n';
+ r.status = result_status::abort;
+
+ break;
+ }
+ }
// Split the argument into prefix (empty if not present) and unquoted
// value. Return nullopt if the prefix is invalid.
//
- auto parse_arg =
- [&step_id_str] (const string& a) -> optional<pair<string, string>>
+ auto parse_arg = [] (const string& a) -> optional<pair<string, string>>
{
size_t p (a.find_first_of (":=\"'"));
@@ -660,10 +814,9 @@ build (size_t argc, const char* argv[])
// Return command arguments for the specified step id. Arguments with more
// specific prefixes come last.
//
- auto step_args = [&step_id_str] (const std::multimap<string, string>& args,
- step_id step,
- optional<step_id> fallback = nullopt)
- -> strings
+ auto step_args = [] (const std::multimap<string, string>& args,
+ step_id step,
+ optional<step_id> fallback = nullopt) -> strings
{
strings r;
const string& sid (step_id_str[static_cast<size_t> (step)]);
@@ -807,11 +960,13 @@ build (size_t argc, const char* argv[])
// for the build2 process. Return true if the dist meta-operation
// succeeds.
//
- auto redist = [&trace, &wre] (operation_result& r,
- const dir_path& dist_root,
- const dir_path& pkg_dir, // <name>-<version>
- const char* import = nullptr,
- const small_vector<string, 1>& envvars = {})
+ auto redist = [&trace, &wre, &bkp_step, &bkp_status, &last_cmd]
+ (step_id step,
+ operation_result& r,
+ const dir_path& dist_root,
+ const dir_path& pkg_dir, // <name>-<version>
+ const char* import = nullptr,
+ const small_vector<string, 1>& envvars = {})
{
// Temporarily change the current directory to the distribution root
// parent directory from the configuration directory to shorten the
@@ -832,8 +987,10 @@ build (size_t argc, const char* argv[])
dir_path redist_root ("re" + dn.string ());
r.status |= run_b (
+ step,
envvars,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"config.dist.root=" + redist_root.string (),
import,
@@ -907,7 +1064,9 @@ build (size_t argc, const char* argv[])
// to cc.
//
r.status |= run_b (
+ step_id::bpkg_module_create,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-V",
"create(" + module_dir.representation () + ",cc)",
"config.config.load=~build2",
@@ -921,7 +1080,9 @@ build (size_t argc, const char* argv[])
// bpkg create --existing
//
r.status |= run_bpkg (
+ step_id::bpkg_module_create,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"create",
"--existing");
@@ -934,7 +1095,9 @@ build (size_t argc, const char* argv[])
// bpkg.module.configure.add (bpkg.configure.add)
//
r.status |= run_bpkg (
+ step_id::bpkg_module_configure_add,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"add",
@@ -956,7 +1119,9 @@ build (size_t argc, const char* argv[])
// bpkg.module.configure.fetch (bpkg.configure.fetch)
//
r.status |= run_bpkg (
+ step_id::bpkg_module_configure_fetch,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"fetch",
@@ -978,7 +1143,9 @@ build (size_t argc, const char* argv[])
// [bpkg.module.configure.build]
//
r.status |= run_bpkg (
+ step_id::bpkg_module_configure_build,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"build",
"--configure-only",
@@ -1012,7 +1179,10 @@ build (size_t argc, const char* argv[])
// Note that we reuse the configure operation log for the dist
// meta-operation.
//
- if (!redist (r, dist_root, pkg_dir))
+ if (!redist (step_id::bpkg_module_configure_build,
+ r,
+ dist_root,
+ pkg_dir))
break;
rm.status |= r.status;
@@ -1035,7 +1205,9 @@ build (size_t argc, const char* argv[])
// [bpkg.module.update]
//
r.status |= run_bpkg (
+ step_id::bpkg_module_update,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"update",
pkg);
@@ -1069,7 +1241,9 @@ build (size_t argc, const char* argv[])
// [bpkg.module.test]
//
r.status |= run_bpkg (
+ step_id::bpkg_module_test,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"test",
"--package-cwd",
@@ -1113,7 +1287,9 @@ build (size_t argc, const char* argv[])
// importable in this configuration (see above about bootstrap).
//
r.status |= run_bpkg (
+ step_id::bpkg_create,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-V",
"create",
"-d", build_dir.string (),
@@ -1134,7 +1310,9 @@ build (size_t argc, const char* argv[])
// bpkg.configure.add
//
r.status |= run_bpkg (
+ step_id::bpkg_configure_add,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"add",
step_args (env_args, step_id::bpkg_configure_add),
@@ -1149,7 +1327,9 @@ build (size_t argc, const char* argv[])
// bpkg.configure.fetch
//
r.status |= run_bpkg (
+ step_id::bpkg_configure_fetch,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"fetch",
step_args (env_args, step_id::bpkg_configure_fetch),
@@ -1167,7 +1347,9 @@ build (size_t argc, const char* argv[])
if (!module) // Note: the module is already built in the pre-step.
{
r.status |= run_bpkg (
+ step_id::bpkg_configure_build,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"build",
"--configure-only",
@@ -1188,7 +1370,7 @@ build (size_t argc, const char* argv[])
if (dist)
{
- if (!redist (r, dist_root, pkg_dir))
+ if (!redist (step_id::bpkg_configure_build, r, dist_root, pkg_dir))
break;
rm.status |= r.status;
@@ -1209,7 +1391,9 @@ build (size_t argc, const char* argv[])
// bpkg.update
//
r.status |= run_bpkg (
+ step_id::bpkg_update,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"update",
step_args (env_args, step_id::bpkg_update),
@@ -1288,6 +1472,7 @@ build (size_t argc, const char* argv[])
// as a special dependency for the test package.
//
auto test = [&trace, &wre,
+ &bkp_step, &bkp_status, &last_cmd,
&step_args, &config_args, &env_args,
&pm,
&redist]
@@ -1298,18 +1483,6 @@ build (size_t argc, const char* argv[])
const char* import = nullptr,
const small_vector<string, 1>& envvars = {})
{
- auto args = [installed, &step_args] (
- const std::multimap<string, string>& args,
- step_id test_separate_installed_step,
- step_id test_separate_step,
- step_id main_step)
- {
- return installed
- ? step_args (args, test_separate_installed_step, main_step)
- : step_args (args, test_separate_step, main_step);
-
- };
-
for (const test_dependency& td: pm.tests)
{
const string& pkg (td.name.string ());
@@ -1321,25 +1494,22 @@ build (size_t argc, const char* argv[])
//
// bpkg.test-separate[-installed].configure.build (bpkg.configure.build)
//
+ step_id s (installed
+ ? step_id::bpkg_test_separate_installed_configure_build
+ : step_id::bpkg_test_separate_configure_build);
+
r.status |= run_bpkg (
+ s,
envvars,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"build",
"--configure-only",
"--checkout-root", dist_root,
"--yes",
-
- args (env_args,
- step_id::bpkg_test_separate_installed_configure_build,
- step_id::bpkg_test_separate_configure_build,
- step_id::bpkg_configure_build),
-
- args (config_args,
- step_id::bpkg_test_separate_installed_configure_build,
- step_id::bpkg_test_separate_configure_build,
- step_id::bpkg_configure_build),
-
+ step_args (env_args, s, step_id::bpkg_configure_build),
+ step_args (config_args, s, step_id::bpkg_configure_build),
import,
"--",
td.string (),
@@ -1371,7 +1541,7 @@ build (size_t argc, const char* argv[])
dist_root);
if (!pkg_dir.empty () &&
- !redist (r, dist_root, pkg_dir, import, envvars))
+ !redist (s, r, dist_root, pkg_dir, import, envvars))
return false;
}
catch (const system_error& e)
@@ -1385,22 +1555,19 @@ build (size_t argc, const char* argv[])
//
// bpkg.test-separate[-installed].update (bpkg.update)
//
+ s = installed
+ ? step_id::bpkg_test_separate_installed_update
+ : step_id::bpkg_test_separate_update;
+
r.status |= run_bpkg (
+ s,
envvars,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"update",
-
- args (env_args,
- step_id::bpkg_test_separate_installed_update,
- step_id::bpkg_test_separate_update,
- step_id::bpkg_update),
-
- args (config_args,
- step_id::bpkg_test_separate_installed_update,
- step_id::bpkg_test_separate_update,
- step_id::bpkg_update),
-
+ step_args (env_args, s, step_id::bpkg_update),
+ step_args (config_args, s, step_id::bpkg_update),
import,
pkg);
@@ -1416,23 +1583,20 @@ build (size_t argc, const char* argv[])
//
// bpkg.test-separate[-installed].test (bpkg.test)
//
+ s = installed
+ ? step_id::bpkg_test_separate_installed_test
+ : step_id::bpkg_test_separate_test;
+
r.status |= run_bpkg (
+ s,
envvars,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"test",
"--package-cwd", // See above for details.
-
- args (env_args,
- step_id::bpkg_test_separate_installed_test,
- step_id::bpkg_test_separate_test,
- step_id::bpkg_test),
-
- args (config_args,
- step_id::bpkg_test_separate_installed_test,
- step_id::bpkg_test_separate_test,
- step_id::bpkg_test),
-
+ step_args (env_args, s, step_id::bpkg_test),
+ step_args (config_args, s, step_id::bpkg_test),
import,
pkg);
@@ -1465,7 +1629,9 @@ build (size_t argc, const char* argv[])
// bpkg.test
//
r.status |= run_bpkg (
+ step_id::bpkg_test,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"test",
"--package-cwd", // See above for details.
@@ -1538,7 +1704,9 @@ build (size_t argc, const char* argv[])
// bpkg.install
//
r.status |= run_bpkg (
+ step_id::bpkg_install,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"install",
step_args (env_args, step_id::bpkg_install),
@@ -1634,7 +1802,9 @@ build (size_t argc, const char* argv[])
dir_path out_dir ("build-installed");
r.status |= run_b (
+ step_id::b_test_installed_create,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-V",
"create('" + out_dir.representation () + "'" + mods + ")",
step_args (env_args, step_id::b_test_installed_create),
@@ -1660,8 +1830,10 @@ build (size_t argc, const char* argv[])
dir_path subprj_out_dir (out_dir / d);
r.status |= run_b (
+ step_id::b_test_installed_configure,
envvars,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"configure('" +
subprj_src_dir.representation () + "'@'" +
@@ -1686,8 +1858,10 @@ build (size_t argc, const char* argv[])
// b.test-installed.test
//
r.status |= run_b (
+ step_id::b_test_installed_test,
envvars,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
test_specs,
step_args (env_args, step_id::b_test_installed_test),
@@ -1710,7 +1884,9 @@ build (size_t argc, const char* argv[])
dir_path config_dir ("build-installed-bpkg");
r.status |= run_bpkg (
+ step_id::bpkg_test_installed_create,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-V",
"create",
"-d", config_dir.string (),
@@ -1738,7 +1914,9 @@ build (size_t argc, const char* argv[])
// bpkg.test-installed.configure.add (bpkg.configure.add)
//
r.status |= run_bpkg (
+ step_id::bpkg_test_installed_configure_add,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"add",
@@ -1760,7 +1938,9 @@ build (size_t argc, const char* argv[])
// bpkg.test-installed.configure.fetch (bpkg.configure.fetch)
//
r.status |= run_bpkg (
+ step_id::bpkg_test_installed_configure_fetch,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"fetch",
@@ -1808,7 +1988,9 @@ build (size_t argc, const char* argv[])
// bpkg.uninstall
//
r.status |= run_bpkg (
+ step_id::bpkg_uninstall,
trace, r.log, wre,
+ bkp_step, bkp_status, last_cmd,
"-v",
"uninstall",
step_args (env_args, step_id::bpkg_uninstall),
@@ -1981,24 +2163,29 @@ startup ()
//
strings os;
+ // Use the name=value notation for options to minimize the number of
+ // arguments passed to the environment setup executable. Note that the
+ // etc/environments/default-*.bat scripts can only handle the limited
+ // number of arguments.
+ //
if (ops.systemd_daemon ())
os.push_back ("--systemd-daemon");
if (ops.verbose_specified ())
- {
- os.push_back ("--verbose");
- os.push_back (to_string (ops.verbose ()));
- }
+ os.push_back ("--verbose=" + to_string (ops.verbose ()));
if (ops.tftp_host_specified ())
- {
- os.push_back ("--tftp-host");
- os.push_back (ops.tftp_host ());
- }
+ os.push_back ("--tftp-host=" + ops.tftp_host ());
+
+ os.push_back (string ("--environment=") + pp.effect_string ());
// Note that we use the effective (absolute) path instead of recall since
// we may have changed the CWD.
//
+ // Also note that the worker can ask the user if to continue the task
+ // execution when the interactive build breakpoint is reached. Thus, we
+ // don't redirect stdin to /dev/null.
+ //
// Exit code 2 signals abnormal termination but where the worker uploaded
// the result itself.
//
@@ -2007,7 +2194,7 @@ startup ()
// nobody listening on the other end anymore).
//
string tg (tm.target.string ());
- switch (run_exit (trace, pp, tg, argv0.effect_string (), os))
+ switch (run_io_exit (trace, 0, 2, 2, pp, tg, argv0.effect_string (), os))
{
case 3:
case 2: return 1;
diff --git a/doc/manual.cli b/doc/manual.cli
index 8285366..d08a6ff 100644
--- a/doc/manual.cli
+++ b/doc/manual.cli
@@ -63,6 +63,15 @@ machines and sends this information as part of the request. The controller
responds with a build task manifest that identifies a specific build machine
to use.
+In the task request the agent specifies if only non-interactive, interactive,
+or both build kinds are supported. If interactive builds are supported, it
+additionally provides the login information for interactive build sessions. If
+the controller responds with an interactive build task, then its manifest
+specifies the breakpoint the worker must stop the task execution at and prompt
+the user whether to continue or abort the execution. The user can log into the
+build machine, potentially perform some troubleshooting, and, when done,
+either answer the prompt or just shutdown the machine.
+
If the controller has higher-level controllers (for example, \c{brep}), then
it aggregates the available build machines from its agents and polls these
controllers (just as an agent would), forwarding build tasks to suitable
@@ -328,6 +337,7 @@ target: <target-triplet>
[environment]: <environment-name>
[config]: <config-args>
[warning-regex]: <warning-regex>
+[interactive]: <breakpoint>
\
@@ -495,6 +505,21 @@ Note that this built-in list also covers GCC and Clang warnings (for the
English locale).
+\h2#arch-task-interactive|\c{interactive}|
+
+\
+[interactive]: <breakpoint>
+\
+
+The task execution step to stop at. Can only be present if the agent has
+specified \c{interactive-mode} with either the \c{true} or \c{both} value in
+the task request.
+
+The breakpoint can either be a primary step id of the worker script or the
+special \c{error} or \c{warning} value. See \l{#arch-worker Worker Logic} for
+details.
+
+
\h#arch-result|Result Manifest|
The result manifest describes a build result. The manifest synopsis is
@@ -615,6 +640,8 @@ description of each value in subsequent sections.
agent: <name>
toolchain-name: <name>
toolchain-version: <standard-version>
+[interactive-mode]: false|true|both
+[interactive-login]: <login>
[fingerprint]: <agent-fingerprint>
\
@@ -647,6 +674,28 @@ toolchain-version: <standard-version>
The \c{build2} toolchain version being used by the agent.
+\h2#arch-task-req-interactive-mode|\c{interactive-mode}|
+
+\
+[interactive-mode]: false|true|both
+\
+
+The agent's capability to perform build tasks only non-interactively
+(\c{false}), only interactively (\c{true}), or both (\c{both}).
+
+If it is not specified, then the \c{false} value is assumed.
+
+
+\h2#arch-task-req-interactive-login|\c{interactive-login}|
+
+\
+[interactive-login]: <login>
+\
+
+The login information for the interactive build session. Must be present only
+if \c{interactive-mode} is specified with the \c{true} or \c{both} value.
+
+
\h2#arch-task-req-fingerprint|\c{fingerprint}|
\
@@ -742,8 +791,8 @@ The session id as returned by the controller in the task response.
\
The answer to the private key challenge as posed by the controller in the task
-response. It must be present only if the challenge value was present in the
-task response.
+response. It must be present only if the \c{challenge} value was present in
+the task response.
\h#arch-worker|Worker Logic|
@@ -947,6 +996,13 @@ then its \c{dist} meta-operation is also tested as a part of the
\c{bpkg[.*].configure.build} steps by re-distributing the source directory in
the load distribution mode after configuration.
+If the build is interactive, then the worker pauses its execution at the
+specified breakpoint and prompts the user whether to continue or abort the
+execution. If the breakpoint is a step id, then the worker pauses prior to
+executing every command of the specified step. Otherwise, the breakpoint
+denotes the result status and the worker pauses if the command results with
+the specified or more critical status (see \l{#arch-result Result Manifest}).
+
As an example, the following POSIX shell script can be used to setup the
environment for building C and C++ packages with GCC 9 on most Linux
distributions.
diff --git a/tests/integration/testscript b/tests/integration/testscript
index e9b0db1..1408946 100644
--- a/tests/integration/testscript
+++ b/tests/integration/testscript
@@ -126,6 +126,22 @@ rep_type = git
rfp = yes
#\
+#\
+pkg = libcmark-gfm-extensions
+ver = 0.29.0-a.1+7
+rep_url = https://pkg.cppget.org/1/alpha
+rep_type = pkg
+rfp = yes
+#\
+
+#\
+pkg = non-existing
+ver = 0.1.0
+rep_url = https://pkg.cppget.org/1/alpha
+rep_type = pkg
+rfp = yes
+#\
+
# Note that we also need to make sure that the installed package libraries are
# properly imported when configuring and running tests, and that the installed
# executables are runnable.
@@ -133,9 +149,13 @@ rfp = yes
config = "\"config.install.root='$~/install'\" \
bpkg:--fetch-timeout=60 \
\"config.bin.rpath='$~/install/lib'\" \
+config.cc.coptions=-Wall \
b.test-installed.configure:\"config.cc.loptions=-L'$~/install/lib'\" \
bpkg.test-installed.create:\"config.cc.loptions=-L'$~/install/lib'\""
+#interactive="interactive: bpkg.configure.build"
+#interactive="interactive: warning"
+
+cat <<"EOI" >=task
: 1
name: $pkg
@@ -146,6 +166,7 @@ bpkg.test-installed.create:\"config.cc.loptions=-L'$~/install/lib'\""
machine: $machine
target: $target
config: $config
+ $interactive
EOI
+if ("$environment" != "")
@@ -188,9 +209,9 @@ a = $0
chmod ugo+x $env;
sleep $wait;
$w --verbose 3 --startup --tftp-host $tftp --environments $~ \
- &?build-module/*** &build/*** \
+ &?build-module/*** &?build/*** \
&?build-installed/*** &?build-installed-bpkg/*** \
- &?dist/*** &?redist/*** \
+ &?dist/*** &?redist/*** \
&?dist-installed/*** &?redist-installed/*** \
- &task.manifest 2>|
+ &task.manifest <| 2>|
}