aboutsummaryrefslogtreecommitdiff
path: root/bpkg/pkg-build.cxx
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2015-12-09 11:32:50 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2015-12-09 11:32:50 +0200
commitcbd8296c7b86f7fc368d1133da3be3670b7923be (patch)
tree1f8c10aeaf0e19a7d198b06800c2e2a72e760499 /bpkg/pkg-build.cxx
parent82a8cf379ee19dbc66c62bfe78b436d4ab6b497a (diff)
Clean up command names, add aliases
Diffstat (limited to 'bpkg/pkg-build.cxx')
-rw-r--r--bpkg/pkg-build.cxx1224
1 files changed, 1224 insertions, 0 deletions
diff --git a/bpkg/pkg-build.cxx b/bpkg/pkg-build.cxx
new file mode 100644
index 0000000..cb108eb
--- /dev/null
+++ b/bpkg/pkg-build.cxx
@@ -0,0 +1,1224 @@
+// file : bpkg/pkg-build.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <bpkg/pkg-build>
+
+#include <map>
+#include <list>
+#include <iterator> // make_move_iterator()
+#include <iostream> // cout
+#include <algorithm> // find()
+#include <functional> // reference_wrapper
+
+#include <butl/utility> // reverse_iterate()
+
+#include <bpkg/types>
+#include <bpkg/package>
+#include <bpkg/package-odb>
+#include <bpkg/utility>
+#include <bpkg/database>
+#include <bpkg/diagnostics>
+#include <bpkg/satisfaction>
+#include <bpkg/manifest-utility>
+
+#include <bpkg/common-options>
+
+#include <bpkg/pkg-fetch>
+#include <bpkg/pkg-unpack>
+#include <bpkg/pkg-update>
+#include <bpkg/pkg-verify>
+#include <bpkg/pkg-configure>
+#include <bpkg/pkg-disfigure>
+
+using namespace std;
+using namespace butl;
+
+namespace bpkg
+{
+ // @@ TODO
+ //
+ // - Detect and complain about dependency cycles.
+ // - Configuration vars (both passed and preserved)
+ //
+
+ // Try to find a package that optionally satisfies the specified
+ // version constraint. Look in the specified repository, its
+ // prerequisite repositories, and their complements, recursively
+ // (note: recursivity applies to complements, not prerequisites).
+ // Return the package and the repository in which it was found or
+ // NULL for both if not found.
+ //
+ std::pair<shared_ptr<available_package>, shared_ptr<repository>>
+ find_available (database& db,
+ const string& name,
+ const shared_ptr<repository>& r,
+ const optional<dependency_constraint>& c)
+ {
+ using query = query<available_package>;
+
+ query q (query::id.name == name);
+ const auto& vm (query::id.version);
+
+ // If there is a constraint, then translate it to the query. Otherwise,
+ // get the latest version.
+ //
+ bool order (true);
+ if (c)
+ {
+ const version& v (c->version);
+
+ // Note that the constraint's version is always rhs (libfoo >= 1.2.3).
+ //
+ switch (c->operation)
+ {
+ case comparison::eq: q = q && vm == v; order = false; break;
+ case comparison::lt: q = q && vm < v; break;
+ case comparison::gt: q = q && vm > v; break;
+ case comparison::le: q = q && vm <= v; break;
+ case comparison::ge: q = q && vm >= v; break;
+ }
+ }
+
+ if (order)
+ q += order_by_version_desc (vm);
+
+ // Filter the result based on the repository to which each version
+ // belongs.
+ //
+ return filter_one (r, db.query<available_package> (q));
+ }
+
+ // Create a transient (or fake, if you prefer) available_package
+ // object corresponding to the specified selected object. Note
+ // that the package locations list is left empty and that the
+ // returned repository could be NULL if the package is an orphan.
+ //
+ std::pair<shared_ptr<available_package>, shared_ptr<repository>>
+ make_available (const common_options& options,
+ const dir_path& cd,
+ database& db,
+ const shared_ptr<selected_package>& sp)
+ {
+ assert (sp != nullptr && sp->state != package_state::broken);
+
+ // First see if we can find its repository.
+ //
+ shared_ptr<repository> ar (
+ db.find<repository> (
+ sp->repository.canonical_name ()));
+
+ // The package is in at least fetched state, which means we should
+ // be able to get its manifest.
+ //
+ const optional<path>& a (sp->archive);
+ const optional<dir_path>& d (sp->src_root);
+
+ package_manifest m (
+ sp->state == package_state::fetched
+ ? pkg_verify (options, a->absolute () ? *a : cd / *a, true)
+ : pkg_verify (d->absolute () ? *d : cd / *d, true));
+
+ return make_pair (make_shared<available_package> (move (m)), move (ar));
+ }
+
+ // A "dependency-ordered" list of packages and their prerequisites.
+ // That is, every package on the list only possibly depending on the
+ // ones after it. In a nutshell, the usage is as follows: we first
+ // add one or more packages (the "initial selection"; for example, a
+ // list of packages the user wants built). The list then satisfies all
+ // the prerequisites of the packages that were added, recursively. At
+ // the end of this process we have an ordered list of all the packages
+ // that we have to build, from last to first, in order to build our
+ // initial selection.
+ //
+ // This process is split into two phases: satisfaction of all the
+ // dependencies (the collect() function) and ordering of the list
+ // (the order() function).
+ //
+ // During the satisfaction phase, we collect all the packages, their
+ // prerequisites (and so on, recursively) in a map trying to satisfy
+ // any dependency constraints. Specifically, during this step, we may
+ // "upgrade" or "downgrade" a package that is already in a map as a
+ // result of another package depending on it and, for example, requiring
+ // a different version. One notable side-effect of this process is that
+ // we may end up with a lot more packages in the map than we will have
+ // on the list. This is because some of the prerequisites of "upgraded"
+ // or "downgraded" packages may no longer need to be built.
+ //
+ // Note also that we don't try to do exhaustive constraint satisfaction
+ // (i.e., there is no backtracking). Specifically, if we have two
+ // candidate packages each satisfying a constraint of its dependent
+ // package, then if neither of them satisfy both constraints, then we
+ // give up and ask the user to resolve this manually by explicitly
+ // specifying the version that will satisfy both constraints.
+ //
+ struct build_package
+ {
+ shared_ptr<selected_package> selected; // NULL if not selected.
+ shared_ptr<available_package> available; // Can be NULL, fake/transient.
+ shared_ptr<bpkg::repository> repository; // Can be NULL (orphan) or root.
+
+ // Hold flags. Note that we can only "increase" the values that are
+ // already in the selected package.
+ //
+ bool hold_package;
+ bool hold_version;
+
+ // Constraint value plus, normally, the dependent package name that
+ // placed this constraint but can also be some other name for the
+ // initial selection (e.g., package version specified by the user
+ // on the command line).
+ //
+ struct constraint_type
+ {
+ string dependent;
+ dependency_constraint value;
+
+ constraint_type () = default;
+ constraint_type (string d, dependency_constraint v)
+ : dependent (move (d)), value (move (v)) {}
+ };
+
+ vector<constraint_type> constraints;
+
+ // True if we need to reconfigure this package. If available package
+ // is NULL, then reconfigure must be true (this is a dependent that
+ // needs to be reconfigured because its prerequisite is being up/down-
+ // graded or reconfigured). Note that in some cases reconfigure is
+ // naturally implied. For example, if an already configured package
+ // is being up/down-graded. For such cases we don't guarantee that
+ // the reconfigure flag is true. We only make sure to set it for
+ // cases that would otherwise miss the need for the reconfiguration.
+ // As a result, use the reconfigure() accessor which detects both
+ // explicit and implied cases.
+ //
+ // At first, it may seem that this flag is redundant and having the
+ // available package set to NULL is sufficient. But consider the case
+ // where the user asked us to build a package that is already in the
+ // configured state (so all we have to do is pkg-update). Next, add
+ // to this a prerequisite package that is being upgraded. Now our
+ // original package has to be reconfigured. But without this flag
+ // we won't know (available for our package won't be NULL).
+ //
+ bool reconfigure_;
+
+ bool
+ reconfigure () const
+ {
+ return selected != nullptr &&
+ selected->state == package_state::configured &&
+ (reconfigure_ || // Must be checked first, available could be NULL.
+ selected->version != available->version);
+ }
+ };
+
+ struct build_packages: list<reference_wrapper<build_package>>
+ {
+ // Collect the package. Return true if this package version was,
+ // in fact, added to the map and false if it was already there
+ // or the existing version was preferred.
+ //
+ bool
+ collect (const common_options& options,
+ const dir_path& cd,
+ database& db,
+ build_package&& pkg)
+ {
+ using std::swap; // ...and not list::swap().
+
+ tracer trace ("collect");
+
+ assert (pkg.available != nullptr); // No dependents allowed here.
+ auto i (map_.find (pkg.available->id.name));
+
+ // If we already have an entry for this package name, then we
+ // have to pick one over the other.
+ //
+ if (i != map_.end ())
+ {
+ const string& n (i->first);
+
+ // At the end we want p1 to point to the object that we keep
+ // and p2 to the object whose constraints we should copy.
+ //
+ build_package* p1 (&i->second.package);
+ build_package* p2 (&pkg);
+
+ // If versions are the same, then all we have to do is copy the
+ // constraint (p1/p2 already point to where we would want them to).
+ //
+ if (p1->available->version != p2->available->version)
+ {
+ using constraint_type = build_package::constraint_type;
+
+ // If the versions differ, we have to pick one. Start with the
+ // newest version since if both satisfy, then that's the one we
+ // should prefer. So get the first to try into p1 and the second
+ // to try -- into p2.
+ //
+ if (p2->available->version > p1->available->version)
+ swap (p1, p2);
+
+ // See if pv's version satisfies pc's constraints. Return the
+ // pointer to the unsatisfied constraint or NULL if all are
+ // satisfied.
+ //
+ auto test = [] (build_package* pv, build_package* pc)
+ -> const constraint_type*
+ {
+ for (const constraint_type& c: pc->constraints)
+ if (!satisfies (pv->available->version, c.value))
+ return &c;
+
+ return nullptr;
+ };
+
+ // First see if p1 satisfies p2's constraints.
+ //
+ if (auto c2 = test (p1, p2))
+ {
+ // If not, try the other way around.
+ //
+ if (auto c1 = test (p2, p1))
+ {
+ const string& d1 (c1->dependent);
+ const string& d2 (c2->dependent);
+
+ fail << "unable to satisfy constraints on package " << n <<
+ info << d1 << " depends on (" << n << " " << c1->value << ")" <<
+ info << d2 << " depends on (" << n << " " << c2->value << ")" <<
+ info << "available " << n << " " << p1->available->version <<
+ info << "available " << n << " " << p2->available->version <<
+ info << "explicitly specify " << n << " version to manually "
+ << "satisfy both constraints";
+ }
+ else
+ swap (p1, p2);
+ }
+
+ level4 ([&]{trace << "pick " << n << " " << p1->available->version
+ << " over " << p2->available->version;});
+ }
+
+ // See if we are replacing the object. If not, then we don't
+ // need to collect its prerequisites since that should have
+ // already been done. Remember, p1 points to the object we
+ // want to keep.
+ //
+ bool replace (p1 != &i->second.package);
+
+ if (replace)
+ {
+ swap (*p1, *p2);
+ swap (p1, p2); // Setup for constraints copying below.
+ }
+
+ p1->constraints.insert (p1->constraints.end (),
+ make_move_iterator (p2->constraints.begin ()),
+ make_move_iterator (p2->constraints.end ()));
+
+ if (!replace)
+ return false;
+ }
+ else
+ {
+ string n (pkg.available->id.name); // Note: copy; see emplace() below.
+
+ level4 ([&]{trace << "add " << n << " " << pkg.available->version;});
+
+ // This is the first time we are adding this package name to the
+ // map. If it is already selected, then we need to make sure that
+ // packages that already depend on it (called dependents) are ok
+ // with the up/downgrade. We will also have to keep doing this
+ // every time we choose a new available package above. So what
+ // we are going to do is copy the dependents' constrains over to
+ // our constraint list; this way they will be automatically taken
+ // into account by the rest of the logic.
+ //
+ const shared_ptr<selected_package>& sp (pkg.selected);
+ const shared_ptr<available_package>& ap (pkg.available);
+
+ int r;
+ if (sp != nullptr &&
+ sp->state == package_state::configured &&
+ (r = sp->version.compare (ap->version)) != 0)
+ {
+ using query = query<package_dependent>;
+
+ for (const auto& pd: db.query<package_dependent> (query::name == n))
+ {
+ if (!pd.constraint)
+ continue;
+
+ const version& v (ap->version);
+ const dependency_constraint& c (*pd.constraint);
+
+ if (satisfies (v, c))
+ {
+ pkg.constraints.emplace_back (pd.name, c);
+ continue;
+ }
+
+ fail << "unable to " << (r < 0 ? "up" : "down") << "grade "
+ << "package " << n << " " << sp->version << " to " << v <<
+ info << pd.name << " depends on (" << n << " " << c << ")" <<
+ info << "explicitly specify " << n << " version to manually "
+ << "satisfy this constraint";
+ }
+ }
+
+ i = map_.emplace (move (n), data_type {end (), move (pkg)}).first;
+ }
+
+ // Now collect all the prerequisites recursively. But first "prune"
+ // this process if the package is already configured since that would
+ // mean all its prerequisites are configured as well. Note that this
+ // is not merely an optimization: the package could be an orphan in
+ // which case the below logic will fail (no repository in which to
+ // search for prerequisites). By skipping the prerequisite check we
+ // are able to gracefully handle configured orphans.
+ //
+ const build_package& p (i->second.package);
+ const shared_ptr<selected_package>& sp (p.selected);
+ const shared_ptr<available_package>& ap (p.available);
+
+ if (sp != nullptr &&
+ sp->version == ap->version &&
+ sp->state == package_state::configured)
+ return true;
+
+ // Show how we got here if things go wrong.
+ //
+ auto g (
+ make_exception_guard (
+ [&ap] ()
+ {
+ info << "while satisfying " << ap->id.name << " " << ap->version;
+ }));
+
+ const shared_ptr<repository>& ar (p.repository);
+ const string& name (ap->id.name);
+
+ for (const dependency_alternatives& da: ap->dependencies)
+ {
+ if (da.conditional) // @@ TODO
+ fail << "conditional dependencies are not yet supported";
+
+ if (da.size () != 1) // @@ TODO
+ fail << "multiple dependency alternatives not yet supported";
+
+ const dependency& d (da.front ());
+
+ // The first step is to always find the available package even
+ // if, in the end, it won't be the one we select. If we cannot
+ // find the package then that means the repository is broken.
+ // And if we have no repository to look in, then that means the
+ // package is an orphan (we delay this check until we actually
+ // need the repository to allow orphans without prerequisites).
+ //
+ if (ar == nullptr)
+ fail << "package " << name << " " << ap->version << " is orphaned" <<
+ info << "explicitly upgrade it to a new version";
+
+ auto rp (find_available (db, d.name, ar, d.constraint));
+
+ if (rp.first == nullptr)
+ {
+ diag_record dr;
+ dr << fail << "unknown prerequisite " << d << " of package " << name;
+
+ if (!ar->location.empty ())
+ dr << info << "repository " << ar->location << " appears to "
+ << "be broken";
+ }
+
+ // Next see if this package is already selected. If we already
+ // have it in the configuraion and it satisfies our dependency
+ // constraint, then we don't want to be forcing its upgrade (or,
+ // worse, downgrade).
+ //
+ bool force (false);
+ shared_ptr<selected_package> dsp (db.find<selected_package> (d.name));
+ if (dsp != nullptr)
+ {
+ if (dsp->state == package_state::broken)
+ fail << "unable to build broken package " << d.name <<
+ info << "use 'pkg-purge --force' to remove";
+
+ if (satisfies (dsp->version, d.constraint))
+ rp = make_available (options, cd, db, dsp);
+ else
+ // Remember that we may be forcing up/downgrade; we will deal
+ // with it below.
+ //
+ force = true;
+ }
+
+ build_package dp {
+ dsp,
+ rp.first,
+ rp.second,
+ false, // Hold package.
+ false, // Hold version.
+ {}, // Constraints.
+ false}; // Reconfigure.
+
+ // Add our constraint, if we have one.
+ //
+ if (d.constraint)
+ dp.constraints.emplace_back (name, *d.constraint);
+
+ // Now collect this prerequisite. If it was actually collected
+ // (i.e., it wasn't already there) and we are forcing an upgrade
+ // and the version is not held, then warn, unless we are running
+ // quiet. Downgrade or upgrade of a held version -- refuse.
+ //
+ if (collect (options, cd, db, move (dp)) && force)
+ {
+ const version& sv (dsp->version);
+ const version& av (rp.first->version);
+
+ bool u (av > sv);
+ bool f (dsp->hold_version || !u); // Fail if downgrade or held.
+
+ if (verb || f)
+ {
+ bool c (d.constraint);
+ diag_record dr;
+
+ (f ? dr << fail : dr << warn)
+ << "package " << name << " dependency on "
+ << (c ? "(" : "") << d << (c ? ")" : "") << " is forcing "
+ << (u ? "up" : "down") << "grade of " << d.name << " " << sv
+ << " to " << av;
+
+ if (dsp->hold_version)
+ dr << info << "package version " << d.name << " " << sv
+ << " is held";
+
+ if (f)
+ dr << info << "explicitly request version "
+ << (u ? "up" : "down") << "grade to continue";
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // Order the previously-collected package with the specified name
+ // returning its positions. If reorder is true, then reorder this
+ // package to be considered as "early" as possible.
+ //
+ iterator
+ order (const string& name, bool reorder = true)
+ {
+ // Every package that we order should have already been collected.
+ //
+ auto mi (map_.find (name));
+ assert (mi != map_.end ());
+
+ // If this package is already in the list, then that would also
+ // mean all its prerequisites are in the list and we can just
+ // return its position. Unless we want it reordered.
+ //
+ iterator& pos (mi->second.position);
+ if (pos != end ())
+ {
+ if (reorder)
+ erase (pos);
+ else
+ return pos;
+ }
+
+ // Order all the prerequisites of this package and compute the
+ // position of its "earliest" prerequisite -- this is where it
+ // will be inserted.
+ //
+ build_package& p (mi->second.package);
+ const shared_ptr<selected_package>& sp (p.selected);
+ const shared_ptr<available_package>& ap (p.available);
+
+ assert (ap != nullptr); // No dependents allowed here.
+
+ // Unless this package needs something to be before it, add it to
+ // the end of the list.
+ //
+ iterator i (end ());
+
+ // Figure out if j is before i, in which case set i to j. The goal
+ // here is to find the position of our "earliest" prerequisite.
+ //
+ auto update = [this, &i] (iterator j)
+ {
+ for (iterator k (j); i != j && k != end ();)
+ if (++k == i)
+ i = j;
+ };
+
+ // Similar to collect(), we can prune if the package is already
+ // configured, right? Not so fast. While in collect() we didn't
+ // need to add prerequisites of such a package, it doesn't mean
+ // that they actually never ended up in the map via another way.
+ // For example, some can be a part of the initial selection. And
+ // in that case we must order things properly.
+ //
+ // So here we are going to do things differently depending on
+ // whether the package is already configured or not. If it is,
+ // then that means we can use its prerequisites list. Otherwise,
+ // we use the manifest data.
+ //
+ if (sp != nullptr &&
+ sp->version == ap->version &&
+ sp->state == package_state::configured)
+ {
+ for (const auto& p: sp->prerequisites)
+ {
+ const string& name (p.first.object_id ());
+
+ // The prerequisites may not necessarily be in the map.
+ //
+ if (map_.find (name) != map_.end ())
+ update (order (name, false));
+ }
+ }
+ else
+ {
+ // We are iterating in reverse so that when we iterate over
+ // the dependency list (also in reverse), prerequisites will
+ // be built in the order that is as close to the manifest as
+ // possible.
+ //
+ for (const dependency_alternatives& da:
+ reverse_iterate (p.available->dependencies))
+ {
+ assert (!da.conditional && da.size () == 1); // @@ TODO
+ const dependency& d (da.front ());
+
+ update (order (d.name, false));
+ }
+ }
+
+ return pos = insert (i, p);
+ }
+
+ // If a configured package is being up/down-graded then that means
+ // all its dependents could be affected and we have to reconfigure
+ // them. This function examines every package that is already on
+ // the list and collects and orders all its dependents.
+ //
+ // Should we reconfigure just the direct depends or also include
+ // indirect, recursively? Consider this plauisible scenario as an
+ // example: We are upgrading a package to a version that provides
+ // an additional API. When its direct dependent gets reconfigured,
+ // it notices this new API and exposes its own extra functionality
+ // that is based on it. Now it would make sense to let its own
+ // dependents (which would be our original package's indirect ones)
+ // to also notice this.
+ //
+ void
+ collect_order_dependents (database& db)
+ {
+ // For each package on the list we want to insert all its dependents
+ // before it so that they get configured after the package on which
+ // they depend is configured (remember, our build order is reverse,
+ // with the last package being built first). This applies to both
+ // packages that are already on the list as well as the ones that
+ // we add, recursively.
+ //
+ for (auto i (begin ()); i != end (); ++i)
+ {
+ const build_package& p (*i);
+
+ // Prune if this is not a configured package being up/down-graded
+ // or reconfigured.
+ //
+ if (p.reconfigure ())
+ collect_order_dependents (db, i);
+ }
+ }
+
+ void
+ collect_order_dependents (database& db, iterator pos)
+ {
+ tracer trace ("collect_order_dependents");
+
+ const build_package& p (*pos);
+ const string& n (p.selected->name);
+
+ using query = query<package_dependent>;
+
+ for (auto& pd: db.query<package_dependent> (query::name == n))
+ {
+ string& dn (pd.name);
+
+ // We can have three cases here: the package is already on the
+ // list, the package is in the map (but not on the list) and it
+ // is in neither.
+ //
+ auto i (map_.find (dn));
+
+ if (i != map_.end ())
+ {
+ build_package& dp (i->second.package);
+
+ // Force reconfiguration in both cases.
+ //
+ dp.reconfigure_ = true;
+
+ if (i->second.position == end ())
+ {
+ // Clean the build_package object up to make sure we don't
+ // inadvertently force up/down-grade.
+ //
+ dp.available = nullptr;
+ dp.repository = nullptr;
+
+ i->second.position = insert (pos, dp);
+ }
+ }
+ else
+ {
+ shared_ptr<selected_package> dsp (db.load<selected_package> (dn));
+
+ i = map_.emplace (
+ move (dn),
+ data_type
+ {
+ end (),
+ build_package {
+ move (dsp),
+ nullptr,
+ nullptr,
+ false, // Hold package.
+ false, // Hold version.
+ {}, // Constraints.
+ true} // Reconfigure.
+ }).first;
+
+ i->second.position = insert (pos, i->second.package);
+ }
+
+ // Collect our own dependents inserting them before us.
+ //
+ collect_order_dependents (db, i->second.position);
+ }
+ }
+
+ private:
+ struct data_type
+ {
+ iterator position; // Note: can be end(), see collect().
+ build_package package;
+ };
+
+ map<string, data_type> map_;
+ };
+
+ int
+ pkg_build (const pkg_build_options& o, cli::scanner& args)
+ {
+ tracer trace ("pkg_build");
+
+ const dir_path& c (o.directory ());
+ level4 ([&]{trace << "configuration: " << c;});
+
+ if (!args.more ())
+ fail << "package name argument expected" <<
+ info << "run 'bpkg help pkg-build' for more information";
+
+ database db (open (c, trace));
+
+ // Note that the session spans all our transactions. The idea here is
+ // that selected_package objects in the build_packages list below will
+ // be cached in this session. When subsequent transactions modify any
+ // of these objects, they will modify the cached instance, which means
+ // our list will always "see" their updated state.
+ //
+ session s;
+
+ // Assemble the list of packages we will need to build.
+ //
+ build_packages pkgs;
+ strings names;
+ {
+ transaction t (db.begin ());
+
+ shared_ptr<repository> root (db.load<repository> (""));
+
+ while (args.more ())
+ {
+ const char* s (args.next ());
+
+ // Reduce all the potential variations (archive, directory, package
+ // name, package name/version) to a single available_package object.
+ //
+ string n;
+ version v;
+
+ shared_ptr<repository> ar;
+ shared_ptr<available_package> ap;
+
+ // Is this a package archive?
+ //
+ try
+ {
+ path a (s);
+ if (exists (a))
+ {
+ package_manifest m (pkg_verify (o, a, true, false));
+
+ // This is a package archive (note that we shouldn't throw
+ // failed from here on).
+ //
+ level4 ([&]{trace << "archive " << a;});
+ n = m.name;
+ v = m.version;
+ ar = root;
+ ap = make_shared<available_package> (move (m));
+ ap->locations.push_back (package_location {root, move (a)});
+ }
+ }
+ catch (const invalid_path&)
+ {
+ // Not a valid path so cannot be an archive.
+ }
+ catch (const failed&)
+ {
+ // Not a valid package archive.
+ }
+
+ // Is this a package directory?
+ //
+ try
+ {
+ dir_path d (s);
+ if (exists (d))
+ {
+ package_manifest m (pkg_verify (d, true, false));
+
+ // This is a package directory (note that we shouldn't throw
+ // failed from here on).
+ //
+ level4 ([&]{trace << "directory " << d;});
+ n = m.name;
+ v = m.version;
+ ap = make_shared<available_package> (move (m));
+ ar = root;
+ ap->locations.push_back (package_location {root, move (d)});
+ }
+ }
+ catch (const invalid_path&)
+ {
+ // Not a valid path so cannot be an archive.
+ }
+ catch (const failed&)
+ {
+ // Not a valid package archive.
+ }
+
+ // Then it got to be a package name with optional version.
+ //
+ if (ap == nullptr)
+ {
+ n = parse_package_name (s);
+ v = parse_package_version (s);
+ level4 ([&]{trace << "package " << n << "; version " << v;});
+
+ // Either get the user-specified version or the latest.
+ //
+ auto rp (
+ v.empty ()
+ ? find_available (db, n, root, nullopt)
+ : find_available (db, n, root,
+ dependency_constraint {comparison::eq, v}));
+
+ ap = rp.first;
+ ar = rp.second;
+ }
+
+ // Load the package that may have already been selected and
+ // figure out what exactly we need to do here. The end goal
+ // is the available_package object corresponding to the actual
+ // package that we will be building (which may or may not be
+ // the same as the selected package).
+ //
+ shared_ptr<selected_package> sp (db.find<selected_package> (n));
+
+ if (sp != nullptr && sp->state == package_state::broken)
+ fail << "unable to build broken package " << n <<
+ info << "use 'pkg-purge --force' to remove";
+
+ bool found (true);
+
+ // If the user asked for a specific version, then that's what
+ // we ought to be building.
+ //
+ if (!v.empty ())
+ {
+ for (;;)
+ {
+ if (ap != nullptr) // Must be that version, see above.
+ break;
+
+ // Otherwise, our only chance is that the already selected
+ // object is that exact version.
+ //
+ if (sp != nullptr && sp->version == v)
+ break; // Derive ap from sp below.
+
+ found = false;
+ break;
+ }
+ }
+ //
+ // No explicit version was specified by the user.
+ //
+ else
+ {
+ if (ap != nullptr)
+ {
+ // Even if this package is already in the configuration, should
+ // we have a newer version, we treat it as an upgrade request;
+ // otherwise, why specify the package in the first place? We just
+ // need to check if what we already have is "better" (i.e., newer).
+ //
+ if (sp != nullptr && ap->id.version < sp->version)
+ ap = nullptr; // Derive ap from sp below.
+ }
+ else
+ {
+ if (sp == nullptr)
+ found = false;
+
+ // Otherwise, derive ap from sp below.
+ }
+ }
+
+ if (!found)
+ {
+ diag_record dr;
+
+ dr << fail << "unknown package " << n;
+ if (!v.empty ())
+ dr << " " << v;
+
+ // Let's help the new user out here a bit.
+ //
+ if (db.query_value<repository_count> () == 0)
+ dr << info << "configuration " << c << " has no repositories"
+ << info << "use 'bpkg cfg-add' to add a repository";
+ else if (db.query_value<available_package_count> () == 0)
+ dr << info << "configuration " << c << " has no available packages"
+ << info << "use 'bpkg cfg-fetch' to fetch available packages "
+ << "list";
+ }
+
+ // If the available_package object is still NULL, then it means
+ // we need to get one corresponding to the selected package.
+ //
+ if (ap == nullptr)
+ {
+ assert (sp != nullptr);
+
+ auto rp (make_available (o, c, db, sp));
+ ap = rp.first;
+ ar = rp.second; // Could be NULL (orphan).
+ }
+
+ // Finally add this package to the list.
+ //
+ level4 ([&]{trace << "collect " << ap->id.name << " "
+ << ap->version;});
+
+ build_package p {
+ move (sp),
+ move (ap),
+ move (ar),
+ true, // Hold package.
+ !v.empty (), // Hold version.
+ {}, // Constraints.
+ false}; // Reconfigure.
+
+ // "Fix" the version the user asked for by adding the '==' constraint.
+ //
+ if (!v.empty ())
+ p.constraints.emplace_back (
+ "command line",
+ dependency_constraint {comparison::eq, v});
+
+ pkgs.collect (o, c, db, move (p));
+ names.push_back (n);
+ }
+
+ // Now that we have collected all the package versions that we need
+ // to build, arrange them in the "dependency order", that is, with
+ // every package on the list only possibly depending on the ones
+ // after it. Iterate over the names we have collected on the previous
+ // step in reverse so that when we iterate over the packages (also in
+ // reverse), things will be built as close as possible to the order
+ // specified by the user (it may still get altered if there are
+ // dependencies between the specified packages).
+ //
+ for (const string& n: reverse_iterate (names))
+ pkgs.order (n);
+
+ // Finally, collect and order all the dependents that we will need
+ // to reconfigure because of the up/down-grades of packages that
+ // are now on the list.
+ //
+ pkgs.collect_order_dependents (db);
+
+ t.commit ();
+ }
+
+ // Print what we are going to do, then ask for the user's confirmation.
+ //
+ if (o.print_only () || !o.yes ())
+ {
+ for (const build_package& p: reverse_iterate (pkgs))
+ {
+ const shared_ptr<selected_package>& sp (p.selected);
+ const shared_ptr<available_package>& ap (p.available);
+
+ const char* act;
+ string n;
+ version v;
+
+ if (ap == nullptr)
+ {
+ // This is a dependent needing reconfiguration.
+ //
+ assert (sp != nullptr && p.reconfigure ());
+
+ n = sp->name;
+ act = "reconfigure";
+ }
+ else
+ {
+ n = ap->id.name;
+ v = ap->version;
+
+ // Even if we already have this package selected, we have to
+ // make sure it is configured and updated.
+ //
+ if (sp == nullptr || sp->version == v)
+ act = p.reconfigure () ? "reconfigure/build" : "build";
+ else
+ act = sp->version < v ? "upgrade" : "downgrade";
+ }
+
+ if (o.print_only ())
+ cout << act << " " << n << (v.empty () ? "" : " ") << v << endl;
+ else if (verb)
+ text << act << " " << n << (v.empty () ? "" : " ") << v;
+ }
+ }
+
+ if (o.print_only ())
+ return 0;
+
+ // Ask the user if we should continue.
+ //
+ if (!(o.yes () || yn_prompt ("continue? [Y/n]", 'y')))
+ return 1;
+
+ // Ok, we have "all systems go". The overall action plan is as follows.
+ //
+ // 1. disfigure up/down-graded, reconfigured [left to right]
+ // 2. purge up/down-graded
+ // 3. fetch new, up/down-graded
+ // 4. unpack new, up/down-graded
+ // 5. configure all [right to left]
+ // 6. build user selection [right to left]
+ //
+ // Note that for some actions, e.g., purge or fetch, the order is not
+ // really important. We will, however, do it right to left since that
+ // is the order closest to that of the user selection.
+ //
+ // We are also going to combine purge/fetch/unpack into a single step
+ // and use the replace mode so it will become just fetch/unpack.
+ //
+
+ // disfigure
+ //
+ for (const build_package& p: pkgs)
+ {
+ // We are only interested in configured packages that are either
+ // up/down-graded or need reconfiguration (e.g., dependents).
+ //
+ if (!p.reconfigure ())
+ continue;
+
+ const shared_ptr<selected_package>& sp (p.selected);
+
+ // Each package is disfigured in its own transaction, so that we
+ // always leave the configuration in a valid state.
+ //
+ transaction t (db.begin ());
+ pkg_disfigure (c, o, t, sp); // Commits the transaction.
+ assert (sp->state == package_state::unpacked);
+
+ if (verb)
+ text << "disfigured " << sp->name << " " << sp->version;
+ }
+
+ // fetch/unpack
+ //
+ for (build_package& p: reverse_iterate (pkgs))
+ {
+ shared_ptr<selected_package>& sp (p.selected);
+ const shared_ptr<available_package>& ap (p.available);
+
+ if (ap == nullptr) // Skip dependents.
+ continue;
+
+ // Fetch if this is a new package or if we are up/down-grading.
+ //
+ if (sp == nullptr || sp->version != ap->version)
+ {
+ sp.reset (); // For the directory case below.
+
+ // Distinguish between the package and archive/directory cases.
+ //
+ const package_location& pl (ap->locations[0]); // Got to have one.
+
+ if (pl.repository.object_id () != "") // Special root?
+ {
+ transaction t (db.begin ());
+ sp = pkg_fetch (o,
+ c,
+ t,
+ ap->id.name,
+ ap->version,
+ true); // Replace; commits the transaction.
+ }
+ else if (exists (pl.location)) // Directory case is handled by unpack.
+ {
+ transaction t (db.begin ());
+ sp = pkg_fetch (o,
+ c,
+ t,
+ pl.location, // Archive path.
+ true, // Replace
+ false); // Don't purge; commits the transaction.
+ }
+
+ if (sp != nullptr) // Actually unpacked something?
+ {
+ assert (sp->state == package_state::fetched);
+
+ if (verb)
+ text << "fetched " << sp->name << " " << sp->version;
+ }
+ }
+
+ // Unpack. Note that the package can still be NULL if this is the
+ // directory case (see the fetch code above).
+ //
+ if (sp == nullptr || sp->state == package_state::fetched)
+ {
+ if (sp != nullptr)
+ {
+ transaction t (db.begin ());
+ sp = pkg_unpack (o, c, t, ap->id.name); // Commits the transaction.
+ }
+ else
+ {
+ const package_location& pl (ap->locations[0]);
+ assert (pl.repository.object_id () == ""); // Special root.
+
+ transaction t (db.begin ());
+ sp = pkg_unpack (c,
+ t,
+ path_cast<dir_path> (pl.location),
+ true, // Replace.
+ false); // Don't purge; commits the transaction.
+ }
+
+ assert (sp->state == package_state::unpacked);
+
+ if (verb)
+ text << "unpacked " << sp->name << " " << sp->version;
+ }
+ }
+
+ // configure
+ //
+ for (const build_package& p: reverse_iterate (pkgs))
+ {
+ const shared_ptr<selected_package>& sp (p.selected);
+
+ assert (sp != nullptr);
+
+ // We configure everything that isn't already configured.
+ //
+ if (sp->state == package_state::configured)
+ continue;
+
+ transaction t (db.begin ());
+ pkg_configure (c, o, t, sp, strings ()); // Commits the transaction.
+ assert (sp->state == package_state::configured);
+
+ if (verb)
+ text << "configured " << sp->name << " " << sp->version;
+ }
+
+ // Small detour: update the hold state. While we could have tried
+ // to "weave" it into one of the previous actions, things there
+ // are already convoluted enough.
+ //
+ for (const build_package& p: reverse_iterate (pkgs))
+ {
+ const shared_ptr<selected_package>& sp (p.selected);
+ assert (sp != nullptr);
+
+ // Note that we should only "increase" the hold state.
+ //
+ bool hp (p.hold_package && sp->hold_package != p.hold_package);
+ bool hv (p.hold_version && sp->hold_version != p.hold_version);
+
+ if (hp || hv)
+ {
+ if (hp) sp->hold_package = true;
+ if (hv) sp->hold_version = true;
+
+ transaction t (db.begin ());
+ db.update (sp);
+ t.commit ();
+
+ if (verb > 1)
+ {
+ if (hp)
+ text << "hold package " << sp->name;
+
+ if (hv)
+ text << "hold version " << sp->name << " " << sp->version;
+ }
+ }
+ }
+
+ if (o.configure_only ())
+ return 0;
+
+ // update
+ //
+ for (const build_package& p: reverse_iterate (pkgs))
+ {
+ const shared_ptr<selected_package>& sp (p.selected);
+
+ // Update the user selection only.
+ //
+ if (find (names.begin (), names.end (), sp->name) == names.end ())
+ continue;
+
+ pkg_update (c, o, sp);
+
+ if (verb)
+ text << "updated " << sp->name << " " << sp->version;
+ }
+
+ return 0;
+ }
+}