diff options
Diffstat (limited to 'bpkg/pkg-bindist.cxx')
-rw-r--r-- | bpkg/pkg-bindist.cxx | 689 |
1 files changed, 689 insertions, 0 deletions
diff --git a/bpkg/pkg-bindist.cxx b/bpkg/pkg-bindist.cxx new file mode 100644 index 0000000..4639746 --- /dev/null +++ b/bpkg/pkg-bindist.cxx @@ -0,0 +1,689 @@ +// file : bpkg/pkg-bindist.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <bpkg/pkg-bindist.hxx> + +#include <list> +#include <iostream> // cout + +#include <libbutl/json/serializer.hxx> + +#include <bpkg/package.hxx> +#include <bpkg/package-odb.hxx> +#include <bpkg/package-query.hxx> +#include <bpkg/database.hxx> +#include <bpkg/pkg-verify.hxx> +#include <bpkg/diagnostics.hxx> +#include <bpkg/system-package-manager.hxx> + +using namespace std; +using namespace butl; + +namespace bpkg +{ + using package = system_package_manager::package; + using packages = system_package_manager::packages; + + // 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<selected_package>& 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<shared_ptr<available_package>, + lazy_shared_ptr<repository_fragment>> 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<available_package> (move (m)); + + // Fake the location (only used for diagnostics). + // + ap.second = make_shared<repository_fragment> ( + 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<language, 1>& 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<language, 1>& langs, + const selected_package& p, + bool recursive) + { + for (const auto& pr: p.prerequisites) + { + const lazy_shared_ptr<selected_package>& 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<selected_package> d (ld.load ()); + + // Packaging stuff that is spread over multiple configurations is just + // too 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<available_package>& 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. + // + enum class recursive_mode {auto_, full, separate}; + + optional<recursive_mode> 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 if (m == "separate") rec = recursive_mode::separate; + else if (m != "none") + dr << fail << "unknown --recursive mode '" << m << "'"; + } + + if (o.private_ ()) + { + if (!rec) + { + dr << fail << "--private specified without --recursive"; + } + else if (*rec == recursive_mode::separate) + { + dr << fail << "--private specified without --recursive=separate"; + } + } + + if (!dr.empty ()) + dr << info << "run 'bpkg help pkg-bindist' for more information"; + } + + if (o.structured_result_specified ()) + { + if (o.no_result ()) + fail << "both --structured-result and --no-result specified"; + + if (o.structured_result () != "json") + fail << "unknown --structured-result format '" + << o.structured_result () << "'"; + } + + // Sort arguments into package names and configuration variables. + // + vector<package_name> 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"; + } + + // Note that we shouldn't need to install anything or use sudo. + // + pair<unique_ptr<system_package_manager>, string> spm ( + make_production_system_package_manager (o, + host_triplet, + o.distribution (), + o.architecture ())); + if (spm.first == 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" << + info << "specify --distribution=archive to generate installation " + << "archive" << + info << "consider specifying --os-release-* if unable to correctly " + << "auto-detect host operating system"; + } + + 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; + + // Generate one binary package. + // + using binary_file = system_package_manager::binary_file; + using binary_files = system_package_manager::binary_files; + + struct result + { + binary_files bins; + packages deps; + shared_ptr<selected_package> pkg; + }; + + bool dependent_config (false); + + auto generate = [&o, &vars, + rec, &spm, + &c, &db, + &dependent_config] (const vector<package_name>& pns, + bool first) -> result + { + // 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<language, 1> langs; + + for (const package_name& n: pns) + { + shared_ptr<selected_package> p (db.find<selected_package> (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"; + + // Make sure there are no dependent configuration variables. The + // rationale here is that we most likely don't want to generate a + // binary package in a configuration that is specific to some + // dependents. + // + for (const config_variable& v: p->config_variables) + { + switch (v.source) + { + case config_source::dependent: + { + if (!o.allow_dependent_config ()) + { + fail << "configuration variable " << v.name << " is imposed " + << " by dependent package" << + info << "specify it as user configuration to allow" << + info << "or specify --allow-dependent-config"; + } + + dependent_config = true; + break; + } + case config_source::user: + case config_source::reflect: + break; + } + + if (dependent_config) + break; + } + + // 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<available_package>& 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 or specified with the seperate mode + // 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::separate + ? &deps + : *rec == recursive_mode::full ? &pkgs : nullptr), + deps, + type, + langs, + r, + rec.has_value ()); + } + + // 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<selected_package>& 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;})); + + optional<bool> recursive_full; + if (rec && *rec != recursive_mode::separate) + recursive_full = (*rec == recursive_mode::full); + + // 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. + // + binary_files r (spm.first->generate (pkgs, + deps, + vars, + db.config, + pm, + type, langs, + recursive_full, + first)); + + return result {move (r), move (deps), move (pkgs.front ().selected)}; + }; + + list<result> rs; // Note: list for reference stability. + + // Generate packages for dependencies, recursively, suppressing + // duplicates. Note: recursive lambda. + // + auto generate_deps = [&generate, &rs] (const packages& deps, + const auto& generate_deps) -> void + { + for (const package& d: deps) + { + const shared_ptr<selected_package>& p (d.selected); + + // Skip system dependencies. + // + if (p->substate == package_substate::system) + continue; + + // Make sure we don't generate the same dependency multiple times. + // + if (find_if (rs.begin (), rs.end (), + [&p] (const result& r) + { + return r.pkg == p; + }) != rs.end ()) + continue; + + if (verb >= 1) + text << "generating package for dependency " << p->name; + + rs.push_back (generate ({p->name}, false /* first */)); + generate_deps (rs.back ().deps, generate_deps); + } + }; + + // Generate top-level package(s). + // + rs.push_back (generate (pns, true /* first */)); + + // Generate dependencies, if requested. + // + if (rec && rec == recursive_mode::separate) + generate_deps (rs.back ().deps, generate_deps); + + t.commit (); + + if (rs.front ().bins.empty ()) + return 0; // Assume prepare-only mode or similar. + + if (o.no_result ()) + ; + else if (!o.structured_result_specified ()) + { + if (verb) + { + const string& d (o.distribution_specified () + ? o.distribution () + : spm.first->os_release.name_id); + + for (auto b (rs.begin ()), i (b); i != rs.end (); ++i) + { + const selected_package& p (*i->pkg); + + string ver (p.version.string (false /* ignore_revision */, + true /* ignore_iteration */)); + + diag_record dr (text); + + dr << "generated " << d << " package for " + << (i != b ? "dependency " : "") + << p.name << '/' << ver << ':'; + + for (const binary_file& f: i->bins) + dr << "\n " << f.path; + } + } + } + else + { + json::stream_serializer s (cout); + + auto member = [&s] (const char* n, const string& v, const char* d = "") + { + if (v != d) + s.member (n, v); + }; + + auto package = [&s, &member] (const result& r) + { + const selected_package& p (*r.pkg); + const binary_files& bfs (r.bins); + + string ver (p.version.string (false /* ignore_revision */, + true /* ignore_iteration */)); + + s.begin_object (); // package + { + member ("name", p.name.string ()); + member ("version", ver); + member ("system_version", bfs.system_version); + s.member_begin_array ("files"); + for (const binary_file& bf: bfs) + { + s.begin_object (); // file + { + member ("type", bf.type); + member ("path", bf.path.string ()); + member ("system_name", bf.system_name); + } + s.end_object (); // file + }; + s.end_array (); + } + s.end_object (); // package + }; + + s.begin_object (); // bindist_result + { + member ("distribution", spm.second); + member ("architecture", spm.first->arch); + + s.member_begin_object ("os_release"); + { + const auto& r (spm.first->os_release); + + member ("name_id", r.name_id); + + if (!r.like_ids.empty ()) + { + s.member_begin_array ("like_ids"); + for (const string& id: r.like_ids) s.value (id); + s.end_array (); + } + + member ("version_id", r.version_id); + member ("variant_id", r.variant_id); + + member ("name", r.name); + member ("version_codename", r.version_codename); + member ("variant", r.variant); + } + s.end_object (); // os_release + + member ("recursive", o.recursive (), "none"); + if (o.private_ ()) s.member ("private", true); + if (dependent_config) s.member ("dependent_config", true); + + s.member_name ("package"); + package (rs.front ()); + + if (rs.size () > 1) + { + s.member_begin_array ("dependencies"); + for (auto i (rs.begin ()); ++i != rs.end (); ) package (*i); + s.end_array (); + } + } + s.end_object (); // bindist_result + + cout << endl; + } + + return 0; + } + + pkg_bindist_options + merge_options (const default_options<pkg_bindist_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<pkg_bindist_options>& 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 ()); + }); + } +} |