From 854c668b5e63e26a9d7a6e55226a0940638e0453 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Thu, 9 Feb 2023 15:46:32 +0200 Subject: Add pkg-bindist command (generate binary distribution package) This commit includes an implementation for Debian and alike. --- bpkg/pkg-bindist.cxx | 448 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 448 insertions(+) create mode 100644 bpkg/pkg-bindist.cxx (limited to 'bpkg/pkg-bindist.cxx') diff --git a/bpkg/pkg-bindist.cxx b/bpkg/pkg-bindist.cxx new file mode 100644 index 0000000..e3ec9fa --- /dev/null +++ b/bpkg/pkg-bindist.cxx @@ -0,0 +1,448 @@ +// file : bpkg/pkg-bindist.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace butl; + +namespace bpkg +{ + using package = system_package_manager::package; + using packages = system_package_manager::packages; + using recursive_mode = system_package_manager::recursive_mode; + + // Find the available package(s) for the specified selected package. + // + // Specifically, for non-system packages we look for a single available + // package. For system packages we look for all the available packages + // analogous to pkg-build. If none are found then we assume the + // --sys-no-stub option was used to configure this package and return an + // empty list. @@ What if it was configured with a specific bpkg version or + // `*`? + // + static available_packages + find_available_packages (const common_options& co, + database& db, + const shared_ptr& p) + { + assert (p->state == package_state::configured); + + available_packages r; + if (p->substate == package_substate::system) + { + r = find_available_all (repo_configs, p->name); + } + else + { + pair, + lazy_shared_ptr> ap ( + find_available_fragment (co, db, p)); + + if (ap.second.loaded () && ap.second == nullptr) + { + // This is an orphan. We used to fail but there is no reason we cannot + // just load its manifest and make an available package out of that. + // And it's handy to be able to run this command on packages built + // from archives. + // + package_manifest m ( + pkg_verify (co, + p->effective_src_root (db.config_orig), + true /* ignore_unknown */, + false /* ignore_toolchain */, + false /* load_buildfiles */, + // Copy potentially fixed up version from selected package. + [&p] (version& v) {v = p->version;})); + + // Fake the buildfile information (not used). + // + m.alt_naming = false; + m.bootstrap_build = "project = " + p->name.string () + '\n'; + + ap.first = make_shared (move (m)); + + // Fake the location (only used for diagnostics). + // + ap.second = make_shared ( + repository_location ( + p->effective_src_root (db.config).representation (), + repository_type::dir)); + + ap.first->locations.push_back ( + package_location {ap.second, current_dir}); + } + + r.push_back (move (ap)); + } + + return r; + } + + // Merge dependency languages for the (ultimate) dependent of the specified + // type. + // + static void + merge_languages (const string& type, + small_vector& langs, + const available_package& ap) + { + for (const language& l: ap.effective_languages ()) + { + // Unless both the dependent and dependency types are libraries, the + // interface/implementation distinction does not apply. + // + bool lib (type == "lib" && ap.effective_type () == "lib"); + + auto i (find_if (langs.begin (), langs.end (), + [&l] (const language& x) + { + return x.name == l.name; + })); + + if (i == langs.end ()) + { + // If this is an implementation language for a dependency, then it is + // also an implementation language for a dependent. The converse, + // howevere, depends on whether this dependency is an interface or + // imlementation of this dependent, which we do not know. So we have + // to assume it's interface. + // + langs.push_back (language {l.name, lib && l.impl}); + } + else + { + i->impl = i->impl && (lib && l.impl); // Merge. + } + } + } + + // Collect dependencies of the specified package, potentially recursively. + // System dependencies go to deps, non-system -- to pkgs, which could be the + // same as deps or NULL, depending on the desired semantics (see the call + // site for details). Find available packages for pkgs and deps and merge + // languages. + // + static void + collect_dependencies (const common_options& co, + database& db, + packages* pkgs, + packages& deps, + const string& type, + small_vector& langs, + const selected_package& p, + bool recursive) + { + for (const auto& pr: p.prerequisites) + { + const lazy_shared_ptr& ld (pr.first); + + // We only consider dependencies from target configurations, similar + // to pkg-install. + // + database& pdb (ld.database ()); + if (pdb.type == host_config_type || pdb.type == build2_config_type) + continue; + + shared_ptr d (ld.load ()); + + // Packaging stuff that is spread over multiple configurations is just + // to hairy so we don't support it. Specifically, it becomes tricky to + // override build options since using a global override will also affect + // host/build2 configurations. + // + if (db != pdb) + fail << "dependency package " << *d << " belongs to different " + << "configuration " << pdb.config_orig; + + // The selected package can only be configured if all its dependencies + // are configured. + // + assert (d->state == package_state::configured); + + bool sys (d->substate == package_substate::system); + packages* ps (sys ? &deps : pkgs); + + // Skip duplicates. + // + if (ps == nullptr || find_if (ps->begin (), ps->end (), + [&d] (const package& p) + { + return p.selected == d; + }) == ps->end ()) + { + const selected_package& p (*d); + + if (ps != nullptr || (recursive && !sys)) + { + available_packages aps (find_available_packages (co, db, d)); + + // Load and merge languages. + // + if (recursive && !sys) + { + const shared_ptr& ap (aps.front ().first); + db.load (*ap, ap->languages_section); + merge_languages (type, langs, *ap); + } + + if (ps != nullptr) + { + dir_path out; + if (ps != &deps) + out = p.effective_out_root (db.config); + + ps->push_back (package {move (d), move (aps), move (out)}); + } + } + + if (recursive && !sys) + collect_dependencies (co, db, pkgs, deps, type, langs, p, recursive); + } + } + } + + int + pkg_bindist (const pkg_bindist_options& o, cli::scanner& args) + { + tracer trace ("pkg_bindist"); + + dir_path c (o.directory ()); + l4 ([&]{trace << "configuration: " << c;}); + + // Verify options. + // + optional rec; + { + diag_record dr; + + if (o.recursive_specified ()) + { + const string& m (o.recursive ()); + + if (m == "auto") rec = recursive_mode::auto_; + else if (m == "full") rec = recursive_mode::full; + else + dr << fail << "unknown mode '" << m << "' specified with --recursive"; + } + else if (o.private_ ()) + dr << fail << "--private specified without --recursive"; + + if (!dr.empty ()) + dr << info << "run 'bpkg help pkg-bindist' for more information"; + } + + // Sort arguments into package names and configuration variables. + // + vector pns; + strings vars; + { + bool sep (false); // Seen `--`. + + while (args.more ()) + { + string a (args.next ()); + + // If we see the `--` separator, then we are done parsing variables + // (while they won't clash with package names, we may be given a + // directory path that contains `=`). + // + if (!sep && a == "--") + { + sep = true; + continue; + } + + if (a.find ('=') != string::npos) + vars.push_back (move (trim (a))); + else + { + try + { + pns.push_back (package_name (move (a))); // Not moved on failure. + } + catch (const invalid_argument& e) + { + fail << "invalid package name '" << a << "': " << e; + } + } + } + + if (pns.empty ()) + fail << "package name argument expected" << + info << "run 'bpkg help pkg-bindist' for more information"; + } + + database db (c, trace, true /* pre_attach */); + + // Similar to pkg-install we disallow generating packages from the + // host/build2 configurations. + // + if (db.type == host_config_type || db.type == build2_config_type) + { + fail << "unable to generate distribution package from " << db.type + << " configuration" << + info << "use target configuration instead"; + } + + // Prepare for the find_available_*() calls. + // + repo_configs.push_back (db); + + transaction t (db); + + // We need to suppress duplicate dependencies for the recursive mode. + // + session ses; + + // Resolve package names to selected packages and verify they are all + // configured. While at it collect their available packages and + // dependencies as well as figure out type and languages. + // + packages pkgs, deps; + string type; + small_vector langs; + + for (const package_name& n: pns) + { + shared_ptr p (db.find (n)); + + if (p == nullptr) + fail << "package " << n << " does not exist in configuration " << c; + + if (p->state != package_state::configured) + fail << "package " << n << " is " << p->state << + info << "expected it to be configured"; + + if (p->substate == package_substate::system) + fail << "package " << n << " is configured as system"; + + // Load the available package for type/languages as well as the mapping + // information. + // + available_packages aps (find_available_packages (o, db, p)); + const shared_ptr& ap (aps.front ().first); + db.load (*ap, ap->languages_section); + + if (pkgs.empty ()) // First. + { + type = ap->effective_type (); + langs = ap->effective_languages (); + } + else + merge_languages (type, langs, *ap); + + const selected_package& r (*p); + pkgs.push_back ( + package {move (p), move (aps), r.effective_out_root (db.config)}); + + // If --recursive is not specified then we want all the immediate + // (system and non-) dependecies in deps. Otherwise, if the recursive + // mode is full, then we want all the transitive non-system dependecies + // in pkgs. In both recursive modes we also want all the transitive + // system dependecies in deps. + // + // Note also that in the auto recursive mode it's possible that some of + // the system dependencies are not really needed. But there is no way + // for us to detect this and it's better to over- than under-specify. + // + collect_dependencies (o, + db, + (rec + ? *rec == recursive_mode::full ? &pkgs : nullptr + : &deps), + deps, + type, + langs, + r, + rec.has_value ()); + } + + t.commit (); + + // Load the package manifest (source of extra metadata). This should be + // always possible since the package is configured and is not system. + // + const shared_ptr& sp (pkgs.front ().selected); + + package_manifest pm ( + pkg_verify (o, + sp->effective_src_root (db.config_orig), + true /* ignore_unknown */, + false /* ignore_toolchain */, + false /* load_buildfiles */, + // Copy potentially fixed up version from selected package. + [&sp] (version& v) {v = sp->version;})); + + // Note that we shouldn't need to install anything or use sudo. + // + unique_ptr spm ( + make_production_system_package_manager (o, + host_triplet, + o.distribution (), + o.architecture ())); + if (spm == nullptr) + { + fail << "no standard distribution package manager for this host " + << "or it is not yet supported" << + info << "consider specifying alternative distribution package " + << "manager with --distribution"; + } + + // Note that we pass type from here in case one day we want to provide an + // option to specify/override it (along with languages). Note that there + // will probably be no way to override type for dependencies. + // + paths r (spm->generate (pkgs, deps, vars, db.config, pm, type, langs, rec)); + + if (r.empty ()) + return 0; // Assume prepare-only mode or similar. + + if (verb && !o.no_result ()) + { + const selected_package& p (*pkgs.front ().selected); + + diag_record dr (text); + + dr << "generated " << spm->os_release.name_id << " package for " + << p.name << '/' << p.version << ':'; + + for (const path& p: r) + dr << "\n " << p; + } + + return 0; + } + + pkg_bindist_options + merge_options (const default_options& defs, + const pkg_bindist_options& cmd) + { + // NOTE: remember to update the documentation if changing anything here. + + return merge_default_options ( + defs, + cmd, + [] (const default_options_entry& e, + const pkg_bindist_options&) + { + const pkg_bindist_options& o (e.options); + + auto forbid = [&e] (const char* opt, bool specified) + { + if (specified) + fail (e.file) << opt << " in default options file"; + }; + + forbid ("--directory|-d", o.directory_specified ()); + }); + } +} -- cgit v1.1