// file : load/load.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file #include // signal() #include #include #include // this_thread::sleep_for() #include // strncmp() #include #include // find(), find_if() #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using std::cout; using std::cerr; using std::endl; using namespace std::this_thread; using namespace odb::core; using namespace butl; using namespace bpkg; using namespace brep; using manifest_name_values = vector; // Operation failed, diagnostics has already been issued. // struct failed {}; static const char* help_info ( " info: run 'brep-load --help' for more information"); static const path packages ("packages.manifest"); static const path repositories ("repositories.manifest"); // Retry executing bpkg on recoverable errors for about 10 seconds. // // Should we just exit with some "bpkg recoverable" code instead and leave it // to the caller to perform retries? Feels like it's better to handle such // errors ourselves rather than to complicate every caller. Note that having // some frequently updated prerequisite repository can make these errors quite // probable, even if the internal repositories are rarely updated. // static const size_t bpkg_retries (10); static const std::chrono::seconds bpkg_retry_timeout (1); struct internal_repository { repository_location location; string display_name; repository_location cache_location; optional fingerprint; bool buildable = true; path packages_path () const {return cache_location.path () / packages;} path repositories_path () const {return cache_location.path () / repositories;} }; using internal_repositories = vector; // Parse loadtab file. // // loadtab consists of lines in the following format: // // cache: [fingerprint:] [buildable:(yes|no)] // // Note that if the remote repository location is a pkg repository, then the // repository cache should be its local copy. Otherwise, the cache directory // is expected to contain just repositories.manifest and packages.manifest // files as dumped by bpkg-rep-info, for example: // // $ bpkg rep-info --manifest // --repositories-file repositories.manifest // --packages-file packages.manifest // // // Specifically, the packages.manifest is not a pkg package manifest list. It // contains a raw list of package manifests that may contain values forbidden // for the pkg package manifest list (description-file, changes-file) and may // omit the required ones (sha256sum, description-type). // // @@ Latter, we may also want to support loading bpkg repositories using // manifest files produced by bpkg-rep-info command. This, in particular, // will allow handling CI requests for bpkg repositories. // // The current thinking is that the CI handler will be able to "suggest" // this using (the planned) cache:dir+file:// form. // static internal_repositories load_repositories (path p) { internal_repositories repos; if (p.relative ()) p.complete (); try { ifdstream ifs (p); tab_parser parser (ifs, p.string ()); tab_fields tl; while (!(tl = parser.next ()).empty ()) { size_t n (tl.size ()); // Fields count. size_t i (0); // The field currently being processed. // Report an error for the field currently being processed. If i == n // then we refer to the end-of-line column (presumably reporting a missed // field). // auto bad_line = [&p, &tl, &i, n] (const string& d, size_t offset = 0) { // Offset beyond the end-of-line is meaningless. // assert (i < n || (i == n && offset == 0)); cerr << p << ':' << tl.line << ':' << (i == n ? tl.end_column : tl[i].column + offset) << ": error: " << d << endl; throw failed (); }; internal_repository r; try { r.location = repository_location (tl[i].value); } catch (const invalid_argument& e) { bad_line (e.what ()); } if (r.location.local ()) bad_line ("local repository location"); for (const auto& rp: repos) if (rp.location.canonical_name () == r.location.canonical_name ()) bad_line ("duplicate canonical name"); // Display name field is a required one. // if (++i == n) bad_line ("no display name found"); r.display_name = move (tl[i++].value); // Parse options, that have : form. Currently defined // options are cache (mandatory for now), fingerprint, and buildable. // for (; i < n; ++i) { string nv (tl[i].value); size_t vp; if (strncmp (nv.c_str (), "cache:", vp = 6) == 0) { if (!r.cache_location.empty ()) bad_line ("cache option redefinition"); try { // If the internal repository cache path is relative, then // calculate its absolute path. Such path is considered to be // relative to the configuration file directory path so result is // independent from whichever directory is current for the loader // process. Note that the resulting absolute path should be a valid // repository location. // dir_path cache_path (string (nv, vp)); if (cache_path.relative ()) cache_path = p.directory () / cache_path; // A non-pkg repository cache is not a real repository (see // above). We create the location of the dir type for such a cache // to distinguish it when it comes to the manifest files parsing. // r.cache_location = repository_location ( repository_url (cache_path.string ()), r.location.type () == repository_type::pkg ? r.location.type () : repository_type::dir); // Created from the absolute path repository location can not be // other than absolute. // assert (r.cache_location.absolute ()); } catch (const invalid_path& e) // Thrown by dir_path(). { bad_line (string ("invalid cache path: ") + e.what ()); } catch (const invalid_argument& e) // Thrown by repository_*(). { bad_line (string ("invalid cache path: ") + e.what ()); } if (!file_exists (r.packages_path ())) bad_line ("packages.manifest file does not exist"); if (!file_exists (r.repositories_path ())) bad_line ("repositories.manifest file does not exist"); } else if (strncmp (nv.c_str (), "fingerprint:", vp = 12) == 0) { if (r.fingerprint) bad_line ("fingerprint option redefinition"); r.fingerprint = string (nv, vp); // Sanity check. // if (!r.fingerprint->empty ()) { try { fingerprint_to_sha256 (*r.fingerprint); } catch (const invalid_argument&) { bad_line ("invalid fingerprint"); } } } else if (strncmp (nv.c_str (), "buildable:", vp = 10) == 0) { string v (nv, vp); r.buildable = (v == "yes"); if (!r.buildable && v != "no") bad_line ("invalid buildable option value"); } else bad_line ("invalid option '" + nv + "'"); } // For now cache option is mandatory. // if (r.cache_location.empty ()) bad_line ("no cache option found"); repos.emplace_back (move (r)); } } catch (const tab_parsing& e) { cerr << e << endl; throw failed (); } catch (const io_error& e) { cerr << "error: unable to read " << p << ": " << e << endl; throw failed (); } return repos; } // Check if repositories persistent state is outdated. If any repository // differes from its persistent state or there is a persistent repository // which is not listed in configuration file then the whole persistent // state will be recreated. Will consider optimization later when the // package model, including search related objects, settles down. // static bool changed (const string& tenant, const internal_repositories& repos, database& db) { strings names; for (const internal_repository& r: repos) { shared_ptr pr ( db.find (repository_id (tenant, r.location.canonical_name ()))); if (pr == nullptr || r.location.string () != pr->location.string () || r.display_name != pr->display_name || r.cache_location.path () != pr->cache_location.path () || r.buildable != pr->buildable || file_mtime (r.packages_path ()) != pr->packages_timestamp || file_mtime (r.repositories_path ()) != pr->repositories_timestamp || !pr->internal) return true; names.emplace_back (r.location.canonical_name ()); } using query = query; // Check if there is an internal repository not being listed in the // configuration file. // return !db.query ( query::id.tenant == tenant && query::internal && !query::id.canonical_name.in_range (names.begin (), names.end ())).empty (); } // Start 'bpkg rep-info [options] ' process. // static process repository_info (const options& lo, const string& rl, const cstrings& options) { cstrings args {lo.bpkg ().string ().c_str (), "rep-info"}; args.insert (args.end (), options.begin (), options.end ()); for (const string& o: lo.bpkg_option ()) args.push_back (o.c_str ()); args.push_back (rl.c_str ()); args.push_back (nullptr); try { return process (args.data (), 0, -1, 2); } catch (const process_error& e) { cerr << "error: unable to execute " << args[0] << ": " << e << endl; if (e.child) exit (1); throw failed (); } } // Load the repository packages from the packages.manifest file and persist // the repository. Should be called once per repository. // static void load_packages (const shared_ptr& rp, const repository_location& cl, database& db, bool ignore_unknown, const manifest_name_values& overrides) { // packages_timestamp other than timestamp_nonexistent signals the // repository packages are already loaded. // assert (rp->packages_timestamp == timestamp_nonexistent); vector pms; assert (!cl.empty ()); path p (cl.path () / packages); try { ifdstream ifs (p); rp->packages_timestamp = file_mtime (p); manifest_parser mp (ifs, p.string ()); // If the repository cache directory is not a pkg repository, then the // packages.manifest file it contains is a raw list of the package // manifests that we need to parse manually (see above). // if (cl.type () != repository_type::pkg) { // We put no restrictions on the manifest values presence since it's not // critical for displaying and building if the packages omit some // manifest values (see libbpkg/manifest.hxx for details). Note, though, // that we expect dependency constraints to be complete. // for (manifest_name_value nv (mp.next ()); !nv.empty (); nv = mp.next ()) pms.emplace_back ( mp, move (nv), ignore_unknown, false /* complete_depends */, package_manifest_flags::forbid_incomplete_dependencies); } else pms = pkg_package_manifests (mp, ignore_unknown); } catch (const io_error& e) { cerr << "error: unable to read " << p << ": " << e << endl; throw failed (); } using brep::dependency; for (package_manifest& pm: pms) { shared_ptr p ( db.find (package_id (rp->tenant, pm.name, pm.version))); // sha256sum should always be present if the package manifest comes from // the packages.manifest file belonging to the pkg repository. // assert (pm.sha256sum || cl.type () != repository_type::pkg); if (p == nullptr) { if (rp->internal) { try { pm.override (overrides, "" /* name */); } catch (const manifest_parsing&) { // Overrides are already validated (see below). // assert (false); } // Create internal package object. // optional dsc; optional dst; if (pm.description) { // The description value should not be of the file type if the // package manifest comes from the pkg repository. // assert (!pm.description->file || cl.type () != repository_type::pkg); if (!pm.description->file) { dst = pm.effective_description_type (ignore_unknown); // If the description type is unknown (which may be the case for // some "transitional" period and only if --ignore-unknown is // specified) we just silently drop the description. // assert (dst || ignore_unknown); if (dst) dsc = move (pm.description->text); } } string chn; for (auto& c: pm.changes) { // The changes value should not be of the file type if the package // manifest comes from the pkg repository. // assert (!c.file || cl.type () != repository_type::pkg); if (!c.file) { if (chn.empty ()) chn = move (c.text); else { if (chn.back () != '\n') chn += '\n'; // Always have a blank line as a separator. chn += "\n" + c.text; } } } dependencies ds; for (auto& pda: pm.dependencies) { // Ignore special build2 and bpkg dependencies. We may not have // packages for them and also showing them for every package is // probably not very helpful. // if (pda.buildtime && !pda.empty ()) { const package_name& n (pda.front ().name); if (n == "build2" || n == "bpkg") continue; } ds.emplace_back (pda.conditional, pda.buildtime, move (pda.comment)); for (auto& pd: pda) { // The package member will be assigned during dependency // resolution procedure. // ds.back ().push_back (dependency {move (pd.name), move (pd.constraint), nullptr /* package */}); } } small_vector ts; if (!pm.tests.empty ()) { ts.reserve (pm.tests.size ()); for (bpkg::test_dependency& td: pm.tests) ts.emplace_back (move (td.name), td.type, td.buildtime, move (td.constraint)); } // Cache before the package name is moved. // package_name project (pm.effective_project ()); p = make_shared ( move (pm.name), move (pm.version), move (pm.upstream_version), move (project), pm.priority ? move (*pm.priority) : priority (), move (pm.summary), move (pm.license_alternatives), move (pm.topics), move (pm.keywords), move (dsc), move (dst), move (chn), move (pm.url), move (pm.doc_url), move (pm.src_url), move (pm.package_url), move (pm.email), move (pm.package_email), move (pm.build_email), move (pm.build_warning_email), move (pm.build_error_email), move (ds), move (pm.requirements), move (ts), move (pm.builds), move (pm.build_constraints), move (pm.location), move (pm.fragment), move (pm.sha256sum), rp); } else // Create external package object. // p = make_shared (move (pm.name), move (pm.version), move (pm.builds), move (pm.build_constraints), rp); db.persist (p); } else { // As soon as internal repositories get loaded first, the internal // package can duplicate an internal package only. // assert (!rp->internal || p->internal ()); if (rp->internal) { // Note that the sha256sum manifest value can only be present if the // package comes from the pkg repository. // if (pm.sha256sum) { // Save the package sha256sum if it is not present yet, match // otherwise. // if (!p->sha256sum) p->sha256sum = move (pm.sha256sum); else if (*pm.sha256sum != *p->sha256sum) cerr << "warning: sha256sum mismatch for package " << p->name << " " << p->version << endl << " info: " << p->internal_repository.load ()->location << " has " << *p->sha256sum << endl << " info: " << rp->location << " has " << *pm.sha256sum << endl; } // A non-stub package is buildable if belongs to at least one // buildable repository (see libbrep/package.hxx for details). // Note that if this is an external test package it will be marked as // unbuildable later (see resolve_dependencies() for details). // if (rp->buildable && !p->buildable && !p->stub ()) { p->buildable = true; p->unbuildable_reason = nullopt; } } p->other_repositories.push_back (rp); db.update (p); } } db.persist (rp); // Save the repository state. } // Load the repository manifest values from the repositories.manifest file. // Unless this is a shallow load, also load prerequsite repositories and // their complements state. Update the repository persistent state to save // changed members. Should be called once per persisted internal repository. // static void load_repositories (const options& lo, const shared_ptr& rp, const repository_location& cl, database& db, bool ignore_unknown, bool shallow) { // repositories_timestamp other than timestamp_nonexistent signals that // repository prerequisites are already loaded. // assert (rp->repositories_timestamp == timestamp_nonexistent); const string& tenant (rp->tenant); // Repository is already persisted by the load_packages() function call. // assert (db.find ( repository_id (tenant, rp->canonical_name)) != nullptr); pkg_repository_manifests rpm; assert (!cl.empty ()); path p (cl.path () / repositories); try { ifdstream ifs (p); rp->repositories_timestamp = file_mtime (p); manifest_parser mp (ifs, p.string ()); rpm = pkg_repository_manifests (mp, ignore_unknown); if (rpm.empty ()) rpm.emplace_back (repository_manifest ()); // Add the base repository. } catch (const io_error& e) { cerr << "error: unable to read " << p << ": " << e << endl; throw failed (); } for (auto& rm: rpm) { if (rm.effective_role () == repository_role::prerequisite && !rp->internal) continue; // Ignore the external repository prerequisite entry. if (rm.effective_role () == repository_role::base) { assert (rp->location.remote () && !rp->interface_url); // Update the base repository with manifest values. // rp->interface_url = rm.effective_url (rp->location); // @@ Should we throw if url is not available for external repository ? // Can, basically, repository be available on the web but have no web // interface associated ? // // Yes, there can be no web interface. So we should just not form // links to packages from such repos. // if (rp->interface_url) { // Normalize web interface url adding trailing '/' if not present. // auto& u (*rp->interface_url); assert (!u.empty ()); if (u.back () != '/') u += '/'; } if (rp->internal) { rp->email = move (rm.email); rp->summary = move (rm.summary); rp->description = move (rm.description); // Mismatch of the repository manifest and the certificate information // can be the result of racing condition. // // @@ Need to address properly while fully moving to the bpkg-based // fetching. // @@ Shouldn't we dedicate a specific exit code for such situations? // if (static_cast (rm.certificate) != static_cast (rp->certificate)) { cerr << "error: signing status mismatch for internal repository " << rp->location << endl << " info: try again" << endl; throw failed (); } if (rm.certificate) rp->certificate->pem = move (*rm.certificate); } continue; } // Load prerequisite or complement repository unless this is a shallow // load. // if (shallow) continue; assert (!rm.location.empty ()); repository_location rl; auto bad_location = [&rp, &rm] () { cerr << "error: invalid prerequisite repository location " << rm.location << endl << " info: base (internal) repository location is " << rp->location << endl; throw failed (); }; try { // Absolute path location make no sense for the web interface. // if (rm.location.absolute ()) bad_location (); // Convert the relative repository location to remote one, leave remote // location unchanged. // rl = repository_location (rm.location, rp->location); } catch (const invalid_argument&) { bad_location (); } const auto& cn (rl.canonical_name ()); // Add repository to prerequisites or complements member of the dependent // repository. // auto& rs (rm.effective_role () == repository_role::prerequisite ? rp->prerequisites : rp->complements); rs.emplace_back (db, repository_id (tenant, cn)); shared_ptr pr ( db.find (repository_id (tenant, cn))); if (pr != nullptr) // The prerequisite repository is already loaded. // continue; pr = make_shared (tenant, move (rl)); // If the base repository is internal and the prerequsite repository // location is a relative path, then calculate its cache location. // if (rp->internal && rm.location.relative ()) { // For an internal repository the cache location always comes from the // loadtab file. // assert (cl.path () == rp->cache_location.path ()); try { pr->cache_location = repository_location (rm.location, cl); } catch (const invalid_argument&) { cerr << "error: can't obtain cache location for prerequisite " << "repository '" << rm.location << "'" << endl << " info: base (internal) repository location is " << rp->location << endl << " info: base repository cache location is " << cl << endl; throw failed (); } } // If the (external) prerequisite repository cache location is empty, then // check if the repository is local and, if that's the case, use its // location as a cache location. Otherwise, fetch the repository // information creating a temporary cache for it. // auto_rmdir cdr; // Remove the temporary cache after the repo load. repository_location cl; // Repository temporary cache location. if (pr->cache_location.empty ()) { if (pr->location.local ()) { pr->cache_location = pr->location; } else { dir_path cd; try { cd = dir_path::temp_path ("brep-load-cache"); } catch (const system_error& e) { cerr << "unable to obtain temporary directory: " << e; throw failed (); } // It's highly unlikely but still possible that the temporary cache // directory already exists. This can only happen due to the unclean // loader termination. Let's remove it and retry. // try { if (try_mkdir (cd) == mkdir_status::already_exists) { try_rmdir_r (cd); if (try_mkdir (cd) == mkdir_status::already_exists) throw_generic_error (EEXIST); } } catch (const system_error& e) { cerr << "unable to create directory '" << cd << "': " << e; throw failed (); } cdr = auto_rmdir (cd); path rf (cd / repositories); path pf (cd / packages); // Note that the fetch timeout can be overridden via --bpkg-option. // cstrings args { "--fetch-timeout", "60", // 1 minute. "--deep", "--manifest", "--repositories", "--repositories-file", rf.string ().c_str (), "--packages", "--packages-file", pf.string ().c_str ()}; if (rm.trust) { args.push_back ("--trust"); args.push_back (rm.trust->c_str ()); } // Always add it, so bpkg won't try to prompt for a certificate // authentication if the fingerprint doesn't match. // args.push_back ("--trust-no"); // Retry bpkg-rep-info on recoverable errors, for a while. // for (size_t i (0);; ++i) { if (i != 0) { // Let's follow up the bpkg's diagnostics with the number of // retries left. // cerr << bpkg_retries - i + 1 << " retries left" << endl; sleep_for (bpkg_retry_timeout); } process p (repository_info (lo, pr->location.string (), args)); try { // Bail out from the retry loop on success. // if (p.wait ()) break; // Assume the child issued diagnostics if terminated normally. // if (p.exit->normal ()) { // Retry the manifests fetch on a recoverable error, unless the // retries limit is reached. // if (p.exit->code () == 2 && i != bpkg_retries) continue; } else cerr << "process " << lo.bpkg () << " " << *p.exit << endl; cerr << "error: unable to fetch manifests for " << pr->canonical_name << endl << " info: base repository location is " << rp->location << endl; throw failed (); } catch (const process_error& e) { cerr << "error: unable to fetch manifests for " << pr->canonical_name << ": " << e << endl; throw failed (); } } // Note that this is a non-pkg repository cache and so we create the // dir repository location (see load_repositories(path) for details). // cl = repository_location (repository_url (cd.string ()), repository_type::dir); } } // We don't apply overrides to the external packages. // load_packages (pr, !pr->cache_location.empty () ? pr->cache_location : cl, db, ignore_unknown, manifest_name_values () /* overrides */); load_repositories (lo, pr, !pr->cache_location.empty () ? pr->cache_location : cl, db, ignore_unknown, false /* shallow */); } db.update (rp); } // Check if the package is available from the specified repository, // its prerequisite repositories, or one of their complements, // recursively. // static bool find (const lazy_shared_ptr& r, const package& p, bool prereq = true) { assert (r != nullptr); const auto& o (p.other_repositories); if (r == p.internal_repository || find (o.begin (), o.end (), r) != o.end ()) return true; auto rp (r.load ()); for (const auto& cr: rp->complements) { if (find (lazy_shared_ptr (cr), p, false)) return true; } if (prereq) { for (auto pr: rp->prerequisites) { if (find (lazy_shared_ptr (pr), p, false)) return true; } } return false; } // Resolve package run-time dependencies and external tests. Make sure that // the best matching dependency belongs to the package repositories, their // complements, recursively, or their immediate prerequisite repositories // (only for run-time dependencies). Set the buildable flag to false for the // resolved external tests packages. Fail if unable to resolve a dependency, // unless ignore_unresolved is true in which case leave this dependency // NULL. Should be called once per internal package. // static void resolve_dependencies (package& p, database& db, bool ignore_unresolved) { using brep::dependency; using brep::dependency_alternatives; // Resolve dependencies for internal packages only. // assert (p.internal ()); if (p.dependencies.empty () && p.tests.empty ()) return; auto resolve = [&p, &db] (dependency& d, bool test) { // Dependency should not be resolved yet. // assert (d.package == nullptr); using query = query; query q (query::id.name == d.name); const auto& vm (query::id.version); if (d.constraint) { const version_constraint& c (*d.constraint); assert (c.complete ()); query qs (compare_version_eq (vm, canonical_version (wildcard_version), false /* revision */)); if (c.min_version && c.max_version && *c.min_version == *c.max_version) { const version& v (*c.min_version); q = q && (compare_version_eq (vm, canonical_version (v), v.revision.has_value ()) || qs); } else { query qr (true); if (c.min_version) { const version& v (*c.min_version); canonical_version cv (v); bool rv (v.revision); if (c.min_open) qr = compare_version_gt (vm, cv, rv); else qr = compare_version_ge (vm, cv, rv); } if (c.max_version) { const version& v (*c.max_version); canonical_version cv (v); bool rv (v.revision); if (c.max_open) qr = qr && compare_version_lt (vm, cv, rv); else qr = qr && compare_version_le (vm, cv, rv); } q = q && (qr || qs); } } for (const auto& pp: db.query (q + order_by_version_desc (vm))) { if (find (p.internal_repository, pp, !test)) { d.package.reset (db, pp.id); // If the resolved dependency is an external test, then mark it as // such, unless it is a stub. // if (test) { shared_ptr dp (d.package.load ()); if (!dp->stub ()) { dp->buildable = false; dp->unbuildable_reason = unbuildable_reason::test; db.update (dp); } } return true; } } return false; }; auto bail = [&p] (const dependency& d, const char* what) { cerr << "error: can't resolve " << what << " " << d << " for the package " << p.name << " " << p.version << endl << " info: repository " << p.internal_repository.load ()->location << " appears to be broken" << endl; throw failed (); }; for (dependency_alternatives& da: p.dependencies) { for (dependency& d: da) { // Practically it is enough to resolve at least one dependency // alternative to build a package. Meanwhile here we consider an error // specifying in the manifest file an alternative which can't be // resolved, unless unresolved dependencies are allowed. // if (!resolve (d, false /* test */) && !ignore_unresolved) bail (d, "dependency"); } } for (brep::test_dependency& td: p.tests) { if (!resolve (td, true /* test */) && !ignore_unresolved) bail (td, td.name.string ().c_str ()); } db.update (p); // Update the package state. } using package_ids = vector; // Make sure the package dependency chain doesn't contain the package id. // Throw failed otherwise. Continue the chain with the package id and call // itself recursively for each prerequisite of the package. Should be called // once per internal package. // // @@ This should probably be eventually moved to bpkg. // static void detect_dependency_cycle (const package_id& id, package_ids& chain, database& db) { // Package of one version depending on the same package of another version // is something obscure. So the comparison is made up to a package name. // auto pr = [&id] (const package_id& i) -> bool {return i.name == id.name;}; auto i (find_if (chain.begin (), chain.end (), pr)); if (i != chain.end ()) { cerr << "error: package dependency cycle: "; auto prn = [&db] (const package_id& id) { shared_ptr p (db.load (id)); assert (p->internal () || !p->other_repositories.empty ()); shared_ptr r ( p->internal () ? p->internal_repository.load () : p->other_repositories[0].load ()); cerr << p->name << " " << p->version << " (" << r->canonical_name << ")"; }; for (; i != chain.end (); ++i) { prn (*i); cerr << " -> "; } prn (id); cerr << endl; throw failed (); } chain.push_back (id); shared_ptr p (db.load (id)); for (const auto& da: p->dependencies) { for (const auto& d: da) detect_dependency_cycle (d.package.object_id (), chain, db); } chain.pop_back (); } // Return the certificate information for a signed repository and nullopt for // an unsigned. Note that a repository at the remote location is not trusted // unless the certificate fingerprint is provided (which also means it should // either be signed or the wildcard fingerprint specified). A local repository // location is, instead, trusted by default. If the fingerprint is provided // then the repository is authenticated regardless of the location type. // static optional certificate_info (const options& lo, const repository_location& rl, const optional& fp) { cstrings args { "--cert-fingerprint", "--cert-name", "--cert-organization", "--cert-email", "-q"}; // Don't print info messages. const char* trust ("--trust-no"); if (fp) { if (!fp->empty ()) { args.push_back ("--trust"); args.push_back (fp->c_str ()); } else trust = "--trust-yes"; if (!rl.remote ()) { args.push_back ("--auth"); args.push_back ("all"); } } args.push_back (trust); // Retry bpkg-rep-info on recoverable errors, for a while. // for (size_t i (0);; ++i) { if (i != 0) { // Let's follow up the bpkg's diagnostics with the number of retries // left. // cerr << bpkg_retries - i + 1 << " retries left" << endl; sleep_for (bpkg_retry_timeout); } try { process pr (repository_info (lo, rl.string (), args)); try { ifdstream is ( move (pr.in_ofd), ifdstream::failbit | ifdstream::badbit | ifdstream::eofbit); optional cert; string fingerprint; getline (is, fingerprint); if (!fingerprint.empty ()) { cert = certificate (); cert->fingerprint = move (fingerprint); getline (is, cert->name); getline (is, cert->organization); getline (is, cert->email); } else { // Read out empty lines. // string s; getline (is, s); // Name. getline (is, s); // Organization. getline (is, s); // Email. } // Check that EOF is successfully reached. // is.exceptions (ifdstream::failbit | ifdstream::badbit); if (is.peek () != ifdstream::traits_type::eof ()) throw io_error (""); is.close (); if (pr.wait ()) return cert; // Fall through. // } catch (const io_error&) { // Child exit status doesn't matter. Just wait for the process // completion and fall through. // pr.wait (); } // Assume the child issued diagnostics if terminated normally. // if (pr.exit->normal ()) { // Retry the certificate fetch on a recoverable error, unless the // retries limit is reached. // if (pr.exit->code () == 2 && i != bpkg_retries) continue; } else cerr << "process " << lo.bpkg () << " " << *pr.exit << endl; cerr << "error: unable to fetch certificate information for " << rl.canonical_name () << endl; // Fall through. } catch (const process_error& e) { cerr << "error: unable to fetch certificate information for " << rl.canonical_name () << ": " << e << endl; // Fall through. } throw failed (); } } int main (int argc, char* argv[]) try { // 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 // behavior on a file descriptor basis or for the write() function call. // if (signal (SIGPIPE, SIG_IGN) == SIG_ERR) { cerr << "error: unable to ignore broken pipe (SIGPIPE) signal: " << system_error (errno, generic_category ()) << endl; // Sanitize. throw failed (); } cli::argv_scanner scan (argc, argv, true); options ops (scan); // Version. // if (ops.version ()) { cout << "brep-load " << BREP_VERSION_ID << endl << "libbrep " << LIBBREP_VERSION_ID << endl << "libbbot " << LIBBBOT_VERSION_ID << endl << "libbpkg " << LIBBPKG_VERSION_ID << endl << "libbutl " << LIBBUTL_VERSION_ID << endl << "Copyright (c) " << BREP_COPYRIGHT << "." << endl << "This is free software released under the MIT license." << endl; return 0; } // Help. // if (ops.help ()) { pager p ("brep-load help", false, ops.pager_specified () ? &ops.pager () : nullptr, &ops.pager_option ()); print_usage (p.stream ()); // If the pager failed, assume it has issued some diagnostics. // return p.wait () ? 0 : 1; } if (argc < 2) { cerr << "error: configuration file expected" << endl << help_info << endl; throw failed (); } if (argc > 2) { cerr << "error: unexpected argument encountered" << endl << help_info << endl; throw failed (); } // By default the tenant is empty and assumes a single-tenant mode. Let's // require the specified tenant to be non-empty. // const string& tnt (ops.tenant ()); if (ops.tenant_specified () && tnt.empty ()) { cerr << "error: empty tenant" << endl << help_info << endl; throw failed (); } // Parse and validate overrides, if specified. // manifest_name_values overrides; if (ops.overrides_file_specified ()) try { const string& name (ops.overrides_file ().string ()); ifdstream is (ops.overrides_file ()); manifest_parser mp (is, name); overrides = parse_manifest (mp); is.close (); package_manifest::validate_overrides (overrides, name); } catch (const manifest_parsing& e) { cerr << "error: unable to parse overrides: " << e << endl; throw failed (); } catch (const io_error& e) { cerr << "error: unable to read '" << ops.overrides_file () << "': " << e << endl; throw failed (); } odb::pgsql::database db ( ops.db_user (), ops.db_password (), ops.db_name (), ops.db_host (), ops.db_port (), "options='-c default_transaction_isolation=serializable'"); // Prevent several brep utility instances from updating the package database // simultaneously. // database_lock l (db); transaction t (db.begin ()); // Check that the package database schema matches the current one. // const string ds ("package"); if (schema_catalog::current_version (db, ds) != db.schema_version (ds)) { cerr << "error: package database schema differs from the current one" << endl << " info: use brep-migrate to migrate the database" << endl; throw failed (); } // Note: the interactive tenant implies private. // if (ops.interactive_specified ()) ops.private_ (true); // Load the description of all the internal repositories from the // configuration file. // internal_repositories irs (load_repositories (path (argv[1]))); if (ops.force () || changed (tnt, irs, db)) { // Rebuild repositories persistent state from scratch. // // Note that in the single-tenant mode the tenant must be empty. In the // multi-tenant mode all tenants must be non-empty. So in the // single-tenant mode we erase all database objects (possibly from // multiple tenants). Otherwise, cleanup the specified and the empty // tenants only. // if (tnt.empty ()) // Single-tenant mode. { db.erase_query (); db.erase_query (); db.erase_query (); } else // Multi-tenant mode. { cstrings ts ({tnt.c_str (), ""}); db.erase_query ( query::id.tenant.in_range (ts.begin (), ts.end ())); db.erase_query ( query::id.tenant.in_range (ts.begin (), ts.end ())); db.erase_query ( query::id.in_range (ts.begin (), ts.end ())); } // Persist the tenant. // db.persist (tenant (tnt, ops.private_ (), (ops.interactive_specified () ? ops.interactive () : optional ()))); // On the first pass over the internal repositories we load their // certificate information and packages. // uint16_t priority (1); for (const auto& ir: irs) { optional cert; if (ir.location.type () == repository_type::pkg) cert = certificate_info ( ops, !ir.cache_location.empty () ? ir.cache_location : ir.location, ir.fingerprint); shared_ptr r ( make_shared (tnt, ir.location, move (ir.display_name), move (ir.cache_location), move (cert), ir.buildable, priority++)); load_packages (r, r->cache_location, db, ops.ignore_unknown (), overrides); } // On the second pass over the internal repositories we load their // (not yet loaded) manifest values, complement, and prerequisite // repositories. // for (const auto& ir: irs) { shared_ptr r ( db.load ( repository_id (tnt, ir.location.canonical_name ()))); load_repositories (ops, r, r->cache_location, db, ops.ignore_unknown (), ops.shallow ()); } // Resolve internal packages dependencies and, unless this is a shallow // load, make sure there are no package dependency cycles. // { session s; using query = query; for (auto& p: db.query ( query::id.tenant == tnt && query::internal_repository.canonical_name.is_not_null ())) resolve_dependencies (p, db, ops.shallow ()); if (!ops.shallow ()) { package_ids chain; for (const auto& p: db.query ( query::id.tenant == tnt && query::internal_repository.canonical_name.is_not_null ())) detect_dependency_cycle (p.id, chain, db); } } } t.commit (); return 0; } catch (const database_locked&) { cerr << "brep-load or brep-migrate is running" << endl; return 2; } catch (const recoverable& e) { cerr << "recoverable database error: " << e << endl; return 3; } catch (const cli::exception& e) { cerr << "error: " << e << endl << help_info << endl; return 1; } catch (const failed&) { return 1; // Diagnostics has already been issued. } // Fully qualified to avoid ambiguity with odb exception. // catch (const std::exception& e) { cerr << "error: " << e << endl; return 1; }