aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2023-02-01 11:42:31 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2023-02-01 11:42:31 +0200
commit546391dab6173660acceba6404136e9411ce1388 (patch)
tree79da333fd1f7447c6b9490565f520d1d79a329b7
parent724131b7e03934664621f86df2dc2285ff43dba8 (diff)
Implement system package manager query and install support for Debian
-rw-r--r--bpkg/bpkg.cxx1
-rw-r--r--bpkg/buildfile8
-rw-r--r--bpkg/database.cxx1
-rw-r--r--bpkg/diagnostics.cxx2
-rw-r--r--bpkg/diagnostics.hxx31
-rw-r--r--bpkg/package-query.cxx100
-rw-r--r--bpkg/package-query.hxx25
-rw-r--r--bpkg/package.hxx11
-rw-r--r--bpkg/pkg-build-collect.cxx29
-rw-r--r--bpkg/pkg-build-collect.hxx23
-rw-r--r--bpkg/pkg-build.cli64
-rw-r--r--bpkg/pkg-build.cxx643
-rw-r--r--bpkg/system-package-manager-debian.cxx1411
-rw-r--r--bpkg/system-package-manager-debian.hxx201
-rw-r--r--bpkg/system-package-manager-debian.test.cxx348
-rw-r--r--bpkg/system-package-manager-debian.test.testscript987
-rw-r--r--bpkg/system-package-manager.cxx454
-rw-r--r--bpkg/system-package-manager.hxx270
-rw-r--r--bpkg/system-package-manager.test.cxx124
-rw-r--r--bpkg/system-package-manager.test.hxx104
-rw-r--r--bpkg/system-package-manager.test.testscript101
-rw-r--r--bpkg/system-repository.cxx8
-rw-r--r--bpkg/system-repository.hxx15
-rw-r--r--bpkg/types.hxx5
-rw-r--r--bpkg/utility.cxx2
-rw-r--r--bpkg/utility.hxx10
-rw-r--r--doc/manual.cli279
-rw-r--r--tests/common.testscript4
-rw-r--r--tests/pkg-build.testscript4
-rw-r--r--tests/pkg-drop.testscript4
30 files changed, 4969 insertions, 300 deletions
diff --git a/bpkg/bpkg.cxx b/bpkg/bpkg.cxx
index 3ede99e..76b2533 100644
--- a/bpkg/bpkg.cxx
+++ b/bpkg/bpkg.cxx
@@ -611,6 +611,7 @@ try
cout << "bpkg " << BPKG_VERSION_ID << endl
<< "libbpkg " << LIBBPKG_VERSION_ID << endl
<< "libbutl " << LIBBUTL_VERSION_ID << endl
+ << "host " << host_triplet << endl
<< "Copyright (c) " << BPKG_COPYRIGHT << "." << endl
<< "This is free software released under the MIT license." << endl;
return 0;
diff --git a/bpkg/buildfile b/bpkg/buildfile
index b3b2ba7..ca78218 100644
--- a/bpkg/buildfile
+++ b/bpkg/buildfile
@@ -97,8 +97,10 @@ for t: cxx{**.test...}
# Build options.
#
-obj{utility}: cxx.poptions += -DBPKG_EXE_PREFIX='"'$bin.exe.prefix'"' \
--DBPKG_EXE_SUFFIX='"'$bin.exe.suffix'"'
+obj{utility}: cxx.poptions += \
+"-DBPKG_EXE_PREFIX=\"$bin.exe.prefix\"" \
+"-DBPKG_EXE_SUFFIX=\"$bin.exe.suffix\"" \
+"-DBPKG_HOST_TRIPLET=\"$cxx.target\""
# Pass the copyright notice extracted from the LICENSE file.
#
@@ -107,7 +109,7 @@ copyright = $process.run_regex( \
'Copyright \(c\) (.+) \(see the AUTHORS and LEGAL files\)\.', \
'\1')
-obj{bpkg}: cxx.poptions += -DBPKG_COPYRIGHT=\"$copyright\"
+obj{bpkg}: cxx.poptions += "-DBPKG_COPYRIGHT=\"$copyright\""
# Disable "unknown pragma" warnings.
#
diff --git a/bpkg/database.cxx b/bpkg/database.cxx
index 6b135fc..65e3af8 100644
--- a/bpkg/database.cxx
+++ b/bpkg/database.cxx
@@ -349,7 +349,6 @@ namespace bpkg
else
config_orig = config;
-
string = '[' + config_orig.representation () + ']';
try
diff --git a/bpkg/diagnostics.cxx b/bpkg/diagnostics.cxx
index ef87cab..9232d93 100644
--- a/bpkg/diagnostics.cxx
+++ b/bpkg/diagnostics.cxx
@@ -129,7 +129,7 @@ namespace bpkg
const basic_mark error ("error");
const basic_mark warn ("warning");
const basic_mark info ("info");
- const basic_mark text (nullptr);
+ const basic_mark text (nullptr, nullptr, nullptr, nullptr); // No frame.
const fail_mark fail ("error");
const fail_end endf;
}
diff --git a/bpkg/diagnostics.hxx b/bpkg/diagnostics.hxx
index e8d9f0a..a01d90c 100644
--- a/bpkg/diagnostics.hxx
+++ b/bpkg/diagnostics.hxx
@@ -118,9 +118,37 @@ namespace bpkg
//
using butl::diag_stream;
using butl::diag_epilogue;
+ using butl::diag_frame;
// Diagnostic facility, project specifics.
//
+
+ // Note: diag frames are not applied to text/trace diagnostics.
+ //
+ template <typename F>
+ struct diag_frame_impl: diag_frame
+ {
+ explicit
+ diag_frame_impl (F f): diag_frame (&thunk), func_ (move (f)) {}
+
+ private:
+ static void
+ thunk (const diag_frame& f, const butl::diag_record& r)
+ {
+ static_cast<const diag_frame_impl&> (f).func_ (
+ const_cast<diag_record&> (static_cast<const diag_record&> (r)));
+ }
+
+ const F func_;
+ };
+
+ template <typename F>
+ inline diag_frame_impl<F>
+ make_diag_frame (F f)
+ {
+ return diag_frame_impl<F> (move (f));
+ }
+
struct simple_prologue_base
{
explicit
@@ -184,7 +212,7 @@ namespace bpkg
basic_mark_base (const char* type,
const char* name = nullptr,
const void* data = nullptr,
- diag_epilogue* epilogue = nullptr)
+ diag_epilogue* epilogue = &diag_frame::apply)
: type_ (type), name_ (name), data_ (data), epilogue_ (epilogue) {}
simple_prologue
@@ -288,6 +316,7 @@ namespace bpkg
data,
[](const diag_record& r, butl::diag_writer* w)
{
+ diag_frame::apply (r);
r.flush (w);
throw failed ();
}) {}
diff --git a/bpkg/package-query.cxx b/bpkg/package-query.cxx
index 8d7b652..ea64e71 100644
--- a/bpkg/package-query.cxx
+++ b/bpkg/package-query.cxx
@@ -311,11 +311,11 @@ namespace bpkg
}
// Sort the available package fragments in the package version descending
- // order and suppress duplicate packages.
+ // order and suppress duplicate packages and, optionally, older package
+ // revisions.
//
static void
- sort_dedup (vector<pair<shared_ptr<available_package>,
- lazy_shared_ptr<repository_fragment>>>& pfs)
+ sort_dedup (available_packages& pfs, bool suppress_older_revisions = false)
{
sort (pfs.begin (), pfs.end (),
[] (const auto& x, const auto& y)
@@ -323,22 +323,22 @@ namespace bpkg
return x.first->version > y.first->version;
});
- pfs.erase (unique (pfs.begin(), pfs.end(),
- [] (const auto& x, const auto& y)
- {
- return x.first->version == y.first->version;
- }),
- pfs.end ());
+ pfs.erase (
+ unique (pfs.begin(), pfs.end(),
+ [suppress_older_revisions] (const auto& x, const auto& y)
+ {
+ return x.first->version.compare (y.first->version,
+ suppress_older_revisions) == 0;
+ }),
+ pfs.end ());
}
- vector<pair<shared_ptr<available_package>,
- lazy_shared_ptr<repository_fragment>>>
+ available_packages
find_available (const linked_databases& dbs,
const package_name& name,
const optional<version_constraint>& c)
{
- vector<pair<shared_ptr<available_package>,
- lazy_shared_ptr<repository_fragment>>> r;
+ available_packages r;
for (database& db: dbs)
{
@@ -376,15 +376,13 @@ namespace bpkg
return r;
}
- vector<pair<shared_ptr<available_package>,
- lazy_shared_ptr<repository_fragment>>>
+ available_packages
find_available (const package_name& name,
const optional<version_constraint>& c,
const config_repo_fragments& rfs,
bool prereq)
{
- vector<pair<shared_ptr<available_package>,
- lazy_shared_ptr<repository_fragment>>> r;
+ available_packages r;
for (const auto& dfs: rfs)
{
@@ -546,6 +544,74 @@ namespace bpkg
return make_pair (find_available (options, db, sp), nullptr);
}
+ available_packages
+ find_available_all (const linked_databases& dbs,
+ const package_name& name,
+ bool suppress_older_revisions)
+ {
+ // Collect all the databases linked explicitly and implicitly to the
+ // specified databases, recursively.
+ //
+ // Note that this is a superset of the database cluster, since we descend
+ // into the database links regardless of their types (see
+ // cluster_configs() for details).
+ //
+ linked_databases all_dbs;
+ all_dbs.reserve (dbs.size ());
+
+ auto add = [&all_dbs] (database& db, const auto& add)
+ {
+ if (find (all_dbs.begin (), all_dbs.end (), db) != all_dbs.end ())
+ return;
+
+ all_dbs.push_back (db);
+
+ {
+ const linked_configs& cs (db.explicit_links ());
+ for (auto i (cs.begin_linked ()); i != cs.end (); ++i)
+ add (i->db, add);
+ }
+
+ {
+ const linked_databases& cs (db.implicit_links ());
+ for (auto i (cs.begin_linked ()); i != cs.end (); ++i)
+ add (*i, add);
+ }
+ };
+
+ for (database& db: dbs)
+ add (db, add);
+
+ // Collect all the available packages from all the collected databases.
+ //
+ available_packages r;
+
+ for (database& db: all_dbs)
+ {
+ for (shared_ptr<available_package> ap:
+ pointer_result (
+ query_available (db, name, nullopt /* version_constraint */)))
+ {
+ // An available package should come from at least one fetched
+ // repository fragment.
+ //
+ assert (!ap->locations.empty ());
+
+ // All repository fragments the package comes from are equally good, so
+ // we pick the first one.
+ //
+ r.emplace_back (move (ap), ap->locations[0].repository_fragment);
+ }
+ }
+
+ // Sort the result in the package version descending order and suppress
+ // duplicates and, if requested, older package revisions.
+ //
+ sort_dedup (r, suppress_older_revisions);
+
+ return r;
+ }
+
pair<shared_ptr<available_package>,
lazy_shared_ptr<repository_fragment>>
make_available_fragment (const common_options& options,
diff --git a/bpkg/package-query.hxx b/bpkg/package-query.hxx
index ebe92ac..ee9b595 100644
--- a/bpkg/package-query.hxx
+++ b/bpkg/package-query.hxx
@@ -75,14 +75,13 @@ namespace bpkg
// Try to find packages that optionally satisfy the specified version
// constraint in multiple databases, suppressing duplicates. Return the list
// of packages and repository fragments in which each was found in the
- // package version descending or empty list if none were found. Note that a
- // stub satisfies any constraint.
+ // package version descending order or empty list if none were found. Note
+ // that a stub satisfies any constraint.
//
// Note that we return (loaded) lazy_shared_ptr in order to also convey
// the database to which it belongs.
//
- vector<pair<shared_ptr<available_package>,
- lazy_shared_ptr<repository_fragment>>>
+ available_packages
find_available (const linked_databases&,
const package_name&,
const optional<version_constraint>&);
@@ -94,8 +93,8 @@ namespace bpkg
using config_repo_fragments =
database_map<vector<shared_ptr<repository_fragment>>>;
- vector<pair<shared_ptr<available_package>,
- lazy_shared_ptr<repository_fragment>>>
+
+ available_packages
find_available (const package_name&,
const optional<version_constraint>&,
const config_repo_fragments&,
@@ -173,6 +172,20 @@ namespace bpkg
database&,
const shared_ptr<selected_package>&);
+ // Try to find packages in multiple databases, traversing the explicitly and
+ // implicitly linked databases recursively and suppressing duplicates and,
+ // optionally, older package revisions. Return the list of packages and
+ // repository fragments in which each was found in the package version
+ // descending order or empty list if none were found.
+ //
+ // Note that we return (loaded) lazy_shared_ptr in order to also convey
+ // the database to which it belongs.
+ //
+ available_packages
+ find_available_all (const linked_databases&,
+ const package_name&,
+ bool suppress_older_revisions = true);
+
// 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 fragment
diff --git a/bpkg/package.hxx b/bpkg/package.hxx
index 8796036..e811e62 100644
--- a/bpkg/package.hxx
+++ b/bpkg/package.hxx
@@ -847,7 +847,7 @@ namespace bpkg
//
#pragma db member(tests) id_column("") value_column("test_")
- // distributions
+ // distribution_values
//
#pragma db member(distribution_values) id_column("") value_column("dist_")
@@ -888,6 +888,15 @@ namespace bpkg
available_package () = default;
};
+ // The available packages together with the repository fragments they belong
+ // to.
+ //
+ // Note that lazy_shared_ptr is used to also convey the databases the
+ // objects belong to.
+ //
+ using available_packages = vector<pair<shared_ptr<available_package>,
+ lazy_shared_ptr<repository_fragment>>>;
+
#pragma db view object(available_package)
struct available_package_count
{
diff --git a/bpkg/pkg-build-collect.cxx b/bpkg/pkg-build-collect.cxx
index 12db3ba..8442b15 100644
--- a/bpkg/pkg-build-collect.cxx
+++ b/bpkg/pkg-build-collect.cxx
@@ -29,6 +29,35 @@ namespace bpkg
{
// build_package
//
+ const system_package_status* build_package::
+ system_status () const
+ {
+ assert (action);
+
+ if (*action != build_package::drop && system)
+ {
+ const optional<system_repository>& sys_rep (db.get ().system_repository);
+ assert (sys_rep);
+
+ if (const system_package* sys_pkg = sys_rep->find (name ()))
+ return sys_pkg->system_status;
+ }
+
+ return nullptr;
+ }
+
+ const system_package_status* build_package::
+ system_install () const
+ {
+ if (const system_package_status* s = system_status ())
+ return s->status == system_package_status::partially_installed ||
+ s->status == system_package_status::not_installed
+ ? s
+ : nullptr;
+
+ return nullptr;
+ }
+
bool build_package::
user_selection () const
{
diff --git a/bpkg/pkg-build-collect.hxx b/bpkg/pkg-build-collect.hxx
index e47d9fa..6c79abe 100644
--- a/bpkg/pkg-build-collect.hxx
+++ b/bpkg/pkg-build-collect.hxx
@@ -19,8 +19,9 @@
#include <bpkg/common-options.hxx>
#include <bpkg/pkg-build-options.hxx>
-#include <bpkg/pkg-configure.hxx> // find_database_function()
+#include <bpkg/pkg-configure.hxx> // find_database_function()
#include <bpkg/package-skeleton.hxx>
+#include <bpkg/system-package-manager.hxx>
namespace bpkg
{
@@ -210,6 +211,26 @@ namespace bpkg
//
bool system;
+ // Return the system/distribution package status if this is a system
+ // package (re-)configuration and the package is being managed by the
+ // system package manager (as opposed to user/fallback). Otherwise, return
+ // NULL (so can be used as bool).
+ //
+ // Note on terminology: We call the bpkg package that is being configured
+ // as available from the system as "system package" and we call the
+ // underlying package managed by the system/distribution package manager
+ // as "system/distribution package". See system-package-manager.hxx for
+ // background.
+ //
+ const system_package_status*
+ system_status () const;
+
+ // As above but only return the status if the package needs to be
+ // installed.
+ //
+ const system_package_status*
+ system_install () const;
+
// If this flag is set and the external package is being replaced with an
// external one, then keep its output directory between upgrades and
// downgrades.
diff --git a/bpkg/pkg-build.cli b/bpkg/pkg-build.cli
index 493ffbe..740764b 100644
--- a/bpkg/pkg-build.cli
+++ b/bpkg/pkg-build.cli
@@ -95,12 +95,19 @@ namespace bpkg
A package name (<pkg>) can be prefixed with a package scheme
(<scheme>). Currently the only recognized scheme is \cb{sys} which
instructs \cb{pkg-build} to configure the package as available from the
- system rather than building it from source. If the system package version
- (<ver-spec>) is not specified or is '\cb{/*}', then it is considered to
- be unknown but satisfying any version constraint. If specified,
- <ver-spec> may not be a version constraint. If the version is not
- explicitly specified, then at least a stub package must be available from
- one of the repositories.
+ system rather than building it from source.
+
+ The system package version (<ver-spec>) may not be a version constraint
+ but may be the special '\cb{/*}' value, which indicates that the version
+ should be considered unknown but satisfying any version constraint. If
+ unspecified, then \cb{pkg-build} will attempt to query the system package
+ manager for the installed version unless the system package manager is
+ unsupported or this functionality is disabled with \cb{--sys-no-query},
+ in which case the '\cb{/*}' <ver-spec> is assumed. If the system package
+ manager is supported, then the automatic installation of an available
+ package can be requested with the \cb{--sys-install} option. Note that if
+ the version is not explicitly specified, then at least a stub package
+ must be available from one of the repositories.
Finally, a package can be specified as either the path to the package
archive (<file>) or to the package directory (<dir>\cb{/}; note that it
@@ -293,7 +300,8 @@ namespace bpkg
bool --yes|-y
{
- "Assume the answer to all prompts is \cb{yes}."
+ "Assume the answer to all prompts is \cb{yes}. Note that this excludes
+ the system package manager prompts; see \cb{--sys-yes} for details."
}
string --for|-f
@@ -408,6 +416,48 @@ namespace bpkg
See \l{bpkg-cfg-create(1)} for details on linked configurations."
}
+ bool --sys-no-query
+ {
+ "Do not query the system package manager for the installed versions of
+ packages specified with the \cb{sys} scheme."
+ }
+
+ bool --sys-install
+ {
+ "Instruct the system package manager to install available versions of
+ packages specified with the \cb{sys} scheme that are not already
+ installed. See also the \cb{--sys-no-fetch}, \cb{--sys-yes}, and
+ \cb{--sys-sudo} options."
+ }
+
+ bool --sys-no-fetch
+ {
+ "Do not fetch the system package manager metadata before querying for
+ available versions of packages specified with the \cb{sys} scheme.
+ This option only makes sense together with \cb{--sys-install}."
+ }
+
+ bool --sys-yes
+ {
+ "Assume the answer to the system package manager prompts is \cb{yes}.
+ Note that system package manager interactions may break your system
+ and you should normally only use this option on throw-away setups
+ (test virtual machines, etc)."
+ }
+
+ string --sys-sudo = "sudo"
+ {
+ "<prog>",
+
+ "The \cb{sudo} program to use for system package manager interactions
+ that normally require administrative privileges (fetch package
+ metadata, install packages, etc). If unspecified, \cb{sudo} is used
+ by default. Pass empty or the special \cb{false} value to disable the
+ use of the \cb{sudo} program. Note that the \cb{sudo} program is
+ normally only needed if the system package installation is enabled
+ with the \cb{--sys-install} option."
+ }
+
dir_paths --directory|-d
{
"<dir>",
diff --git a/bpkg/pkg-build.cxx b/bpkg/pkg-build.cxx
index aa34594..4d90df2 100644
--- a/bpkg/pkg-build.cxx
+++ b/bpkg/pkg-build.cxx
@@ -32,7 +32,9 @@
#include <bpkg/pkg-disfigure.hxx>
#include <bpkg/package-query.hxx>
#include <bpkg/package-skeleton.hxx>
+
#include <bpkg/system-repository.hxx>
+#include <bpkg/system-package-manager.hxx>
#include <bpkg/pkg-build-collect.hxx>
@@ -41,10 +43,10 @@ using namespace butl;
namespace bpkg
{
- // @@ Overall TODO:
- //
- // - Configuration vars (both passed and preserved)
+ // System package manager. Resolved lazily if and when needed. Present NULL
+ // value means no system package manager is available for this host.
//
+ static optional<unique_ptr<system_package_manager>> sys_pkg_mgr;
// Current configurations as specified with --directory|-d (or the current
// working directory if none specified).
@@ -171,6 +173,7 @@ namespace bpkg
optional<dir_path> checkout_root;
bool checkout_purge;
strings config_vars; // Only if not system.
+ const system_package_status* system_status; // See struct pkg_arg.
};
using dependency_packages = vector<dependency_package>;
@@ -536,9 +539,7 @@ namespace bpkg
else if (!dsys || !wildcard (*dvc))
c = dvc;
- vector<pair<shared_ptr<available_package>,
- lazy_shared_ptr<repository_fragment>>> afs (
- find_available (nm, c, rfs));
+ available_packages afs (find_available (nm, c, rfs));
if (afs.empty () && dsys && c)
afs = find_available (nm, nullopt, rfs);
@@ -1071,6 +1072,10 @@ namespace bpkg
<< "specified" <<
info << "run 'bpkg help pkg-build' for more information";
+ if (o.sys_no_query () && o.sys_install ())
+ fail << "both --sys-no-query and --sys-install specified" <<
+ info << "run 'bpkg help pkg-build' for more information";
+
if (!args.more () && !o.upgrade () && !o.patch ())
fail << "package name argument expected" <<
info << "run 'bpkg help pkg-build' for more information";
@@ -1537,6 +1542,12 @@ namespace bpkg
string value;
pkg_options options;
strings config_vars;
+
+ // If schema is sys then this member indicates whether the constraint
+ // came from the system package manager (not NULL) or user/fallback
+ // (NULL).
+ //
+ const system_package_status* system_status;
};
auto arg_parsed = [] (const pkg_arg& a) {return !a.name.empty ();};
@@ -1652,23 +1663,132 @@ namespace bpkg
return r;
};
- // Add the system package authoritative information to the database's
- // system repository, unless it already contains authoritative information
- // for this package.
+ // Figure out the system package version unless explicitly specified and
+ // add the system package authoritative information to the database's
+ // system repository unless the database is NULL or it already contains
+ // authoritative information for this package. Return the figured out
+ // system package version as constraint.
//
// Note that it is assumed that all the possible duplicates are handled
// elsewhere/later.
//
- auto add_system_package = [] (database& db,
- const package_name& nm,
- const version& v)
+ auto add_system_package = [&o] (database* db,
+ const package_name& nm,
+ optional<version_constraint> vc,
+ const system_package_status* sps,
+ vector<shared_ptr<available_package>>* stubs)
+ -> pair<version_constraint, const system_package_status*>
{
- assert (db.system_repository);
+ if (!vc)
+ {
+ assert (sps == nullptr);
+
+ // See if we should query the system package manager.
+ //
+ if (!sys_pkg_mgr)
+ sys_pkg_mgr = o.sys_no_query ()
+ ? nullptr
+ : make_system_package_manager (o,
+ host_triplet,
+ o.sys_install (),
+ !o.sys_no_fetch (),
+ o.sys_yes (),
+ o.sys_sudo (),
+ "" /* name */);
+
+ if (*sys_pkg_mgr != nullptr)
+ {
+ system_package_manager& spm (**sys_pkg_mgr);
+
+ // First check the cache.
+ //
+ optional<const system_package_status*> os (
+ spm.pkg_status (nm, nullptr));
- const system_package* sp (db.system_repository->find (nm));
+ available_packages aps;
+ if (!os)
+ {
+ // If no cache hit, then collect the available packages for the
+ // mapping information.
+ //
+ aps = find_available_all (current_configs, nm);
+
+ // If no source/stub for the package (and thus no mapping), issue
+ // diagnostics consistent with other such places.
+ //
+ if (aps.empty ())
+ fail << "unknown package " << nm <<
+ info << "consider specifying " << nm << "/*";
+ }
+
+ // This covers both our diagnostics below as well as anything that
+ // might be issued by pkg_status().
+ //
+ auto df = make_diag_frame (
+ [&nm] (diag_record& dr)
+ {
+ dr << info << "specify " << nm << "/* if package is not "
+ << "installed with system package manager";
+
+ dr << info << "specify --sys-no-query to disable system "
+ << "package manager interactions";
+ });
+
+ if (!os)
+ {
+ os = spm.pkg_status (nm, &aps);
+ assert (os);
+ }
- if (sp == nullptr || !sp->authoritative)
- db.system_repository->insert (nm, v, true /* authoritative */);
+ if ((sps = *os) != nullptr)
+ vc = version_constraint (sps->version);
+ else
+ {
+ diag_record dr (fail);
+
+ dr << "no installed " << (o.sys_install () ? " or available " : "")
+ << "system package for " << nm;
+
+ if (!o.sys_install ())
+ dr << info << "specify --sys-install to try to install it";
+ }
+ }
+ else
+ vc = version_constraint (wildcard_version);
+ }
+ else
+ {
+ // The system package may only have an exact/wildcard version
+ // specified.
+ //
+ assert (vc->min_version == vc->max_version);
+
+ // For system packages not associated with a specific repository
+ // location add the stub package to the imaginary system repository
+ // (see below for details).
+ //
+ if (stubs != nullptr)
+ stubs->push_back (make_shared<available_package> (nm));
+ }
+
+ if (db != nullptr)
+ {
+ assert (db->system_repository);
+
+ const system_package* sp (db->system_repository->find (nm));
+
+ // Note that we don't check for the version match here since that's
+ // handled by check_dup() lambda at a later stage, which covers both
+ // db and no-db cases consistently.
+ //
+ if (sp == nullptr || !sp->authoritative)
+ db->system_repository->insert (nm,
+ *vc->min_version,
+ true /* authoritative */,
+ sps);
+ }
+
+ return make_pair (move (*vc), sps);
};
// Create the parsed package argument. Issue diagnostics and fail if the
@@ -1680,15 +1800,23 @@ namespace bpkg
package_name nm,
optional<version_constraint> vc,
pkg_options os,
- strings vs) -> pkg_arg
+ strings vs,
+ vector<shared_ptr<available_package>>* stubs = nullptr)
+ -> pkg_arg
{
assert (!vc || !vc->empty ()); // May not be empty if present.
if (db == nullptr)
assert (sc == package_scheme::sys && os.dependency ());
- pkg_arg r {
- db, sc, move (nm), move (vc), string (), move (os), move (vs)};
+ pkg_arg r {db,
+ sc,
+ move (nm),
+ move (vc),
+ string () /* value */,
+ move (os),
+ move (vs),
+ nullptr /* system_status */};
// Verify that the package database is specified in the multi-config
// mode, unless this is a system dependency package.
@@ -1707,17 +1835,16 @@ namespace bpkg
{
case package_scheme::sys:
{
- if (!r.constraint)
- r.constraint = version_constraint (wildcard_version);
+ assert (stubs != nullptr);
- // The system package may only have an exact/wildcard version
- // specified.
- //
- assert (r.constraint->min_version == r.constraint->max_version);
-
- if (db != nullptr)
- add_system_package (*db, r.name, *r.constraint->min_version);
+ auto sp (add_system_package (db,
+ r.name,
+ move (r.constraint),
+ nullptr /* system_package_status */,
+ stubs));
+ r.constraint = move (sp.first);
+ r.system_status = sp.second;
break;
}
case package_scheme::none: break; // Nothing to do.
@@ -1739,7 +1866,8 @@ namespace bpkg
nullopt /* constraint */,
move (v),
move (os),
- move (vs)};
+ move (vs),
+ nullptr /* system_status */};
};
vector<pkg_arg> pkg_args;
@@ -1802,13 +1930,6 @@ namespace bpkg
parse_package_version_constraint (
s, sys, version_flags (sc), version_only (sc)));
- // For system packages not associated with a specific repository
- // location add the stub package to the imaginary system
- // repository (see above for details).
- //
- if (sys && vc)
- stubs.push_back (make_shared<available_package> (n));
-
pkg_options& o (ps.options);
// Disregard the (main) database for a system dependency with
@@ -1825,7 +1946,8 @@ namespace bpkg
move (n),
move (vc),
move (o),
- move (ps.config_vars)));
+ move (ps.config_vars),
+ &stubs));
}
else // Add unparsed.
pkg_args.push_back (arg_raw (ps.db,
@@ -2059,7 +2181,8 @@ namespace bpkg
move (n),
move (vc),
ps.options,
- ps.config_vars));
+ ps.config_vars,
+ &stubs));
}
}
}
@@ -2132,7 +2255,7 @@ namespace bpkg
!compare_options (a.options, pa.options) ||
a.config_vars != pa.config_vars))
fail << "duplicate package " << pa.name <<
- info << "first mentioned as " << arg_string (r.first->second) <<
+ info << "first mentioned as " << arg_string (a) <<
info << "second mentioned as " << arg_string (pa);
return !r.second;
@@ -2503,7 +2626,8 @@ namespace bpkg
? move (pa.options.checkout_root ())
: optional<dir_path> ()),
pa.options.checkout_purge (),
- move (pa.config_vars)});
+ move (pa.config_vars),
+ pa.system_status});
continue;
}
@@ -2563,7 +2687,7 @@ namespace bpkg
//
if (pa.constraint)
{
- for (;;)
+ for (;;) // Breakout loop.
{
if (ap != nullptr) // Must be that version, see above.
break;
@@ -3180,12 +3304,11 @@ namespace bpkg
// The system package may only have an exact/wildcard version
// specified.
//
- add_system_package (db,
+ add_system_package (&db,
p.name,
- (p.constraint
- ? *p.constraint->min_version
- : wildcard_version));
-
+ p.constraint,
+ p.system_status,
+ nullptr /* stubs */);
enter (db, p);
};
@@ -4278,10 +4401,11 @@ namespace bpkg
bool update_dependents (false);
// We need the plan and to ask for the user's confirmation only if some
- // implicit action (such as building prerequisite or reconfiguring
- // dependent package) is to be taken or there is a selected package which
- // version must be changed. But if the user explicitly requested it with
- // --plan, then we print it as long as it is not empty.
+ // implicit action (such as building prerequisite, reconfiguring dependent
+ // package, or installing system/distribution packages) is to be taken or
+ // there is a selected package which version must be changed. But if the
+ // user explicitly requested it with --plan, then we print it as long as
+ // it is not empty.
//
string plan;
sha256 csum;
@@ -4292,6 +4416,31 @@ namespace bpkg
o.plan_specified () ||
o.rebuild_checksum_specified ())
{
+ // Map the main system/distribution packages that need to be installed
+ // to the system packages which caused their installation (see
+ // build_package::system_install() for details).
+ //
+ using package_names = vector<reference_wrapper<const package_name>>;
+ using system_map = map<string, package_names>;
+
+ system_map sys_map;
+
+ // Iterate in the reverse order as we will do for printing the action
+ // lines. This way a system-install action line will be printed right
+ // before the bpkg action line of a package which appears first in the
+ // system-install action's 'required by' list.
+ //
+ for (const build_package& p: reverse_iterate (pkgs))
+ {
+ if (const system_package_status* s = p.system_install ())
+ {
+ package_names& ps (sys_map[s->system_name]);
+
+ if (find (ps.begin (), ps.end (), p.name ()) == ps.end ())
+ ps.push_back (p.name ());
+ }
+ }
+
// Start the transaction since we may query available packages for
// skeleton initializations.
//
@@ -4299,200 +4448,253 @@ namespace bpkg
bool first (true); // First entry in the plan.
- for (build_package& p: reverse_iterate (pkgs))
+ // Print the bpkg package action lines.
+ //
+ // Also print the system-install action lines for system/distribution
+ // packages which require installation by the system package manager.
+ // Print them before the respective system package action lines, but
+ // only once per (main) system/distribution package. For example:
+ //
+ // system-install libssl1.1/1.1.1l (required by sys:libssl, sys:libcrypto)
+ // configure sys:libssl/1.1.1 (required by foo)
+ // configure sys:libcrypto/1.1.1 (required by bar)
+ //
+ for (auto i (pkgs.rbegin ()); i != pkgs.rend (); )
{
+ build_package& p (*i);
assert (p.action);
- database& pdb (p.db);
- const shared_ptr<selected_package>& sp (p.selected);
-
string act;
- if (*p.action == build_package::drop)
+ const system_package_status* s;
+ system_map::iterator j;
+
+ if ((s = p.system_install ()) != nullptr &&
+ (j = sys_map.find (s->system_name)) != sys_map.end ())
{
- act = "drop " + sp->string (pdb) + " (unused)";
+ act = "system-install ";
+ act += s->system_name;
+ act += '/';
+ act += s->system_version;
+ act += " (required by ";
+
+ bool first (true);
+ for (const package_name& n: j->second)
+ {
+ if (first)
+ first = false;
+ else
+ act += ", ";
+
+ act += "sys:";
+ act += n.string ();
+ }
+
+ act += ')';
+
need_prompt = true;
+
+ // Make sure that we print this system-install action just once.
+ //
+ sys_map.erase (j);
+
+ // Note that we don't increment i in order to re-iterate this pkgs
+ // entry.
}
else
{
- // Print configuration variables.
- //
- // The idea here is to only print configuration for those packages
- // for which we call pkg_configure*() in execute_plan().
- //
- package_skeleton* cfg (nullptr);
+ ++i;
- string cause;
- if (*p.action == build_package::adjust)
- {
- assert (sp != nullptr && (p.reconfigure () || p.unhold ()));
+ database& pdb (p.db);
+ const shared_ptr<selected_package>& sp (p.selected);
- // This is a dependent needing reconfiguration.
+ if (*p.action == build_package::drop)
+ {
+ act = "drop " + sp->string (pdb) + " (unused)";
+ need_prompt = true;
+ }
+ else
+ {
+ // Print configuration variables.
//
- // This is an implicit reconfiguration which requires the plan to
- // be printed. Will flag that later when composing the list of
- // prerequisites.
+ // The idea here is to only print configuration for those packages
+ // for which we call pkg_configure*() in execute_plan().
//
- if (p.reconfigure ())
- {
- act = "reconfigure";
- cause = "dependent of";
+ package_skeleton* cfg (nullptr);
- if (!o.configure_only ())
- update_dependents = true;
- }
-
- // This is a held package needing unhold.
- //
- if (p.unhold ())
+ string cause;
+ if (*p.action == build_package::adjust)
{
- if (act.empty ())
- act = "unhold";
- else
- act += "/unhold";
- }
-
- act += ' ' + sp->name.string ();
+ assert (sp != nullptr && (p.reconfigure () || p.unhold ()));
- const string& s (pdb.string);
- if (!s.empty ())
- act += ' ' + s;
+ // This is a dependent needing reconfiguration.
+ //
+ // This is an implicit reconfiguration which requires the plan
+ // to be printed. Will flag that later when composing the list
+ // of prerequisites.
+ //
+ if (p.reconfigure ())
+ {
+ act = "reconfigure";
+ cause = "dependent of";
- // This is an adjustment and so there is no available package
- // specified for the build package object and thus the skeleton
- // cannot be present.
- //
- assert (p.available == nullptr && !p.skeleton);
+ if (!o.configure_only ())
+ update_dependents = true;
+ }
- // We shouldn't be printing configurations for plain unholds.
- //
- if (p.reconfigure ())
- {
- // Since there is no available package specified we need to find
- // it (or create a transient one).
+ // This is a held package needing unhold.
//
- cfg = &p.init_skeleton (o, find_available (o, pdb, sp));
- }
- }
- else
- {
- assert (p.available != nullptr); // This is a package build.
+ if (p.unhold ())
+ {
+ if (act.empty ())
+ act = "unhold";
+ else
+ act += "/unhold";
+ }
- // Even if we already have this package selected, we have to
- // make sure it is configured and updated.
- //
- if (sp == nullptr)
- {
- act = p.system ? "configure" : "new";
+ act += ' ' + sp->name.string ();
- // For a new non-system package the skeleton must already be
- // initialized.
+ const string& s (pdb.string);
+ if (!s.empty ())
+ act += ' ' + s;
+
+ // This is an adjustment and so there is no available package
+ // specified for the build package object and thus the skeleton
+ // cannot be present.
//
- assert (p.system || p.skeleton.has_value ());
+ assert (p.available == nullptr && !p.skeleton);
- // Initialize the skeleton if it is not initialized yet.
+ // We shouldn't be printing configurations for plain unholds.
//
- cfg = &(p.skeleton ? *p.skeleton : p.init_skeleton (o));
+ if (p.reconfigure ())
+ {
+ // Since there is no available package specified we need to
+ // find it (or create a transient one).
+ //
+ cfg = &p.init_skeleton (o, find_available (o, pdb, sp));
+ }
}
- else if (sp->version == p.available_version ())
+ else
{
- // If this package is already configured and is not part of the
- // user selection (or we are only configuring), then there is
- // nothing we will be explicitly doing with it (it might still
- // get updated indirectly as part of the user selection update).
+ assert (p.available != nullptr); // This is a package build.
+
+ // Even if we already have this package selected, we have to
+ // make sure it is configured and updated.
//
- if (!p.reconfigure () &&
- sp->state == package_state::configured &&
- (!p.user_selection () ||
- o.configure_only () ||
- p.configure_only ()))
- continue;
+ if (sp == nullptr)
+ {
+ act = p.system ? "configure" : "new";
- act = p.system
- ? "reconfigure"
- : (p.reconfigure ()
- ? (o.configure_only () || p.configure_only ()
- ? "reconfigure"
- : "reconfigure/update")
- : "update");
+ // For a new non-system package the skeleton must already be
+ // initialized.
+ //
+ assert (p.system || p.skeleton.has_value ());
- if (p.reconfigure ())
- {
// Initialize the skeleton if it is not initialized yet.
//
cfg = &(p.skeleton ? *p.skeleton : p.init_skeleton (o));
}
- }
- else
- {
- act = p.system
- ? "reconfigure"
- : sp->version < p.available_version ()
- ? "upgrade"
- : "downgrade";
-
- // For a non-system package up/downgrade the skeleton must
- // already be initialized.
- //
- assert (p.system || p.skeleton.has_value ());
+ else if (sp->version == p.available_version ())
+ {
+ // If this package is already configured and is not part of
+ // the user selection (or we are only configuring), then there
+ // is nothing we will be explicitly doing with it (it might
+ // still get updated indirectly as part of the user selection
+ // update).
+ //
+ if (!p.reconfigure () &&
+ sp->state == package_state::configured &&
+ (!p.user_selection () ||
+ o.configure_only () ||
+ p.configure_only ()))
+ continue;
- // Initialize the skeleton if it is not initialized yet.
- //
- cfg = &(p.skeleton ? *p.skeleton : p.init_skeleton (o));
+ act = p.system
+ ? "reconfigure"
+ : (p.reconfigure ()
+ ? (o.configure_only () || p.configure_only ()
+ ? "reconfigure"
+ : "reconfigure/update")
+ : "update");
- need_prompt = true;
- }
+ if (p.reconfigure ())
+ {
+ // Initialize the skeleton if it is not initialized yet.
+ //
+ cfg = &(p.skeleton ? *p.skeleton : p.init_skeleton (o));
+ }
+ }
+ else
+ {
+ act += p.system
+ ? "reconfigure"
+ : sp->version < p.available_version ()
+ ? "upgrade"
+ : "downgrade";
+
+ // For a non-system package up/downgrade the skeleton must
+ // already be initialized.
+ //
+ assert (p.system || p.skeleton.has_value ());
- if (p.unhold ())
- act += "/unhold";
+ // Initialize the skeleton if it is not initialized yet.
+ //
+ cfg = &(p.skeleton ? *p.skeleton : p.init_skeleton (o));
- act += ' ' + p.available_name_version_db ();
- cause = p.required_by_dependents ? "required by" : "dependent of";
+ need_prompt = true;
+ }
- if (p.configure_only ())
- update_dependents = true;
- }
+ if (p.unhold ())
+ act += "/unhold";
- // Also list dependents for the newly built user-selected
- // dependencies.
- //
- bool us (p.user_selection ());
- string rb;
- if (!us || (!p.user_selection (hold_pkgs) && sp == nullptr))
- {
- // Note: if we are ever tempted to truncate this, watch out for
- // the --rebuild-checksum functionality which uses this. But then
- // it's not clear this information is actually important: can a
- // dependent-dependency structure change without any of the
- // package versions changing? Doesn't feel like it should.
+ act += ' ' + p.available_name_version_db ();
+ cause = p.required_by_dependents ? "required by" : "dependent of";
+
+ if (p.configure_only ())
+ update_dependents = true;
+ }
+
+ // Also list dependents for the newly built user-selected
+ // dependencies.
//
- for (const package_key& pk: p.required_by)
+ bool us (p.user_selection ());
+ string rb;
+ if (!us || (!p.user_selection (hold_pkgs) && sp == nullptr))
{
- // Skip the command-line dependent.
+ // Note: if we are ever tempted to truncate this, watch out for
+ // the --rebuild-checksum functionality which uses this. But
+ // then it's not clear this information is actually important:
+ // can a dependent-dependency structure change without any of
+ // the package versions changing? Doesn't feel like it should.
//
- if (!pk.name.empty ())
- rb += (rb.empty () ? " " : ", ") + pk.string ();
+ for (const package_key& pk: p.required_by)
+ {
+ // Skip the command-line dependent.
+ //
+ if (!pk.name.empty ())
+ rb += (rb.empty () ? " " : ", ") + pk.string ();
+ }
+
+ // If not user-selected, then there should be another (implicit)
+ // reason for the action.
+ //
+ assert (!rb.empty ());
}
- // If not user-selected, then there should be another (implicit)
- // reason for the action.
- //
- assert (!rb.empty ());
- }
+ if (!rb.empty ())
+ act += " (" + cause + rb + ')';
- if (!rb.empty ())
- act += " (" + cause + rb + ')';
+ if (cfg != nullptr && !cfg->empty_print ())
+ {
+ ostringstream os;
+ cfg->print_config (os, o.print_only () ? " " : " ");
+ act += '\n';
+ act += os.str ();
+ }
- if (cfg != nullptr && !cfg->empty_print ())
- {
- ostringstream os;
- cfg->print_config (os, o.print_only () ? " " : " ");
- act += '\n';
- act += os.str ();
+ if (!us)
+ need_prompt = true;
}
-
- if (!us)
- need_prompt = true;
}
if (first)
@@ -4554,13 +4756,14 @@ namespace bpkg
// 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 [right to left]
- // 3.a fetch/unpack new, up/down-graded
- // 3.b checkout new, up/down-graded
- // 4. configure all
- // 5. unhold unheld
- // 6. build user selection [right to left]
+ // 1. system-install not installed system/distribution
+ // 2. disfigure up/down-graded, reconfigured [left to right]
+ // 3. purge up/down-graded [right to left]
+ // 4.a fetch/unpack new, up/down-graded
+ // 4.b checkout new, up/down-graded
+ // 5. configure all
+ // 6. unhold unheld
+ // 7. 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
@@ -4668,6 +4871,40 @@ namespace bpkg
size_t prog_i, prog_n, prog_percent;
+ // system-install
+ //
+ // Install the system/distribution packages required by the respective
+ // system packages (see build_package::system_install() for details).
+ //
+ if (!simulate && o.sys_install ())
+ {
+ // Collect the names of all the system packages being managed by the
+ // system package manager (as opposed to user/fallback), suppressing
+ // duplicates.
+ //
+ vector<package_name> ps;
+
+ for (build_package& p: build_pkgs)
+ {
+ if (p.system_status () &&
+ find (ps.begin (), ps.end (), p.name ()) == ps.end ())
+ {
+ ps.push_back (p.name ());
+ }
+ }
+
+ // Install the system/distribution packages.
+ //
+ if (!ps.empty ())
+ {
+ // Otherwise, we wouldn't get any package statuses.
+ //
+ assert (sys_pkg_mgr && *sys_pkg_mgr != nullptr);
+
+ (*sys_pkg_mgr)->pkg_install (ps);
+ }
+ }
+
// disfigure
//
// Note: similar code in pkg-drop.
diff --git a/bpkg/system-package-manager-debian.cxx b/bpkg/system-package-manager-debian.cxx
new file mode 100644
index 0000000..06b5060
--- /dev/null
+++ b/bpkg/system-package-manager-debian.cxx
@@ -0,0 +1,1411 @@
+// file : bpkg/system-package-manager-debian.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <bpkg/system-package-manager-debian.hxx>
+
+#include <bpkg/diagnostics.hxx>
+
+using namespace butl;
+
+namespace bpkg
+{
+ using package_status = system_package_status_debian;
+
+ // Parse the debian-name (or alike) value.
+ //
+ // Note that for now we treat all the packages from the non-main groups as
+ // extras omitting the -common package (assuming it's pulled by the main
+ // package) as well as -doc and -dbg unless requested with the
+ // extra_{doc,dbg} arguments.
+ //
+ package_status system_package_manager_debian::
+ parse_name_value (const package_name& pn,
+ const string& nv,
+ bool extra_doc,
+ bool extra_dbg)
+ {
+ auto split = [] (const string& s, char d) -> strings
+ {
+ strings r;
+ for (size_t b (0), e (0); next_word (s, b, e, d); )
+ r.push_back (string (s, b, e - b));
+ return r;
+ };
+
+ auto suffix = [] (const string& n, const string& s) -> bool
+ {
+ size_t nn (n.size ());
+ size_t sn (s.size ());
+ return nn > sn && n.compare (nn - sn, sn, s) == 0;
+ };
+
+ auto parse_group = [&split, &suffix] (const string& g,
+ const package_name* pn)
+ {
+ strings ns (split (g, ' '));
+
+ if (ns.empty ())
+ fail << "empty package group";
+
+ package_status r;
+
+ // Handle the "dev instead of main" special case for libraries.
+ //
+ // Note: the lib prefix check is based on the bpkg package name.
+ //
+ // Check that the following name does not end with -dev. This will be
+ // the only way to disambiguate the case where the library name happens
+ // to end with -dev (e.g., libfoo-dev libfoo-dev-dev).
+ //
+ {
+ string& m (ns[0]);
+
+ if (pn != nullptr &&
+ pn->string ().compare (0, 3, "lib") == 0 &&
+ suffix (m, "-dev") &&
+ !(ns.size () > 1 && suffix (ns[1], "-dev")))
+ {
+ r = package_status ("", move (m));
+ }
+ else
+ r = package_status (move (m));
+ }
+
+ // Handle the rest.
+ //
+ for (size_t i (1); i != ns.size (); ++i)
+ {
+ string& n (ns[i]);
+
+ const char* w;
+ if (string* v = (suffix (n, (w = "-dev")) ? &r.dev :
+ suffix (n, (w = "-doc")) ? &r.doc :
+ suffix (n, (w = "-dbg")) ? &r.dbg :
+ suffix (n, (w = "-common")) ? &r.common : nullptr))
+ {
+ if (!v->empty ())
+ fail << "multiple " << w << " package names in '" << g << "'" <<
+ info << "did you forget to separate package groups with comma?";
+
+ *v = move (n);
+ }
+ else
+ r.extras.push_back (move (n));
+ }
+
+ return r;
+ };
+
+ strings gs (split (nv, ','));
+ assert (!gs.empty ()); // *-name value cannot be empty.
+
+ package_status r;
+ for (size_t i (0); i != gs.size (); ++i)
+ {
+ if (i == 0) // Main group.
+ r = parse_group (gs[i], &pn);
+ else
+ {
+ package_status g (parse_group (gs[i], nullptr));
+
+ if (!g.main.empty ()) r.extras.push_back (move (g.main));
+ if (!g.dev.empty ()) r.extras.push_back (move (g.dev));
+ if (!g.doc.empty () && extra_doc) r.extras.push_back (move (g.doc));
+ if (!g.dbg.empty () && extra_dbg) r.extras.push_back (move (g.dbg));
+ if (!g.common.empty () && false) r.extras.push_back (move (g.common));
+ if (!g.extras.empty ()) r.extras.insert (
+ r.extras.end (),
+ make_move_iterator (g.extras.begin ()),
+ make_move_iterator (g.extras.end ()));
+ }
+ }
+
+ return r;
+ }
+
+ // Attempt to determine the main package name from its -dev package based on
+ // the extracted Depends value. Return empty string if unable to.
+ //
+ string system_package_manager_debian::
+ main_from_dev (const string& dev_name,
+ const string& dev_ver,
+ const string& depends)
+ {
+ // The format of the Depends value is a comma-seperated list of dependency
+ // expressions. For example:
+ //
+ // Depends: libssl3 (= 3.0.7-1), libc6 (>= 2.34), libfoo | libbar
+ //
+ // For the main package we look for a dependency in the form:
+ //
+ // <dev-stem>* (= <dev-ver>)
+ //
+ // Usually it is the first one.
+ //
+ string dev_stem (dev_name, 0, dev_name.rfind ("-dev"));
+
+ string r;
+ for (size_t b (0), e (0); next_word (depends, b, e, ','); )
+ {
+ string d (depends, b, e - b);
+ trim (d);
+
+ size_t p (d.find (' '));
+ if (p != string::npos)
+ {
+ if (d.compare (0, dev_stem.size (), dev_stem) == 0) // <dev-stem>*
+ {
+ size_t q (d.find ('(', p + 1));
+ if (q != string::npos && d.back () == ')') // (...)
+ {
+ if (d[q + 1] == '=' && d[q + 2] == ' ') // Equal.
+ {
+ string v (d, q + 3, d.size () - q - 3 - 1);
+ trim (v);
+
+ if (v == dev_ver)
+ {
+ r.assign (d, 0, p);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return r;
+ }
+
+ // Do we use apt or apt-get? From apt(8):
+ //
+ // "The apt(8) commandline is designed as an end-user tool and it may change
+ // behavior between versions. [...]
+ //
+ // All features of apt(8) are available in dedicated APT tools like
+ // apt-get(8) and apt-cache(8) as well. [...] So you should prefer using
+ // these commands (potentially with some additional options enabled) in
+ // your scripts as they keep backward compatibility as much as possible."
+ //
+ // Note also that for some reason both apt-cache and apt-get exit with 100
+ // code on error.
+ //
+ static process_path apt_cache_path;
+ static process_path apt_get_path;
+ static process_path sudo_path;
+
+ // Obtain the installed and candidate versions for the specified list of
+ // Debian packages by executing `apt-cache policy`.
+ //
+ // If the n argument is not 0, then only query the first n packages.
+ //
+ void system_package_manager_debian::
+ apt_cache_policy (vector<package_policy>& pps, size_t n)
+ {
+ if (n == 0)
+ n = pps.size ();
+
+ assert (n != 0 && n <= pps.size ());
+
+ // The --quiet option makes sure we don't get a noice (N) printed to
+ // stderr if the package is unknown. It does not appear to affect error
+ // diagnostics (try temporarily renaming /var/lib/dpkg/status).
+ //
+ cstrings args {"apt-cache", "policy", "--quiet"};
+
+ for (size_t i (0); i != n; ++i)
+ {
+ package_policy& pp (pps[i]);
+
+ const string& n (pp.name);
+ assert (!n.empty ());
+
+ pp.installed_version.clear ();
+ pp.candidate_version.clear ();
+
+ args.push_back (n.c_str ());
+ }
+
+ args.push_back (nullptr);
+
+ // Run with the C locale to make sure there is no localization. Note that
+ // this is not without potential drawbacks, see Debian bug #643787. But
+ // for now it seems to work and feels like the least of two potential
+ // evils.
+ //
+ const char* evars[] = {"LC_ALL=C", nullptr};
+
+ try
+ {
+ if (apt_cache_path.empty () && !simulate_)
+ apt_cache_path = process::path_search (args[0]);
+
+ process_env pe (apt_cache_path, evars);
+
+ if (verb >= 3)
+ print_process (pe, args);
+
+ // Redirect stdout to a pipe. For good measure also redirect stdin to
+ // /dev/null to make sure there are no prompts of any kind.
+ //
+ process pr;
+ if (!simulate_)
+ pr = process (apt_cache_path,
+ args,
+ -2 /* stdin */,
+ -1 /* stdout */,
+ 2 /* stderr */,
+ nullptr /* cwd */,
+ evars);
+ else
+ {
+ strings k;
+ for (size_t i (0); i != n; ++i)
+ k.push_back (pps[i].name);
+
+ const path* f (nullptr);
+ if (installed_)
+ {
+ auto i (simulate_->apt_cache_policy_installed_.find (k));
+ if (i != simulate_->apt_cache_policy_installed_.end ())
+ f = &i->second;
+ }
+ if (f == nullptr && fetched_)
+ {
+ auto i (simulate_->apt_cache_policy_fetched_.find (k));
+ if (i != simulate_->apt_cache_policy_fetched_.end ())
+ f = &i->second;
+ }
+ if (f == nullptr)
+ {
+ auto i (simulate_->apt_cache_policy_.find (k));
+ if (i != simulate_->apt_cache_policy_.end ())
+ f = &i->second;
+ }
+
+ diag_record dr (text);
+ print_process (dr, pe, args);
+ dr << " <" << (f == nullptr || f->empty () ? "/dev/null" : f->string ());
+
+ pr = process (process_exit (0));
+ pr.in_ofd = f == nullptr || f->empty ()
+ ? fdopen_null ()
+ : (f->string () == "-"
+ ? fddup (stdin_fd ())
+ : fdopen (*f, fdopen_mode::in));
+ }
+
+ try
+ {
+ ifdstream is (move (pr.in_ofd), fdstream_mode::skip, ifdstream::badbit);
+
+ // The output of `apt-cache policy <pkg1> <pkg2> ...` are blocks of
+ // lines in the following form:
+ //
+ // <pkg1>:
+ // Installed: 1.2.3-1
+ // Candidate: 1.3.0-2
+ // Version table:
+ // <...>
+ // <pkg2>:
+ // Installed: (none)
+ // Candidate: 1.3.0+dfsg-2+b1
+ // Version table:
+ // <...>
+ //
+ // Where <...> are further lines indented with at least one space. If
+ // a package is unknown, then the entire block (including the first
+ // <pkg>: line) is omitted. The blocks appear in the same order as
+ // packages on the command line and multiple entries for the same
+ // package result in multiple corresponding blocks. It looks like
+ // there should be not blank lines but who really knows.
+ //
+ // Note also that if Installed version is not (none), then the
+ // Candidate version will be that version of better.
+ //
+ {
+ auto df = make_diag_frame (
+ [&pe, &args] (diag_record& dr)
+ {
+ dr << info << "while parsing output of ";
+ print_process (dr, pe, args);
+ });
+
+ size_t i (0);
+
+ string l;
+ for (getline (is, l); !eof (is); )
+ {
+ // Parse the first line of the block.
+ //
+ if (l.empty () || l.front () == ' ' || l.back () != ':')
+ fail << "expected package name instead of '" << l << "'";
+
+ l.pop_back ();
+
+ // Skip until this package.
+ //
+ for (; i != n && pps[i].name != l; ++i) ;
+
+ if (i == n)
+ fail << "unexpected package name '" << l << "'";
+
+ package_policy& pp (pps[i]);
+
+ auto parse_version = [&l] (const string& n) -> string
+ {
+ size_t s (n.size ());
+
+ if (l[0] == ' ' &&
+ l[1] == ' ' &&
+ l.compare (2, s, n) == 0 &&
+ l[2 + s] == ':')
+ {
+ string v (l, 2 + s + 1);
+ trim (v);
+
+ if (!v.empty ())
+ return v == "(none)" ? string () : move (v);
+ }
+
+ fail << "invalid " << n << " version line '" << l << "'" << endf;
+ };
+
+ // Get the installed version line.
+ //
+ if (eof (getline (is, l)))
+ fail << "expected Installed version line after package name";
+
+ pp.installed_version = parse_version ("Installed");
+
+ // Get the candidate version line.
+ //
+ if (eof (getline (is, l)))
+ fail << "expected Candidate version line after Installed version";
+
+ pp.candidate_version = parse_version ("Candidate");
+
+ // Candidate should fallback to Installed.
+ //
+ assert (pp.installed_version.empty () ||
+ !pp.candidate_version.empty ());
+
+ // Skip the rest of the indented lines (or blanks, just in case).
+ //
+ while (!eof (getline (is, l)) && (l.empty () || l.front () == ' ')) ;
+ }
+ }
+
+ is.close ();
+ }
+ catch (const io_error& e)
+ {
+ if (pr.wait ())
+ fail << "unable to read " << args[0] << " policy output: " << e;
+
+ // Fall through.
+ }
+
+ if (!pr.wait ())
+ {
+ diag_record dr (fail);
+ dr << args[0] << " policy exited with non-zero code";
+
+ if (verb < 3)
+ {
+ dr << info << "command line: ";
+ print_process (dr, pe, args);
+ }
+ }
+ }
+ catch (const process_error& e)
+ {
+ error << "unable to execute " << args[0] << ": " << e;
+
+ if (e.child)
+ exit (1);
+
+ throw failed ();
+ }
+ }
+
+ // Execute `apt-cache show` and return the Depends value, if any, for the
+ // specified package and version. Fail if either package or version is
+ // unknown.
+ //
+ string system_package_manager_debian::
+ apt_cache_show (const string& name, const string& ver)
+ {
+ assert (!name.empty () && !ver.empty ());
+
+ string spec (name + '=' + ver);
+
+ // In particular, --quiet makes sure we don't get noices (N) printed to
+ // stderr. It does not appear to affect error diagnostics (try showing
+ // information for an unknown package).
+ //
+ const char* args[] = {
+ "apt-cache", "show", "--quiet", spec.c_str (), nullptr};
+
+ // Note that for this command there seems to be no need to run with the C
+ // locale since the output is presumably not localizable. But let's do it
+ // for good measure and also seeing that we try to backfit some
+ // diagnostics into apt-cache (see no_version below).
+ //
+ const char* evars[] = {"LC_ALL=C", nullptr};
+
+ string r;
+ try
+ {
+ if (apt_cache_path.empty () && !simulate_)
+ apt_cache_path = process::path_search (args[0]);
+
+ process_env pe (apt_cache_path, evars);
+
+ if (verb >= 3)
+ print_process (pe, args);
+
+ // Redirect stdout to a pipe. For good measure also redirect stdin to
+ // /dev/null to make sure there are no prompts of any kind.
+ //
+ process pr;
+ if (!simulate_)
+ pr = process (apt_cache_path,
+ args,
+ -2 /* stdin */,
+ -1 /* stdout */,
+ 2 /* stderr */,
+ nullptr /* cwd */,
+ evars);
+ else
+ {
+ pair<string, string> k (name, ver);
+
+ const path* f (nullptr);
+ if (fetched_)
+ {
+ auto i (simulate_->apt_cache_show_fetched_.find (k));
+ if (i != simulate_->apt_cache_show_fetched_.end ())
+ f = &i->second;
+ }
+ if (f == nullptr)
+ {
+ auto i (simulate_->apt_cache_show_.find (k));
+ if (i != simulate_->apt_cache_show_.end ())
+ f = &i->second;
+ }
+
+ diag_record dr (text);
+ print_process (dr, pe, args);
+ dr << " <" << (f == nullptr || f->empty () ? "/dev/null" : f->string ());
+
+ if (f == nullptr || f->empty ())
+ {
+ text << "E: No packages found";
+ pr = process (process_exit (100));
+ }
+ else
+ {
+ pr = process (process_exit (0));
+ pr.in_ofd = f->string () == "-"
+ ? fddup (stdin_fd ())
+ : fdopen (*f, fdopen_mode::in);
+ }
+ }
+
+ bool no_version (false);
+ try
+ {
+ ifdstream is (move (pr.in_ofd), fdstream_mode::skip, ifdstream::badbit);
+
+ // The output of `apt-cache show <pkg>=<ver>` appears to be a single
+ // Debian control file in the RFC 822 encoding followed by a blank
+ // line. See deb822(5) for details. Here is a representative example:
+ //
+ // Package: libcurl4
+ // Version: 7.85.0-1
+ // Depends: libbrotli1 (>= 0.6.0), libc6 (>= 2.34), ...
+ // Description-en: easy-to-use client-side URL transfer library
+ // libcurl is an easy-to-use client-side URL transfer library.
+ //
+ // Note that if the package is unknown, then we get an error but if
+ // the version is unknown, we get no output (and a note if running
+ // without --quiet).
+ //
+ string l;
+ if (eof (getline (is, l)))
+ {
+ // The unknown version case. Issue diagnostics consistent with the
+ // unknown package case, at least for the English locale.
+ //
+ text << "E: No package version found";
+ no_version = true;
+ }
+ else
+ {
+ auto df = make_diag_frame (
+ [&pe, &args] (diag_record& dr)
+ {
+ dr << info << "while parsing output of ";
+ print_process (dr, pe, args);
+ });
+
+ do
+ {
+ // This line should be the start of a field unless it's a comment
+ // or the terminating blank line. According to deb822(5), there
+ // can be no leading whitespaces before `#`.
+ //
+ if (l.empty ())
+ break;
+
+ if (l[0] == '#')
+ {
+ getline (is, l);
+ continue;
+ }
+
+ size_t p (l.find (':'));
+
+ if (p == string::npos)
+ fail << "expected field name instead of '" << l << "'";
+
+ // Extract the field name. Note that field names are case-
+ // insensitive.
+ //
+ string n (l, 0, p);
+ trim (n);
+
+ // Extract the field value.
+ //
+ string v (l, p + 1);
+ trim (v);
+
+ // If we have more lines see if the following line is part of this
+ // value.
+ //
+ while (!eof (getline (is, l)) && (l[0] == ' ' || l[0] == '\t'))
+ {
+ // This can either be a "folded" or a "multiline" field and
+ // which one it is depends on the field semantics. Here we only
+ // care about Depends and so treat them all as folded (it's
+ // unclear whether Depends must be a simple field).
+ //
+ trim (l);
+ v += ' ';
+ v += l;
+ }
+
+ // See if this is a field of interest.
+ //
+ if (icasecmp (n, "Package") == 0)
+ {
+ assert (v == name); // Sanity check.
+ }
+ else if (icasecmp (n, "Version") == 0)
+ {
+ assert (v == ver); // Sanity check.
+ }
+ else if (icasecmp (n, "Depends") == 0)
+ {
+ r = move (v);
+
+ // Let's not waste time reading any further.
+ //
+ break;
+ }
+ }
+ while (!eof (is));
+ }
+
+ is.close ();
+ }
+ catch (const io_error& e)
+ {
+ if (pr.wait ())
+ fail << "unable to read " << args[0] << " show output: " << e;
+
+ // Fall through.
+ }
+
+ if (!pr.wait () || no_version)
+ {
+ diag_record dr (fail);
+ dr << args[0] << " show exited with non-zero code";
+
+ if (verb < 3)
+ {
+ dr << info << "command line: ";
+ print_process (dr, pe, args);
+ }
+ }
+ }
+ catch (const process_error& e)
+ {
+ error << "unable to execute " << args[0] << ": " << e;
+
+ if (e.child)
+ exit (1);
+
+ throw failed ();
+ }
+
+ return r;
+ }
+
+ // Prepare the common `apt-get <command>` options.
+ //
+ pair<cstrings, const process_path&> system_package_manager_debian::
+ apt_get_common (const char* command)
+ {
+ cstrings args;
+
+ if (!sudo_.empty ())
+ args.push_back (sudo_.c_str ());
+
+ args.push_back ("apt-get");
+ args.push_back (command);
+
+ // Map our verbosity/progress to apt-get --quiet[=<level>]. The levels
+ // appear to have the following behavior:
+ //
+ // 1 -- shows URL being downloaded but no percentage progress is shown.
+ //
+ // 2 -- only shows diagnostics (implies --assume-yes which cannot be
+ // overriden with --assume-no).
+ //
+ // It also appears to automatically use level 1 if stderr is not a
+ // terminal. This can be overrident with --quiet=0.
+ //
+ // Note also that --show-progress does not apply to apt-get update. For
+ // apt-get install it shows additionally progress during unpacking which
+ // looks quite odd.
+ //
+ if (progress_ && *progress_)
+ {
+ args.push_back ("--quiet=0");
+ }
+ else if (verb == 0)
+ {
+ // Only use level 2 if assuming yes.
+ //
+ args.push_back (yes_ ? "--quiet=2" : "--quiet");
+ }
+ else if (progress_ && !*progress_)
+ {
+ args.push_back ("--quiet");
+ }
+
+ if (yes_)
+ {
+ args.push_back ("--assume-yes");
+ }
+ else if (!stderr_term)
+ {
+ // Suppress any prompts if stderr is not a terminal for good measure.
+ //
+ args.push_back ("--assume-no");
+ }
+
+ try
+ {
+ const process_path* pp (nullptr);
+
+ if (!sudo_.empty ())
+ {
+ if (sudo_path.empty () && !simulate_)
+ sudo_path = process::path_search (args[0]);
+
+ pp = &sudo_path;
+ }
+ else
+ {
+ if (apt_get_path.empty () && !simulate_)
+ apt_get_path = process::path_search (args[0]);
+
+ pp = &apt_get_path;
+ }
+
+ return pair<cstrings, const process_path&> (move (args), *pp);
+ }
+ catch (const process_error& e)
+ {
+ error << "unable to execute " << args[0] << ": " << e;
+
+ if (e.child)
+ exit (1);
+
+ throw failed ();
+ }
+ }
+
+ // Execute `apt-get update` to update the package index.
+ //
+ void system_package_manager_debian::
+ apt_get_update ()
+ {
+ pair<cstrings, const process_path&> args_pp (apt_get_common ("update"));
+
+ cstrings& args (args_pp.first);
+ const process_path& pp (args_pp.second);
+
+ args.push_back (nullptr);
+
+ try
+ {
+ if (verb >= 2)
+ print_process (args);
+ else if (verb == 1)
+ text << "updating " << os_release_.name_id << " package index...";
+
+ process pr;
+ if (!simulate_)
+ pr = process (pp, args);
+ else
+ {
+ print_process (args);
+ pr = process (process_exit (simulate_->apt_get_update_fail_ ? 100 : 0));
+ }
+
+ if (!pr.wait ())
+ {
+ diag_record dr (fail);
+ dr << "apt-get update exited with non-zero code";
+
+ if (verb < 2)
+ {
+ dr << info << "command line: ";
+ print_process (dr, args);
+ }
+ }
+
+ if (verb == 1)
+ text << "updated " << os_release_.name_id << " package index";
+ }
+ catch (const process_error& e)
+ {
+ error << "unable to execute " << args[0] << ": " << e;
+
+ if (e.child)
+ exit (1);
+
+ throw failed ();
+ }
+ }
+
+ // Execute `apt-get install` to install the specified packages/versions
+ // (e.g., libfoo or libfoo=1.2.3).
+ //
+ void system_package_manager_debian::
+ apt_get_install (const strings& pkgs)
+ {
+ assert (!pkgs.empty ());
+
+ pair<cstrings, const process_path&> args_pp (apt_get_common ("install"));
+
+ cstrings& args (args_pp.first);
+ const process_path& pp (args_pp.second);
+
+ for (const string& p: pkgs)
+ args.push_back (p.c_str ());
+
+ args.push_back (nullptr);
+
+ try
+ {
+ if (verb >= 2)
+ print_process (args);
+ else if (verb == 1)
+ text << "installing " << os_release_.name_id << " packages...";
+
+ process pr;
+ if (!simulate_)
+ pr = process (pp, args);
+ else
+ {
+ print_process (args);
+ pr = process (process_exit (simulate_->apt_get_install_fail_ ? 100 : 0));
+ }
+
+ if (!pr.wait ())
+ {
+ diag_record dr (fail);
+ dr << "apt-get install exited with non-zero code";
+
+ if (verb < 2)
+ {
+ dr << info << "command line: ";
+ print_process (dr, args);
+ }
+
+ dr << info << "consider resolving the issue manually and retrying "
+ << "the bpkg command";
+ }
+
+ if (verb == 1)
+ text << "installed " << os_release_.name_id << " packages";
+ }
+ catch (const process_error& e)
+ {
+ error << "unable to execute " << args[0] << ": " << e;
+
+ if (e.child)
+ exit (1);
+
+ throw failed ();
+ }
+ }
+
+ optional<const system_package_status*> system_package_manager_debian::
+ pkg_status (const package_name& pn, const available_packages* aps)
+ {
+ // For now we ignore -doc and -dbg package components (but we may want to
+ // have options controlling this later). Note also that we assume -common
+ // is pulled automatically by the main package so we ignore it as well
+ // (see equivalent logic in parse_name_value()).
+ //
+ bool need_doc (false);
+ bool need_dbg (false);
+
+ // First check the cache.
+ //
+ {
+ auto i (status_cache_.find (pn));
+
+ if (i != status_cache_.end ())
+ return i->second ? &*i->second : nullptr;
+
+ if (aps == nullptr)
+ return nullopt;
+ }
+
+ vector<package_status> candidates;
+
+ // Translate our package name to the Debian package names.
+ //
+ {
+ auto df = make_diag_frame (
+ [this, &pn] (diag_record& dr)
+ {
+ dr << info << "while mapping " << pn << " to "
+ << os_release_.name_id << " package name";
+ });
+
+ strings ns (system_package_names (*aps,
+ os_release_.name_id,
+ os_release_.version_id,
+ os_release_.like_ids));
+ if (ns.empty ())
+ {
+ // Attempt to automatically translate our package name (see above for
+ // details).
+ //
+ const string& n (pn.string ());
+
+ // The best we can do in trying to detect whether this is a library is
+ // to check for the lib prefix. Libraries without the lib prefix and
+ // non-libraries with the lib prefix (both of which we do not
+ // recomment) will have to provide a manual mapping.
+ //
+ if (n.compare (0, 3, "lib") == 0)
+ {
+ // Keep the main package name empty as an indication that it is to
+ // be discovered.
+ //
+ candidates.push_back (package_status ("", n + "-dev"));
+ }
+ else
+ candidates.push_back (package_status (n));
+ }
+ else
+ {
+ // Parse each manual mapping.
+ //
+ for (const string& n: ns)
+ {
+ package_status s (parse_name_value (pn, n, need_doc, need_dbg));
+
+ // Suppress duplicates for good measure based on the main package
+ // name (and falling back to -dev if empty).
+ //
+ auto i (find_if (candidates.begin (), candidates.end (),
+ [&s] (const package_status& x)
+ {
+ // Note that it's possible for one mapping to be
+ // specified as -dev only while the other as main
+ // and -dev.
+ //
+ return s.main.empty () || x.main.empty ()
+ ? s.dev == x.dev
+ : s.main == x.main;
+ }));
+ if (i == candidates.end ())
+ candidates.push_back (move (s));
+ else
+ {
+ // Should we verify the rest matches for good measure? But what if
+ // we need to override, as in:
+ //
+ // debian_10-name: libcurl4 libcurl4-openssl-dev
+ // debian_9-name: libcurl4 libcurl4-dev
+ //
+ // Note that for this to work we must get debian_10 values before
+ // debian_9, which is the semantics guaranteed by
+ // system_package_names().
+ }
+ }
+ }
+ }
+
+ // Guess unknown main package given the -dev package and its version.
+ //
+ auto guess_main = [this, &pn] (package_status& s, const string& ver)
+ {
+ string depends (apt_cache_show (s.dev, ver));
+
+ s.main = main_from_dev (s.dev, ver, depends);
+
+ if (s.main.empty ())
+ {
+ fail << "unable to guess main " << os_release_.name_id
+ << " package for " << s.dev << ' ' << ver <<
+ info << s.dev << " Depends value: " << depends <<
+ info << "consider specifying explicit mapping in " << pn
+ << " package manifest";
+ }
+ };
+
+ // Calculate the package status from individual package components.
+ // Return nullopt if there is a component without installed or candidate
+ // version (which means the package cannot be installed).
+ //
+ // The main argument specifies the size of the main group. Only components
+ // from this group are considered for partially_installed determination.
+ //
+ // @@ TODO: we should probably prioritize partially installed with fully
+ // installed main group. Add almost_installed next to partially_installed?
+ //
+ using status_type = package_status::status_type;
+
+ auto status = [] (const vector<package_policy>& pps, size_t main)
+ -> optional<status_type>
+ {
+ bool i (false), u (false);
+
+ for (size_t j (0); j != pps.size (); ++j)
+ {
+ const package_policy& pp (pps[j]);
+
+ if (pp.installed_version.empty ())
+ {
+ if (pp.candidate_version.empty ())
+ return nullopt;
+
+ u = true;
+ }
+ else if (j < main)
+ i = true;
+ }
+
+ return (!u ? package_status::installed :
+ !i ? package_status::not_installed :
+ package_status::partially_installed);
+ };
+
+ // First look for an already fully installed package.
+ //
+ optional<package_status> r;
+
+ {
+ diag_record dr; // Ambiguity diagnostics.
+
+ for (package_status& ps: candidates)
+ {
+ vector<package_policy>& pps (ps.package_policies);
+
+ if (!ps.main.empty ()) pps.emplace_back (ps.main);
+ if (!ps.dev.empty ()) pps.emplace_back (ps.dev);
+ if (!ps.doc.empty () && need_doc) pps.emplace_back (ps.doc);
+ if (!ps.dbg.empty () && need_dbg) pps.emplace_back (ps.dbg);
+ if (!ps.common.empty () && false) pps.emplace_back (ps.common);
+ ps.package_policies_main = pps.size ();
+ for (const string& n: ps.extras) pps.emplace_back (n);
+
+ apt_cache_policy (pps);
+
+ // Handle the unknown main package.
+ //
+ if (ps.main.empty ())
+ {
+ const package_policy& dev (pps.front ());
+
+ // Note that at this stage we can only use the installed -dev
+ // package (since the candidate version may change after fetch).
+ //
+ if (dev.installed_version.empty ())
+ continue;
+
+ guess_main (ps, dev.installed_version);
+ pps.emplace (pps.begin (), ps.main);
+ ps.package_policies_main++;
+ apt_cache_policy (pps, 1);
+ }
+
+ optional<status_type> s (status (pps, ps.package_policies_main));
+
+ if (!s || *s != package_status::installed)
+ continue;
+
+ const package_policy& main (pps.front ());
+
+ ps.status = *s;
+ ps.system_name = main.name;
+ ps.system_version = main.installed_version;
+
+ if (!r)
+ {
+ r = move (ps);
+ continue;
+ }
+
+ if (dr.empty ())
+ {
+ dr << fail << "multiple installed " << os_release_.name_id
+ << " packages for " << pn <<
+ info << "candidate: " << r->main << " " << r->system_version;
+ }
+
+ dr << info << "candidate: " << ps.main << " " << ps.system_version;
+ }
+
+ if (!dr.empty ())
+ dr << info << "consider specifying the desired version manually";
+ }
+
+ // Next look for available versions if we are allowed to install.
+ //
+ if (!r && install_)
+ {
+ // If we weren't instructed to fetch or we already fetched, then we
+ // don't need to re-run apt_cache_policy().
+ //
+ bool requery;
+ if ((requery = fetch_ && !fetched_))
+ {
+ apt_get_update ();
+ fetched_ = true;
+ }
+
+ {
+ diag_record dr; // Ambiguity diagnostics.
+
+ for (package_status& ps: candidates)
+ {
+ vector<package_policy>& pps (ps.package_policies);
+
+ if (requery)
+ apt_cache_policy (pps);
+
+ // Handle the unknown main package.
+ //
+ if (ps.main.empty ())
+ {
+ const package_policy& dev (pps.front ());
+
+ // Note that this time we use the candidate version.
+ //
+ if (dev.candidate_version.empty ())
+ continue; // Not installable.
+
+ guess_main (ps, dev.candidate_version);
+ pps.emplace (pps.begin (), ps.main);
+ ps.package_policies_main++;
+ apt_cache_policy (pps, 1);
+ }
+
+ optional<status_type> s (status (pps, ps.package_policies_main));
+
+ if (!s)
+ {
+ ps.main.clear (); // Not installable.
+ continue;
+ }
+
+ assert (*s != package_status::installed); // Sanity check.
+
+ const package_policy& main (pps.front ());
+
+ // Note that if we are installing something for this main package,
+ // then we always go for the candidate version even though it may
+ // have an installed version that may be good enough (especially if
+ // what we are installing are extras). The reason is that it may as
+ // well not be good enough (especially if we are installing the -dev
+ // package) and there is no straightforward way to change our mind.
+ //
+ ps.status = *s;
+ ps.system_name = main.name;
+ ps.system_version = main.candidate_version;
+
+ // Prefer partially installed to not installed. This makes detecting
+ // ambiguity a bit trickier so we handle partially installed here
+ // and not installed in a separate loop below.
+ //
+ if (ps.status != package_status::partially_installed)
+ continue;
+
+ if (!r)
+ {
+ r = move (ps);
+ continue;
+ }
+
+ auto print_missing = [&dr] (const package_status& s)
+ {
+ for (const package_policy& pp: s.package_policies)
+ if (pp.installed_version.empty ())
+ dr << ' ' << pp.name;
+ };
+
+ if (dr.empty ())
+ {
+ dr << fail << "multiple partially installed "
+ << os_release_.name_id << " packages for " << pn;
+
+ dr << info << "candidate: " << r->main << " " << r->system_version
+ << ", missing components:";
+ print_missing (*r);
+ }
+
+ dr << info << "candidate: " << ps.main << " " << ps.system_version
+ << ", missing components:";
+ print_missing (ps);
+ }
+
+ if (!dr.empty ())
+ dr << info << "consider fully installing the desired package "
+ << "manually and retrying the bpkg command";
+ }
+
+ if (!r)
+ {
+ diag_record dr; // Ambiguity diagnostics.
+
+ for (package_status& ps: candidates)
+ {
+ if (ps.main.empty ())
+ continue;
+
+ assert (ps.status == package_status::not_installed); // Sanity check.
+
+ if (!r)
+ {
+ r = move (ps);
+ continue;
+ }
+
+ if (dr.empty ())
+ {
+ dr << fail << "multiple available " << os_release_.name_id
+ << " packages for " << pn <<
+ info << "candidate: " << r->main << " " << r->system_version;
+ }
+
+ dr << info << "candidate: " << ps.main << " " << ps.system_version;
+ }
+
+ if (!dr.empty ())
+ dr << info << "consider installing the desired package manually and "
+ << "retrying the bpkg command";
+ }
+ }
+
+ if (r)
+ {
+ // Map the Debian version to the bpkg version. But first strip the
+ // revision from Debian version ([<epoch>:]<upstream>[-<revision>]), if
+ // any.
+ //
+ // Note that according to deb-version(5), <upstream> may contain `:`/`-`
+ // but in these cases <epoch>/<revision> must be specified explicitly,
+ // respectively.
+ //
+ string sv (r->system_version, 0, r->system_version.rfind ('-'));
+
+ optional<version> v (
+ downstream_package_version (sv,
+ *aps,
+ os_release_.name_id,
+ os_release_.version_id,
+ os_release_.like_ids));
+
+ if (!v)
+ {
+ // Fallback to using system version as downstream version. But first
+ // strip the epoch, if any.
+ //
+ size_t p (sv.find (':'));
+ if (p != string::npos)
+ sv.erase (0, p + 1);
+
+ try
+ {
+ v = version (sv);
+ }
+ catch (const invalid_argument& e)
+ {
+ fail << "unable to map " << os_release_.name_id << " package "
+ << r->system_name << " version " << sv << " to bpkg package "
+ << pn << " version" <<
+ info << os_release_.name_id << " version is not a valid bpkg "
+ << "version: " << e.what () <<
+ info << "consider specifying explicit mapping in " << pn
+ << " package manifest";
+ }
+ }
+
+ r->version = move (*v);
+ }
+
+ // Cache.
+ //
+ auto i (status_cache_.emplace (pn, move (r)).first);
+ return i->second ? &*i->second : nullptr;
+ }
+
+ void system_package_manager_debian::
+ pkg_install (const vector<package_name>& pns)
+ {
+ assert (!pns.empty ());
+
+ assert (install_ && !installed_);
+ installed_ = true;
+
+ // Collect and merge all the Debian packages/version for the specified
+ // bpkg packages.
+ //
+ struct package
+ {
+ string name;
+ string version; // Empty if unspecified.
+ };
+ vector<package> pkgs;
+
+ for (const package_name& pn: pns)
+ {
+ auto it (status_cache_.find (pn));
+ assert (it != status_cache_.end () && it->second);
+
+ const package_status& ps (*it->second);
+
+ // At first it may seem we don't need to do anything for already fully
+ // installed packages. But it's possible some of them were automatically
+ // installed, meaning that they can be automatically removed if they no
+ // longer have any dependents (see apt-mark(8) for details). Which in
+ // turn means that things may behave differently depending on whether
+ // we've installed a package ourselves or if it was already installed.
+ // So instead we are going to also pass the already fully installed
+ // packages which will make sure they are all set to manually installed.
+ // But we must be careful not to force their upgrade. To achieve this
+ // we will specify the installed version as the desired version.
+ //
+ // Note also that for partially/not installed we don't specify the
+ // version, expecting the candidate version to be installed.
+ //
+ bool fi (ps.status == package_status::installed);
+
+ for (const package_policy& pp: ps.package_policies)
+ {
+ string n (pp.name);
+ string v (fi ? pp.installed_version : string ());
+
+ auto i (find_if (pkgs.begin (), pkgs.end (),
+ [&n] (const package& p)
+ {
+ return p.name == n;
+ }));
+
+ if (i != pkgs.end ())
+ {
+ if (i->version.empty ())
+ i->version = move (v);
+ else
+ // Feels like this cannot happen since we always use the installed
+ // version of the package.
+ //
+ assert (i->version == v);
+ }
+ else
+ pkgs.push_back (package {move (n), move (v)});
+ }
+ }
+
+ // Install.
+ //
+ {
+ // Convert to the `apt-get install` <pkg>[=<ver>] form.
+ //
+ strings specs;
+ specs.reserve (pkgs.size ());
+ for (const package& p: pkgs)
+ {
+ string s (p.name);
+ if (!p.version.empty ())
+ {
+ s += '=';
+ s += p.version;
+ }
+ specs.push_back (move (s));
+ }
+
+ apt_get_install (specs);
+ }
+
+ // Verify that versions we have promised in pkg_status() match what
+ // actually got installed.
+ //
+ {
+ vector<package_policy> pps;
+
+ // Here we just check the main package component of each package.
+ //
+ for (const package_name& pn: pns)
+ {
+ const package_status& ps (*status_cache_.find (pn)->second);
+
+ if (find_if (pps.begin (), pps.end (),
+ [&ps] (const package_policy& pp)
+ {
+ return pp.name == ps.system_name;
+ }) == pps.end ())
+ {
+ pps.push_back (package_policy (ps.system_name));
+ }
+ }
+
+ apt_cache_policy (pps);
+
+ for (const package_name& pn: pns)
+ {
+ const package_status& ps (*status_cache_.find (pn)->second);
+
+ auto i (find_if (pps.begin (), pps.end (),
+ [&ps] (const package_policy& pp)
+ {
+ return pp.name == ps.system_name;
+ }));
+ assert (i != pps.end ());
+
+ const package_policy& pp (*i);
+
+ if (pp.installed_version != ps.system_version)
+ {
+ fail << "unexpected " << os_release_.name_id << " package version "
+ << "for " << ps.system_name <<
+ info << "expected: " << ps.system_version <<
+ info << "installed: " << pp.installed_version <<
+ info << "consider retrying the bpkg command";
+ }
+ }
+ }
+ }
+}
diff --git a/bpkg/system-package-manager-debian.hxx b/bpkg/system-package-manager-debian.hxx
new file mode 100644
index 0000000..9fb93c7
--- /dev/null
+++ b/bpkg/system-package-manager-debian.hxx
@@ -0,0 +1,201 @@
+// file : bpkg/system-package-manager-debian.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BPKG_SYSTEM_PACKAGE_MANAGER_DEBIAN_HXX
+#define BPKG_SYSTEM_PACKAGE_MANAGER_DEBIAN_HXX
+
+#include <map>
+
+#include <bpkg/types.hxx>
+#include <bpkg/utility.hxx>
+
+#include <bpkg/system-package-manager.hxx>
+
+namespace bpkg
+{
+ // The system package manager implementation for Debian and alike (Ubuntu,
+ // etc) using the APT frontend.
+ //
+ // NOTE: the below description is also reproduced in the bpkg manual.
+ //
+ // For background, a library in Debian is normally split up into several
+ // packages: the shared library package (e.g., libfoo1 where 1 is the ABI
+ // version), the development files package (e.g., libfoo-dev), the
+ // documentation files package (e.g., libfoo-doc), the debug symbols
+ // package (e.g., libfoo1-dbg), and the architecture-independent files
+ // (e.g., libfoo1-common). All the packages except -dev are optional
+ // and there is quite a bit of variability here. Here are a few examples:
+ //
+ // libz3-4 libz3-dev
+ //
+ // libssl1.1 libssl-dev libssl-doc
+ // libssl3 libssl-dev libssl-doc
+ //
+ // libcurl4 libcurl4-openssl-dev libcurl4-doc
+ // libcurl3-gnutls libcurl4-gnutls-dev libcurl4-doc (yes, 3 and 4)
+ //
+ // Based on that, it seems our best bet when trying to automatically map our
+ // library package name to Debian package names is to go for the -dev
+ // package first and figure out the shared library package from that based
+ // on the fact that the -dev package should have the == dependency on the
+ // shared library package with the same version and its name should normally
+ // start with the -dev package's stem.
+ //
+ // For executable packages there is normally no -dev packages but -dbg,
+ // -doc, and -common are plausible.
+ //
+ // The format of the debian-name (or alike) manifest value is a comma-
+ // separated list of one or more package groups:
+ //
+ // <package-group> [, <package-group>...]
+ //
+ // Where each <package-group> is the space-separated list of one or more
+ // package names:
+ //
+ // <package-name> [ <package-name>...]
+ //
+ // All the packages in the group should be "package components" (for the
+ // lack of a better term) of the same "logical package", such as -dev, -doc,
+ // -common packages. They normally have the same version.
+ //
+ // The first group is called the main group and the first package in the
+ // group is called the main package. Note that all the groups are consumed
+ // (installed) but only the main group is produced (packaged).
+ //
+ // We allow/recommend specifying the -dev package instead of the main
+ // package for libraries (the bpkg package name starts with lib), seeing
+ // that we are capable of detecting the main package automatically. If the
+ // library name happens to end with -dev (which poses an ambiguity), then
+ // the -dev package should be specified explicitly as the second package to
+ // disambiguate this situation (if a non-library name happened to start with
+ // lib and end with -dev, well, you are out of luck, I guess).
+ //
+ // Note also that for now we treat all the packages from the non-main groups
+ // as extras but in the future we may decide to sort them out like the main
+ // group (see parse_name_value() for details).
+ //
+ // The Debian package version has the [<epoch>:]<upstream>[-<revision>] form
+ // (see deb-version(5) for details). If no explicit mapping to the bpkg
+ // version is specified with the debian-to-downstream-version (or alike)
+ // manifest values or none match, then we fallback to using the <upstream>
+ // part as the bpkg version. If explicit mapping is specified, then we match
+ // it against the [<epoch>:]<upstream> parts ignoring <revision>.
+ //
+ struct system_package_status_debian: system_package_status
+ {
+ string main;
+ string dev;
+ string doc;
+ string dbg;
+ string common;
+ strings extras;
+
+ // The `apt-cache policy` output.
+ //
+ struct package_policy
+ {
+ string name;
+ string installed_version; // Empty if none.
+ string candidate_version; // Empty if none and no installed_version.
+
+ explicit
+ package_policy (string n): name (move (n)) {}
+ };
+
+ vector<package_policy> package_policies;
+ size_t package_policies_main = 0; // Size of the main group.
+
+ explicit
+ system_package_status_debian (string m, string d = {})
+ : main (move (m)), dev (move (d))
+ {
+ assert (!main.empty () || !dev.empty ());
+ }
+
+ system_package_status_debian () = default;
+ };
+
+ class system_package_manager_debian: public system_package_manager
+ {
+ public:
+ virtual optional<const system_package_status*>
+ pkg_status (const package_name&, const available_packages*) override;
+
+ virtual void
+ pkg_install (const vector<package_name>&) override;
+
+ public:
+ // Expects os_release::name_id to be "debian" or os_release::like_ids to
+ // contain "debian".
+ //
+ using system_package_manager::system_package_manager;
+
+ // Implementation details exposed for testing (see definitions for
+ // documentation).
+ //
+ public:
+ using package_status = system_package_status_debian;
+ using package_policy = package_status::package_policy;
+
+ void
+ apt_cache_policy (vector<package_policy>&, size_t = 0);
+
+ string
+ apt_cache_show (const string&, const string&);
+
+ void
+ apt_get_update ();
+
+ void
+ apt_get_install (const strings&);
+
+ pair<cstrings, const process_path&>
+ apt_get_common (const char*);
+
+ static package_status
+ parse_name_value (const package_name&, const string&, bool, bool);
+
+ static string
+ main_from_dev (const string&, const string&, const string&);
+
+ // If simulate is not NULL, then instead of executing the actual apt-cache
+ // and apt-get commands simulate their execution: (1) for apt-cache by
+ // printing their command lines and reading the results from files
+ // specified in the below apt_cache_* maps and (2) for apt-get by printing
+ // their command lines and failing if requested.
+ //
+ // In the (1) case if the corresponding map entry does not exist or the
+ // path is empty, then act as if the specified package/version is
+ // unknown. If the path is special "-" then read from stdin. For apt-cache
+ // different post-fetch and (for policy) post-install results can be
+ // specified (if the result is not found in one of the later maps, the
+ // previous map is used as a fallback). Note that the keys in the
+ // apt_cache_policy_* maps are the package sets and the corresponding
+ // result file is expected to contain (or not) the results for all of
+ // them. See apt_cache_policy() and apt_cache_show() implementations for
+ // details on the expected results.
+ //
+ struct simulation
+ {
+ std::map<strings, path> apt_cache_policy_;
+ std::map<strings, path> apt_cache_policy_fetched_;
+ std::map<strings, path> apt_cache_policy_installed_;
+
+ std::map<pair<string, string>, path> apt_cache_show_;
+ std::map<pair<string, string>, path> apt_cache_show_fetched_;
+
+ bool apt_get_update_fail_ = false;
+ bool apt_get_install_fail_ = false;
+ };
+
+ const simulation* simulate_ = nullptr;
+
+ protected:
+ bool fetched_ = false; // True if already fetched metadata.
+ bool installed_ = false; // True if already installed.
+
+ std::map<package_name, optional<system_package_status_debian>> status_cache_;
+ };
+}
+
+#endif // BPKG_SYSTEM_PACKAGE_MANAGER_DEBIAN_HXX
diff --git a/bpkg/system-package-manager-debian.test.cxx b/bpkg/system-package-manager-debian.test.cxx
new file mode 100644
index 0000000..a033400
--- /dev/null
+++ b/bpkg/system-package-manager-debian.test.cxx
@@ -0,0 +1,348 @@
+// file : bpkg/system-package-manager-debian.test.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <bpkg/system-package-manager-debian.hxx>
+
+#include <map>
+#include <iostream>
+
+#include <bpkg/types.hxx>
+#include <bpkg/utility.hxx>
+
+#undef NDEBUG
+#include <cassert>
+
+#include <bpkg/system-package-manager.test.hxx>
+
+using namespace std;
+
+namespace bpkg
+{
+ using package_status = system_package_status_debian;
+ using package_policy = package_status::package_policy;
+
+ using butl::manifest_parser;
+ using butl::manifest_parsing;
+
+ // Usage: args[0] <command> ...
+ //
+ // Where <command> is one of:
+ //
+ // apt-cache-policy <pkg>... result comes from stdin
+ //
+ // apt-cache-show <pkg> <ver> result comes from stdin
+ //
+ // parse-name-value <pkg> debian-name value from stdin
+ //
+ // main-from-dev <dev-pkg> <dev-ver> depends comes from stdin
+ //
+ // build <query-pkg>... [--install [--no-fetch] <install-pkg>...]
+ //
+ // The stdin of the build command is used to read the simulation description
+ // which consists of lines in the following forms (blanks are ignored):
+ //
+ // manifest: <query-pkg> <file>
+ //
+ // Available package manifest for one of <query-pkg>. If none is
+ // specified, then a stub is automatically added.
+ //
+ // apt-cache-policy[-{fetched,installed}]: <sys-pkg>... <file>
+ //
+ // Values for simulation::apt_cache_policy_*. If <file> is the special `!`
+ // value, then make the entry empty.
+ //
+ // apt-cache-show[-fetched]: <sys-pkg> <sys-ver> <file>
+ //
+ // Values for simulation::apt_cache_show_*. If <file> is the special `!`
+ // value, then make the entry empty.
+ //
+ // apt-get-update-fail: true
+ // apt-get-install-fail: true
+ //
+ // Values for simulation::apt_get_{update,install}_fail_.
+ //
+ int
+ main (int argc, char* argv[])
+ try
+ {
+ assert (argc >= 2); // <command>
+
+ string cmd (argv[1]);
+
+ // @@ TODO: add option to customize? Maybe option before command?
+ //
+ os_release osr {"debian", {}, "10", "", "Debian", "", ""};
+
+ if (cmd == "apt-cache-policy")
+ {
+ assert (argc >= 3); // <pkg>...
+
+ strings key;
+ vector<package_policy> pps;
+ for (int i (2); i != argc; ++i)
+ {
+ key.push_back (argv[i]);
+ pps.push_back (package_policy (argv[i]));
+ }
+
+ system_package_manager_debian::simulation s;
+ s.apt_cache_policy_.emplace (move (key), path ("-"));
+
+ system_package_manager_debian m (move (osr),
+ host_triplet,
+ false /* install */,
+ false /* fetch */,
+ nullopt /* progress */,
+ false /* yes */,
+ "sudo");
+ m.simulate_ = &s;
+
+ m.apt_cache_policy (pps);
+
+ for (const package_policy& pp: pps)
+ {
+ cout << pp.name << " '"
+ << pp.installed_version << "' '"
+ << pp.candidate_version << "'\n";
+ }
+ }
+ else if (cmd == "apt-cache-show")
+ {
+ assert (argc == 4); // <pkg> <ver>
+
+ pair<string, string> key (argv[2], argv[3]);
+
+ system_package_manager_debian::simulation s;
+ s.apt_cache_show_.emplace (key, path ("-"));
+
+ system_package_manager_debian m (move (osr),
+ host_triplet,
+ false /* install */,
+ false /* fetch */,
+ nullopt /* progress */,
+ false /* yes */,
+ "sudo");
+ m.simulate_ = &s;
+
+ cout << m.apt_cache_show (key.first, key.second) << '\n';
+ }
+ else if (cmd == "parse-name-value")
+ {
+ assert (argc == 3); // <pkg>
+
+ package_name pn (argv[2]);
+
+ string v;
+ getline (cin, v);
+
+ package_status s (
+ system_package_manager_debian::parse_name_value (pn, v, false, false));
+
+ if (!s.main.empty ()) cout << "main: " << s.main << '\n';
+ if (!s.dev.empty ()) cout << "dev: " << s.dev << '\n';
+ if (!s.doc.empty ()) cout << "doc: " << s.doc << '\n';
+ if (!s.dbg.empty ()) cout << "dbg: " << s.dbg << '\n';
+ if (!s.common.empty ()) cout << "common: " << s.common << '\n';
+ if (!s.extras.empty ())
+ {
+ cout << "extras:";
+ for (const string& e: s.extras)
+ cout << ' ' << e;
+ cout << '\n';
+ }
+ }
+ else if (cmd == "main-from-dev")
+ {
+ assert (argc == 4); // <dev-pkg> <dev-ver>
+
+ string n (argv[2]);
+ string v (argv[3]);
+ string d;
+ getline (cin, d);
+
+ cout << system_package_manager_debian::main_from_dev (n, v, d) << '\n';
+ }
+ else if (cmd == "build")
+ {
+ assert (argc >= 3); // <query-pkg>...
+
+ strings qps;
+ map<string, available_packages> aps;
+
+ // Parse <query-pkg>...
+ //
+ int argi (2);
+ for (; argi != argc; ++argi)
+ {
+ string a (argv[argi]);
+
+ if (a.compare (0, 2, "--") == 0)
+ break;
+
+ aps.emplace (a, available_packages {});
+ qps.push_back (move (a));
+ }
+
+ // Parse --install [--no-fetch]
+ //
+ bool install (false);
+ bool fetch (true);
+
+ for (; argi != argc; ++argi)
+ {
+ string a (argv[argi]);
+
+ if (a == "--install") install = true;
+ else if (a == "--no-fetch") fetch = false;
+ else break;
+ }
+
+ // Parse the description.
+ //
+ system_package_manager_debian::simulation s;
+
+ for (string l; !eof (getline (cin, l)); )
+ {
+ if (l.empty ())
+ continue;
+
+ size_t p (l.find (':')); assert (p != string::npos);
+ string k (l, 0, p);
+
+ if (k == "manifest")
+ {
+ size_t q (l.rfind (' ')); assert (q != string::npos);
+ string n (l, p + 2, q - p - 2); trim (n);
+ string f (l, q + 1); trim (f);
+
+ auto i (aps.find (n));
+ if (i == aps.end ())
+ fail << "unknown package " << n << " in '" << l << "'";
+
+ i->second.push_back (make_available_from_manifest (n, f));
+ }
+ else if (
+ map<strings, path>* policy =
+ k == "apt-cache-policy" ? &s.apt_cache_policy_ :
+ k == "apt-cache-policy-fetched" ? &s.apt_cache_policy_fetched_ :
+ k == "apt-cache-policy-installed" ? &s.apt_cache_policy_installed_ :
+ nullptr)
+ {
+ size_t q (l.rfind (' ')); assert (q != string::npos);
+ string n (l, p + 2, q - p - 2); trim (n);
+ string f (l, q + 1); trim (f);
+
+ strings ns;
+ for (size_t b (0), e (0); next_word (n, b, e); )
+ ns.push_back (string (n, b, e - b));
+
+ if (f == "!")
+ f.clear ();
+
+ policy->emplace (move (ns), path (move (f)));
+ }
+ else if (map<pair<string, string>, path>* show =
+ k == "apt-cache-show" ? &s.apt_cache_show_ :
+ k == "apt-cache-show-fetched" ? &s.apt_cache_show_fetched_ :
+ nullptr)
+ {
+ size_t q (l.rfind (' ')); assert (q != string::npos);
+ string n (l, p + 2, q - p - 2); trim (n);
+ string f (l, q + 1); trim (f);
+
+ q = n.find (' '); assert (q != string::npos);
+ pair<string, string> nv (string (n, 0, q), string (n, q + 1));
+ trim (nv.second);
+
+ if (f == "!")
+ f.clear ();
+
+ show->emplace (move (nv), path (move (f)));
+ }
+ else if (k == "apt-get-update-fail")
+ {
+ s.apt_get_update_fail_ = true;
+ }
+ else if (k == "apt-get-install-fail")
+ {
+ s.apt_get_install_fail_ = true;
+ }
+ else
+ fail << "unknown keyword '" << k << "' in simulation description";
+ }
+
+ // Fallback to stubs and sort in the version descending order.
+ //
+ for (pair<const string, available_packages>& p: aps)
+ {
+ if (p.second.empty ())
+ p.second.push_back (make_available_stub (p.first));
+
+ sort_available (p.second);
+ }
+
+ system_package_manager_debian m (move (osr),
+ host_triplet,
+ install,
+ fetch,
+ nullopt /* progress */,
+ false /* yes */,
+ "sudo");
+ m.simulate_ = &s;
+
+ // Query each package.
+ //
+ for (const string& n: qps)
+ {
+ package_name pn (n);
+
+ const system_package_status* s (*m.pkg_status (pn, &aps[n]));
+
+ assert (*m.pkg_status (pn, nullptr) == s); // Test caching.
+
+ if (s == nullptr)
+ fail << "no installed " << (install ? "or available " : "")
+ << "system package for " << pn;
+
+ cout << pn << ' ' << s->version
+ << " (" << s->system_name << ' ' << s->system_version << ") ";
+
+ switch (s->status)
+ {
+ case package_status::installed: cout << "installed"; break;
+ case package_status::partially_installed: cout << "part installed"; break;
+ case package_status::not_installed: cout << "not installed"; break;
+ }
+
+ cout << '\n';
+ }
+
+ // Install if requested.
+ //
+ if (install)
+ {
+ assert (argi != argc); // <install-pkg>...
+
+ vector<package_name> ips;
+ for (; argi != argc; ++argi)
+ ips.push_back (package_name (argv[argi]));
+
+ m.pkg_install (ips);
+ }
+ }
+ else
+ fail << "unknown command '" << cmd << "'";
+
+ return 0;
+ }
+ catch (const failed&)
+ {
+ return 1;
+ }
+}
+
+int
+main (int argc, char* argv[])
+{
+ return bpkg::main (argc, argv);
+}
diff --git a/bpkg/system-package-manager-debian.test.testscript b/bpkg/system-package-manager-debian.test.testscript
new file mode 100644
index 0000000..b1a0030
--- /dev/null
+++ b/bpkg/system-package-manager-debian.test.testscript
@@ -0,0 +1,987 @@
+# file : bpkg/system-package-manager-debian.test.testscript
+# license : MIT; see accompanying LICENSE file
+
+: apt-cache-policy
+:
+{
+ test.arguments += apt-cache-policy
+
+ : basics
+ :
+ $* libssl3 libssl1.1 libssl-dev libsqlite5 libxerces-c-dev <<EOI 2>>EOE >>EOO
+ libssl3:
+ Installed: 3.0.7-1
+ Candidate: 3.0.7-2
+ Version table:
+ 3.0.7-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ *** 3.0.7-1 100
+ 100 /var/lib/dpkg/status
+ libssl1.1:
+ Installed: 1.1.1n-0+deb11u3
+ Candidate: 1.1.1n-0+deb11u3
+ Version table:
+ *** 1.1.1n-0+deb11u3 100
+ 100 /var/lib/dpkg/status
+ libssl-dev:
+ Installed: 3.0.7-1
+ Candidate: 3.0.7-2
+ Version table:
+ 3.0.7-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ *** 3.0.7-1 100
+ 100 /var/lib/dpkg/status
+ libxerces-c-dev:
+ Installed: (none)
+ Candidate: 3.2.4+debian-1
+ Version table:
+ 3.2.4+debian-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ LC_ALL=C apt-cache policy --quiet libssl3 libssl1.1 libssl-dev libsqlite5 libxerces-c-dev <-
+ EOE
+ libssl3 '3.0.7-1' '3.0.7-2'
+ libssl1.1 '1.1.1n-0+deb11u3' '1.1.1n-0+deb11u3'
+ libssl-dev '3.0.7-1' '3.0.7-2'
+ libsqlite5 '' ''
+ libxerces-c-dev '' '3.2.4+debian-1'
+ EOO
+
+ : empty
+ :
+ $* libsqlite5 <:'' 2>>EOE >>EOO
+ LC_ALL=C apt-cache policy --quiet libsqlite5 <-
+ EOE
+ libsqlite5 '' ''
+ EOO
+
+ : none-none
+ :
+ $* pulseaudio <<EOI 2>>EOE >>EOO
+ pulseaudio:
+ Installed: (none)
+ Candidate: (none)
+ Version table:
+ 1:11.1-1ubuntu7.5 -1
+ 500 http://au.archive.ubuntu.com/ubuntu bionic-updates/main amd64 Packages
+ 1:11.1-1ubuntu7 -1
+ 500 http://au.archive.ubuntu.com/ubuntu bionic/main amd64 Packages
+ EOI
+ LC_ALL=C apt-cache policy --quiet pulseaudio <-
+ EOE
+ pulseaudio '' ''
+ EOO
+}
+
+: apt-cache-show
+:
+{
+ test.arguments += apt-cache-show
+
+ # Note: put Depends last to test folded/multiline parsing.
+ #
+ : basics
+ :
+ $* libssl1.1 1.1.1n-0+deb11u3 <<EOI 2>>EOE >>EOO
+ Package: libssl1.1
+ Status: install ok installed
+ Priority: optional
+ Section: libs
+ Installed-Size: 4120
+ Maintainer: Debian OpenSSL Team <pkg-openssl-devel@lists.alioth.debian.org>
+ Architecture: amd64
+ Multi-Arch: same
+ Source: openssl
+ Version: 1.1.1n-0+deb11u3
+ Breaks: isync (<< 1.3.0-2), lighttpd (<< 1.4.49-2), python-boto (<< 2.44.0-1.1), python-httplib2 (<< 0.11.3-1), python-imaplib2 (<< 2.57-5), python3-boto (<< 2.44.0-1.1), python3-imaplib2 (<< 2.57-5)
+ Description: Secure Sockets Layer toolkit - shared libraries
+ This package is part of the OpenSSL project's implementation of the SSL
+ and TLS cryptographic protocols for secure communication over the
+ Internet.
+ .
+ It provides the libssl and libcrypto shared libraries.
+ Description-md5: 88547c6206c7fbc4fcc7d09ce100d210
+ Homepage: https://www.openssl.org/
+ Depends: libc6 (>= 2.25), debconf (>= 0.5) | debconf-2.0
+
+ EOI
+ LC_ALL=C apt-cache show --quiet libssl1.1=1.1.1n-0+deb11u3 <-
+ EOE
+ libc6 (>= 2.25), debconf (>= 0.5) | debconf-2.0
+ EOO
+
+ : no-depends
+ :
+ $* libssl1.1 1.1.1n-0+deb11u3 <<EOI 2>>EOE >''
+ Package: libssl1.1
+ Status: install ok installed
+ Priority: optional
+ Section: libs
+ Installed-Size: 4120
+ Maintainer: Debian OpenSSL Team <pkg-openssl-devel@lists.alioth.debian.org>
+ Architecture: amd64
+ Multi-Arch: same
+ Source: openssl
+ Version: 1.1.1n-0+deb11u3
+ Breaks: isync (<< 1.3.0-2), lighttpd (<< 1.4.49-2), python-boto (<< 2.44.0-1.1), python-httplib2 (<< 0.11.3-1), python-imaplib2 (<< 2.57-5), python3-boto (<< 2.44.0-1.1), python3-imaplib2 (<< 2.57-5)
+ Description: Secure Sockets Layer toolkit - shared libraries
+ This package is part of the OpenSSL project's implementation of the SSL
+ and TLS cryptographic protocols for secure communication over the
+ Internet.
+ .
+ It provides the libssl and libcrypto shared libraries.
+ Description-md5: 88547c6206c7fbc4fcc7d09ce100d210
+ Homepage: https://www.openssl.org/
+
+ EOI
+ LC_ALL=C apt-cache show --quiet libssl1.1=1.1.1n-0+deb11u3 <-
+ EOE
+}
+
+: parse-name-value
+:
+{
+ test.arguments += parse-name-value
+
+ : basics
+ :
+ $* libssl <<EOI >>EOO
+ libssl3 libssl-common libssl-doc libssl-dev libssl-dbg libssl-extras, libc6 libc-dev libc-common libc-doc, libz-dev
+ EOI
+ main: libssl3
+ dev: libssl-dev
+ doc: libssl-doc
+ dbg: libssl-dbg
+ common: libssl-common
+ extras: libssl-extras libc6 libc-dev libz-dev
+ EOO
+
+ : non-lib
+ :
+ $* sqlite3 <<EOI >>EOO
+ sqlite3 sqlite3-common sqlite3-doc
+ EOI
+ main: sqlite3
+ doc: sqlite3-doc
+ common: sqlite3-common
+ EOO
+
+ : lib-dev
+ :
+ $* libssl <<EOI >>EOO
+ libssl-dev
+ EOI
+ dev: libssl-dev
+ EOO
+
+ : non-lib-dev
+ :
+ $* ssl-dev <<EOI >>EOO
+ ssl-dev
+ EOI
+ main: ssl-dev
+ EOO
+
+ : lib-custom-dev
+ :
+ $* libfoo-dev <<EOI >>EOO
+ libfoo-dev libfoo-dev-dev
+ EOI
+ main: libfoo-dev
+ dev: libfoo-dev-dev
+ EOO
+}
+
+: main-from-dev
+:
+{
+ test.arguments += main-from-dev
+
+ : first
+ :
+ $* libssl-dev 3.0.7-1 <<EOI >'libssl3'
+ libssl3 (= 3.0.7-1), debconf (>= 0.5) | debconf-2.0
+ EOI
+
+ : not-first
+ :
+ $* libxerces-c-dev 3.2.4+debian-1 <<EOI >'libxerces-c3.2'
+ libc6-dev | libc-dev, libicu-dev, libxerces-c3.2 (= 3.2.4+debian-1)
+ EOI
+
+ : exact
+ :
+ $* libexpat1-dev 2.5.0-1 <<EOI >'libexpat1'
+ libexpat1 (= 2.5.0-1), libc6-dev | libc-dev
+ EOI
+
+ : not-stem
+ :
+ $* libcurl4-openssl-dev 7.87.0-2 <<EOI >''
+ libcurl4 (= 7.87.0-2)
+ EOI
+}
+
+: build
+:
+{
+ test.arguments += build
+
+ : libsqlite3
+ :
+ {
+ : installed
+ :
+ cat <<EOI >=libsqlite3-dev.policy;
+ libsqlite3-dev:
+ Installed: 3.40.1-1
+ Candidate: 3.40.1-1
+ Version table:
+ *** 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ 100 /var/lib/dpkg/status
+ EOI
+ cat <<EOI >=libsqlite3-dev.show;
+ Package: libsqlite3-dev
+ Version: 3.40.1-1
+ Depends: libsqlite3-0 (= 3.40.1-1), libc-dev
+ EOI
+ cat <<EOI >=libsqlite3-0.policy;
+ libsqlite3-0:
+ Installed: 3.40.1-1
+ Candidate: 3.40.1-1
+ Version table:
+ *** 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ 100 /var/lib/dpkg/status
+ EOI
+ $* libsqlite3 --install libsqlite3 <<EOI 2>>EOE >>EOO
+ apt-cache-policy: libsqlite3-dev libsqlite3-dev.policy
+ apt-cache-show: libsqlite3-dev 3.40.1-1 libsqlite3-dev.show
+ apt-cache-policy: libsqlite3-0 libsqlite3-0.policy
+ EOI
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev <libsqlite3-dev.policy
+ LC_ALL=C apt-cache show --quiet libsqlite3-dev=3.40.1-1 <libsqlite3-dev.show
+ LC_ALL=C apt-cache policy --quiet libsqlite3-0 <libsqlite3-0.policy
+ sudo apt-get install --quiet --assume-no libsqlite3-0=3.40.1-1 libsqlite3-dev=3.40.1-1
+ LC_ALL=C apt-cache policy --quiet libsqlite3-0 <libsqlite3-0.policy
+ EOE
+ libsqlite3 3.40.1 (libsqlite3-0 3.40.1-1) installed
+ EOO
+
+
+ : part-installed
+ :
+ cat <<EOI >=libsqlite3-dev.policy;
+ libsqlite3-dev:
+ Installed: (none)
+ Candidate: 3.40.1-1
+ Version table:
+ 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libsqlite3-dev.show;
+ Package: libsqlite3-dev
+ Version: 3.40.1-1
+ Depends: libsqlite3-0 (= 3.40.1-1), libc-dev
+ EOI
+ cat <<EOI >=libsqlite3-0.policy;
+ libsqlite3-0:
+ Installed: 3.40.1-1
+ Candidate: 3.40.1-1
+ Version table:
+ *** 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ 100 /var/lib/dpkg/status
+ EOI
+ $* libsqlite3 --install libsqlite3 <<EOI 2>>EOE >>EOO
+ apt-cache-policy: libsqlite3-dev libsqlite3-dev.policy
+ apt-cache-show: libsqlite3-dev 3.40.1-1 libsqlite3-dev.show
+ apt-cache-policy: libsqlite3-0 libsqlite3-0.policy
+ EOI
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev <libsqlite3-dev.policy
+ sudo apt-get update --quiet --assume-no
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev <libsqlite3-dev.policy
+ LC_ALL=C apt-cache show --quiet libsqlite3-dev=3.40.1-1 <libsqlite3-dev.show
+ LC_ALL=C apt-cache policy --quiet libsqlite3-0 <libsqlite3-0.policy
+ sudo apt-get install --quiet --assume-no libsqlite3-0 libsqlite3-dev
+ LC_ALL=C apt-cache policy --quiet libsqlite3-0 <libsqlite3-0.policy
+ EOE
+ libsqlite3 3.40.1 (libsqlite3-0 3.40.1-1) part installed
+ EOO
+
+
+ : part-installed-upgrade
+ :
+ cat <<EOI >=libsqlite3-dev.policy;
+ libsqlite3-dev:
+ Installed: (none)
+ Candidate: 3.39.4-1
+ Version table:
+ 3.39.4-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libsqlite3-dev.policy-fetched;
+ libsqlite3-dev:
+ Installed: (none)
+ Candidate: 3.40.1-1
+ Version table:
+ 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libsqlite3-dev.show-fetched;
+ Package: libsqlite3-dev
+ Version: 3.40.1-1
+ Depends: libsqlite3-0 (= 3.40.1-1), libc-dev
+ EOI
+ cat <<EOI >=libsqlite3-0.policy-fetched;
+ libsqlite3-0:
+ Installed: 3.39.4-1
+ Candidate: 3.40.1-1
+ Version table:
+ 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ *** 3.39.4-1 100
+ 100 /var/lib/dpkg/status
+ EOI
+ cat <<EOI >=libsqlite3-0.policy-installed;
+ libsqlite3-0:
+ Installed: 3.40.1-1
+ Candidate: 3.40.1-1
+ Version table:
+ *** 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ 100 /var/lib/dpkg/status
+ EOI
+ $* libsqlite3 --install libsqlite3 <<EOI 2>>EOE >>EOO
+ apt-cache-policy: libsqlite3-dev libsqlite3-dev.policy
+ apt-cache-policy-fetched: libsqlite3-dev libsqlite3-dev.policy-fetched
+ apt-cache-show: libsqlite3-dev 3.40.1-1 libsqlite3-dev.show-fetched
+ apt-cache-policy-fetched: libsqlite3-0 libsqlite3-0.policy-fetched
+ apt-cache-policy-installed: libsqlite3-0 libsqlite3-0.policy-installed
+ EOI
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev <libsqlite3-dev.policy
+ sudo apt-get update --quiet --assume-no
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev <libsqlite3-dev.policy-fetched
+ LC_ALL=C apt-cache show --quiet libsqlite3-dev=3.40.1-1 <libsqlite3-dev.show-fetched
+ LC_ALL=C apt-cache policy --quiet libsqlite3-0 <libsqlite3-0.policy-fetched
+ sudo apt-get install --quiet --assume-no libsqlite3-0 libsqlite3-dev
+ LC_ALL=C apt-cache policy --quiet libsqlite3-0 <libsqlite3-0.policy-installed
+ EOE
+ libsqlite3 3.40.1 (libsqlite3-0 3.40.1-1) part installed
+ EOO
+
+
+ # Note that the semantics is unrealistic (maybe background apt-get update
+ # happenned in between).
+ #
+ : part-installed-upgrade-version-change
+ :
+ cat <<EOI >=libsqlite3-dev.policy;
+ libsqlite3-dev:
+ Installed: (none)
+ Candidate: 3.39.4-1
+ Version table:
+ 3.39.4-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libsqlite3-dev.show;
+ Package: libsqlite3-dev
+ Version: 3.39.4-1
+ Depends: libsqlite3-0 (= 3.39.4-1), libc-dev
+ EOI
+ cat <<EOI >=libsqlite3-0.policy;
+ libsqlite3-0:
+ Installed: 3.39.4-1
+ Candidate: 3.39.4-1
+ Version table:
+ *** 3.39.4-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ 100 /var/lib/dpkg/status
+ EOI
+ cat <<EOI >=libsqlite3-0.policy-installed;
+ libsqlite3-0:
+ Installed: 3.40.1-1
+ Candidate: 3.40.1-1
+ Version table:
+ *** 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ 100 /var/lib/dpkg/status
+ EOI
+ $* libsqlite3 --install --no-fetch libsqlite3 <<EOI 2>>EOE >>EOO != 0
+ apt-cache-policy: libsqlite3-dev libsqlite3-dev.policy
+ apt-cache-show: libsqlite3-dev 3.39.4-1 libsqlite3-dev.show
+ apt-cache-policy: libsqlite3-0 libsqlite3-0.policy
+ apt-cache-policy-installed: libsqlite3-0 libsqlite3-0.policy-installed
+ EOI
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev <libsqlite3-dev.policy
+ LC_ALL=C apt-cache show --quiet libsqlite3-dev=3.39.4-1 <libsqlite3-dev.show
+ LC_ALL=C apt-cache policy --quiet libsqlite3-0 <libsqlite3-0.policy
+ sudo apt-get install --quiet --assume-no libsqlite3-0 libsqlite3-dev
+ LC_ALL=C apt-cache policy --quiet libsqlite3-0 <libsqlite3-0.policy-installed
+ error: unexpected debian package version for libsqlite3-0
+ info: expected: 3.39.4-1
+ info: installed: 3.40.1-1
+ info: consider retrying the bpkg command
+ EOE
+ libsqlite3 3.39.4 (libsqlite3-0 3.39.4-1) part installed
+ EOO
+
+
+ : not-installed
+ :
+ cat <<EOI >=libsqlite3-dev.policy;
+ libsqlite3-dev:
+ Installed: (none)
+ Candidate: 3.40.1-1
+ Version table:
+ 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libsqlite3-dev.show;
+ Package: libsqlite3-dev
+ Version: 3.40.1-1
+ Depends: libsqlite3-0 (= 3.40.1-1), libc-dev
+ EOI
+ cat <<EOI >=libsqlite3-0.policy;
+ libsqlite3-0:
+ Installed: (none)
+ Candidate: 3.40.1-1
+ Version table:
+ 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libsqlite3-0.policy-installed;
+ libsqlite3-0:
+ Installed: 3.40.1-1
+ Candidate: 3.40.1-1
+ Version table:
+ *** 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ 100 /var/lib/dpkg/status
+ EOI
+ $* libsqlite3 --install libsqlite3 <<EOI 2>>EOE >>EOO
+ apt-cache-policy: libsqlite3-dev libsqlite3-dev.policy
+ apt-cache-show: libsqlite3-dev 3.40.1-1 libsqlite3-dev.show
+ apt-cache-policy: libsqlite3-0 libsqlite3-0.policy
+ apt-cache-policy-installed: libsqlite3-0 libsqlite3-0.policy-installed
+ EOI
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev <libsqlite3-dev.policy
+ sudo apt-get update --quiet --assume-no
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev <libsqlite3-dev.policy
+ LC_ALL=C apt-cache show --quiet libsqlite3-dev=3.40.1-1 <libsqlite3-dev.show
+ LC_ALL=C apt-cache policy --quiet libsqlite3-0 <libsqlite3-0.policy
+ sudo apt-get install --quiet --assume-no libsqlite3-0 libsqlite3-dev
+ LC_ALL=C apt-cache policy --quiet libsqlite3-0 <libsqlite3-0.policy-installed
+ EOE
+ libsqlite3 3.40.1 (libsqlite3-0 3.40.1-1) not installed
+ EOO
+
+
+ : no-install
+ :
+ cat <<EOI >=libsqlite3-dev.policy;
+ libsqlite3-dev:
+ Installed: (none)
+ Candidate: 3.40.1-1
+ Version table:
+ 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ $* libsqlite3 <<EOI 2>>EOE != 0
+ apt-cache-policy: libsqlite3-dev libsqlite3-dev.policy
+ EOI
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev <libsqlite3-dev.policy
+ error: no installed system package for libsqlite3
+ EOE
+
+
+ : not-available
+ :
+ $* libsqlite3 --install libsqlite3 <<EOI 2>>EOE != 0
+ apt-cache-policy: libsqlite3-dev !
+ EOI
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev </dev/null
+ sudo apt-get update --quiet --assume-no
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev </dev/null
+ error: no installed or available system package for libsqlite3
+ EOE
+
+
+ : not-available-no-fetch
+ :
+ $* libsqlite3 --install --no-fetch libsqlite3 <<EOI 2>>EOE != 0
+ apt-cache-policy: libsqlite3-dev !
+ EOI
+ LC_ALL=C apt-cache policy --quiet libsqlite3-dev </dev/null
+ error: no installed or available system package for libsqlite3
+ EOE
+ }
+
+ : sqlite3
+ :
+ {
+ : installed
+ :
+ cat <<EOI >=sqlite3.policy;
+ sqlite3:
+ Installed: 3.40.1-1
+ Candidate: 3.40.1-1
+ Version table:
+ *** 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ 100 /var/lib/dpkg/status
+ EOI
+ $* sqlite3 --install sqlite3 <<EOI 2>>EOE >>EOO
+ apt-cache-policy: sqlite3 sqlite3.policy
+ EOI
+ LC_ALL=C apt-cache policy --quiet sqlite3 <sqlite3.policy
+ sudo apt-get install --quiet --assume-no sqlite3=3.40.1-1
+ LC_ALL=C apt-cache policy --quiet sqlite3 <sqlite3.policy
+ EOE
+ sqlite3 3.40.1 (sqlite3 3.40.1-1) installed
+ EOO
+
+ : not-installed
+ :
+ cat <<EOI >=sqlite3.policy;
+ sqlite3:
+ Installed: (none)
+ Candidate: 3.39.4-1
+ Version table:
+ 3.39.4-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=sqlite3.policy-fetched;
+ sqlite3:
+ Installed: (none)
+ Candidate: 3.40.1-1
+ Version table:
+ 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=sqlite3.policy-installed;
+ sqlite3:
+ Installed: 3.40.1-1
+ Candidate: 3.40.1-1
+ Version table:
+ *** 3.40.1-1 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ 100 /var/lib/dpkg/status
+ EOI
+ $* sqlite3 --install sqlite3 <<EOI 2>>EOE >>EOO
+ apt-cache-policy: sqlite3 sqlite3.policy
+ apt-cache-policy-fetched: sqlite3 sqlite3.policy-fetched
+ apt-cache-policy-installed: sqlite3 sqlite3.policy-installed
+ EOI
+ LC_ALL=C apt-cache policy --quiet sqlite3 <sqlite3.policy
+ sudo apt-get update --quiet --assume-no
+ LC_ALL=C apt-cache policy --quiet sqlite3 <sqlite3.policy-fetched
+ sudo apt-get install --quiet --assume-no sqlite3
+ LC_ALL=C apt-cache policy --quiet sqlite3 <sqlite3.policy-installed
+ EOE
+ sqlite3 3.40.1 (sqlite3 3.40.1-1) not installed
+ EOO
+ }
+
+ : libssl
+ :
+ {
+ +cat <<EOI >=libcrypto.manifest
+ : 1
+ name: libcrypto
+ version: 1.1.1+18
+ upstream-version: 1.1.1n
+ debian-name: libssl1.1 libssl-dev
+ debian-to-downstream-version: /1\.1\.1[a-z]/1.1.1/
+ summary: OpenSSL libcrypto
+ license: OpenSSL
+ EOI
+ +cat <<EOI >=libssl.manifest
+ : 1
+ name: libssl
+ version: 1.1.1+18
+ upstream-version: 1.1.1n
+ debian-name: libssl1.1 libssl-dev
+ debian-to-downstream-version: /1\.1\.1[a-z]/1.1.1/
+ summary: OpenSSL libssl
+ license: OpenSSL
+ EOI
+
+ : installed
+ :
+ ln -s ../libcrypto.manifest ./;
+ ln -s ../libssl.manifest ./;
+ cat <<EOI >=libssl1.1+libssl-dev.policy;
+ libssl1.1:
+ Installed: 1.1.1n-0+deb11u3
+ Candidate: 1.1.1n-0+deb11u3
+ Version table:
+ *** 1.1.1n-0+deb11u3 100
+ 100 /var/lib/dpkg/status
+ libssl-dev:
+ Installed: 1.1.1n-0+deb11u3
+ Candidate: 1.1.1n-0+deb11u3
+ Version table:
+ *** 1.1.1n-0+deb11u3 100
+ 100 /var/lib/dpkg/status
+ EOI
+ cat <<EOI >=libssl1.1.policy-installed;
+ libssl1.1:
+ Installed: 1.1.1n-0+deb11u3
+ Candidate: 1.1.1n-0+deb11u3
+ Version table:
+ *** 1.1.1n-0+deb11u3 100
+ 100 /var/lib/dpkg/status
+ EOI
+ $* libcrypto libssl --install libcrypto libssl <<EOI 2>>EOE >>EOO
+ manifest: libcrypto libcrypto.manifest
+ manifest: libssl libssl.manifest
+
+ apt-cache-policy: libssl1.1 libssl-dev libssl1.1+libssl-dev.policy
+ apt-cache-policy-installed: libssl1.1 libssl1.1.policy-installed
+ EOI
+ LC_ALL=C apt-cache policy --quiet libssl1.1 libssl-dev <libssl1.1+libssl-dev.policy
+ LC_ALL=C apt-cache policy --quiet libssl1.1 libssl-dev <libssl1.1+libssl-dev.policy
+ sudo apt-get install --quiet --assume-no libssl1.1=1.1.1n-0+deb11u3 libssl-dev=1.1.1n-0+deb11u3
+ LC_ALL=C apt-cache policy --quiet libssl1.1 <libssl1.1.policy-installed
+ EOE
+ libcrypto 1.1.1 (libssl1.1 1.1.1n-0+deb11u3) installed
+ libssl 1.1.1 (libssl1.1 1.1.1n-0+deb11u3) installed
+ EOO
+
+ : part-installed
+ :
+ ln -s ../libcrypto.manifest ./;
+ ln -s ../libssl.manifest ./;
+ cat <<EOI >=libssl1.1+libssl-dev.policy;
+ libssl1.1:
+ Installed: 1.1.1n-0+deb11u3
+ Candidate: 1.1.1n-0+deb11u3
+ Version table:
+ *** 1.1.1n-0+deb11u3 100
+ 100 /var/lib/dpkg/status
+ libssl-dev:
+ Installed: (none)
+ Candidate: 1.1.1n-0+deb11u3
+ Version table:
+ 1.1.1n-0+deb11u3 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libssl1.1.policy-installed;
+ libssl1.1:
+ Installed: 1.1.1n-0+deb11u3
+ Candidate: 1.1.1n-0+deb11u3
+ Version table:
+ *** 1.1.1n-0+deb11u3 100
+ 100 /var/lib/dpkg/status
+ EOI
+ $* libcrypto libssl --install libcrypto libssl <<EOI 2>>EOE >>EOO
+ manifest: libcrypto libcrypto.manifest
+ manifest: libssl libssl.manifest
+
+ apt-cache-policy: libssl1.1 libssl-dev libssl1.1+libssl-dev.policy
+ apt-cache-policy-installed: libssl1.1 libssl1.1.policy-installed
+ EOI
+ LC_ALL=C apt-cache policy --quiet libssl1.1 libssl-dev <libssl1.1+libssl-dev.policy
+ sudo apt-get update --quiet --assume-no
+ LC_ALL=C apt-cache policy --quiet libssl1.1 libssl-dev <libssl1.1+libssl-dev.policy
+ LC_ALL=C apt-cache policy --quiet libssl1.1 libssl-dev <libssl1.1+libssl-dev.policy
+ sudo apt-get install --quiet --assume-no libssl1.1 libssl-dev
+ LC_ALL=C apt-cache policy --quiet libssl1.1 <libssl1.1.policy-installed
+ EOE
+ libcrypto 1.1.1 (libssl1.1 1.1.1n-0+deb11u3) part installed
+ libssl 1.1.1 (libssl1.1 1.1.1n-0+deb11u3) part installed
+ EOO
+
+ : not-installed
+ :
+ ln -s ../libcrypto.manifest ./;
+ ln -s ../libssl.manifest ./;
+ cat <<EOI >=libssl1.1+libssl-dev.policy;
+ libssl1.1:
+ Installed: (none)
+ Candidate: 1.1.1n-0+deb11u3
+ Version table:
+ *** 1.1.1n-0+deb11u3 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ libssl-dev:
+ Installed: (none)
+ Candidate: 1.1.1n-0+deb11u3
+ Version table:
+ 1.1.1n-0+deb11u3 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libssl1.1.policy-installed;
+ libssl1.1:
+ Installed: 1.1.1n-0+deb11u3
+ Candidate: 1.1.1n-0+deb11u3
+ Version table:
+ *** 1.1.1n-0+deb11u3 100
+ 100 /var/lib/dpkg/status
+ EOI
+ $* libcrypto libssl --install libcrypto libssl <<EOI 2>>EOE >>EOO
+ manifest: libcrypto libcrypto.manifest
+ manifest: libssl libssl.manifest
+
+ apt-cache-policy: libssl1.1 libssl-dev libssl1.1+libssl-dev.policy
+ apt-cache-policy-installed: libssl1.1 libssl1.1.policy-installed
+ EOI
+ LC_ALL=C apt-cache policy --quiet libssl1.1 libssl-dev <libssl1.1+libssl-dev.policy
+ sudo apt-get update --quiet --assume-no
+ LC_ALL=C apt-cache policy --quiet libssl1.1 libssl-dev <libssl1.1+libssl-dev.policy
+ LC_ALL=C apt-cache policy --quiet libssl1.1 libssl-dev <libssl1.1+libssl-dev.policy
+ sudo apt-get install --quiet --assume-no libssl1.1 libssl-dev
+ LC_ALL=C apt-cache policy --quiet libssl1.1 <libssl1.1.policy-installed
+ EOE
+ libcrypto 1.1.1 (libssl1.1 1.1.1n-0+deb11u3) not installed
+ libssl 1.1.1 (libssl1.1 1.1.1n-0+deb11u3) not installed
+ EOO
+ }
+
+ : libcurl
+ :
+ {
+ # Note that libcurl3-gnutls libcurl4-gnutls-dev is not a mistake.
+ #
+ # Note also that there is a third flavor, libcurl3-nss libcurl4-nss-dev,
+ # but we omit it to keep the tests manageable.
+ #
+ #
+ +cat <<EOI >=libcurl.manifest
+ : 1
+ name: libcurl
+ version: 7.84.0
+ debian-name: libcurl4 libcurl4-openssl-dev libcurl4-doc
+ debian-name: libcurl3-gnutls libcurl4-gnutls-dev libcurl4-doc
+ summary: C library for transferring data with URLs
+ license: curl
+ EOI
+
+
+ : one-full-installed
+ :
+ ln -s ../libcurl.manifest ./;
+ cat <<EOI >=libcurl4+libcurl4-openssl-dev.policy;
+ libcurl4:
+ Installed: 7.85.0-1
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ *** 7.85.0-1 100
+ 100 /var/lib/dpkg/status
+ libcurl4-openssl-dev:
+ Installed: 7.85.0-1
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ *** 7.85.0-1 100
+ 100 /var/lib/dpkg/status
+ EOI
+ cat <<EOI >=libcurl3-gnutls+libcurl4-gnutls-dev.policy;
+ libcurl3-gnutls:
+ Installed: 7.85.0-1
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ *** 7.85.0-1 100
+ 100 /var/lib/dpkg/status
+ libcurl4-gnutls-dev:
+ Installed: (none)
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libcurl4.policy-installed;
+ libcurl4:
+ Installed: 7.85.0-1
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ *** 7.85.0-1 100
+ 100 /var/lib/dpkg/status
+ EOI
+ $* libcurl --install libcurl <<EOI 2>>EOE >>EOO
+ manifest: libcurl libcurl.manifest
+
+ apt-cache-policy: libcurl4 libcurl4-openssl-dev libcurl4+libcurl4-openssl-dev.policy
+ apt-cache-policy: libcurl3-gnutls libcurl4-gnutls-dev libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ apt-cache-policy-installed: libcurl4 libcurl4.policy-installed
+ EOI
+ LC_ALL=C apt-cache policy --quiet libcurl4 libcurl4-openssl-dev <libcurl4+libcurl4-openssl-dev.policy
+ LC_ALL=C apt-cache policy --quiet libcurl3-gnutls libcurl4-gnutls-dev <libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ sudo apt-get install --quiet --assume-no libcurl4=7.85.0-1 libcurl4-openssl-dev=7.85.0-1
+ LC_ALL=C apt-cache policy --quiet libcurl4 <libcurl4.policy-installed
+ EOE
+ libcurl 7.85.0 (libcurl4 7.85.0-1) installed
+ EOO
+
+
+ : one-part-installed
+ :
+ ln -s ../libcurl.manifest ./;
+ cat <<EOI >=libcurl4+libcurl4-openssl-dev.policy;
+ libcurl4:
+ Installed: 7.85.0-1
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ *** 7.85.0-1 100
+ 100 /var/lib/dpkg/status
+ libcurl4-openssl-dev:
+ Installed: (none)
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libcurl3-gnutls+libcurl4-gnutls-dev.policy;
+ libcurl3-gnutls:
+ Installed: (none)
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ libcurl4-gnutls-dev:
+ Installed: (none)
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libcurl4.policy-installed;
+ libcurl4:
+ Installed: 7.87.0-2
+ Candidate: 7.87.0-2
+ Version table:
+ *** 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ 100 /var/lib/dpkg/status
+ EOI
+ $* libcurl --install libcurl <<EOI 2>>EOE >>EOO
+ manifest: libcurl libcurl.manifest
+
+ apt-cache-policy: libcurl4 libcurl4-openssl-dev libcurl4+libcurl4-openssl-dev.policy
+ apt-cache-policy: libcurl3-gnutls libcurl4-gnutls-dev libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ apt-cache-policy-installed: libcurl4 libcurl4.policy-installed
+ EOI
+ LC_ALL=C apt-cache policy --quiet libcurl4 libcurl4-openssl-dev <libcurl4+libcurl4-openssl-dev.policy
+ LC_ALL=C apt-cache policy --quiet libcurl3-gnutls libcurl4-gnutls-dev <libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ sudo apt-get update --quiet --assume-no
+ LC_ALL=C apt-cache policy --quiet libcurl4 libcurl4-openssl-dev <libcurl4+libcurl4-openssl-dev.policy
+ LC_ALL=C apt-cache policy --quiet libcurl3-gnutls libcurl4-gnutls-dev <libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ sudo apt-get install --quiet --assume-no libcurl4 libcurl4-openssl-dev
+ LC_ALL=C apt-cache policy --quiet libcurl4 <libcurl4.policy-installed
+ EOE
+ libcurl 7.87.0 (libcurl4 7.87.0-2) part installed
+ EOO
+
+
+ : none-installed
+ :
+ ln -s ../libcurl.manifest ./;
+ cat <<EOI >=libcurl4+libcurl4-openssl-dev.policy;
+ libcurl4:
+ Installed: (none)
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ libcurl4-openssl-dev:
+ Installed: (none)
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libcurl3-gnutls+libcurl4-gnutls-dev.policy;
+ libcurl3-gnutls:
+ Installed: (none)
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ libcurl4-gnutls-dev:
+ Installed: (none)
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ $* libcurl --install libcurl <<EOI 2>>EOE != 0
+ manifest: libcurl libcurl.manifest
+
+ apt-cache-policy: libcurl4 libcurl4-openssl-dev libcurl4+libcurl4-openssl-dev.policy
+ apt-cache-policy: libcurl3-gnutls libcurl4-gnutls-dev libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ EOI
+ LC_ALL=C apt-cache policy --quiet libcurl4 libcurl4-openssl-dev <libcurl4+libcurl4-openssl-dev.policy
+ LC_ALL=C apt-cache policy --quiet libcurl3-gnutls libcurl4-gnutls-dev <libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ sudo apt-get update --quiet --assume-no
+ LC_ALL=C apt-cache policy --quiet libcurl4 libcurl4-openssl-dev <libcurl4+libcurl4-openssl-dev.policy
+ LC_ALL=C apt-cache policy --quiet libcurl3-gnutls libcurl4-gnutls-dev <libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ error: multiple available debian packages for libcurl
+ info: candidate: libcurl4 7.87.0-2
+ info: candidate: libcurl3-gnutls 7.87.0-2
+ info: consider installing the desired package manually and retrying the bpkg command
+ EOE
+
+
+ : both-part-installed
+ :
+ ln -s ../libcurl.manifest ./;
+ cat <<EOI >=libcurl4+libcurl4-openssl-dev.policy;
+ libcurl4:
+ Installed: 7.85.0-1
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ *** 7.85.0-1 100
+ 100 /var/lib/dpkg/status
+ libcurl4-openssl-dev:
+ Installed: (none)
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ cat <<EOI >=libcurl3-gnutls+libcurl4-gnutls-dev.policy;
+ libcurl3-gnutls:
+ Installed: 7.85.0-1
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ *** 7.85.0-1 100
+ 100 /var/lib/dpkg/status
+ libcurl4-gnutls-dev:
+ Installed: (none)
+ Candidate: 7.87.0-2
+ Version table:
+ 7.87.0-2 500
+ 500 http://deb.debian.org/debian bookworm/main amd64 Packages
+ EOI
+ $* libcurl --install libcurl <<EOI 2>>EOE != 0
+ manifest: libcurl libcurl.manifest
+
+ apt-cache-policy: libcurl4 libcurl4-openssl-dev libcurl4+libcurl4-openssl-dev.policy
+ apt-cache-policy: libcurl3-gnutls libcurl4-gnutls-dev libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ EOI
+ LC_ALL=C apt-cache policy --quiet libcurl4 libcurl4-openssl-dev <libcurl4+libcurl4-openssl-dev.policy
+ LC_ALL=C apt-cache policy --quiet libcurl3-gnutls libcurl4-gnutls-dev <libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ sudo apt-get update --quiet --assume-no
+ LC_ALL=C apt-cache policy --quiet libcurl4 libcurl4-openssl-dev <libcurl4+libcurl4-openssl-dev.policy
+ LC_ALL=C apt-cache policy --quiet libcurl3-gnutls libcurl4-gnutls-dev <libcurl3-gnutls+libcurl4-gnutls-dev.policy
+ error: multiple partially installed debian packages for libcurl
+ info: candidate: libcurl4 7.87.0-2, missing components: libcurl4-openssl-dev
+ info: candidate: libcurl3-gnutls 7.87.0-2, missing components: libcurl4-gnutls-dev
+ info: consider fully installing the desired package manually and retrying the bpkg command
+ EOE
+ }
+}
diff --git a/bpkg/system-package-manager.cxx b/bpkg/system-package-manager.cxx
new file mode 100644
index 0000000..9f5dad0
--- /dev/null
+++ b/bpkg/system-package-manager.cxx
@@ -0,0 +1,454 @@
+// file : bpkg/system-package-manager.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <bpkg/system-package-manager.hxx>
+
+#include <sstream>
+
+#include <libbutl/regex.hxx>
+#include <libbutl/semantic-version.hxx>
+
+#include <bpkg/package.hxx>
+#include <bpkg/package-odb.hxx>
+#include <bpkg/database.hxx>
+#include <bpkg/diagnostics.hxx>
+
+#include <bpkg/system-package-manager-debian.hxx>
+
+using namespace std;
+using namespace butl;
+
+namespace bpkg
+{
+ system_package_manager::
+ ~system_package_manager ()
+ {
+ // vtable
+ }
+
+ unique_ptr<system_package_manager>
+ make_system_package_manager (const common_options& co,
+ const target_triplet& host,
+ bool install,
+ bool fetch,
+ bool yes,
+ const string& sudo,
+ const string& name)
+ {
+ optional<bool> progress (co.progress () ? true :
+ co.no_progress () ? false :
+ optional<bool> ());
+
+ unique_ptr<system_package_manager> r;
+
+ if (optional<os_release> osr = host_os_release (host))
+ {
+ auto is_or_like = [&osr] (const char* id)
+ {
+ return (osr->name_id == id ||
+ find_if (osr->like_ids.begin (), osr->like_ids.end (),
+ [id] (const string& n)
+ {
+ return n == id;
+ }) != osr->like_ids.end ());
+ };
+
+ if (host.class_ == "linux")
+ {
+ if (is_or_like ("debian") ||
+ is_or_like ("ubuntu"))
+ {
+ if (!name.empty () && name != "debian")
+ fail << "unsupported package manager '" << name << "' for "
+ << osr->name_id << " host";
+
+ // If we recognized this as Debian-like in an ad hoc manner, then
+ // add debian to like_ids.
+ //
+ if (osr->name_id != "debian" && !is_or_like ("debian"))
+ osr->like_ids.push_back ("debian");
+
+ r.reset (new system_package_manager_debian (
+ move (*osr), host, install, fetch, progress, yes, sudo));
+ }
+ }
+ }
+
+ if (r == nullptr)
+ {
+ if (!name.empty ())
+ fail << "unsupported package manager '" << name << "' for host "
+ << host;
+ }
+
+ return r;
+ }
+
+ // Return the version id parsed as a semantic version if it is not empty and
+ // the "0" semantic version otherwise. Issue diagnostics and fail on parsing
+ // errors.
+ //
+ // Note: the name_id argument is only used for diagnostics.
+ //
+ static inline semantic_version
+ parse_version_id (const string& version_id, const string& name_id)
+ {
+ if (version_id.empty ())
+ return semantic_version (0, 0, 0);
+
+ try
+ {
+ return semantic_version (version_id, semantic_version::allow_omit_minor);
+ }
+ catch (const invalid_argument& e)
+ {
+ fail << "invalid version '" << version_id << "' for " << name_id
+ << " host: " << e << endf;
+ }
+ }
+
+ // Parse the <distribution> component of the specified <distribution>-*
+ // value into the distribution name and version (return as "0" if not
+ // present). Issue diagnostics and fail on parsing errors.
+ //
+ // Note: the value_name, ap, and af arguments are only used for diagnostics.
+ //
+ static pair<string, semantic_version>
+ parse_distribution (string&& d,
+ const string& value_name,
+ const shared_ptr<available_package>& ap,
+ const lazy_shared_ptr<repository_fragment>& af)
+ {
+ string dn (move (d)); // <name>[_<version>]
+ size_t p (dn.rfind ('_')); // Version-separating underscore.
+
+ // If the '_' separator is present, then make sure that the right-hand
+ // part looks like a version (not empty and only contains digits and
+ // dots).
+ //
+ if (p != string::npos)
+ {
+ if (p != dn.size () - 1)
+ {
+ for (size_t i (p + 1); i != dn.size (); ++i)
+ {
+ if (!digit (dn[i]) && dn[i] != '.')
+ {
+ p = string::npos;
+ break;
+ }
+ }
+ }
+ else
+ p = string::npos;
+ }
+
+ // Parse the distribution version if present and leave it "0" otherwise.
+ //
+ semantic_version dv (0, 0, 0);
+ if (p != string::npos)
+ try
+ {
+ dv = semantic_version (dn,
+ p + 1,
+ semantic_version::allow_omit_minor);
+
+ dn.resize (p);
+ }
+ catch (const invalid_argument& e)
+ {
+ // Note: the repository fragment may have no database associated when
+ // used in tests.
+ //
+ shared_ptr<repository_fragment> f (af.get_eager ());
+ database* db (!(f != nullptr && !af.loaded ()) // Not transient?
+ ? &af.database ()
+ : nullptr);
+
+ diag_record dr (fail);
+ dr << "invalid distribution version '" << string (dn, p + 1)
+ << "' in value " << value_name << " for package " << ap->id.name
+ << ' ' << ap->version;
+
+ if (db != nullptr)
+ dr << *db;
+
+ dr << " in repository " << (f != nullptr ? f : af.load ())->location
+ << ": " << e;
+ }
+
+ return make_pair (move (dn), move (dv));
+ }
+
+ strings system_package_manager::
+ system_package_names (const available_packages& aps,
+ const string& name_id,
+ const string& version_id,
+ const vector<string>& like_ids)
+ {
+ assert (!aps.empty ());
+
+ semantic_version vid (parse_version_id (version_id, name_id));
+
+ // Return those <name>[_<version>]-name distribution values of the
+ // specified available packages whose <name> component matches the
+ // specified distribution name and the <version> component (assumed as "0"
+ // if not present) is less or equal the specified distribution version.
+ // Suppress duplicate values.
+ //
+ auto name_values = [&aps] (const string& n, const semantic_version& v)
+ {
+ strings r;
+
+ // For each available package sort the system package names in the
+ // distribution version descending order and then append them to the
+ // resulting list, keeping this order and suppressing duplicates.
+ //
+ using name_version = pair<string, semantic_version>;
+ vector<name_version> nvs; // Reuse the buffer.
+
+ for (const auto& a: aps)
+ {
+ nvs.clear ();
+
+ const shared_ptr<available_package>& ap (a.first);
+
+ for (const distribution_name_value& dv: ap->distribution_values)
+ {
+ if (optional<string> d = dv.distribution ("-name"))
+ {
+ pair<string, semantic_version> dnv (
+ parse_distribution (move (*d), dv.name, ap, a.second));
+
+ if (dnv.first == n && dnv.second <= v)
+ {
+ // Add the name/version pair to the sorted vector.
+ //
+ name_version nv (make_pair (dv.value, move (dnv.second)));
+
+ nvs.insert (upper_bound (nvs.begin (), nvs.end (), nv,
+ [] (const name_version& x,
+ const name_version& y)
+ {return x.second > y.second;}),
+ move (nv));
+ }
+ }
+ }
+
+ // Append the sorted names to the resulting list.
+ //
+ for (name_version& nv: nvs)
+ {
+ if (find_if (r.begin (), r.end (),
+ [&nv] (const string& n) {return nv.first == n;}) ==
+ r.end ())
+ {
+ r.push_back (move (nv.first));
+ }
+ }
+ }
+
+ return r;
+ };
+
+ // Collect distribution values for those <distribution>-name names which
+ // match the name id and refer to the version which is less or equal than
+ // the version id.
+ //
+ strings r (name_values (name_id, vid));
+
+ // If the resulting list is empty and the like ids are specified, then
+ // re-collect but now using the like id and "0" version id instead.
+ //
+ if (r.empty ())
+ {
+ for (const string& like_id: like_ids)
+ {
+ r = name_values (like_id, semantic_version (0, 0, 0));
+ if (!r.empty ())
+ break;
+ }
+ }
+
+ return r;
+ }
+
+ optional<version> system_package_manager::
+ downstream_package_version (const string& system_version,
+ const available_packages& aps,
+ const string& name_id,
+ const string& version_id,
+ const vector<string>& like_ids)
+ {
+ semantic_version vid (parse_version_id (version_id, name_id));
+
+ // Iterate over the passed available packages (in version descending
+ // order) and over the <name>[_<version>]-to-downstream-version
+ // distribution values they contain. Only consider those values whose
+ // <name> component matches the specified distribution name and the
+ // <version> component (assumed as "0" if not present) is less or equal
+ // the specified distribution version. For such values match the regex
+ // pattern against the passed system version and if it matches consider
+ // the replacement as the resulting downstream version candidate. Return
+ // this downstream version if the distribution version is equal to the
+ // specified one. Otherwise (the version is less), continue iterating
+ // while preferring downstream version candidates for greater distribution
+ // versions. Note that here we are trying to use a version mapping for the
+ // distribution version closest (but never greater) to the specified
+ // distribution version. So, for example, if both following values contain
+ // a matching mapping, then for debian 11 we prefer the downstream version
+ // produced by the debian_10-to-downstream-version value:
+ //
+ // debian_9-to-downstream-version
+ // debian_10-to-downstream-version
+ //
+ auto downstream_version = [&aps, &system_version]
+ (const string& n,
+ const semantic_version& v) -> optional<version>
+ {
+ optional<version> r;
+ semantic_version rv;
+
+ for (const auto& a: aps)
+ {
+ const shared_ptr<available_package>& ap (a.first);
+
+ for (const distribution_name_value& nv: ap->distribution_values)
+ {
+ if (optional<string> d = nv.distribution ("-to-downstream-version"))
+ {
+ pair<string, semantic_version> dnv (
+ parse_distribution (move (*d), nv.name, ap, a.second));
+
+ if (dnv.first == n && dnv.second <= v)
+ {
+ auto bad_value = [&nv, &ap, &a] (const string& d)
+ {
+ // Note: the repository fragment may have no database
+ // associated when used in tests.
+ //
+ const lazy_shared_ptr<repository_fragment>& af (a.second);
+ shared_ptr<repository_fragment> f (af.get_eager ());
+ database* db (!(f != nullptr && !af.loaded ()) // Not transient?
+ ? &af.database ()
+ : nullptr);
+
+ diag_record dr (fail);
+ dr << "invalid distribution value '" << nv.name << ": "
+ << nv.value << "' for package " << ap->id.name << ' '
+ << ap->version;
+
+ if (db != nullptr)
+ dr << *db;
+
+ dr << " in repository "
+ << (f != nullptr ? f : af.load ())->location << ": " << d;
+ };
+
+ // Parse the distribution value into the regex pattern and the
+ // replacement.
+ //
+ // Note that in the future we may add support for some regex
+ // flags.
+ //
+ pair<string, string> rep;
+ try
+ {
+ size_t end;
+ const string& val (nv.value);
+ rep = regex_replace_parse (val.c_str (), val.size (), end);
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value (e.what ());
+ }
+
+ // Match the regex pattern against the system version and skip
+ // the value if it doesn't match or proceed to parsing the
+ // downstream version resulting from the regex replacement
+ // otherwise.
+ //
+ string dv;
+ try
+ {
+ regex re (rep.first, regex::ECMAScript);
+
+ pair<string, bool> rr (
+ regex_replace_match (system_version, re, rep.second));
+
+ // Skip the regex if it doesn't match.
+ //
+ if (!rr.second)
+ continue;
+
+ dv = move (rr.first);
+ }
+ catch (const regex_error& e)
+ {
+ // Print regex_error description if meaningful (no space).
+ //
+ ostringstream os;
+ os << "invalid regex pattern '" << rep.first << "'" << e;
+ bad_value (os.str ());
+ }
+
+ // Parse the downstream version.
+ //
+ try
+ {
+ version ver (dv);
+
+ // If the distribution version is equal to the specified one,
+ // then we are done. Otherwise, save the version if it is
+ // preferable and continue iterating.
+ //
+ // Note that bailing out immediately in the former case is
+ // essential. Otherwise, we can potentially fail later on, for
+ // example, some ill-formed regex which is already fixed in
+ // some newer package.
+ //
+ if (dnv.second == v)
+ return ver;
+
+ if (!r || rv < dnv.second)
+ {
+ r = move (ver);
+ rv = move (dnv.second);
+ }
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value ("resulting downstream version '" + dv +
+ "' is invalid: " + e.what ());
+ }
+ }
+ }
+ }
+ }
+
+ return r;
+ };
+
+ // Try to deduce the downstream version using the
+ // <distribution>-to-downstream-version values that match the name id and
+ // refer to the version which is less or equal than the version id.
+ //
+ optional<version> r (downstream_version (name_id, vid));
+
+ // If the downstream version is not deduced and the like ids are
+ // specified, then re-try but now using the like id and "0" version id
+ // instead.
+ //
+ if (!r)
+ {
+ for (const string& like_id: like_ids)
+ {
+ r = downstream_version (like_id, semantic_version (0, 0, 0));
+ if (r)
+ break;
+ }
+ }
+
+ return r;
+ }
+}
diff --git a/bpkg/system-package-manager.hxx b/bpkg/system-package-manager.hxx
new file mode 100644
index 0000000..9a9c443
--- /dev/null
+++ b/bpkg/system-package-manager.hxx
@@ -0,0 +1,270 @@
+// file : bpkg/system-package-manager.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BPKG_SYSTEM_PACKAGE_MANAGER_HXX
+#define BPKG_SYSTEM_PACKAGE_MANAGER_HXX
+
+#include <libbpkg/manifest.hxx> // version
+#include <libbpkg/package-name.hxx>
+
+#include <bpkg/types.hxx>
+#include <bpkg/utility.hxx>
+
+#include <bpkg/package.hxx>
+#include <bpkg/common-options.hxx>
+#include <bpkg/host-os-release.hxx>
+
+namespace bpkg
+{
+ // The system/distribution package manager interface. Used by both pkg-build
+ // (to query and install system packages) and by pkg-bindist (to build
+ // them).
+ //
+ // Note that currently the result of a query is a single available version.
+ // While some package managers may support having multiple available
+ // versions and may even allow installing multiple versions in parallel,
+ // supporting this on our side will complicate things quite a bit. While we
+ // can probably plug multiple available versions into our constraint
+ // satisfaction machinery, the rabbit hole goes deeper than that since, for
+ // example, different bpkg packages can be mapped to the same system
+ // package, as is the case for libcrypto/libssl which are both mapped to
+ // libssl on Debian. This means we will need to somehow coordinate (and
+ // likely backtrack) version selection between unrelated bpkg packages
+ // because only one underlying system version can be selected. (One
+ // simplified way to handle this would be to detect that different versions
+ // we selected and fail asking the user to resolve this manually.)
+ //
+ // Additionally, parallel installation is unlikely to be suppored for the
+ // packages we are interested in due to the underlying limitations.
+ // Specifically, the packages that we are primarily interested in are
+ // libraries with headers and executables (tools). While most package
+ // managers (e.g., Debian, Fedora) are able to install multiple libraries in
+ // parallel, they normally can only install a single set of headers, static
+ // libraries, pkg-config files, etc., (e.g., -dev/-devel package) at a time
+ // due to them being installed into the same location (e.g., /usr/include).
+ // The same holds for executables, which are installed into the same
+ // location (e.g., /usr/bin).
+ //
+ // It is possible that a certain library has made arrangements for
+ // multiple of its versions to co-exist. For example, hypothetically, our
+ // libssl package could be mapped to both libssl1.1 libssl1.1-dev and
+ // libssl3 libssl3-dev which could be installed at the same time (note
+ // that it is not the case in reality; there is only libssl-dev). However,
+ // in this case, we should probably also have two packages with separate
+ // names (e.g., libssl and libssl3) that can also co-exist. An example of
+ // this would be libQt5Core and libQt6Core. (Note that strictly speaking
+ // there could be different degrees of co-existence: for the system
+ // package manager it is sufficient for different versions not to clobber
+ // each other's files while for us we may also need the ability to use
+ // different versions in the base build).
+ //
+ // Note also that the above reasoning is quite C/C++-centric and it's
+ // possible that multiple versions of libraries (or equivalent) for other
+ // languages (e.g., Rust) can always co-exist. Plus, even in the case of
+ // C/C++ libraries, there is still the plausible case of picking one of
+ // the multiple available version.
+ //
+ // On the other hand, the ultimate goal of system package managers, at least
+ // traditional ones like Debian and Fedora, is to end up with a single,
+ // usually the latest available, version of the package that is used by
+ // everyone. In fact, if one looks at a stable distributions of Debian and
+ // Fedora, they normally provide only a single version of each package. This
+ // decision will also likely simplify the implementation. For example, on
+ // Debian, it's straightforward to get the installed and candidate versions
+ // (e.g., from apt-cache policy). But getting all the possible versions that
+ // can be installed without having to specify the release explicitly is a
+ // lot less straightforward (see the apt-cache command documentation in The
+ // Debian Administrator's Handbook for background).
+ //
+ // So for now we keep it simple and pick a single available version but can
+ // probably revise this decision later.
+ //
+ struct system_package_status
+ {
+ // Downstream (as in, bpkg package) version.
+ //
+ bpkg::version version;
+
+ // System (as in, distribution) package name and version for diagnostics.
+ //
+ // Note that this status may represent multiple system packages (for
+ // example, libfoo and libfoo-dev) and here we have only the
+ // main/representative package name (for example, libfoo).
+ //
+ string system_name;
+ string system_version;
+
+ // The system package can be either "available already installed",
+ // "available partially installed" (for example, libfoo but not
+ // libfoo-dev is installed) or "available not yet installed".
+ //
+ enum status_type {installed, partially_installed, not_installed};
+
+ status_type status = not_installed;
+ };
+
+ class system_package_manager
+ {
+ public:
+ // Query the system package status.
+ //
+ // This function has two modes: cache-only (available_packages is NULL)
+ // and full (available_packages is not NULL). In the cache-only mode this
+ // function returns the status of this package if it has already been
+ // queried and nullopt otherwise. This allows the caller to only collect
+ // all the available packages (for the name/version mapping information)
+ // if really necessary.
+ //
+ // The returned status can be NULL, which indicates that no such package
+ // is available from the system package manager. Note that NULL is also
+ // returned if no fully installed package is available from the system and
+ // package installation is not enabled (see the constructor below).
+ //
+ // Note also that the implementation is expected to issue appropriate
+ // progress and diagnostics if fetching package metadata (again see the
+ // constructor below).
+ //
+ virtual optional<const system_package_status*>
+ pkg_status (const package_name&, const available_packages*) = 0;
+
+ // Install the specified subset of the previously-queried packages.
+ // Should only be called if installation is enabled (see the constructor
+ // below).
+ //
+ // Note that this function should be called only once after the final set
+ // of the required system packages has been determined. And the specified
+ // subset should contain all the selected packages, including the already
+ // fully installed. This allows the implementation to merge and de-
+ // duplicate the system package set to be installed (since some bpkg
+ // packages may be mapped to the same system package), perform post-
+ // installation verifications (such as making sure the versions of already
+ // installed packages have not changed due to upgrades), change properties
+ // of already installed packages (e.g., mark them as manually installed in
+ // Debian), etc.
+ //
+ // Note also that the implementation is expected to issue appropriate
+ // progress and diagnostics.
+ //
+ virtual void
+ pkg_install (const vector<package_name>&) = 0;
+
+ public:
+ // If install is true, then enable package installation.
+ //
+ // If fetch is false, then do not re-fetch the system package repository
+ // metadata (that is, available packages/versions) before querying for the
+ // available version of the not yet installed or partially installed
+ // packages.
+ //
+ system_package_manager (os_release&& osr,
+ const target_triplet& host,
+ bool install,
+ bool fetch,
+ optional<bool> progress,
+ bool yes,
+ string sudo)
+ : os_release_ (osr),
+ host_ (host),
+ progress_ (progress),
+ install_ (install),
+ fetch_ (fetch),
+ yes_ (yes),
+ sudo_ (sudo != "false" ? move (sudo) : string ()) {}
+
+ virtual
+ ~system_package_manager ();
+
+ // Implementation details.
+ //
+ public:
+ // Given the available packages (as returned by find_available_all())
+ // return the list of system package names as mapped by the
+ // <distribution>-name values.
+ //
+ // The name_id, version_id, and like_ids are the values from os_release
+ // (refer there for background). If version_id is empty, then it's treated
+ // as "0".
+ //
+ // First consider <distribution>-name values corresponding to name_id.
+ // Assume <distribution> has the <name>[_<version>] form, where <version>
+ // is a semver-like version (e.g, 10, 10.15, or 10.15.1) and return all
+ // the values that are equal or less than the specified version_id
+ // (include the value with the absent <version>). In a sense, absent
+ // <version> can be treated as a 0 semver-like version.
+ //
+ // If no value is found then repeat the above process for every like_ids
+ // entry (from left to right) instead of name_id with version_id equal 0.
+ //
+ // If still no value is found, then return empty list (in which case the
+ // caller may choose to fallback to the downstream package name or do
+ // something more elaborate, like translate version_id to one of the
+ // like_id's version and try that).
+ //
+ // Note that multiple -name values per same distribution can be returned
+ // as, for example, for the following distribution values:
+ //
+ // debian_10-name: libcurl4 libcurl4-doc libcurl4-openssl-dev
+ // debian_10-name: libcurl3-gnutls libcurl4-gnutls-dev (yes, 3 and 4)
+ //
+ // Note also that the values are returned in the "override order", that is
+ // from the newest package version to oldest and then from the highest
+ // distribution version to lowest.
+ //
+ static strings
+ system_package_names (const available_packages&,
+ const string& name_id,
+ const string& version_id,
+ const vector<string>& like_ids);
+
+ // Given the system package version and available packages (as returned by
+ // find_available_all()) return the downstream package version as mapped
+ // by one of the <distribution>-to-downstream-version values.
+ //
+ // The rest of the arguments as well as the overalls semantics is the same
+ // as in system_package_names() above. That is, first consider
+ // <distribution>-to-downstream-version values corresponding to
+ // name_id. If none match, then repeat the above process for every
+ // like_ids entry with version_id equal 0. If still no match, then return
+ // nullopt (in which case the caller may choose to fallback to the system
+ // package version or do something more elaborate).
+ //
+ static optional<version>
+ downstream_package_version (const string& system_version,
+ const available_packages&,
+ const string& name_id,
+ const string& version_id,
+ const vector<string>& like_ids);
+ protected:
+ os_release os_release_;
+ target_triplet host_;
+ optional<bool> progress_; // --[no]-progress (see also stderr_term)
+
+ // The --sys-* option values.
+ //
+ bool install_;
+ bool fetch_;
+ bool yes_;
+ string sudo_;
+ };
+
+ // Create a package manager instance corresponding to the specified host
+ // target and optional manager name. If name is empty, return NULL if there
+ // is no support for this platform. Currently recognized names:
+ //
+ // debian -- Debian and alike (Ubuntu, etc) using the APT frontend.
+ // fedora -- Fedora and alike (RHEL, Centos, etc) using the DNF frontend.
+ //
+ // Note: the name can be used to select an alternative package manager
+ // implementation on platforms that support multiple.
+ //
+ unique_ptr<system_package_manager>
+ make_system_package_manager (const common_options&,
+ const target_triplet&,
+ bool install,
+ bool fetch,
+ bool yes,
+ const string& sudo,
+ const string& name);
+}
+
+#endif // BPKG_SYSTEM_PACKAGE_MANAGER_HXX
diff --git a/bpkg/system-package-manager.test.cxx b/bpkg/system-package-manager.test.cxx
new file mode 100644
index 0000000..1a669da
--- /dev/null
+++ b/bpkg/system-package-manager.test.cxx
@@ -0,0 +1,124 @@
+// file : bpkg/system-package-manager.test.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <bpkg/system-package-manager.hxx>
+
+#include <iostream>
+
+#include <bpkg/types.hxx>
+#include <bpkg/utility.hxx>
+
+#undef NDEBUG
+#include <cassert>
+
+#include <bpkg/system-package-manager.test.hxx>
+
+using namespace std;
+
+namespace bpkg
+{
+ // Usage: args[0] <command> ...
+ //
+ // Where <command> is one of:
+ //
+ // system-package-names <name-id> <ver-id> [<like-id>...] -- <pkg> <file>...
+ //
+ // Where <pkg> is a package name, <file> is a package manifest file.
+ //
+ // downstream-package-version <name-id> <ver-id> [<like-id>...] -- <ver> <pkg> <file>...
+ //
+ // Where <ver> is a system version to translate, <pkg> is a package
+ // name, and <file> is a package manifest file.
+ //
+ int
+ main (int argc, char* argv[])
+ try
+ {
+ assert (argc >= 2); // <command>
+
+ int argi (1);
+ string cmd (argv[argi++]);
+
+ os_release osr;
+ if (cmd == "system-package-names" ||
+ cmd == "downstream-package-version")
+ {
+ assert (argc >= 4); // <name-id> <ver-id>
+
+ osr.name_id = argv[argi++];
+ osr.version_id = argv[argi++];
+
+ for (; argi != argc; ++argi)
+ {
+ string a (argv[argi]);
+
+ if (a == "--")
+ break;
+
+ osr.like_ids.push_back (move (a));
+ }
+ }
+
+ if (cmd == "system-package-names")
+ {
+ assert (argi != argc); // --
+ string a (argv[argi++]);
+ assert (a == "--");
+
+ assert (argi != argc); // <pkg>
+ string pn (argv[argi++]);
+
+ assert (argi != argc); // <file>
+ available_packages aps;
+ for (; argi != argc; ++argi)
+ aps.push_back (make_available_from_manifest (pn, argv[argi]));
+ sort_available (aps);
+
+ strings ns (
+ system_package_manager::system_package_names (
+ aps, osr.name_id, osr.version_id, osr.like_ids));
+
+ for (const string& n: ns)
+ cout << n << '\n';
+ }
+ else if (cmd == "downstream-package-version")
+ {
+ assert (argi != argc); // --
+ string a (argv[argi++]);
+ assert (a == "--");
+
+ assert (argi != argc); // <ver>
+ string sv (argv[argi++]);
+
+ assert (argi != argc); // <pkg>
+ string pn (argv[argi++]);
+
+ assert (argi != argc); // <file>
+ available_packages aps;
+ for (; argi != argc; ++argi)
+ aps.push_back (make_available_from_manifest (pn, argv[argi]));
+ sort_available (aps);
+
+ optional<version> v (
+ system_package_manager::downstream_package_version (
+ sv, aps, osr.name_id, osr.version_id, osr.like_ids));
+
+ if (v)
+ cout << *v << '\n';
+ }
+ else
+ fail << "unknown command '" << cmd << "'";
+
+ return 0;
+ }
+ catch (const failed&)
+ {
+ return 1;
+ }
+}
+
+int
+main (int argc, char* argv[])
+{
+ return bpkg::main (argc, argv);
+}
diff --git a/bpkg/system-package-manager.test.hxx b/bpkg/system-package-manager.test.hxx
new file mode 100644
index 0000000..0eb6717
--- /dev/null
+++ b/bpkg/system-package-manager.test.hxx
@@ -0,0 +1,104 @@
+// file : bpkg/system-package-manager.test.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BPKG_SYSTEM_PACKAGE_MANAGER_TEST_HXX
+#define BPKG_SYSTEM_PACKAGE_MANAGER_TEST_HXX
+
+#include <bpkg/system-package-manager.hxx>
+
+#include <algorithm> // sort()
+
+#include <bpkg/types.hxx>
+#include <bpkg/utility.hxx>
+
+#include <libbutl/manifest-parser.hxx>
+
+#include <libbpkg/manifest.hxx>
+
+#include <bpkg/package.hxx>
+
+namespace bpkg
+{
+ // Parse the manifest as if it comes from a git repository with a single
+ // package and make an available package out of it.
+ //
+ inline
+ pair<shared_ptr<available_package>, lazy_shared_ptr<repository_fragment>>
+ make_available_from_manifest (const string& n, const string& f)
+ {
+ using butl::manifest_parser;
+ using butl::manifest_parsing;
+
+ try
+ {
+ ifdstream ifs (f);
+ manifest_parser mp (ifs, f);
+
+ package_manifest m (mp,
+ false /* ignore_unknown */,
+ true /* complete_values */);
+
+ assert (m.name.string () == n);
+
+ m.alt_naming = false;
+ m.bootstrap_build = "project = " + n + '\n';
+
+ shared_ptr<available_package> ap (
+ make_shared<available_package> (move (m)));
+
+ lazy_shared_ptr<repository_fragment> af (
+ make_shared<repository_fragment> (
+ repository_location ("https://example.com/" + n,
+ repository_type::git)));
+
+ ap->locations.push_back (package_location {af, current_dir});
+
+ return make_pair (move (ap), move (af));
+ }
+ catch (const manifest_parsing& e)
+ {
+ fail (e.name, e.line, e.column) << e.description << endf;
+ }
+ catch (const io_error& e)
+ {
+ fail << "unable to read from " << f << ": " << e << endf;
+ }
+ }
+
+ // Make an available stub package as if it comes from git repository with
+ // a single package.
+ //
+ inline
+ pair<shared_ptr<available_package>, lazy_shared_ptr<repository_fragment>>
+ make_available_stub (const string& n)
+ {
+ shared_ptr<available_package> ap (
+ make_shared<available_package> (package_name (n)));
+
+ lazy_shared_ptr<repository_fragment> af (
+ make_shared<repository_fragment> (
+ repository_location ("https://example.com/" + n,
+ repository_type::git)));
+
+ ap->locations.push_back (package_location {af, current_dir});
+
+ return make_pair (move (ap), move (af));
+ }
+
+ // Sort available packages in the version descending order.
+ //
+ inline void
+ sort_available (available_packages& aps)
+ {
+ using element_type =
+ pair<shared_ptr<available_package>, lazy_shared_ptr<repository_fragment>>;
+
+ std::sort (aps.begin (), aps.end (),
+ [] (const element_type& x, const element_type& y)
+ {
+ return x.first->version > y.first->version;
+ });
+ }
+}
+
+#endif // BPKG_SYSTEM_PACKAGE_MANAGER_TEST_HXX
diff --git a/bpkg/system-package-manager.test.testscript b/bpkg/system-package-manager.test.testscript
new file mode 100644
index 0000000..dc672f5
--- /dev/null
+++ b/bpkg/system-package-manager.test.testscript
@@ -0,0 +1,101 @@
+# file : bpkg/system-package-manager.test.testscript
+# license : MIT; see accompanying LICENSE file
+
+: system-package-names
+:
+{
+ test.arguments += system-package-names
+
+ : basics
+ :
+ cat <<EOI >=libcurl7.64.manifest;
+ : 1
+ name: libcurl
+ version: 7.64.0
+ debian-name: libcurl2 libcurl2-dev
+ summary: curl
+ license: curl
+ EOI
+ cat <<EOI >=libcurl7.84.manifest;
+ : 1
+ name: libcurl
+ version: 7.84.0
+ debian_9-name: libcurl2 libcurl2-dev libcurl2-doc
+ debian_10-name: libcurl4 libcurl4-openssl-dev
+ debian_10-name: libcurl3-gnutls libcurl4-gnutls-dev
+ summary: curl
+ license: curl
+ EOI
+
+ $* debian 10 -- libcurl libcurl7.64.manifest libcurl7.84.manifest >>EOO;
+ libcurl4 libcurl4-openssl-dev
+ libcurl3-gnutls libcurl4-gnutls-dev
+ libcurl2 libcurl2-dev libcurl2-doc
+ libcurl2 libcurl2-dev
+ EOO
+ $* debian 9 -- libcurl libcurl7.64.manifest libcurl7.84.manifest >>EOO;
+ libcurl2 libcurl2-dev libcurl2-doc
+ libcurl2 libcurl2-dev
+ EOO
+ $* debian '' -- libcurl libcurl7.64.manifest libcurl7.84.manifest >>EOO;
+ libcurl2 libcurl2-dev
+ EOO
+ $* ubuntu 16.04 debian -- libcurl libcurl7.64.manifest libcurl7.84.manifest >>EOO
+ libcurl2 libcurl2-dev
+ EOO
+}
+
+: downstream-package-version
+:
+{
+ test.arguments += downstream-package-version
+
+ : basics
+ :
+ cat <<EOI >=libssl1.manifest;
+ : 1
+ name: libssl
+ version: 1.1.1
+ upstream-version: 1.1.1n
+ debian-to-downstream-version: /1\.1\.1[a-z]/1.1.1/
+ summary: openssl
+ license: openssl
+ EOI
+ cat <<EOI >=libssl3.manifest;
+ : 1
+ name: libssl
+ version: 3.0.0
+ debian-to-downstream-version: /([3-9])\.([0-9]+)\.([0-9]+)/\1.\2.\3/
+ summary: openssl
+ license: openssl
+ EOI
+ $* debian 10 -- 1.1.1l libssl libssl1.manifest libssl3.manifest >'1.1.1';
+ $* debian 10 -- 3.0.7 libssl libssl1.manifest libssl3.manifest >'3.0.7';
+ $* debian '' -- 1.1.1l libssl libssl1.manifest libssl3.manifest >'1.1.1';
+ $* debian '' -- 3.0.7 libssl libssl1.manifest libssl3.manifest >'3.0.7';
+ $* ubuntu 16.04 debian -- 1.1.1l libssl libssl1.manifest libssl3.manifest >'1.1.1';
+ $* ubuntu 16.05 debian -- 3.0.7 libssl libssl1.manifest libssl3.manifest >'3.0.7'
+
+ : order
+ :
+ cat <<EOI >=libssl1.manifest;
+ : 1
+ name: libssl
+ version: 1.1.1
+ debian-to-downstream-version: /.*/0/
+ summary: openssl
+ license: openssl
+ EOI
+ cat <<EOI >=libssl3.manifest;
+ : 1
+ name: libssl
+ version: 3.0.0
+ debian_9-to-downstream-version: /.*/9/
+ debian_10-to-downstream-version: /.*/10/
+ summary: openssl
+ license: openssl
+ EOI
+ $* debian 10 -- 1 libssl libssl1.manifest libssl3.manifest >'10';
+ $* debian 9 -- 1 libssl libssl1.manifest libssl3.manifest >'9';
+ $* debian 8 -- 1 libssl libssl1.manifest libssl3.manifest >'0'
+}
diff --git a/bpkg/system-repository.cxx b/bpkg/system-repository.cxx
index d7a47b7..c308ddb 100644
--- a/bpkg/system-repository.cxx
+++ b/bpkg/system-repository.cxx
@@ -6,9 +6,12 @@
namespace bpkg
{
const version& system_repository::
- insert (const package_name& name, const version& v, bool authoritative)
+ insert (const package_name& name,
+ const version& v,
+ bool authoritative,
+ const system_package_status* s)
{
- auto p (map_.emplace (name, system_package {v, authoritative}));
+ auto p (map_.emplace (name, system_package {v, authoritative, s}));
if (!p.second)
{
@@ -22,6 +25,7 @@ namespace bpkg
{
sp.authoritative = authoritative;
sp.version = v;
+ sp.system_status = s;
}
}
diff --git a/bpkg/system-repository.hxx b/bpkg/system-repository.hxx
index f33d622..31e14d1 100644
--- a/bpkg/system-repository.hxx
+++ b/bpkg/system-repository.hxx
@@ -12,6 +12,8 @@
#include <bpkg/types.hxx>
#include <bpkg/utility.hxx>
+#include <bpkg/system-package-manager.hxx>
+
namespace bpkg
{
// A map of discovered system package versions. The information can be
@@ -30,16 +32,25 @@ namespace bpkg
version_type version;
bool authoritative;
+
+ // If the information is authoritative then this member indicates whether
+ // the version came from the system package manager (not NULL) or
+ // user/fallback (NULL).
+ //
+ const system_package_status* system_status;
};
class system_repository
{
public:
const version&
- insert (const package_name& name, const version&, bool authoritative);
+ insert (const package_name& name,
+ const version&,
+ bool authoritative,
+ const system_package_status* = nullptr);
const system_package*
- find (const package_name& name)
+ find (const package_name& name) const
{
auto i (map_.find (name));
return i != map_.end () ? &i->second : nullptr;
diff --git a/bpkg/types.hxx b/bpkg/types.hxx
index 2b6a1f8..7a7b2c7 100644
--- a/bpkg/types.hxx
+++ b/bpkg/types.hxx
@@ -33,6 +33,7 @@
#include <libbutl/optional.hxx>
#include <libbutl/fdstream.hxx>
#include <libbutl/small-vector.hxx>
+#include <libbutl/target-triplet.hxx>
#include <libbutl/default-options.hxx>
namespace bpkg
@@ -127,6 +128,10 @@ namespace bpkg
using butl::ofdstream;
using butl::fdstream_mode;
+ // <libbutl/target-triplet.hxx>
+ //
+ using butl::target_triplet;
+
// <libbutl/default-options.hxx>
//
using butl::default_options_files;
diff --git a/bpkg/utility.cxx b/bpkg/utility.cxx
index 52114df..b79c85b 100644
--- a/bpkg/utility.cxx
+++ b/bpkg/utility.cxx
@@ -46,6 +46,8 @@ namespace bpkg
const dir_path current_dir (".");
+ const target_triplet host_triplet (BPKG_HOST_TRIPLET);
+
map<dir_path, dir_path> tmp_dirs;
bool keep_tmp;
diff --git a/bpkg/utility.hxx b/bpkg/utility.hxx
index 8e7260a..69a02d3 100644
--- a/bpkg/utility.hxx
+++ b/bpkg/utility.hxx
@@ -10,7 +10,7 @@
#include <cstring> // strcmp(), strchr()
#include <utility> // move(), forward(), declval(), make_pair()
#include <cassert> // assert()
-#include <iterator> // make_move_iterator()
+#include <iterator> // make_move_iterator(), back_inserter()
#include <algorithm> // *
#include <libbutl/ft/lang.hxx>
@@ -33,6 +33,7 @@ namespace bpkg
using std::make_pair;
using std::make_shared;
using std::make_move_iterator;
+ using std::back_inserter;
using std::to_string;
using std::strcmp;
@@ -51,6 +52,7 @@ namespace bpkg
using butl::trim;
using butl::trim_left;
using butl::trim_right;
+ using butl::next_word;
using butl::make_guard;
using butl::make_exception_guard;
@@ -59,6 +61,8 @@ namespace bpkg
using butl::setenv;
using butl::unsetenv;
+ using butl::eof;
+
// <libbutl/process.hxx>
//
using butl::process_start_callback;
@@ -99,6 +103,10 @@ namespace bpkg
extern const dir_path current_dir; // ./
+ // Host target triplet for which we were built.
+ //
+ extern const target_triplet host_triplet;
+
// Temporary directory facility.
//
// An entry normally maps <cfg-dir> to <cfg-dir>/.bpkg/tmp/ but can also map
diff --git a/doc/manual.cli b/doc/manual.cli
index 10b3047..cc40cb2 100644
--- a/doc/manual.cli
+++ b/doc/manual.cli
@@ -2390,80 +2390,73 @@ automatically added, for example, when the \l{#manifest-package-list-pkg
package list manifest} is created.
-\h2#manifest-package-distribution|\c{*-name}|
+\h2#manifest-package-distribution|\c{*-{name, version, to-downstream-version\}}|
\
-[*-name]: <name> [<name>...]
-[*-version]: <string>
-[*-to-downstream-version]: <regex>
+[<distribution>-name]: <name> [<name>...]
+[<distribution>-version]: <string>
+[<distribution>-to-downstream-version]: <regex>
-<regex> = /<patten>/<replacement>/
+<distribution> = <name>[_<version>]
+<regex> = /<pattern>/<replacement>/
\
-The binary distribution packages where the substring matched by \c{*} in
-\c{*-name} denotes the distribution name which has the
-\c{<name>[\b{_}<version>]} form. For example:
-
-\
-debian
-debian_10
-fedora_32
-ubuntu_16.04
-freebsd_12.1
-windows_10
-macos_10
-macos_10.15
-macos_12
-\
-
-If the \c{*-name} values are specified, then the packages required for these
-binary distributions can be created using the \l{bpkg-pkg-bindist(1)}
-command. The \c{*-name} values contain whitespace separated lists of the
-distribution package names. Normally such a list contains a single primary
-package name, but can additionally contain the related package names which
-cannot automatically be deduced from the primary package name. For example:
-
-\
-debian_10-name: libssl1.1 libssl-dev
-fedora_32-name: openssl-libs
-\
-
-If the distribution package version differs from the upstream package version
-(the \c{upstream-version} value, if present, and the \c{version} value
-otherwise), then the matching \c{*-version} value needs to be specified. For
+The binary distribution package name and version mapping. The \c{-name} value
+specifies the distribution package(s) this \c{bpkg} package maps to. If
+unspecified, then appropriate name(s) are automatically derived from the
+\c{bpkg} package name (\l{#manifest-package-name \c{name}}). Similarly, the
+\c{-version} value specifies the distribution package version. If unspecified,
+then the \c{upstream-version} value is used if specified and the \c{bpkg}
+version otherwise (\l{#manifest-package-version \c{version}}). While the
+\c{-to-downstream-version} values specify the reverse mapping, that is, from
+the distribution version to the \c{bpkg} version. If unspecified or none
+match, then the appropriate part of the distribution version is used. For
example:
\
+name: libssl
+version 1.1.1+18
debian-name: libssl1.1 libssl-dev
debian-version: 1.1.1n
+debian-to-downstream-version: /1\.1\.1[a-z]/1.1.1/
+debian-to-downstream-version: /([3-9])\.([0-9]+)\.([0-9]+)/\1.\2.\3/
\
-To specify the distribution package version to be the same as the \cb{bpkg}
-package version, the special \c{$} value can be used. For example:
+If \c{upstream-version} is specified but the the distribution package version
+should be the same as the \c{bpkg} package version, then the special \c{$}
+\c{-version} value can be used. For example:
\
debian-version: $
\
-When configuring a package as a system dependency the \l{bpkg-pkg-build(1)}
-command needs to map the binary distribution package versions back to the
-\cb{bpkg} package versions, so it can verify the version constraints imposed
-by the dependent packages on this dependency package. Unless such a mapping is
-maintained externally, it should be specified at the \cb{bpkg} package level
-by using the \c{*-to-downstream-version} values. For example:
+The \c{<distribution>} name prefix consists of the distribution name followed
+by the optional distribution version. If the version is omitted, then the
+value applies to all versions. Some examples of distribution names and
+versions:
\
-debian-name: libssl1.1 libssl-dev
-debian-version: 1.1.1n
-debian-to-downstream-version: /([^.])\.([^.])\.([^.])n/\1.\2.\3+14/
-debian-to-downstream-version: /([^.])\.([^.])\.([^.])o/\1.\2.\3+15/
-debian-to-downstream-version: /([^.])\.([^.])\.([^.])p/\1.\2.\3+16/
+debian
+debian_10
+ubuntu_16.04
+fedora_32
+rhel_8.5
+freebsd_12.1
+windows_10
+macos_10
+macos_10.15
+macos_12
\
-If \cb{bpkg} finds the matching regular expression pattern, then it uses the
-corresponding replacement to produce the \cb{bpkg} package version. Otherwise,
-it excludes this binary distribution package version from the consideration.
+Note also that some distributions are like others (for example, \c{ubuntu} is
+like \c{debian}) and the corresponding \"base\" distribution values are
+considered if no \"derived\" values are specified.
+The exact format of the \c{-name} value and the distribution version part that
+is matched against the \c{-to-downstream-version} pattern are
+distribution-specific. For details, see
+\l{#bindist-mapping-debian Debian Package Mapping} and
+\l{#bindist-mapping-fedora Fedora Package Mapping}.
\h#manifest-package-list-pkg|Package List Manifest for \cb{pkg} Repositories|
@@ -2918,6 +2911,188 @@ signature: <sig>
The signature of the \c{packages.manifest} file. It should be calculated by
encrypting the above \c{sha256sum} value with the repository certificate's
private key and then \c{base64}-encoding the result.
+
+
+\h1#bindist-mapping|Binary Distribution Package Mapping|
+
+
+\h#bindist-mapping-debian|Debian Package Mapping|
+
+This section describes the distribution package mapping for Debian and
+alike (Ubuntu, etc).
+
+A library in Debian is normally split up into several packages: the shared
+library package (e.g., \c{libfoo1} where \c{1} is the ABI version), the
+development files package (e.g., \c{libfoo-dev}), the documentation files
+package (e.g., \c{libfoo-doc}), the debug symbols package (e.g.,
+\c{libfoo1-dbg}), and the architecture-independent files (e.g.,
+\c{libfoo1-common}). All the packages except \c{-dev} are optional and there
+is quite a bit of variability. Here are a few examples:
+
+\
+libz3-4 libz3-dev
+
+libssl1.1 libssl-dev libssl-doc
+libssl3 libssl-dev libssl-doc
+
+libcurl4 libcurl4-openssl-dev libcurl4-doc
+libcurl3-gnutls libcurl4-gnutls-dev libcurl4-doc
+\
+
+For executable packages there is normally no \c{-dev} packages but \c{-dbg},
+\c{-doc}, and \c{-common} are plausible.
+
+Based on that, our approach when trying to automatically map a \c{bpkg}
+library package name to Debian package names is to go for the \c{-dev} package
+first and figure out the shared library package from that based on the fact
+that the \c{-dev} package should have the \c{==} dependency on the shared
+library package with the same version and its name should normally start with
+the \c{-dev} package's stem.
+
+The format of the \c{debian-name} (or alike) manifest value is a
+comma-separated list of one or more package groups:
+
+\
+<package-group> [, <package-group>...]
+\
+
+Where each \c{<package-group>} is the space-separated list of one or more
+package names:
+
+\
+<package-name> [ <package-name>...]
+\
+
+All the packages in the group should be \"package components\" (for the lack
+of a better term) of the same \"logical package\", such as \c{-dev}, \c{-doc},
+\c{-common} packages. They normally have the same version.
+
+The first group is called the main group and the first package in the
+group is called the main package. Note that all the groups are consumed
+(installed) but only the main group is produced (packaged).
+
+We allow/recommend specifying the \c{-dev} package instead of the main package
+for libraries (the \c{bpkg} package name starts with \c{lib}), seeing that we
+are capable of detecting the main package automatically (see above). If the
+library name happens to end with \c{-dev} (which poses an ambiguity), then the
+\c{-dev} package should be specified explicitly as the second package to
+disambiguate this situation.
+
+The Debian package version has the \c{[<epoch>:]<upstream>[-<revision>]} form
+(see \cb{deb-version(5)} for details). If no explicit mapping to the \c{bpkg}
+version is specified with the \c{debian-to-downstream-version} (or alike)
+manifest values or none match, then we fallback to using the \c{<upstream>}
+part as the \c{bpkg} version. If explicit mapping is specified, then we match
+it against the \c{[<epoch>:]<upstream>} parts ignoring \c{<revision>}.
+
+
+\h#bindist-mapping-fedora|Fedora Package Mapping|
+
+This section describes the distribution package mapping for Fedora and alike
+(Red Hat Enterprise Linux, Centos, etc).
+
+A library in Fedora is normally split up into several packages: the shared
+library package (e.g., \c{libfoo}), the development files package (e.g.,
+\c{libfoo-devel}), the static library package (e.g., \c{libfoo-static}; may
+also be placed into the \c{-devel} package), the documentation files package
+(e.g., \c{libfoo-doc}), the debug symbols and source files packages (e.g.,
+\c{libfoo-debuginfo} and \c{libfoo-debugsource}), and the common or
+architecture-independent files (e.g., \c{libfoo-common}). All the packages
+except \c{-devel} are optional and there is quite a bit of variability. In
+particular, the \c{lib} prefix in \c{libfoo} is not a requirement (unlike in
+Debian) and is normally present only if upstream name has it (see some
+examples below).
+
+For application packages there is normally no \c{-devel} packages but
+\c{-debug*}, \c{-doc}, and \c{-common} are plausible.
+
+For mixed packages which include both applications and libraries, the shared
+library package normally has the \c{-libs} suffix (e.g., \c{foo-libs}).
+
+A package name may also include an upstream version based suffix if
+multiple versions of the package can be installed simultaneously (e.g.,
+\c{libfoo1.1} \c{libfoo1.1-devel}, \c{libfoo2} \c{libfoo2-devel}).
+
+Terminology-wise, the term \"base package\" (sometime also \"main package\")
+normally refers to either the application or shared library package (as
+decided by the package maintainer in the spec file) with the suffixed packages
+(\c{-devel}, \c{-doc}, etc) called \"subpackages\".
+
+Here are a few examples:
+
+\
+libpq libpq-devel
+
+zlib zlib-devel zlib-static
+
+xerces-c xerces-c-devel xerces-c-doc
+
+libsigc++20 libsigc++20-devel libsigc++20-doc
+libsigc++30 libsigc++30-devel libsigc++30-doc
+
+icu libicu libicu-devel libicu-doc
+
+openssl openssl-libs openssl-devel
+
+curl libcurl libcurl-devel
+
+sqlite sqlite-libs sqlite-devel sqlite-doc
+
+community-mysql community-mysql-libs community-mysql-devel
+community-mysql-common community-mysql-server
+\
+
+Based on that, our approach when trying to automatically map a \c{bpkg}
+library package name to Fedora package names is to go for the \c{-devel}
+package first and figure out the shared library package from that based on the
+fact that the \c{-devel} package should have the \c{==} dependency on the
+shared library package with the same version and its name should normally
+start with the \c{-devel} package's stem and potentially end with the
+\c{-libs} suffix. If failed to find the \c{-devel} package, we re-try but now
+using the \c{bpkg} project name instead of the package name (see, for example,
+\c{openssl}, \c{sqlite}).
+
+The format of the \c{fedora-name} (or alike) manifest value value is a
+comma-separated list of one or more package groups:
+
+\
+<package-group> [, <package-group>...]
+\
+
+Where each \c{<package-group>} is the space-separated list of one or more
+package names:
+
+\
+<package-name> [ <package-name>...]
+\
+
+All the packages in the group should belong to the same \"logical package\",
+such as \c{-devel}, \c{-doc}, \c{-common} packages. They normally have the
+same version.
+
+The first group is called the main group and the first package in the
+group is called the main package. Note that all the groups are consumed
+(installed) but only the main group is produced (packaged).
+
+(Note that above we use the term \"logical package\" instead of \"base
+package\" since the main package may not be the base package, for example
+being the \c{-libs} subpackage.)
+
+We allow/recommend specifying the \c{-devel} package instead of the main
+package for libraries (the \c{bpkg} package name starts with \c{lib}), seeing
+that we are capable of detecting the main package automatically (see
+above). If the library name happens to end with \c{-devel} (which poses an
+ambiguity), then the \c{-devel} package should be specified explicitly as the
+second package to disambiguate this situation.
+
+The Fedora package version has the \c{[<epoch>:]<version>-<release>} form (see
+Fedora Package Versioning Guidelines for details). If no explicit mapping
+to the \c{bpkg} version is specified with the \c{fedora-to-downstream-version}
+(or alike) manifest values or none match, then we fallback to using the
+\c{<version>} part as the \c{bpkg} version. If explicit mapping is specified,
+then we match it against the \c{[<epoch>:]<version>} parts ignoring
+\c{<release>}.
+
"
//@@ TODO items (grep).
diff --git a/tests/common.testscript b/tests/common.testscript
index 17174d9..30fcf7e 100644
--- a/tests/common.testscript
+++ b/tests/common.testscript
@@ -32,11 +32,13 @@ test.options += --default-options $options_guard \
# (for example, to make sure that configuration post-test state is valid and is
# as expected).
#
+# Disable the use of the system package manager for the pkg-build command.
+#
cfg_create = [cmdline] $* cfg-create
cfg_info = [cmdline] $* cfg-info
cfg_link = [cmdline] $* cfg-link
cfg_unlink = [cmdline] $* cfg-unlink
-pkg_build = [cmdline] $* pkg-build
+pkg_build = [cmdline] $* pkg-build --sys-no-query
pkg_checkout = [cmdline] $* pkg-checkout
pkg_configure = [cmdline] $* pkg-configure
pkg_disfigure = [cmdline] $* pkg-disfigure
diff --git a/tests/pkg-build.testscript b/tests/pkg-build.testscript
index 198e319..aaa7bc4 100644
--- a/tests/pkg-build.testscript
+++ b/tests/pkg-build.testscript
@@ -461,6 +461,10 @@ rep_list += -d cfg
#
test.options += --no-progress
+# Disable the use of the system package manager.
+#
+test.arguments += --sys-no-query
+
: libfoo
:
: Test building different versions of libfoo.
diff --git a/tests/pkg-drop.testscript b/tests/pkg-drop.testscript
index a2e58f3..7504d6c 100644
--- a/tests/pkg-drop.testscript
+++ b/tests/pkg-drop.testscript
@@ -458,7 +458,9 @@ $* libfoo/1.0.0 2>>~%EOE% != 0
: linked-configs
:
{
- pkg_build = [cmdline] $0 pkg-build --yes 2>!
+ # Get rid of -d option.
+ #
+ pkg_build = [cmdline] $0 pkg-build --yes --sys-no-query 2>!
: 3-configs
: