// file : bbot/worker.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file #ifndef _WIN32 # include // signal() #else # include // SetErrorMode(), Sleep() #endif #include #include #include // strchr(), strncmp() #include #include #include #include #include #include // to_utf8(), eof() #include #include #include #include #include #include #include #include #include #include #include using namespace butl; using namespace bbot; using std::cout; using std::endl; namespace bbot { int main (int argc, char* argv[]); static int build (size_t argc, const char* argv[]); process_path argv0; worker_options ops; dir_path env_dir; // Note that upload can be quite large and take a while to upload under high // load. // const size_t tftp_blksize (1468); // Between 512 (default) and 65464. const size_t tftp_put_timeout (3600); // 1 hour (also the default). const size_t tftp_get_timeout (10); // 10 seconds. const size_t tftp_get_retries (3); // Task request retries (see startup()). } static bool exists (const dir_path& d) try { return dir_exists (d); } catch (const system_error& e) { fail << "unable to stat path " << d << ": " << e << endf; } static bool exists (const path& f) try { return file_exists (f, true /* follow_symlinks */); } catch (const system_error& e) { fail << "unable to stat path " << f << ": " << e << endf; } static dir_path current_directory () try { return dir_path::current_directory (); } catch (const system_error& e) { fail << "unable to obtain current directory: " << e << endf; } static void #ifndef _WIN32 mk_p (tracer& t, string* log, const dir_path& d, bool sudo = false) { if (sudo) { if (log != nullptr) *log += "sudo mkdir -p " + d.representation () + '\n'; run_io (t, 0, 1, 2, "sudo", "mkdir", "-p", d); } else #else mk_p (tracer& t, string* log, const dir_path& d, bool = false) { #endif try { if (verb >= 3) t << "mkdir -p " << d; if (log != nullptr) *log += "mkdir -p " + d.representation () + '\n'; try_mkdir_p (d); } catch (const system_error& e) { fail << "unable to create directory " << d << ": " << e << endf; } } static void mk (tracer& t, string* log, const dir_path& d) try { if (verb >= 3) t << "mkdir " << d; if (log != nullptr) *log += "mkdir " + d.representation () + '\n'; try_mkdir (d); } catch (const system_error& e) { fail << "unable to create directory " << d << ": " << e << endf; } static bool empty (const dir_path& d) try { return dir_empty (d); } catch (const system_error& e) { fail << "unable to scan directory " << d << ": " << e << endf; } static void cp_into (tracer& t, string* log, const path& p, const dir_path& d) try { if (verb >= 3) t << "cp " << p << ' ' << d; if (log != nullptr) *log += "cp " + p.string () + ' ' + d.representation () + '\n'; cpfile_into (p, d); } catch (const system_error& e) { fail << "unable to copy file " << p << " into " << d << ": " << e << endf; } static void mv (tracer& t, string* log, const dir_path& from, const dir_path& to) try { if (verb >= 3) t << "mv " << from << ' ' << to; if (log != nullptr) *log += "mv " + from.representation () + ' ' + to.representation () + "\n"; mvdir (from, to); } catch (const system_error& e) { fail << "unable to move directory '" << from << "' to '" << to << "': " << e << endf; } static void mv_into (tracer& t, string* log, const path& from, const dir_path& into) try { if (verb >= 3) t << "mv " << from << ' ' << into; if (log != nullptr) *log += "mv " + from.representation () + ' ' + into.representation () + "\n"; mventry_into (from, into); } catch (const system_error& e) { fail << "unable to move entry '" << from << "' into '" << into << "': " << e << endf; } static void rm_r (tracer& t, string* log, const dir_path& d) try { if (verb >= 3) t << "rm -r " << d; if (log != nullptr) *log += "rm -r " + d.representation () + '\n'; rmdir_r (d); } catch (const system_error& e) { fail << "unable to remove directory " << d << ": " << e << endf; } static dir_path change_wd (tracer& t, string* log, const dir_path& d, bool create = false) try { if (create) mk_p (t, log, d); dir_path r (current_directory ()); if (verb >= 3) t << "cd " << d; if (log != nullptr) *log += "cd " + d.representation () + '\n'; dir_path::current_directory (d); return r; } catch (const system_error& e) { fail << "unable to change current directory to " << d << ": " << e << endf; } // Step IDs. // // NOTE: keep ids ordered according to the sequence of steps and remember to // update unreachable breakpoint checks if changing anything here. // enum class step_id { // Common fallbacks for bpkg_*_create/b_test_installed_create and // bpkg_*_configure_build/b_test_installed_configure, respectively. Note: // not breakpoints. // b_create, b_configure, // Note that bpkg_module_* options are only used if the main package is a // build system module (using just ~build2 otherwise). They also have no // fallback (build system modules are just too different to try to handle // them together with target and host; e.g., install root). However, // bpkg_module_create is complemented with arguments from un-prefixed step // ids, the same way as other *.create[_for_*] steps (note that un-prefixed // steps are not fallbacks, they are always added first). // bpkg_create, // Breakpoint and base. bpkg_target_create, //: b_create, bpkg_create bpkg_host_create, //: b_create, bpkg_create bpkg_module_create, //: no fallback bpkg_link, bpkg_configure_add, bpkg_configure_fetch, // Global (as opposed to package-specific) bpkg-pkg-build options (applies // to all *_configure_build* steps). Note: not a breakpoint. // bpkg_global_configure_build, // Note that bpkg_configure_build serves as a breakpoint for the // bpkg-pkg-build call that configures (at once) the main package and all // its external tests. // bpkg_configure_build, // Breakpoint and base. bpkg_target_configure_build, //: b_configure, bpkg_configure_build bpkg_host_configure_build, //: b_configure, bpkg_configure_build bpkg_module_configure_build, //: b_configure, bpkg_configure_build bpkg_update, bpkg_test, // Note that separate test packages are configured as part of the // bpkg_configure_build step above with options taken from // bpkg_{target,host}_configure_build, depending on tests package type. // bpkg_test_separate_update, //: bpkg_update bpkg_test_separate_test, //: bpkg_test // Note that we only perform the installation tests if this is a target // package or a self-hosted configuration. Also note that this step is // considered disabled if any of the bpkg_bindist_* steps is explicitly // enabled. // bpkg_install, bbot_install_ldconfig, // Note: disabled by default. // Note that the bpkg_bindist_* steps are mutually exclusive and the latest // status change for them (via the leading +/- characters in the prefix) // overrides all the previous ones. Disabled by default. // bpkg_bindist_debian, bpkg_bindist_fedora, bpkg_bindist_archive, // Note that this step is considered disabled unless one of the // bpkg_bindist_* steps is explicitly enabled. Note: not a breakpoint. // bbot_sys_install, bbot_sys_install_apt_get_update, bbot_sys_install_apt_get_install, bbot_sys_install_dnf_install, bbot_sys_install_tar_extract, bbot_sys_install_ldconfig, // Note: disabled by default. // Note: skipped for modules. // b_test_installed_create, //: b_create b_test_installed_configure, //: b_configure b_test_installed_test, // Note that for a host package this can involve both run-time and build- // time tests (which means we may also need a shared configuration for // modules). // // The *_for_{target,host,module} denote main package type, not // configuration being created, which will always be target (more precisely, // target or host, but host only in a self-hosted case, which means it's // the same as target). // // Note that if this is a non-self-hosted configuration, we can only end up // here if building target package and so can just use *_create and *_build // values in buildtabs. // bpkg_test_separate_installed_create, // Breakpoint and base. bpkg_test_separate_installed_create_for_target, //: bpkg_test_separate_installed_create bpkg_test_separate_installed_create_for_host, //: bpkg_test_separate_installed_create bpkg_test_separate_installed_create_for_module, //: no fallback bpkg_test_separate_installed_link, // breakpoint only bpkg_test_separate_installed_configure_add, //: bpkg_configure_add bpkg_test_separate_installed_configure_fetch, //: bpkg_configure_fetch bpkg_test_separate_installed_configure_build, // Breakpoint and base. bpkg_test_separate_installed_configure_build_for_target, //: bpkg_test_separate_installed_configure_build bpkg_test_separate_installed_configure_build_for_host, //: bpkg_test_separate_installed_configure_build bpkg_test_separate_installed_configure_build_for_module, //: bpkg_test_separate_installed_configure_build bpkg_test_separate_installed_update, //: bpkg_update bpkg_test_separate_installed_test, //: bpkg_test bbot_sys_uninstall_apt_get_remove, bbot_sys_uninstall_dnf_remove, bpkg_uninstall, bbot_bindist_upload, // Note: disabled by default, not a breakpoint. // Note that this step is considered disabled unless the upload/ directory // is not empty. Note: not a breakpoint. // bbot_upload, bbot_upload_tar_create, bbot_upload_tar_list, end }; static const strings step_id_str { "b.create", "b.configure", "bpkg.create", "bpkg.target.create", "bpkg.host.create", "bpkg.module.create", "bpkg.link", "bpkg.configure.add", "bpkg.configure.fetch", "bpkg.global.configure.build", "bpkg.configure.build", "bpkg.target.configure.build", "bpkg.host.configure.build", "bpkg.module.configure.build", "bpkg.update", "bpkg.test", "bpkg.test-separate.update", "bpkg.test-separate.test", "bpkg.install", "bbot.install.ldconfig", "bpkg.bindist.debian", "bpkg.bindist.fedora", "bpkg.bindist.archive", "bbot.sys-install", "bbot.sys-install.apt-get.update", "bbot.sys-install.apt-get.install", "bbot.sys-install.dnf.install", "bbot.sys-install.tar.extract", "bbot.sys-install.ldconfig", "b.test-installed.create", "b.test-installed.configure", "b.test-installed.test", "bpkg.test-separate-installed.create", "bpkg.test-separate-installed.create_for_target", "bpkg.test-separate-installed.create_for_host", "bpkg.test-separate-installed.create_for_module", "bpkg.test-separate-installed.link", "bpkg.test-separate-installed.configure.add", "bpkg.test-separate-installed.configure.fetch", "bpkg.test-separate-installed.configure.build", "bpkg.test-separate-installed.configure.build_for_target", "bpkg.test-separate-installed.configure.build_for_host", "bpkg.test-separate-installed.configure.build_for_module", "bpkg.test-separate-installed.update", "bpkg.test-separate-installed.test", "bbot.sys-uninstall.apt-get.remove", "bbot.sys-uninstall.dnf.remove", "bpkg.uninstall", "bbot.bindist.upload", "bbot.upload", "bbot.upload.tar.create", "bbot.upload.tar.list", "end"}; static inline const string& to_string (step_id s) { return step_id_str[static_cast (s)]; } using std::regex; namespace regex_constants = std::regex_constants; using regexes = vector; // UTF-8-sanitize and log the line. Also print it to a tracer, if specified, // or to stderr otherwise at verbosity level 3 or higher. // static void log_line (string&& l, string& log, tracer* trace = nullptr) { if (verb >= 3) { if (trace != nullptr) *trace << l; else text << l; } to_utf8 (l, '?', codepoint_types::graphic, U"\n\r\t"); log += l; log += '\n'; } #ifndef _WIN32 const char* comment_begin ("#"); #else const char* comment_begin ("rem"); #endif static void log_step_id (tracer& t, string& log, step_id id) { string ts (to_string (system_clock::now (), "%Y-%m-%d %H:%M:%S %Z", true /* special */, true /* local */)); const string& sid (to_string (id)); l3 ([&]{t << "step id: " << sid << ' ' << ts;}); log += comment_begin; log += " step id: "; log += sid; log += ' '; log += ts; log += '\n'; } // Add the specified string to the log as a comment. Unless the string is // empty (e.g., a blank line to separate comments), also trace it. // static void log_comment (tracer& t, string& log, const string& s) { if (!s.empty ()) l3 ([&]{t << s;}); log += comment_begin; if (!s.empty ()) { log += ' '; log += s; } log += '\n'; } // 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. // // Redirect stdout to stderr if the out_* arguments are not specified (out_str // is NULL and out_file is empty; must never be specified both). Otherwise, // save the process output into the variable referenced by out_str, if // specified, and to the file referenced by out_file otherwise. Note: in the // former case assumes that the output will always fit into the pipe buffer. // // 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. // // For the special end step no command is executed. In this case only the user // is potentially prompted and the step is traced/logged. // // If specified, the pre-run callback is called after the step id is logged // but before the command is logged/executed. // using pre_run_function = void (); template static result_status run_cmd (step_id step, tracer& t, string& log, const function& pre_run, optional* out_str, const path& out_file, const regexes& warn_detect, const string& name, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const process_env& pe, A&&... a) { assert (out_str == nullptr || out_file.empty ()); 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 = [&aux_env, &last_cmd, &next_cmd, &t, &log] (const string& what) { diag_record dr (text); dr << '\n' << what << '\n' << " current dir: " << current_directory () << '\n' << " environment: " << ops.env_script () << ' ' << ops.env_target (); if (!aux_env.empty ()) { dr << '\n' << " auxiliary environment:"; for (const string& e: aux_env) dr << '\n' << " " << e; } 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]")) { log_line ("execution aborted by interactive user", log, &t); throw abort (); } }; auto prompt_step = [step, &t, &log, &bkp_step, &prompt, &pre_run] () { // Prompt the user if the breakpoint is reached. // if (bkp_step && *bkp_step == step) prompt (to_string (step) + " step reached"); log_step_id (t, log, step); if (pre_run) pre_run (); }; try { // Trace, log, and save the command line. // auto cmdc = [&t, &log, &next_cmd, &prompt_step] (const char* c[], size_t n) { std::ostringstream os; process::print (os, c, n); next_cmd = os.str (); prompt_step (); // Log the command to be executed. // t (c, n); log += next_cmd; log += '\n'; }; result_status r (result_status::success); if (step != step_id::end) { try { // Redirect stdout to stderr, if the caller is not interested in it. // // Text mode seems appropriate. // fdpipe out_pipe (out_str != nullptr ? fdopen_pipe () : fdpipe ()); fdpipe err_pipe (fdopen_pipe ()); // If the output file is specified, then open "half-pipe". // if (!out_file.empty ()) try { out_pipe.out = fdopen (out_file, fdopen_mode::out | fdopen_mode::create); } catch (const io_error& e) { fail << "unable to open " << out_file << ": " << e; } process pr ( process_start_callback ( cmdc, fdopen_null (), // Never reads from stdin. out_pipe.out != nullfd ? out_pipe.out.get () : 2, err_pipe, pe, forward (a)...)); out_pipe.out.close (); err_pipe.out.close (); { // Skip on exception. // ifdstream is (move (err_pipe.in), fdstream_mode::skip, ifdstream::badbit); for (string l; !eof (getline (is, l)); ) { // Match the log line with the warning-detecting regular // expressions until the first match. // if (r != result_status::warning) { for (const regex& re: warn_detect) { // Only examine the first 512 bytes. Long lines (e.g., linker // command lines) could trigger implementation-specific // limitations (like stack overflow). Plus, it is a // performance concern. // if (regex_search (l.begin (), (l.size () < 512 ? l.end () : l.begin () + 512), re)) { r = result_status::warning; break; } } } log_line (move (l), log); } } if (!pr.wait ()) { const process_exit& e (*pr.exit); log_line (name + ' ' + to_string (e), log, &t); r = e.normal () ? result_status::error : result_status::abnormal; } // Only read the buffered output if the process terminated normally. // if (out_str != nullptr && pr.exit->normal ()) { // Note: shouldn't throw since the output is buffered. // ifdstream is (move (out_pipe.in)); *out_str = is.read_text (); } last_cmd = move (next_cmd); if (bkp_status && r >= *bkp_status) { next_cmd.clear (); // Note: used by prompt(). prompt (!r ? "error occured" : "warning is issued"); } } catch (const process_error& e) { fail << "unable to execute " << name << ": " << e; } catch (const io_error& e) { fail << "unable to read " << name << " diagnostics: " << e; } } else { next_cmd.clear (); // Note: used by prompt_step(). prompt_step (); } return r; } catch (const abort&) { return result_status::abort; } } template static result_status run_cmd (step_id step, tracer& t, string& log, optional* out_str, const path& out_file, const regexes& warn_detect, const string& name, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const process_env& pe, A&&... a) { return run_cmd (step, t, log, nullptr /* pre_run */, out_str, out_file, warn_detect, name, bkp_step, bkp_status, aux_env, last_cmd, pe, forward (a)...); } template static result_status run_bpkg (step_id step, const V& envvars, tracer& t, string& log, const function& pre_run, optional& out, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const char* verbosity, const string& cmd, A&&... a) { return run_cmd (step, t, log, pre_run, &out, path () /* out_file */, warn_detect, "bpkg " + cmd, bkp_step, bkp_status, aux_env, last_cmd, process_env ("bpkg", envvars), verbosity, cmd, forward (a)...); } template static result_status run_bpkg (step_id step, const V& envvars, tracer& t, string& log, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const char* verbosity, const string& cmd, A&&... a) { return run_cmd (step, t, log, nullptr /* out_str */, path () /* out_file */, warn_detect, "bpkg " + cmd, bkp_step, bkp_status, aux_env, last_cmd, process_env ("bpkg", envvars), verbosity, cmd, forward (a)...); } template static result_status run_bpkg (step_id step, tracer& t, string& log, const function& pre_run, optional& out, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const char* verbosity, const string& cmd, A&&... a) { const char* const* envvars (nullptr); return run_bpkg (step, envvars, t, log, pre_run, out, warn_detect, bkp_step, bkp_status, aux_env, last_cmd, verbosity, cmd, forward (a)...); } template static result_status run_bpkg (step_id step, const V& envvars, tracer& t, string& log, const path& out, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const char* verbosity, const string& cmd, A&&... a) { return run_cmd (step, t, log, nullptr /* out_str */, out, warn_detect, "bpkg " + cmd, bkp_step, bkp_status, aux_env, last_cmd, process_env ("bpkg", envvars), verbosity, cmd, forward (a)...); } template static result_status run_bpkg (step_id step, tracer& t, string& log, const path& out, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const char* verbosity, const string& cmd, A&&... a) { const char* const* envvars (nullptr); return run_bpkg (step, envvars, t, log, out, warn_detect, bkp_step, bkp_status, aux_env, last_cmd, verbosity, cmd, forward (a)...); } template static result_status run_bpkg (step_id step, tracer& t, string& log, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const char* verbosity, const string& cmd, A&&... a) { const char* const* envvars (nullptr); return run_bpkg (step, envvars, t, log, warn_detect, bkp_step, bkp_status, aux_env, last_cmd, verbosity, cmd, forward (a)...); } template static result_status run_b (step_id step, const V& envvars, tracer& t, string& log, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const char* verbosity, const strings& buildspecs, A&&... a) { string name ("b"); for (const string& s: buildspecs) { if (!name.empty ()) name += ' '; name += s; } return run_cmd (step, t, log, nullptr /* out_str */, path () /* out_file */, warn_detect, name, bkp_step, bkp_status, aux_env, last_cmd, process_env ("b", envvars), verbosity, buildspecs, forward (a)...); } template static result_status run_b (step_id step, const V& envvars, tracer& t, string& log, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const char* verbosity, const string& buildspec, A&&... a) { return run_cmd (step, t, log, nullptr /* out_str */, path () /* out_file */, warn_detect, "b " + buildspec, bkp_step, bkp_status, aux_env, last_cmd, process_env ("b", envvars), verbosity, buildspec, forward (a)...); } template static result_status run_b (step_id step, tracer& t, string& log, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const char* verbosity, const string& buildspec, A&&... a) { const char* const* envvars (nullptr); return run_b (step, envvars, t, log, warn_detect, bkp_step, bkp_status, aux_env, last_cmd, verbosity, buildspec, forward (a)...); } template static result_status run_ldconfig (step_id step, tracer& t, string& log, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, A&&... a) { return run_cmd (step, t, log, nullptr /* out_str */, path () /* out_file*/, warn_detect, "sudo ldconfig", bkp_step, bkp_status, aux_env, last_cmd, process_env ("sudo"), "ldconfig", forward (a)...); } template static result_status run_apt_get (step_id step, tracer& t, string& log, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const string& cmd, A&&... a) { // Note: dumps some of its diagnostics to stdout. // return run_cmd (step, t, log, nullptr /* out_str */, path () /* out_file*/, warn_detect, "sudo apt-get " + cmd, bkp_step, bkp_status, aux_env, last_cmd, process_env ("sudo"), "apt-get", cmd, forward (a)...); } template static result_status run_dnf (step_id step, tracer& t, string& log, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, const string& cmd, A&&... a) { // Note: dumps some of its diagnostics to stdout. // return run_cmd (step, t, log, nullptr /* out_str */, path () /* out_file*/, warn_detect, "sudo dnf " + cmd, bkp_step, bkp_status, aux_env, last_cmd, process_env ("sudo"), "dnf", cmd, forward (a)...); } #ifndef _WIN32 template static result_status run_tar (step_id step, tracer& t, string& log, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, bool sudo, A&&... a) { return run_cmd (step, t, log, nullptr /* out_str */, path () /* out_file*/, warn_detect, sudo ? "sudo tar" : "tar", bkp_step, bkp_status, aux_env, last_cmd, process_env (sudo ? "sudo" : "tar"), sudo ? "tar" : nullptr, forward (a)...); } #else template static result_status run_tar (step_id step, tracer& t, string& log, const regexes& warn_detect, const optional& bkp_step, const optional& bkp_status, const strings& aux_env, string& last_cmd, bool /* sudo */, A&&... a) { // Note: using bsdtar which can unpack .zip archives (and also not an MSYS // executable). // return run_cmd (step, t, log, nullptr /* out_str */, path () /* out_file*/, warn_detect, "bsdtar", bkp_step, bkp_status, aux_env, last_cmd, process_env ("bsdtar"), forward (a)...); } #endif // Upload compressed manifest to the specified TFTP URL with curl. Issue // diagnostics and throw failed on invalid manifest or process management // errors and throw io_error for input/output errors or non-zero curl exit. // template static void upload_manifest (tracer& trace, const string& url, const T& m, const string& what) { try { // Piping the data directly into curl's stdin sometimes results in the // broken pipe error on the client and partial/truncated upload on the // server. This happens quite regularly on older Linux distributions // (e.g., Debian 8, Ubuntu 16.04) but also sometimes on Windows. On the // other hand, uploading from a file appears to work reliably (we still // get an odd error on Windows from time to time with larger uploads). // // Let's not break lines in the manifest values not to further increase // the size of the manifest encoded representation. Also here we don't // care much about readability of the manifest since it will only be read // by the bbot agent anyway. // #if 0 // Note: need to add compression support if re-enable this. tftp_curl c (trace, path ("-"), nullfd, curl::put, url, "--tftp-blksize", tftp_blksize, "--max-time", tftp_put_timeout); manifest_serializer s (c.out, url, true /* long_lines */); m.serialize (s); c.out.close (); #else auto_rmfile tmp; try { tmp = auto_rmfile (path::temp_path (what + "-manifest.lz4")); ofdstream ofs (tmp.path, fdopen_mode::binary); olz4stream ozs (ofs, 9, 5 /* 256KB */, nullopt /* content_size */); manifest_serializer s (ozs, tmp.path.string (), true /* long_lines */); m.serialize (s); ozs.close (); ofs.close (); } catch (const io_error& e) // In case not derived from system_error. { fail << "unable to save " << what << " manifest: " << e; } catch (const system_error& e) { fail << "unable to save " << what << " manifest: " << e; } tftp_curl c (trace, tmp.path, nullfd, curl::put, url, "--tftp-blksize", tftp_blksize, "--max-time", tftp_put_timeout); #endif if (!c.wait ()) throw_generic_ios_failure (EIO, "non-zero curl exit code"); } catch (const manifest_serialization& e) { fail << "invalid " << what << " manifest: " << e.description; } catch (const process_error& e) { fail << "unable to execute curl: " << e; } catch (const system_error& e) { const auto& c (e.code ()); if (c.category () == generic_category ()) throw_generic_ios_failure (c.value (), e.what ()); else throw_system_ios_failure (c.value (), e.what ()); } } static strings parse_auxiliary_environment (const string&, const char*); // See below. static const string worker_checksum ("5"); // Logic version. static int bbot:: build (size_t argc, const char* argv[]) { using std::map; using std::multimap; using string_parser::unquote; using serializer = manifest_serializer; using serialization = manifest_serialization; using namespace bpkg; tracer trace ("build"); // Our overall plan is as follows: // // 1. Parse the task manifest (it should be in CWD). // // 2. Run bpkg to create the package/tests configurations, add the // repository to them, and configure, build, test, optionally install or // alternatively bindist and sys-install, test installed, and // (sys-)uninstall the package all while saving the logs in the result // manifest. // // 3. Upload the result manifest and, optionally, the build artifacts. // // NOTE: consider updating worker_checksum if making any logic changes. // // Note also that we are being "watched" by the startup version of us which // will upload an appropriate result in case we exit with an error. So here // for abnormal situations (like a failure to parse the manifest), we just // fail. // task_manifest tm ( parse_manifest (path ("task.manifest"), "task")); // Reset the dependency checksum if the task's worker checksum doesn't match // the current one. // if (!tm.worker_checksum || *tm.worker_checksum != worker_checksum) tm.dependency_checksum = nullopt; result_manifest rm { tm.name, tm.version, result_status::success, operation_results {}, worker_checksum, nullopt /* dependency_checksum */ }; auto add_result = [&rm] (string o) -> operation_result& { rm.results.push_back ( operation_result {move (o), result_status::success, ""}); return rm.results.back (); }; dir_path rwd; // Root working directory. // Archive of the build artifacts for upload. // path upload_archive ("upload.tar"); // 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. // optional bkp_step; optional bkp_status; string last_cmd; // Used in the user prompt. // Parse the auxiliary environment, if present, to dump it into the // configure operation log and to use it in the interactive build user // prompt. Note that this environment is already set by the parent process. // strings aux_env; for (;;) // The "breakout" loop. { auto fail_operation = [&trace] (operation_result& r, const string& e, result_status s, const string& name = "", uint64_t line = 0, uint64_t column = 0) { string prefix; if (!name.empty ()) { prefix += name; prefix += ':'; if (line != 0) { prefix += to_string (line); prefix += ':'; if (column != 0) { prefix += to_string (column); prefix += ':'; } } prefix += ' '; } l3 ([&]{trace << prefix << e;}); r.log += prefix + "error: " + e + '\n'; r.status = s; }; // Regular expressions that detect different forms of build2 toolchain // warnings. Accidently (or not), they also cover GCC and Clang warnings // (for the English locale). // // The expressions will be matched multiple times, so let's make the // matching faster, with the potential cost of making regular expressions // creation slower. // regex::flag_type f (regex_constants::optimize); // ECMAScript is implied. regexes wre { regex ("^warning: ", f), regex ("^.+: warning: ", f)}; for (const string& re: tm.unquoted_warning_regex ()) wre.emplace_back (re, f); if (tm.interactive && *tm.interactive != "none") { 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 (i); break; } } } if (!bkp_step && !bkp_status) { fail_operation (add_result ("configure"), "invalid interactive build breakpoint '" + b + '\'', result_status::abort); break; } } // Parse the auxiliary environment, if present. // if (tm.auxiliary_environment) { // Note: cannot throw since has already been called successfully by the // parent process. // aux_env = parse_auxiliary_environment (*tm.auxiliary_environment, comment_begin); } // Split the argument into prefix (empty if not present) and unquoted // value (absent if not present) and determine the step status. If the // prefix is present and is prepended with the '+'/'-' character, then the // respective step needs to be enabled/disabled. Return nullopt if the // prefix is invalid. // // Note that arguments with absent values are normally used to // enable/disable steps and are omitted from the command lines. // struct argument { string prefix; // Absent if the argument value is an empty unquoted string. // optional value; // True - enable, false - disable, nullopt - neutral. // optional step_status; }; auto parse_arg = [] (const string& a) -> optional { size_t p (a.find_first_of (":=\"'")); auto value = [] (const string& v) { return !v.empty () ? unquote (v) : optional (); }; if (p == string::npos || a[p] != ':') // No prefix. return argument {string (), value (a), nullopt}; string prefix (a, 0, p); optional enable; if (prefix[0] == '+' || prefix[0] == '-') { enable = (prefix[0] == '+'); prefix.erase (0, 1); if (prefix != "bpkg.update" && prefix != "bpkg.test" && prefix != "bpkg.test-separate.update" && prefix != "bpkg.test-separate.test" && prefix != "bpkg.install" && prefix != "bbot.install.ldconfig" && prefix != "bpkg.bindist.debian" && prefix != "bpkg.bindist.fedora" && prefix != "bpkg.bindist.archive" && prefix != "bbot.sys-install" && prefix != "bbot.sys-install.ldconfig" && prefix != "b.test-installed.test" && prefix != "bpkg.test-separate-installed.update" && prefix != "bpkg.test-separate-installed.test" && prefix != "bbot.bindist.upload" && prefix != "bbot.upload") { return nullopt; // Prefix is invalid. } } for (const string& id: step_id_str) { size_t n (prefix.size ()); if (id.compare (0, n, prefix) == 0 && (id.size () == n || (id.size () > n && id[n] == '.'))) return argument {move (prefix), value (a.substr (p + 1)), enable}; } return nullopt; // Prefix is invalid. }; // Keep track of explicitly enabled/disabled steps. // map step_statuses; // Return true if the step is explicitly enabled via a +:[] // environment/configuration argument. // auto step_enabled = [&step_statuses] (step_id step) -> bool { auto i (step_statuses.find (to_string (step))); return i != step_statuses.end () && i->second; }; // Return true if the step is explicitly disabled via a -:[] // environment/configuration argument. // auto step_disabled = [&step_statuses] (step_id step) -> bool { auto i (step_statuses.find (to_string (step))); return i != step_statuses.end () && !i->second; }; // Save a step status. // // Note that since the bpkg.bindist.* steps are mutually exclusive we only // keep the latest status change (see above for details). // auto step_status = [&step_statuses] (const string& step, bool status) { if (step.compare (0, 13, "bpkg.bindist.") == 0) { step_statuses.erase ("bpkg.bindist.debian"); step_statuses.erase ("bpkg.bindist.fedora"); step_statuses.erase ("bpkg.bindist.archive"); } step_statuses[step] = status; }; // Parse the environment, target configuration, and build package // configuration arguments. // // NOTE: keep this parsing order intact so that, for example, a build // package configuration argument can override step status specified // by a target configuration argument. // // Parse environment arguments. // multimap modules; multimap env_args; for (size_t i (1); i != argc; ++i) { const char* a (argv[i]); optional v (parse_arg (a)); if (!v) fail << "invalid environment argument prefix in '" << a << "'"; bool mod (v->value && (*v->value)[0] != '-' && v->value->find ('=') == string::npos); if (mod && !v->prefix.empty () && v->prefix != "b.create" && v->prefix != "bpkg.create" && v->prefix != "bpkg.target.create" && v->prefix != "bpkg.host.create" && v->prefix != "bpkg.module.create" && v->prefix != "b.test-installed.create" && v->prefix != "bpkg.test-separate-installed.create" && v->prefix != "bpkg.test-separate-installed.create_for_target" && v->prefix != "bpkg.test-separate-installed.create_for_host" && v->prefix != "bpkg.test-separate-installed.create_for_module") fail << "invalid module prefix in '" << a << "'"; if (v->step_status) step_status (v->prefix, *v->step_status); if (v->value) (mod ? modules : env_args).emplace (make_pair (move (v->prefix), move (*v->value))); } // Parse target configuration arguments. Report failures to the bbot // controller. // multimap tgt_args; for (const string& c: tm.target_config) { optional v (parse_arg (c)); if (!v) { rm.status |= result_status::abort; l3 ([&]{trace << "invalid target configuration argument prefix in " << "'" << c << "'";}); break; } if (v->value && (*v->value)[0] != '-' && v->value->find ('=') == string::npos) { rm.status |= result_status::abort; l3 ([&]{trace << "invalid target configuration argument '" << c << "'";}); break; } if (v->step_status) step_status (v->prefix, *v->step_status); if (v->value) tgt_args.emplace (make_pair (move (v->prefix), move (*v->value))); } if (!rm.status) break; // Parse the build package configuration represented as a whitespace // separated list of the following potentially quoted bpkg-pkg-build // command arguments: // //