// file : bpkg/system-package-manager-fedora.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file #include #include #include #include using namespace butl; namespace bpkg { using package_status = system_package_status_fedora; // Translate host CPU to Fedora package architecture. // string system_package_manager_fedora:: arch_from_target (const target_triplet& h) { const string& c (h.cpu); return c == "i386" || c == "i486" || c == "i586" || c == "i686" ? "i686" : c; } // Parse the fedora-name (or alike) value. The first argument is the package // type. // // 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 -debug* unless requested with the // extra_{doc,debug*} arguments. Note that we treat -static as -devel (since // we can't know whether the static library is needed or not). // package_status system_package_manager_fedora:: parse_name_value (const string& pt, const string& nv, bool extra_doc, bool extra_debuginfo, bool extra_debugsource) { 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 string* pt) { strings ns (split (g, ' ')); if (ns.empty ()) fail << "empty package group"; package_status r; // Handle the "devel instead of main" special case for libraries. // // Check that the following name does not end with -devel. This will be // the only way to disambiguate the case where the library name happens // to end with -devel (e.g., libfoo-devel libfoo-devel-devel). // { string& m (ns[0]); if (pt != nullptr && *pt == "lib" && suffix (m, "-devel") && !(ns.size () > 1 && suffix (ns[1], "-devel"))) { 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 = "-devel")) ? &r.devel : suffix (n, (w = "-static")) ? &r.static_ : suffix (n, (w = "-doc")) ? &r.doc : suffix (n, (w = "-debuginfo")) ? &r.debuginfo : suffix (n, (w = "-debugsource")) ? &r.debugsource : 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], &pt); else { package_status g (parse_group (gs[i], nullptr)); if (!g.main.empty ()) r.extras.push_back (move (g.main)); if (!g.devel.empty ()) r.extras.push_back (move (g.devel)); if (!g.static_.empty ()) r.extras.push_back (move (g.static_)); if (!g.doc.empty () && extra_doc) r.extras.push_back (move (g.doc)); if (!g.debuginfo.empty () && extra_debuginfo) r.extras.push_back (move (g.debuginfo)); if (!g.debugsource.empty () && extra_debugsource) r.extras.push_back (move (g.debugsource)); 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 -devel package based // on the extracted (by dnf_repoquery_requires()) dependencies, passed as a // list of the package name/version pairs. Return empty string if unable to. // string system_package_manager_fedora:: main_from_devel (const string& devel_name, const string& devel_ver, const vector>& depends) { // For the main package we first look for a dependency with the // -libs name and the devel_ver version. Failed that, we try // just . // // Note that the order is important since for a mixed package we need to // end up with the -libs sub-package rather than with the base package as, // for example, in the following case: // // sqlite-devel 3.36.0-3.fc35 -> // sqlite 3.36.0-3.fc35 // sqlite-libs 3.36.0-3.fc35 // string devel_stem (devel_name, 0, devel_name.rfind ("-devel")); auto find = [&devel_ver, &depends] (const string& n) { auto i (find_if (depends.begin (), depends.end (), [&n, &devel_ver] (const pair& d) { return d.first == n && d.second == devel_ver; })); return i != depends.end () ? i->first : string (); }; string r (find (devel_stem + "-libs")); return !r.empty () ? r : find (devel_stem); } static process_path dnf_path; static process_path sudo_path; // Obtain the installed and candidate versions for the specified list of // Fedora packages by executing `dnf list`. // // If the n argument is not 0, then only query the first n packages. // void system_package_manager_fedora:: dnf_list (vector& pis, size_t n) { if (n == 0) n = pis.size (); assert (n != 0 && n <= pis.size ()); // The --quiet option makes sure we don't get 'Last metadata expiration // check: ' printed to stderr. It does not appear to affect // error diagnostics (try specifying a single unknown package). // cstrings args { "dnf", "list", "--all", // Look for both installed and available. "--cacheonly", // Don't automatically update the metadata. "--quiet"}; for (size_t i (0); i != n; ++i) { package_info& pi (pis[i]); string& n (pi.name); assert (!n.empty ()); pi.installed_version.clear (); pi.candidate_version.clear (); pi.installed_arch.clear (); pi.candidate_arch.clear (); args.push_back (n.c_str ()); } // Note that `dnf list` fails if there are no matching packages to print. // Thus, let's hack around this by adding the rpm package to the list, so // that at least one package is always present and the command can never // fail for that reason. // // Also note that we still allow the rpm package to appear in the // specified package list. // bool rpm (false); args.push_back ("rpm"); args.push_back (nullptr); // Run with the C locale to make sure there is no localization. // const char* evars[] = {"LC_ALL=C", nullptr}; try { if (dnf_path.empty () && !simulate_) dnf_path = process::path_search (args[0], false /* init */); process_env pe (dnf_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 (dnf_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 (pis[i].name); const path* f (nullptr); if (installed_) { auto i (simulate_->dnf_list_installed_.find (k)); if (i != simulate_->dnf_list_installed_.end ()) f = &i->second; } if (f == nullptr && fetched_) { auto i (simulate_->dnf_list_fetched_.find (k)); if (i != simulate_->dnf_list_fetched_.end ()) f = &i->second; } if (f == nullptr) { auto i (simulate_->dnf_list_.find (k)); if (i != simulate_->dnf_list_.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 `dnf list ...` is the 2 groups of lines // in the following form: // // Installed Packages // . 13.0.0-3.fc35 @ // . 69.1-6.fc35 @ // Available Packages // . 13.0.1-1.fc35 // . 1.2.11-32.fc35 // // Where unknown packages are omitted. The lines order does not // necessarily match the order of the packages on the command line. // It looks like there should be not blank lines but who really knows. // // Note also that if a package appears in the 'Installed Packages' // group, then it only appears in the 'Available Packages' if the // candidate version is better. Only the single (best) available // version is listed, which we call the candidate version. // { auto df = make_diag_frame ( [&pe, &args] (diag_record& dr) { dr << info << "while parsing output of "; print_process (dr, pe, args); }); // Keep track of whether we are inside of the 'Installed Packages' // or 'Available Packages' sections. // optional installed; for (string l; !eof (getline (is, l)); ) { if (l == "Installed Packages") { if (installed) fail << "unexpected line '" << l << "'"; installed = true; continue; } if (l == "Available Packages") { if (installed && !*installed) fail << "duplicate line '" << l << "'"; installed = false; continue; } if (!installed) fail << "unexpected line '" << l << "'"; // Parse the package name. // size_t e (l.find (' ')); if (l.empty () || e == 0) fail << "expected package name in '" << l << "'"; if (e == string::npos) fail << "expected package version in '" << l << "'"; string p (l, 0, e); // Parse the package version. // size_t b (l.find_first_not_of (' ', e + 1)); if (b == string::npos) fail << "expected package version in '" << l << "'"; // It doesn't not seem that the repository id can be absent. Even // if the package is installed manually it is assumed to come from // some special repository (@commandline, etc). For example: // // # dnf install ./libsigc++30-3.0.7-2.fc35.x86_64.rpm // # rpm -i ./libsigc++30-devel-3.0.7-2.fc35.x86_64.rpm // # dnf list --quiet libsigc++30.x86_64 libsigc++30-devel.x86_64 // Installed Packages // libsigc++30.x86_64 3.0.7-2.fc35 @@commandline // libsigc++30-devel.x86_64 3.0.7-2.fc35 @@System // // Thus, we assume that the version is always followed with the // space character. // e = l.find (' ', b + 1); if (e == string::npos) fail << "expected package repository in '" << l << "'"; string v (l, b, e - b); // While we don't really care about the rest of the line, let's // verify that it contains a repository id, for good measure. // b = l.find_first_not_of (' ', e + 1); if (b == string::npos) fail << "expected package repository in '" << l << "'"; // Separate the architecture from the package name. // e = p.rfind ('.'); if (e == string::npos || e == 0 || e == p.size () - 1) fail << "can't extract architecture for package " << p << " in '" << l << "'"; string a (p, e + 1); // Skip the package if its architecture differs from the host // architecture. // if (a != arch && a != "noarch") continue; p.resize (e); if (p == "rpm") rpm = true; // Find the package info to update. // auto i (find_if (pis.begin (), pis.end (), [&p] (const package_info& pi) {return pi.name == p;})); if (i == pis.end ()) { // Skip the special rpm package which may not be present in the // list. // if (p == "rpm") continue; fail << "unexpected package " << p << '.' << a << ' ' << v << " in '" << l << "'"; } string& ver (*installed ? i->installed_version : i->candidate_version); if (!ver.empty ()) fail << "multiple " << (*installed ? "installed " : "available ") << "versions of package " << p << '.' << a << info << "version: " << ver << info << "version: " << v; ver = move (v); (*installed ? i->installed_arch : i->candidate_arch) = move (a); } } is.close (); } catch (const io_error& e) { if (pr.wait ()) fail << "unable to read " << args[0] << " list output: " << e; // Fall through. } if (!pr.wait ()) { diag_record dr (fail); dr << args[0] << " list 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 (); } if (!rpm) fail << "rpm package doesn't exist"; // Note that if a Fedora package is installed but the repository doesn't // contain a better version, then this package won't appear in the // 'Available Packages' section of the `dnf list` output and thus the // candidate_version will stay empty. Let's set it to the installed // version in this case to be consistent with the Debian's semantics and // keep the Fedora and Debian system package manager implementations // aligned. // for (size_t i (0); i != n; ++i) { package_info& pi (pis[i]); if (pi.candidate_version.empty () && !pi.installed_version.empty ()) { pi.candidate_version = pi.installed_version; pi.candidate_arch = pi.installed_arch; } } } // Execute `dnf repoquery --requires` for the specified // package/version/architecture and return its dependencies as a list of the // name/version pairs. // // It is expected that the specified package/version/architecture is known // (e.g., returned by the `dnf list` command). Note that if that's not the // case (can happen due to a race), then an empty list is returned. This, // however, is ok for our current usage since in this case we will shortly // fail with the 'unable to guess main package' error anyway. // // Note that the returned dependencies are always of the host architecture // or noarch. For example: // // dhcp-client-12:4.4.3-4.P1.fc35.x86_64 -> // dhcp-common-12:4.4.3-4.P1.fc35.noarch // coreutils-8.32-36.fc35.x86_64 // ... // // rust-uuid+std-devel-1.2.1-1.fc35.noarch -> // rust-uuid-devel-1.2.1-1.fc35.noarch // cargo-1.65.0-1.fc35.x86_64 // vector> system_package_manager_fedora:: dnf_repoquery_requires (const string& name, const string& ver, const string& qarch, bool installed) { assert (!name.empty () && !ver.empty () && !arch.empty ()); // Qualify the package with the architecture suffix. // // Note that for reasons unknown, the below command may still print some // dependencies with different architecture (see the below example). It // feels sensible to just skip them. // string spec (name + '-' + ver + '.' + qarch); // The --quiet option makes sure we don't get 'Last metadata expiration // check: ' printed to stderr. It does not appear to affect // error diagnostics (try specifying an unknown option). // cstrings args { "dnf", "repoquery", "--requires", "--quiet", "--cacheonly", // Don't automatically update the metadata. "--resolve", // Resolve requirements to packages/versions. "--qf", "%{name} %{arch} %{epoch}:%{version}-%{release}"}; // Note that installed packages which are not available from configured // repositories (e.g. packages installed from local rpm files or temporary // local repositories, package versions not available anymore from their // original repositories, etc) are not seen by `dnf repoquery` by // default. It also turned out that the --installed option not only limits // the resulting set to the installed packages, but also makes `dnf // repoquery` to see all the installed packages, including the unavailable // ones. Thus, we always add this option to query dependencies of the // installed packages. // if (installed) { args.push_back ("--installed"); // dnf(8) also recommends to use --disableexcludes together with // --install to make sure that all installed packages will be listed and // no configuration file may influence the result. // args.push_back ("--disableexcludes=all"); } args.push_back (spec.c_str ()); args.push_back (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. // const char* evars[] = {"LC_ALL=C", nullptr}; vector> r; try { if (dnf_path.empty () && !simulate_) dnf_path = process::path_search (args[0], false /* init */); process_env pe (dnf_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 (dnf_path, args, -2 /* stdin */, -1 /* stdout */, 2 /* stderr */, nullptr /* cwd */, evars); else { simulation::package k {name, ver, qarch, installed}; const path* f (nullptr); if (fetched_) { auto i (simulate_->dnf_repoquery_requires_fetched_.find (k)); if (i != simulate_->dnf_repoquery_requires_fetched_.end ()) f = &i->second; } if (f == nullptr) { auto i (simulate_->dnf_repoquery_requires_.find (k)); if (i != simulate_->dnf_repoquery_requires_.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 the command will be the sequence of the package lines // in the ` ` form (per the -qf option above). So // for example for the libicu-devel-69.1-6.fc35.x86_64 package it is // as follows: // // bash i686 0:5.1.8-3.fc35 // bash x86_64 0:5.1.8-3.fc35 // glibc i686 0:2.34-49.fc35 // glibc x86_64 0:2.34-49.fc35 // libicu x86_64 0:69.1-6.fc35 // libicu-devel i686 0:69.1-6.fc35 // libicu-devel x86_64 0:69.1-6.fc35 // pkgconf-pkg-config i686 0:1.8.0-1.fc35 // pkgconf-pkg-config x86_64 0:1.8.0-1.fc35 // // Note that there is also a self-dependency. // for (string l; !eof (getline (is, l)); ) { // Parse the package name. // size_t e (l.find (' ')); if (l.empty () || e == 0) fail << "expected package name in '" << l << "'"; if (e == string::npos) fail << "expected package architecture in '" << l << "'"; string p (l, 0, e); // Parse the package architecture. // size_t b (e + 1); e = l.find (' ', b); if (e == string::npos) fail << "expected package version in '" << l << "'"; string a (l, b, e - b); if (a.empty ()) fail << "expected package architecture in '" << l << "'"; // Parse the package version. // string v (l, e + 1); // Strip the '0:' epoch from the package version to align with // versions retrieved by other functions (dnf_list(), etc). // e = v.find (':'); if (e == string::npos || e == 0) fail << "no epoch for package version in '" << l << "'"; if (e == 1 && v[0] == '0') v.erase (0, 2); // Skip a potential self-dependency and dependencies of a different // architecture. // if (p == name || (a != arch && a != "noarch")) continue; r.emplace_back (move (p), move (v)); } is.close (); } catch (const io_error& e) { if (pr.wait ()) fail << "unable to read " << args[0] << " repoquery --requires " << "output: " << e; // Fall through. } if (!pr.wait ()) { diag_record dr (fail); dr << args[0] << " repoquery --requires 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 options for commands which update the system. // pair system_package_manager_fedora:: dnf_common (const char* command, optional fetch_timeout, strings& args_storage) { // Pre-allocate the required number of entries in the arguments storage. // if (fetch_timeout) args_storage.reserve (1); cstrings args; if (!sudo_.empty ()) args.push_back (sudo_.c_str ()); args.push_back ("dnf"); args.push_back (command); // Map our verbosity/progress to dnf --quiet and --verbose options. // // Note that all the diagnostics, including the progress indication and // general information (like what's being installed) but excluding error // messages, is printed to stdout. So we fix this by redirecting stdout to // stderr. By default the progress bar for network transfers is printed, // unless stdout is not a terminal. The --quiet option disables printing // the plan and all the progress indication, but not the confirmation // prompt nor error messages. // if (progress_ && *progress_) { // Print the progress bar by default, unless this is not a terminal // (there is no way to force it). } else if (verb == 0 || (progress_ && !*progress_)) { args.push_back ("--quiet"); } if (yes_) { args.push_back ("--assumeyes"); } else if (!stderr_term) { // Suppress any prompts if stderr is not a terminal for good measure. // args.push_back ("--assumeno"); } // Add the network operations timeout configuration options, if requested. // if (fetch_timeout) { args_storage.push_back ( "--setopt=timeout=" + to_string (*fetch_timeout)); args.push_back (args_storage.back ().c_str ()); args.push_back ("--setopt=minrate=0"); } try { const process_path* pp (nullptr); if (!sudo_.empty ()) { if (sudo_path.empty () && !simulate_) sudo_path = process::path_search (args[0], false /* init */); pp = &sudo_path; } else { if (dnf_path.empty () && !simulate_) dnf_path = process::path_search (args[0], false /* init */); pp = &dnf_path; } return pair (move (args), *pp); } catch (const process_error& e) { fail << "unable to execute " << args[0] << ": " << e << endf; } } // Execute `dnf makecache` to download and cache the repositories metadata. // void system_package_manager_fedora:: dnf_makecache () { strings args_storage; pair args_pp ( dnf_common ("makecache", fetch_timeout_, args_storage)); cstrings& args (args_pp.first); const process_path& pp (args_pp.second); args.push_back ("--refresh"); args.push_back (nullptr); try { if (verb >= 2) print_process (args); else if (verb == 1) text << "updating " << os_release.name_id << " repositories metadata..."; process pr; if (!simulate_) { // Redirect stdout to stderr. // pr = process (pp, args, 0 /* stdin */, 2 /* stdout */); } else { print_process (args); pr = process (process_exit (simulate_->dnf_makecache_fail_ ? 1 : 0)); } if (!pr.wait ()) { diag_record dr (fail); dr << "dnf makecache 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 << " repositories metadata"; } catch (const process_error& e) { error << "unable to execute " << args[0] << ": " << e; if (e.child) exit (1); throw failed (); } } // Execute `dnf install` to install the specified packages (e.g., // libfoo.x86_64 or libfoo-1.2.3-1.fc35.x86_64). // // Note that the package name can only contain alpha-numeric characters, // '-', '.', '_', and '+' (see Guidelines for Naming Fedora Packages for // details). If specified, both the version (1.2.3) and release (1.fc35) // parts are mandatory and may only contain alpha-numeric characters, `.`, // `_`, `+`, `~`, and `^` (see the RPM spec file format documentation for // details). Thus, package specs (which are actually wildcards) are // generally ambiguous, so that libfoo-1.2.3-1.fc35.x86_64 may theoretically // be a package name and libfoo-bar a specific package version. // // By default, `dnf install` tries to interpret the spec as the // -[:]-. form prior to trying the // . form until any matched packages are found (see SPECIFYING // PACKAGES section of dnf(8) for more details on the spec matching // rules). We could potentially use `dnf install-nevra` command for the // package version specs and `dnf install-na` for the package name specs. // Let's, however, keep it simple for now given that clashes for our // use-case are presumably not very likely. // void system_package_manager_fedora:: dnf_install (const strings& pkgs) { assert (!pkgs.empty ()); strings args_storage; pair args_pp ( dnf_common ("install", fetch_timeout_, args_storage)); cstrings& args (args_pp.first); const process_path& pp (args_pp.second); // Note that we can't use --cacheonly here to prevent the metadata update, // since the install command then expects the package RPM files to also be // cached and fails if that's not the case. Thus we have to override the // metadata_expire=never configuration option instead. Which makes the // whole thing quite hairy and of dubious value -- there is nothing wrong // with letting it re-fetch the metadata during install (which in fact may // save us from attempting to download no longer existing packages). // #if 0 args.push_back ("--setopt=metadata_expire=never"); #endif 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_) { // Redirect stdout to stderr. // pr = process (pp, args, 0 /* stdin */, 2 /* stdout */); } else { print_process (args); pr = process (process_exit (simulate_->dnf_install_fail_ ? 100 : 0)); } if (!pr.wait ()) { diag_record dr (fail); dr << "dnf 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 (); } } // Execute `dnf mark install` to mark the installed packages as installed by // the user (see dnf_install() for details on the package specs). // // Note that an installed package may be marked as installed by the user // rather than as a dependency. In particular, such a package will never be // automatically removed as an unused dependency. This mark can be added and // removed by the `dnf mark install` and `dnf mark remove` commands, // respectively. Besides that, this mark is automatically added by `dnf // install` for a package specified on the command line, but only if it is // not yet installed. Note that this mark will not be added automatically // for an already installed package even if it is upgraded explicitly. For // example: // // $ sudo dnf install libsigc++30-devel-3.0.2-2.fc32 --repofrompath test,./repo --setopt=gpgcheck=0 --assumeyes // Installed: libsigc++30-3.0.2-2.fc32.x86_64 libsigc++30-devel-3.0.2-2.fc32.x86_64 // // $ sudo dnf install --best libsigc++30 --assumeyes // Upgraded: libsigc++30-3.0.7-2.fc35.x86_64 libsigc++30-devel-3.0.7-2.fc35.x86_64 // // $ sudo dnf remove libsigc++30-devel --assumeyes // Removed: libsigc++30-3.0.7-2.fc35.x86_64 libsigc++30-devel-3.0.7-2.fc35.x86_64 // void system_package_manager_fedora:: dnf_mark_install (const strings& pkgs) { assert (!pkgs.empty ()); strings args_storage; pair args_pp ( dnf_common ("mark", nullopt /* fetch_timeout */, args_storage)); cstrings& args (args_pp.first); const process_path& pp (args_pp.second); args.push_back ("install"); args.push_back ("--cacheonly"); for (const string& p: pkgs) args.push_back (p.c_str ()); args.push_back (nullptr); try { if (verb >= 2) print_process (args); process pr; if (!simulate_) { // Redirect stdout to stderr. // pr = process (pp, args, 0 /* stdin */, 2 /* stdout */); } else { print_process (args); pr = process (process_exit (simulate_->dnf_mark_install_fail_ ? 1 : 0)); } if (!pr.wait ()) { diag_record dr (fail); dr << "dnf mark 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"; } } catch (const process_error& e) { error << "unable to execute " << args[0] << ": " << e; if (e.child) exit (1); throw failed (); } } optional system_package_manager_fedora:: status (const package_name& pn, const available_packages* aps) { // 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; } optional r (status (pn, *aps)); // Cache. // auto i (status_cache_.emplace (pn, move (r)).first); return i->second ? &*i->second : nullptr; } optional system_package_manager_fedora:: status (const package_name& pn, const available_packages& aps) { tracer trace ("system_package_manager_fedora::status"); // For now we ignore -doc and -debug* package components (but we may want // to have options controlling this later). Note also that we assume // -common is pulled automatically by the base package so we ignore it as // well (see equivalent logic in parse_name_value()). // bool need_doc (false); bool need_debuginfo (false); bool need_debugsource (false); vector candidates; // Translate our package name to the Fedora package names. // { auto df = make_diag_frame ( [this, &pn] (diag_record& dr) { dr << info << "while mapping " << pn << " to " << os_release.name_id << " package name"; }); // Without explicit type, 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 (or // explicit type). // // Note that using the first (latest) available package as a source of // type information seems like a reasonable choice. // const string& pt (!aps.empty () ? aps.front ().first->effective_type () : package_manifest::effective_type (nullopt, pn)); strings ns; if (!aps.empty ()) ns = system_package_names (aps, os_release.name_id, os_release.version_id, os_release.like_ids, true /* native */); if (ns.empty ()) { // Attempt to automatically translate our package name. Failed that we // should try to use the project name, if present, as a fallback. // const string& n (pn.string ()); // Note that theoretically different available packages can have // different project names. But taking it from the latest version // feels good enough. // const shared_ptr& ap (!aps.empty () ? aps.front ().first : nullptr); string f (ap != nullptr && ap->project && *ap->project != pn ? ap->project->string () : empty_string); if (pt == "lib") { // If there is no project name let's try to use the package name // with the lib prefix stripped as a fallback. Note that naming // library packages without the lib prefix is quite common in Fedora // (xerces-c, uuid-c++, etc). // if (f.empty ()) f = string (n, 3); f += "-devel"; // Keep the base package name empty as an indication that it is to // be discovered. // candidates.push_back (package_status ("", n + "-devel", move (f))); } else candidates.push_back (package_status (n, "", move (f))); } else { // Parse each manual mapping. // for (const string& n: ns) { package_status s (parse_name_value (pt, n, need_doc, need_debuginfo, need_debugsource)); // Suppress duplicates for good measure based on the base package // name (and falling back to -devel 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 -devel only while the other as // main and -devel. // return s.main.empty () || x.main.empty () ? s.devel == x.devel : 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: // // fedora_35-name: libfoo libfoo-bar-devel // fedora_34-name: libfoo libfoo-devel // // Note that for this to work we must get fedora_35 values before // fedora_34, which is the semantics guaranteed by // system_package_names(). } } } } // Guess unknown main package given the -devel package, its version, and // architecture. Failed that, assume the package to be a binless library // and leave the main member of the package_status object empty. // auto guess_main = [this, &trace] (package_status& s, const string& ver, const string& qarch, bool installed) { vector> depends ( dnf_repoquery_requires (s.devel, ver, qarch, installed)); s.main = main_from_devel (s.devel, ver, depends); if (s.main.empty ()) { if (verb >= 4) { diag_record dr (trace); dr << "unable to guess main package for " << s.devel << ' ' << ver; if (!depends.empty ()) { dr << ", depends on"; for (auto b (depends.begin ()), i (b); i != depends.end (); ++i) dr << (i == b ? " " : ", ") << i->first << ' ' << i->second; } else dr << ", has no dependencies"; } } }; // 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& pis, size_t main) -> optional { bool i (false), u (false); for (size_t j (0); j != pis.size (); ++j) { const package_info& pi (pis[j]); if (pi.installed_version.empty ()) { if (pi.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 r; { diag_record dr; // Ambiguity diagnostics. for (package_status& ps: candidates) { vector& pis (ps.package_infos); // Query both main and fallback packages with a single dns_list() // invocation. // if (!ps.main.empty ()) pis.emplace_back (ps.main); if (!ps.devel.empty ()) pis.emplace_back (ps.devel); if (!ps.fallback.empty ()) pis.emplace_back (ps.fallback); if (!ps.static_.empty ()) pis.emplace_back (ps.static_); if (!ps.doc.empty () && need_doc) pis.emplace_back (ps.doc); if (!ps.debuginfo.empty () && need_debuginfo) pis.emplace_back (ps.debuginfo); if (!ps.debugsource.empty () && need_debugsource) pis.emplace_back (ps.debugsource); if (!ps.common.empty () && false) pis.emplace_back (ps.common); ps.package_infos_main = pis.size (); for (const string& n: ps.extras) pis.emplace_back (n); dnf_list (pis); // Handle the fallback package name, if specified. // // Specifically, if the main/devel package is known to the system // package manager we use that. Otherwise, if the fallback package is // known we use that. And if neither is known, then we skip this // candidate (ps). // if (!ps.fallback.empty ()) { assert (pis.size () > 1); // devel+fallback or main+fallback package_info& mp (pis[0]); // Main/devel package info. package_info& fp (pis[1]); // Fallback package info. // Note that at this stage we can only use the installed main/devel // and fallback packages (since the candidate versions may change // after fetch). // // Also note that this logic prefers installed fallback package to // potentially available non-fallback package. // if (mp.installed_version.empty ()) { if (!fp.installed_version.empty ()) { // Use the fallback. // (ps.main.empty () ? ps.devel : ps.main) = move (ps.fallback); mp = move (fp); } else continue; // Skip the candidate at this stage. } // Whether it was used or not, cleanup the fallback information. // ps.fallback.clear (); pis.erase (pis.begin () + 1); --ps.package_infos_main; } // Handle the unknown main package. // if (ps.main.empty ()) { const package_info& devel (pis.front ()); // Note that at this stage we can only use the installed -devel // package (since the candidate version may change after fetch). // if (devel.installed_version.empty ()) continue; guess_main (ps, devel.installed_version, devel.installed_arch, true /* installed */); if (!ps.main.empty ()) // Not a binless library? { pis.emplace (pis.begin (), ps.main); ps.package_infos_main++; dnf_list (pis, 1); } } optional s (status (pis, ps.package_infos_main)); if (!s || *s != package_status::installed) continue; const package_info& main (pis.front ()); // Main/devel. 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->system_name << ' ' << r->system_version; } dr << info << "candidate: " << ps.system_name << ' ' << 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. Indicate // the non-installable candidates by setting both their main and -devel // package names to empty strings. // if (!r && install_) { // If we weren't instructed to fetch or we already fetched, then we // don't need to re-run dnf_list(). // bool requery; if ((requery = fetch_ && !fetched_)) { dnf_makecache (); fetched_ = true; } { diag_record dr; // Ambiguity diagnostics. for (package_status& ps: candidates) { vector& pis (ps.package_infos); if (requery) dnf_list (pis); // Handle the fallback package name, if specified. // if (!ps.fallback.empty ()) { assert (pis.size () > 1); // devel+fallback or main+fallback package_info& mp (pis[0]); // Main/devel package info. package_info& fp (pis[1]); // Fallback package info. // Note that this time we use the candidate versions. // if (mp.candidate_version.empty ()) { string& main (!ps.main.empty () ? ps.main : ps.devel); if (!fp.candidate_version.empty ()) { // Use the fallback. // main = move (ps.fallback); mp = move (fp); } else { // Otherwise, we would have resolved the name on the previous // stage. // assert (mp.installed_version.empty () && fp.installed_version.empty ()); // Main/devel package is not installable. // main.clear (); continue; } } // Whether it was used or not, cleanup the fallback information. // ps.fallback.clear (); pis.erase (pis.begin () + 1); --ps.package_infos_main; } // Handle the unknown main package. // if (ps.main.empty ()) { const package_info& devel (pis.front ()); // Note that this time we use the candidate version. // if (devel.candidate_version.empty ()) { // Not installable. // ps.devel.clear (); continue; } guess_main (ps, devel.candidate_version, devel.candidate_arch, devel.candidate_version == devel.installed_version); if (!ps.main.empty ()) // Not a binless library? { pis.emplace (pis.begin (), ps.main); ps.package_infos_main++; dnf_list (pis, 1); } } optional s (status (pis, ps.package_infos_main)); if (!s) { // Not installable. // ps.main.clear (); ps.devel.clear (); continue; } assert (*s != package_status::installed); // Sanity check. const package_info& main (pis.front ()); // Main/devel. // 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 // -devel 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_info& pi: s.package_infos) if (pi.installed_version.empty ()) dr << ' ' << pi.name; }; if (dr.empty ()) { dr << fail << "multiple partially installed " << os_release.name_id << " packages for " << pn; dr << info << "candidate: " << r->system_name << ' ' << r->system_version << ", missing components:"; print_missing (*r); } dr << info << "candidate: " << ps.system_name << ' ' << 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 () && ps.devel.empty ()) // Not installable? 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->system_name << ' ' << r->system_version; } dr << info << "candidate: " << ps.system_name << ' ' << ps.system_version; } if (!dr.empty ()) dr << info << "consider installing the desired package manually and " << "retrying the bpkg command"; } } if (r) { // Map the Fedora version to the bpkg version. But first strip the // release from Fedora version ([:]-). // string sv (r->system_version, 0, r->system_version.rfind ('-')); optional v; if (!aps.empty ()) 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. Also convert the potential pre-release // separator to the bpkg version pre-release separator. // size_t p (sv.find (':')); if (p != string::npos) sv.erase (0, p + 1); // Consider the first '~' character as a pre-release separator. Note // that if there are more of them, then we will fail since '~' is an // invalid character for bpkg version. // p = sv.find ('~'); if (p != string::npos) sv[p] = '-'; 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); } return r; } void system_package_manager_fedora:: install (const vector& pns) { assert (!pns.empty ()); assert (install_ && !installed_); installed_ = true; // Collect and merge all the Fedora packages/versions for the specified // bpkg packages. // struct package { string name; string version; // Empty if unspecified. string arch; // Always specified. }; vector pkgs; // 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 dnf(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 what we are going to do is to run `dnf install` only if there are // any non-fully installed packages. In this case we will pass all the // packages, including the fully installed ones. But we must be careful // not to force their upgrade. To achieve this we will specify the // installed version as the desired version. Whether we run `dnf install` // or not we will also always run `dnf mark install` afterwards for all // the packages to mark them as installed by the user. // // Note also that for partially/not installed we don't specify the // version, expecting the candidate version to be installed. We, however, // still specify the candidate architecture in this case, since for // reasons unknown dnf may install a package of a different architecture // otherwise. // bool install (false); 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); bool fi (ps.status == package_status::installed); if (!fi) install = true; for (const package_info& pi: ps.package_infos) { string n (pi.name); string v (fi ? pi.installed_version : string ()); string a (fi ? pi.installed_arch : pi.candidate_arch); 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); i->arch = move (a); } else // Feels like this cannot happen since we always use the installed // version of the package. // assert (i->version == v && i->arch == a); } else pkgs.push_back (package {move (n), move (v), move (a)}); } } // Convert to the -[:]-. package spec // for the installed packages and to the spec for partially/not // installed ones (see dnf_install() for details on the package specs). // strings specs; specs.reserve (pkgs.size ()); for (const package& p: pkgs) { string s (p.name); if (!p.version.empty ()) { s += '-'; s += p.version; } s += '.'; s += p.arch; specs.push_back (move (s)); } // Install. // if (install) dnf_install (specs); // Mark as installed by the user. // dnf_mark_install (specs); // Verify that versions we have promised in status() match what actually // got installed. // if (install) { vector pis; // 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 (pis.begin (), pis.end (), [&ps] (const package_info& pi) { return pi.name == ps.system_name; }) == pis.end ()) { pis.push_back (package_info (ps.system_name)); } } dnf_list (pis); for (const package_name& pn: pns) { const package_status& ps (*status_cache_.find (pn)->second); auto i (find_if (pis.begin (), pis.end (), [&ps] (const package_info& pi) { return pi.name == ps.system_name; })); assert (i != pis.end ()); const package_info& pi (*i); if (pi.installed_version != ps.system_version) { fail << "unexpected " << os_release.name_id << " package version " << "for " << ps.system_name << info << "expected: " << ps.system_version << info << "installed: " << pi.installed_version << info << "consider retrying the bpkg command"; } } } } // Map non-system bpkg package to system package name(s) and version. // // This is used both to map the package being generated and its // dependencies. What should we do with extras returned in package_status? // We can't really generate any of them (which files would we place in // them?) nor can we list them as dependencies (we don't know their system // versions). So it feels like the only sensible choice is to ignore extras. // // In a sense, we have a parallel arrangement going on here: binary packages // that we generate don't have extras (i.e., they include everything // necessary in the "standard" packages from the main group) and when we // punch a system dependency based on a non-system bpkg package, we assume // it was generated by us and thus doesn't have any extras. Or, to put it // another way, if you want the system dependency to refer to a "native" // system package with extras you need to configure it as a system bpkg // package. // // In fact, this extends to package names. For example, unless custom // mapping is specified, we will generate libsqlite3 and libsqlite3-devel // while native names are sqlite-libs and sqlite-devel. While this duality // is not ideal, presumably we will normally only be producing our binary // packages if there are no suitable native packages. And for a few // exceptions (e.g., our package is "better" in some way, such as configured // differently or fixes a critical bug), we will just have to provide // appropriate manual mapping that makes sure the names match (the extras is // still a potential problem though -- we will only have them as // dependencies if we build against a native system package; maybe we can // add them manually with an option). // package_status system_package_manager_fedora:: map_package (const package_name& pn, const version& pv, const available_packages& aps) const { // We should only have one available package corresponding to this package // name/version. // assert (aps.size () == 1); const shared_ptr& ap (aps.front ().first); const lazy_shared_ptr& rf (aps.front ().second); // Without explicit type, 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 (or explicit // type). // const string& pt (ap->effective_type ()); strings ns (system_package_names (aps, os_release.name_id, os_release.version_id, os_release.like_ids, false /* native */)); package_status r; if (ns.empty ()) { // Automatically translate our package name similar to the consumption // case above. Except here we don't attempt to deduce main from -devel // or fallback to the project name, naturally. // const string& n (pn.string ()); if (pt == "lib") r = package_status (n, n + "-devel"); else r = package_status (n); } else { // Even though we only pass one available package, we may still end up // with multiple mappings. In this case we take the first, per the // documentation. // r = parse_name_value (pt, ns.front (), false /* need_doc */, false /* need_debuginfo */, false /* need_debugsource */); // If this is -devel without main, then derive main by stripping the // -devel suffix. This feels tighter than just using the bpkg package // name. // if (r.main.empty ()) { assert (!r.devel.empty ()); r.main.assign (r.devel, 0, r.devel.size () - 6); } } // Map the version. // // To recap, a Fedora package version has the following form: // // [:]- // // Where has the following form: // // [.] // // For details on the ordering semantics, see the Fedora Versioning // Guidelines. While overall unsurprising, the only notable exceptions are // `~`, which sorts before anything else and is commonly used for upstream // pre-releases, and '^', which sorts after anything else and is // supposedly used for upstream post-release snapshots. For example, // 0.1.0~alpha.1-1.fc35 sorts earlier than 0.1.0-1.fc35. // // Ok, so how do we map our version to that? To recap, the bpkg version // has the following form: // // [+-][-][+] // // Let's start with the case where neither distribution nor upstream // version is specified and we need to derive everything from the bpkg // version. // // // // On one hand, if we keep the epoch, it won't necessarily match // Fedora's native package epoch. But on the other it will allow our // binary packages from different epochs to co-exist. Seeing that this // can be easily overridden with a custom distribution version, let's // keep it. // // Note that while the Fedora start/default epoch is 0, ours is 1 (we // use the 0 epoch for stub packages). So we will need to shift this // value range. // // // [-] // // Our upstream version maps naturally to Fedora's . That is, // our upstream version format/semantics is a subset of Fedora's // . // // If this is a pre-release, then we could fail (that is, don't allow // pre-releases) but then we won't be able to test on pre-release // packages, for example, to make sure the name mapping is correct. // Plus sometimes it's useful to publish pre-releases. We could ignore // it, but then such packages will be indistinguishable from each other // and the final release, which is not ideal. On the other hand, Fedora // has the mechanism (`~`) which is essentially meant for this, so let's // use it. We will use as is since its format is the same as // and thus should map naturally. // // // // // Similar to epoch, our revision won't necessarily match Fedora's // native package release number. But on the other hand it will allow us // to establish a correspondence between source and binary packages. // Plus, upgrades between binary package releases will be handled // naturally. Also note that the revision is mandatory in Fedora. // Seeing that we allow overriding the releases with a custom // distribution version (see below), let's use it. // // Note that the Fedora start release number is 1 and our revision is // 0. So we will need to shift this value range. // // Another related question is whether we should do anything about the // distribution tag (.fc35, .el8, etc). Given that the use of hardcoded // distribution tags in RPM spec files is strongly discouraged we will // just rely on the standard approach to include the appropriate tag // (while allowing the user to redefine it with an option). Note that // the distribution tag is normally specified for the Release and // Requires directives using the %{?dist} macro expansion and can be // left unspecified for the Requires directive. For example: // // Name: curl // Version: 7.87.0 // Release: 1%{?dist} // Requires: libcurl%{?_isa} >= %{version}-%{release} // %global libpsl_version 1.2.3 // Requires: libpsl%{?_isa} >= %{libpsl_version} // // The next case to consider is when we have the upstream version // (upstream-version manifest value). After some rumination it feels // correct to use it in place of the - components in the // above mapping (upstream version itself cannot have epoch). In other // words, we will add the pre-release and revision components from the // bpkg version. If this is not the desired semantics, then it can always // be overrided with the distribution version. // // Finally, we have the distribution version. The and // components are straightforward: they should be specified by the // distribution version as required. This leaves pre-release and // release. It feels like in most cases we would want these copied over // from the bpkg version automatically -- it's too tedious and error- // prone to maintain them manually. However, we want the user to have the // full override ability. So instead, if empty release is specified, as in // 1.2.3-, then we automatically add bpkg revision. Similarly, if empty // pre-release is specified, as in 1.2.3~, then we add bpkg pre-release. // To add both automatically, we would specify 1.2.3~- (other combinations // are 1.2.3~b.1- and 1.2.3~-1). If specified, the release must not // contain the distribution tag, since it is deduced automatically using // the %{?dist} macro expansion if required. Also, since the release // component is mandatory in Fedora, if it is omitted together with the // separating dash we will add the release 1 automatically. // // Note also that per the RPM spec file format documentation neither // version nor release components may contain `:` or `-`. Note that the // bpkg upstream version may not contain either. // string& sv (r.system_version); if (optional ov = system_package_version (ap, rf, os_release.name_id, os_release.version_id, os_release.like_ids)) { string& dv (*ov); size_t n (dv.size ()); // Find the package release and upstream pre-release positions, if any. // size_t rp (dv.rfind ('-')); size_t pp (dv.rfind ('~', rp)); // Copy over the [:] part. // sv.assign (dv, 0, pp < rp ? pp : rp); // Add pre-release copying over the bpkg version value if empty. // if (pp != string::npos) { if (size_t pn = (rp != string::npos ? rp : n) - (pp + 1)) { sv.append (dv, pp, pn + 1); } else { if (pv.release) { assert (!pv.release->empty ()); // Cannot be earliest special. sv += '~'; sv += *pv.release; } } } // Add release copying over the bpkg version revision if empty. // if (rp != string::npos) { if (size_t rn = n - (rp + 1)) { sv.append (dv, rp, rn + 1); } else { sv += '-'; sv += to_string (pv.revision ? *pv.revision + 1 : 1); } } else sv += "-1"; // Default to 1 since the release is mandatory. } else { if (ap->upstream_version) { const string& uv (*ap->upstream_version); // Make sure the upstream version doesn't contain ':' and '-' // characters since they are not allowed in the component // (see the RPM spec file format documentation for details). // // Note that this verification is not exhaustive and here we only make // sure that these characters are only used to separate the version // components. // size_t p (uv.find (":-")); if (p != string::npos) fail << "'" << uv[p] << "' character in upstream-version manifest " << "value " << uv << " of package " << pn << ' ' << ap->version << info << "consider specifying explicit " << os_release.name_id << " version mapping in " << pn << " package manifest"; sv += uv; } else { // Add epoch unless maps to 0. // assert (pv.epoch != 0); // Cannot be a stub. if (pv.epoch != 1) { sv = to_string (pv.epoch - 1); sv += ':'; } sv += pv.upstream; } // Add pre-release. // if (pv.release) { assert (!pv.release->empty ()); // Cannot be earliest special. sv += '~'; sv += *pv.release; } // Add revision. // sv += '-'; sv += to_string (pv.revision ? *pv.revision + 1 : 1); } return r; } // Evaluate the specified expressions expanding the contained macros by // executing `rpm --eval --eval ...` and return the list of // the resulting lines read from the process stdout. Note that an expression // may potentially end up with multiple lines which the caller is expected // to deal with (ensure fixed number of lines, eval only one expression, // etc). // strings system_package_manager_fedora:: rpm_eval (const cstrings& opts, const cstrings& expressions) { strings r; if (expressions.empty ()) return r; cstrings args; args.reserve (2 + opts.size () + expressions.size () * 2); args.push_back ("rpm"); for (const char* o: opts) args.push_back (o); for (const char* e: expressions) { args.push_back ("--eval"); args.push_back (e); } args.push_back (nullptr); try { process_path pp (process::path_search (args[0])); process_env pe (pp); if (verb >= 3) print_process (pe, args); process pr (pp, args, -2 /* stdin */, -1 /* stdout */, 2); try { ifdstream is (move (pr.in_ofd), fdstream_mode::skip, ifdstream::badbit); // The number of lines is normally equal to or greater than the number // of expressions. // r.reserve (expressions.size ()); for (string l; !eof (getline (is, l)); ) r.push_back (move (l)); is.close (); } catch (const io_error& e) { if (pr.wait ()) fail << "unable to read " << args[0] << " --eval output: " << e; // Fall through. } if (!pr.wait ()) { diag_record dr (fail); dr << args[0] << " 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; } // Some background on creating Fedora packages (for a bit more detailed // overview see the RPM Packaging Guide). // // An RPM package consists of the cpio archive, which contains the package // files plus the RPM header file with metadata about the package. The RPM // package manager uses this metadata to determine dependencies, where to // install files, and other information. There are two types of RPM // packages: source RPM and binary RPM. A source RPM contains source code, // optionally patches to apply, and the spec file, which describes how to // build the source code into a binary RPM. A binary RPM contains the // binaries built from the sources package. While it's possible to create // the package completely manually without using any of the Fedora tools, we // are not going to go this route (see reasons mentioned in the Debian // implementation for the list of issues with this approach). // // Based on this our plan is to produce an RPM spec file and then invoke // rpmbuild to produce the binary package from that. While this approach is // normally used to build things from source, it feels like we should be // able to pretend that we are. Specifially, we can implement the %install // section of the spec file to invoke the build system and install all the // packages directly from their bpkg locations. // // Note that the -debuginfo sub-packages are generated by default and all we // need to do from our side is to compile with debug information (-g), // failed which we get a warning from rpmbuild. We will also disable // generating the -debugsource sub-packages since that would require to set // up the source files infrastructure in the ~/rpmbuild/BUILD/ directory, // which feels too hairy for now. // // Note: this setup requires rpmdevtools (rpmdev-setuptree) and its // dependency rpm-build and rpm packages. // auto system_package_manager_fedora:: generate (const packages& pkgs, const packages& deps, const strings& vars, const dir_path& cfg_dir, const package_manifest& pm, const string& pt, const small_vector& langs, optional recursive_full, bool /* first */) -> binary_files { tracer trace ("system_package_manager_fedora::generate"); assert (!langs.empty ()); // Should be effective. const shared_ptr& sp (pkgs.front ().selected); const package_name& pn (sp->name); const version& pv (sp->version); // Use version without iteration in paths, etc. // string pvs (pv.string (false /* ignore_revision */, true /* ignore_iteration */)); const available_packages& aps (pkgs.front ().available); bool lib (pt == "lib"); bool priv (ops_->private_ ()); // Private installation. // For now we only know how to handle libraries with C-common interface // languages. But we allow other implementation languages. // if (lib) { for (const language& l: langs) if (!l.impl && l.name != "c" && l.name != "c++" && l.name != "cc") fail << l.name << " libraries are not yet supported"; } // Return true if this package uses the specified language, only as // interface language if intf_only is true. // auto lang = [&langs] (const char* n, bool intf_only = false) -> bool { return find_if (langs.begin (), langs.end (), [n, intf_only] (const language& l) { return (!intf_only || !l.impl) && l.name == n; }) != langs.end (); }; // As a first step, figure out the system names and version of the package // we are generating and all the dependencies, diagnosing anything fishy. // If the main package is not present for a dependency, then set the main // package name to an empty string. // // Note that there should be no duplicate dependencies and we can sidestep // the status cache. // package_status st (map_package (pn, pv, aps)); vector sdeps; sdeps.reserve (deps.size ()); for (const package& p: deps) { const shared_ptr& sp (p.selected); const available_packages& aps (p.available); package_status s; if (sp->substate == package_substate::system) { // Note that for a system dependency the main package name is already // empty if it is not present in the distribution. // optional os (status (sp->name, aps)); if (!os || os->status != package_status::installed) fail << os_release.name_id << " package for " << sp->name << " system package is no longer installed"; // For good measure verify the mapped back version still matches // configured. Note that besides the normal case (queried by the // system package manager), it could have also been specified by the // user as an actual version or a wildcard. Ignoring this check for a // wildcard feels consistent with the overall semantics. // if (sp->version != wildcard_version && sp->version != os->version) { fail << "current " << os_release.name_id << " package version for " << sp->name << " system package does not match configured" << info << "configured version: " << sp->version << info << "current version: " << os->version << " (" << os->system_version << ')'; } s = move (*os); // Note that the system version retrieved with status() likely // contains the distribution tag in its release component. We, // however, don't want it to ever be mentioned in the spec file and so // just strip it right away. This will also make it consistent with // the non-system dependencies. // string& v (s.system_version); size_t p (v.find_last_of ("-.")); assert (p != string::npos); // The release is mandatory. if (v[p] == '.') v.resize (p); } else { s = map_package (sp->name, sp->version, aps); // Set the main package name to an empty string if we wouldn't be // generating the main package for this dependency (binless library // without the -common sub-package). // assert (aps.size () == 1); const optional& t (aps.front ().first->type); if (s.common.empty () && package_manifest::effective_type (t, sp->name) == "lib") { strings sos (package_manifest::effective_type_sub_options (t)); if (find (sos.begin (), sos.end (), "binless") != sos.end ()) s.main.clear (); } } sdeps.push_back (move (s)); } // We only allow the standard -debug* sub-package names. // if (!st.debuginfo.empty () && st.debuginfo != st.main + "-debuginfo") fail << "generation of -debuginfo packages with custom names not " << "supported" << info << "use " << st.main << "-debuginfo name instead"; if (!st.debugsource.empty () && st.debuginfo != st.main + "-debugsource") fail << "generation of -debugsource packages with custom names not " << "supported" << info << "use " << st.main << "-debugsource name instead"; // Prepare the common extra options that need to be passed to both // rpmbuild and rpm. // strings common_opts {"--target", arch}; // Add the dist macro (un)definition if --fedora-dist-tag is specified. // if (ops_->fedora_dist_tag_specified ()) { string dist (ops_->fedora_dist_tag ()); if (!dist.empty ()) { bool f (dist.front () == '+'); bool b (dist.back () == '+'); if (f && b) // Note: covers just `+`. fail << "invalid distribution tag '" << dist << "'"; // If the distribution tag is specified with a leading/trailing '+', // then we query the default tag value and modify it using the // specified suffix/prefix. // // Note that we rely on the fact that the dist tag doesn't depend on // the --target option which we also pass to rpmbuild. // if (f || b) { string affix (move (dist)); strings expansions (rpm_eval (cstrings (), cstrings {"%{?dist}"})); if (expansions.size () != 1) fail << "one line expected as an expansion of macro %{?dist}"; dist = move (expansions[0]); // Normally, the default distribution tag starts with the dot, in // which case we insert the prefix after it. Note, however, that the // tag can potentially be re/un-defined (for example in // ~/.rpmmacros), so we need to also handle the potential absence of // the leading dot inserting the prefix right at the beginning in // this case. // if (f) dist.append (affix, 1, affix.size () - 1); else dist.insert (dist[0] == '.' ? 1 : 0, affix, 0, affix.size () - 1); } else { // Insert the leading dot into the distribution tag if missing. // if (dist.front () != '.') dist.insert (dist.begin (), '.'); } common_opts.push_back ("--define=dist " + dist); } else common_opts.push_back ("--define=dist %{nil}"); } // Evaluate the specified expressions expanding the contained macros. Make // sure these macros are expanded to the same values as if used in the // being generated spec file. // // Note that %{_docdir} and %{_licensedir} macros are set internally by // rpmbuild (may depend on DocDir spec file directive, etc which we will // not use) and thus cannot be queried with `rpm --eval` out of the // box. To allow using these macros in the expressions, we provide their // definitions to their default values on the command line. // auto eval = [&common_opts, this] (const cstrings& expressions) { cstrings opts; opts.reserve (common_opts.size () + 2 + ops_->fedora_query_option ().size ()); // Pass the rpmbuild/rpm common options. // for (const string& o: common_opts) opts.push_back (o.c_str ()); // Pass the %{_docdir} and %{_licensedir} macro definitions. // opts.push_back ("--define=_docdir %{_defaultdocdir}"); opts.push_back ("--define=_licensedir %{_defaultlicensedir}"); // Pass any additional options specified by the user. // for (const string& o: ops_->fedora_query_option ()) opts.push_back (o.c_str ()); return rpm_eval (opts, expressions); }; // We override every config.install.* variable in order not to pick // anything configured. Note that we add some more in the spec file below. // // We make use of the substitution since in the recursive mode // we may be installing multiple projects. Note that the // directory component is automatically removed if this functionality is // not enabled. One side-effect of using is that we will be // using the bpkg package name instead of the Fedora package name. But // perhaps that's correct: while in Fedora the source package name (which // is the same as the main binary package name) does not necessarily // correspond to the "logical" package name, we still want to use the // logical name (consider libsqlite3 which is mapped to sqlite-libs and // sqlite-devel; we don't want to be sqlite-libs). To keep // things consistent we use the bpkg package name for as well. // // Let's only use those directory macros which we can query with `rpm // --eval` (see eval() lambda for details). Note that this means our // installed_entries paths (see below) may not correspond exactly to where // things will actually be installed during rpmbuild. But that shouldn't // be an issue since we make sure to never use these paths directly in the // spec file (always using macros instead). // // NOTE: make sure to update the expressions evaluation and the %files // sections below if changing anything here. // strings config { "config.install.root=%{_prefix}/", "config.install.data_root=%{_exec_prefix}/", "config.install.exec_root=%{_exec_prefix}/", "config.install.bin=%{_bindir}/", "config.install.sbin=%{_sbindir}/", // On Fedora shared libraries should be executable. // "config.install.lib=%{_libdir}//", "config.install.lib.mode=755", "config.install.libexec=%{_libexecdir}///", "config.install.pkgconfig=lib/pkgconfig/", "config.install.etc=%{_sysconfdir}/", "config.install.include=%{_includedir}//", "config.install.include_arch=include/", "config.install.share=%{_datadir}/", "config.install.data=share///", "config.install.buildfile=share/build2/export//", "config.install.doc=%{_docdir}///", "config.install.legal=%{_licensedir}///", "config.install.man=%{_mandir}/", "config.install.man1=man/man1/", "config.install.man2=man/man2/", "config.install.man3=man/man3/", "config.install.man4=man/man4/", "config.install.man5=man/man5/", "config.install.man6=man/man6/", "config.install.man7=man/man7/", "config.install.man8=man/man8/"}; config.push_back ("config.install.private=" + (priv ? pn.string () : "[null]")); // Add user-specified configuration variables last to allow them to // override anything. // for (const string& v: vars) config.push_back (v); // Note that we need to expand macros in the configuration variables // before passing them to the below installed_entries() call. // // Also note that we expand the variables passed on the command line as // well. While this can be useful, it can also be surprising. However, it // is always possible to escape the '%' character which introduces the // macro expansion, which in most cases won't be necessary since an // undefined macro expansion is preserved literally. // // While at it, also obtain some other information that we will need down // the road. // strings expansions; // Installed entry directories for sorting out the installed files into // the %files sections of the sub-packages. // // We put exported buildfiles into the main package, which makes sense // after some meditation: they normally contain rules and are bundled // either with a tool (say, thrift), a module (say, libbuild2-thrift), or // an add-on package (say, thrift-build2). // dir_path bindir; dir_path sbindir; dir_path libexecdir; dir_path confdir; dir_path incdir; dir_path bfdir; dir_path libdir; dir_path pkgdir; // Not queried, set as libdir/pkgconfig/. dir_path sharedir; dir_path docdir; dir_path mandir; dir_path licensedir; dir_path build2dir; // Note that the ~/rpmbuild/{.,BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} // directory paths used by rpmbuild are actually defined as the // %{_topdir}, %{_builddir}, %{_buildrootdir}, %{_rpmdir}, %{_sourcedir}, // %{_specdir}, and %{_srcrpmdir} RPM macros. These macros can potentially // be redefined in RPM configuration files, in particular, in // ~/.rpmmacros. // dir_path topdir; // ~/rpmbuild/ dir_path specdir; // ~/rpmbuild/SPECS/ // RPM file absolute path template. // // Note that %{_rpmfilename} normally expands as the following template: // // %{ARCH}/%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}.rpm // string rpmfile; { cstrings expressions; expressions.reserve (config.size () + 13); for (const string& c: config) expressions.push_back (c.c_str ()); expressions.push_back ("%{?_bindir}"); expressions.push_back ("%{?_sbindir}"); expressions.push_back ("%{?_libexecdir}"); expressions.push_back ("%{?_sysconfdir}"); expressions.push_back ("%{?_includedir}"); expressions.push_back ("%{?_libdir}"); expressions.push_back ("%{?_datadir}"); expressions.push_back ("%{?_docdir}"); expressions.push_back ("%{?_mandir}"); expressions.push_back ("%{?_licensedir}"); expressions.push_back ("%{?_topdir}"); expressions.push_back ("%{?_specdir}"); expressions.push_back ("%{_rpmdir}/%{_rpmfilename}"); // Note that we rely on the fact that these macros are defined while // refer to them in the spec file, etc. Thus, let's verify that and fail // early if that's not the case for whatever reason. // expressions.push_back ("%{?_rpmdir}"); expressions.push_back ("%{?_rpmfilename}"); expressions.push_back ("%{?_usrsrc}"); expressions.push_back ("%{?buildroot}"); // Note that if the architecture passed with the --target option is // invalid, then rpmbuild will fail with some ugly diagnostics since // %{_arch} macro stays unexpanded in some commands executed by // rpmbuild. Thus, let's verify that the architecture is recognized by // rpmbuild and fail early if that's not the case. // expressions.push_back ("%{?_arch}"); expansions = eval (expressions); // Shouldn't happen unless some paths contain newlines, which we don't // care about. // if (expansions.size () != expressions.size ()) fail << "number of RPM directory path expansions differs from number " << "of path expressions"; // Pop the string/directory expansions. // auto pop_string = [&expansions, &expressions] () { assert (!expansions.empty ()); string r (move (expansions.back ())); if (r.empty ()) fail << "macro '" << expressions.back () << "' expands into empty " << "string"; expansions.pop_back (); expressions.pop_back (); return r; }; auto pop_path = [&expansions, &expressions] () { assert (!expansions.empty ()); try { path r (move (expansions.back ())); if (r.empty ()) fail << "macro '" << expressions.back () << "' expands into empty " << "path"; expansions.pop_back (); expressions.pop_back (); return r; } catch (const invalid_path& e) { fail << "macro '" << expressions.back () << "' expands into invalid " << "path '" << e.path << "'" << endf; } }; auto pop_dir = [&pop_path] () { return path_cast (pop_path ()); }; // The source of a potentially invalid architecture is likely to be the // --architecture option specified by the user. But can probably also be // some mis-configuration. // if (expansions.back ().empty ()) // %{?_arch} fail << "unknown target architecture '" << arch << "'"; // We only need the following macro expansions for the verification. // pop_string (); // %{?_arch} pop_dir (); // %{?buildroot} pop_dir (); // %{?_usrsrc} pop_string (); // %{?_rpmfilename} pop_dir (); // %{?_rpmdir} rpmfile = pop_string (); specdir = pop_dir (); topdir = pop_dir (); // Let's tighten things up and only look for the installed files in // / (if specified) to make sure there is nothing stray. // dir_path pd (priv ? pn.string () : ""); licensedir = pop_dir () / pd; mandir = pop_dir (); docdir = pop_dir () / pd; sharedir = pop_dir (); build2dir = sharedir / dir_path ("build2"); bfdir = build2dir / dir_path ("export"); sharedir /= pd; libdir = pop_dir () / pd; pkgdir = libdir / dir_path ("pkgconfig"); incdir = pop_dir () / pd; confdir = pop_dir (); libexecdir = pop_dir () / pd; sbindir = pop_dir (); bindir = pop_dir (); // Only configuration variables expansions must remain. // assert (expansions.size () == config.size ()); } // Note that the conventional place for all the inputs and outputs of the // rpmbuild operations is the directory tree rooted at ~/rpmbuild/. We // won't fight with rpmbuild and will use this tree as the user would // do while creating the binary package manually. // // Specifially, we will create the RPM spec file in ~/rpmbuild/SPECS/, // install the package(s) under the ~/rpmbuild/BUILDROOT// // chroot, and expect the generated RPM files under ~/rpmbuild/RPMS/. // // That, in particular, means that we have no use for the --output-root // directory. We will also make sure that we don't overwrite an existing // RPM spec file unless --wipe-output is specified. // if (ops_->output_root_specified () && ops_->output_root () != topdir) fail << "--output-root|-o must be " << topdir << " if specified"; // Note that in Fedora the Name spec file directive names the source // package as well as the main binary package and the spec file should // match this name. // // @@ TODO (maybe/later): it's unclear whether it's possible to rename // the main binary package. Maybe makes sense to investigate if/when // we decide to generate source packages. // path spec (specdir / (st.main + ".spec")); if (exists (spec) && !ops_->wipe_output ()) fail << "RPM spec file " << spec << " already exists" << info << "use --wipe-output to remove but be careful"; // Note that we can use weak install scope for the auto recursive mode // since we know dependencies cannot be spread over multiple linked // configurations. // string scope (!recursive_full || *recursive_full ? "project" : "weak"); // Get the map of files that will end up in the binary packages. // installed_entry_map ies ( installed_entries (*ops_, pkgs, expansions, scope)); if (ies.empty ()) fail << "specified package(s) do not install any files"; if (verb >= 4) { for (const auto& p: ies) { diag_record dr (trace); dr << "installed entry: " << p.first; if (p.second.target != nullptr) dr << " -> " << p.second.target->first; // Symlink. else dr << ' ' << p.second.mode; } } // As an optimization, don't generate the main and -debug* packages for a // binless library unless it also specifies the -common sub-package. // // If this is a binless library, then verify that it doesn't install any // executable, library, or configuration files. Also verify that it has // the -devel sub-package but doesn't specify the -static sub-package. // bool binless (false); if (lib) { assert (aps.size () == 1); const shared_ptr& ap (aps.front ().first); strings sos (package_manifest::effective_type_sub_options (ap->type)); if (find (sos.begin (), sos.end (), "binless") != sos.end ()) { // Verify installed files. // auto bad_install = [&pn, &pv] (const string& w) { fail << "binless library " << pn << ' ' << pv << " installs " << w; }; auto verify_not_installed = [&ies, &bad_install] (const dir_path& d) { auto p (ies.find_sub (d)); if (p.first != p.second) bad_install (p.first->first.string ()); }; verify_not_installed (bindir); verify_not_installed (sbindir); verify_not_installed (libexecdir); // It would probably be better not to fail here but generate the main // package instead (as we do if the -common sub-package is also being // generated). Then, however, it would not be easy to detect if a // dependency has the main package or not (see sdeps initialization // for details). // verify_not_installed (confdir); for (auto p (ies.find_sub (libdir)); p.first != p.second; ++p.first) { const path& f (p.first->first); if (!f.sub (pkgdir)) bad_install (f.string ()); } // Verify sub-packages. // if (st.devel.empty ()) fail << "binless library " << pn << ' ' << pv << " doesn't have " << os_release.name_id << " -devel package"; if (!st.static_.empty ()) fail << "binless library " << pn << ' ' << pv << " has " << os_release.name_id << ' ' << st.static_ << " package"; binless = true; } } bool gen_main (!binless || !st.common.empty ()); // If we don't generate the main package (and thus the -common // sub-package), then fail if there are any data files installed. It would // probably be better not to fail but generate the main package instead in // this case. Then, however, it would not be easy to detect if a // dependency has the main package or not. // if (!gen_main) { for (auto p (ies.find_sub (sharedir)); p.first != p.second; ++p.first) { const path& f (p.first->first); if (!f.sub (docdir) && !f.sub (mandir) && !f.sub (licensedir)) { fail << "binless library " << pn << ' ' << pv << " installs " << f << info << "consider specifying -common package in explicit " << os_release.name_id << " name mapping in package manifest"; } } for (auto p (ies.find_sub (bfdir)); p.first != p.second; ++p.first) { const path& f (p.first->first); fail << "binless library " << pn << ' ' << pv << " installs " << f << info << "consider specifying -common package in explicit " << os_release.name_id << " name mapping in package manifest"; } } if (verb >= 3) { auto print_status = [] (diag_record& dr, const package_status& s, const string& main) { dr << (main.empty () ? "" : " ") << main << (s.devel.empty () ? "" : " ") << s.devel << (s.static_.empty () ? "" : " ") << s.static_ << (s.doc.empty () ? "" : " ") << s.doc << (s.debuginfo.empty () ? "" : " ") << s.debuginfo << (s.debugsource.empty () ? "" : " ") << s.debugsource << (s.common.empty () ? "" : " ") << s.common << ' ' << s.system_version; }; { diag_record dr (trace); dr << "package:"; print_status (dr, st, gen_main ? st.main : empty_string); } for (const package_status& s: sdeps) { diag_record dr (trace); dr << "dependency:"; print_status (dr, s, s.main); } } // Prepare the data for the RPM spec file. // // Url directive. // string url (pm.package_url ? pm.package_url->string () : pm.url ? pm.url->string () : string ()); // Packager directive. // string packager; if (ops_->fedora_packager_specified ()) { packager = ops_->fedora_packager (); } else { const email* e (pm.package_email ? &*pm.package_email : pm.email ? &*pm.email : nullptr); if (e == nullptr) fail << "unable to determine packager from manifest" << info << "specify explicitly with --fedora-packager"; // In certain places (e.g., %changelog), Fedora expect this to be in the // `John Doe ` form while we often specify just the // email address (e.g., to the mailing list). Try to detect such a case // and complete it to the desired format. // if (e->find (' ') == string::npos && e->find ('@') != string::npos) { // Try to use comment as name, if any. // if (!e->comment.empty ()) { packager = e->comment; // Strip the potential trailing dot. // if (packager.back () == '.') packager.pop_back (); } else packager = pn.string () + " package maintainer"; packager += " <" + *e + '>'; } else packager = *e; } // Version, Release, and Epoch directives. // struct system_version { string epoch; string version; string release; }; auto parse_system_version = [] (const string& v) { system_version r; size_t e (v.find (':')); if (e != string::npos) r.epoch = string (v, 0, e); size_t b (e != string::npos ? e + 1 : 0); e = v.find ('-', b); assert (e != string::npos); // Release is required. r.version = string (v, b, e - b); b = e + 1; r.release = string (v, b); return r; }; system_version sys_version (parse_system_version (st.system_version)); // License directive. // // The directive value is a SPDX license expression. Note that the OR/AND // operators must be specified in upper case and the AND operator has a // higher precedence than OR. // string license; for (const licenses& ls: pm.license_alternatives) { if (!license.empty ()) license += " OR "; for (auto b (ls.begin ()), i (b); i != ls.end (); ++i) { if (i != b) license += " AND "; license += *i; } } // Create the ~/rpmbuild directory tree if it doesn't exist yet. // if (!exists (topdir)) { cstrings args {"rpmdev-setuptree", nullptr}; try { process_path pp (process::path_search (args[0])); process_env pe (pp); if (verb >= 3) print_process (pe, args); process pr (pp, args); if (!pr.wait ()) { diag_record dr (fail); dr << args[0] << " 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 (); } // For good measure verify that ~/rpmbuild directory now exists. // if (!exists (topdir)) fail << "unable to create RPM build directory " << topdir; } // We cannot easily detect architecture-independent packages (think // libbutl.bash) and providing an option feels like the best we can do. // Note that the noarch value means architecture-independent and any other // value means architecture-dependent. // const string& build_arch (ops_->fedora_build_arch_specified () ? ops_->fedora_build_arch () : empty_string); // The RPM spec file. // // Note that we try to do a reasonably thorough job (e.g., using macros // rather than hardcoding values, trying to comply with Fedora guidelines // and recommendations, etc) with the view that this can be used as a // starting point for manual packaging. // // NOTE: if changing anything here make sure that all the macros expanded // in the spec file unconditionally are defined (see above how we do // that for the _usrsrc macro as an example). // try { ofdstream os (spec); // Note that Fedora Packaging Guidelines recommend to declare the // package dependencies in the architecture-specific fashion using the // %{?_isa} macro in the corresponding Requires directive (e.g., // `Requires: foo%{?_isa}` which would expand to something like // `Requires: foo(x86-64)`). We, however, cannot easily detect if the // distribution packages which correspond to the bpkg package // dependencies are architecture-specific or not. Thus, we will generate // the architecture-independent Requires directives for them which // postpones the architecture resolution until the package installation // time by dnf. We could potentially still try to guess if the // dependency package is architecture-specific or not based on its // languages, but let's keep it simple for now seeing that it's not a // deal breaker. // // Also note that we will generate the architecture-specific // dependencies on our own sub-packages, unless the --fedora-build-arch // option has been specified, and for the C/C++ language related // dependencies (glibc, etc). In other words, we will not try to craft // the architecture specifier ourselves when we cannot use %{?_isa}. // string isa (build_arch.empty () ? "%{?_isa}" : ""); // Add the Requires directive(s), optionally separating them from the // previous directives with an empty line. // auto add_requires = [&os] (bool& first, const string& v) { if (first) { os << '\n'; first = false; } os << "Requires: " << v << '\n'; }; auto add_requires_list = [&add_requires] (bool& first, const strings& vs) { for (const string& v: vs) add_requires (first, v); }; // Add the Requires directives for language dependencies of a // sub-package. Deduce the language dependency packages (such as glibc, // libstdc++, etc), unless they are specified explicitly via the // --fedora-*-langreq options. If single option with an empty value is // specified, then no language dependencies are added. The valid // sub-package suffixes are '' (main package), '-devel', and '-static'. // auto add_lang_requires = [&lang, &add_requires, &add_requires_list] (bool& first, const string& suffix, const strings& options, bool intf_only = false) { if (!options.empty ()) { if (options.size () != 1 || !options[0].empty ()) add_requires_list (first, options); } else { // Add dependency on libstdc++ and glibc packages. // // It doesn't seems that the -static sub-package needs to define any // default C/C++ language dependencies. That is a choice of the // dependent packages which may want to link the standard libraries // either statically or dynamically, so let's leave if for them to // arrange. // if (suffix != "-static") { // If this is an undetermined C-common library, we assume it may // be C++ (better to over- than under-specify). // bool cc (lang ("cc", intf_only)); if (cc || lang ("c++", intf_only)) add_requires (first, string ("libstdc++") + suffix + "%{?_isa}"); if (cc || lang ("c", intf_only)) add_requires (first, string ("glibc") + suffix + "%{?_isa}"); } } }; // We need to add the mandatory Summary and %description directives both // for the main package and for the sub-packages. In the Summary // directives we will use the `summary` package manifest value. In the // %description directives we will just describe the sub-package content // since using the `description` package manifest value is not going to // be easy: it can be arbitrarily long and may not even be plain text // (it's commonly the contents of the README.md file). // // We will disable automatic dependency discovery for all sub-packages // using the `AutoReqProv: no` directive since we have an accurate set // and some of them may not be system packages. // // The common information and the main package. // { os << "Name: " << st.main << '\n' << "Version: " << sys_version.version << '\n' << "Release: " << sys_version.release << "%{?dist}" << '\n'; if (!sys_version.epoch.empty ()) os << "Epoch: " << sys_version.epoch << '\n'; os << "License: " << license << '\n' << "Summary: " << pm.summary << '\n' << "Url: " << url << '\n'; if (!packager.empty ()) os << "Packager: " << packager << '\n'; #if 0 os << "#Source: https://pkg.cppget.org/1/???/" << pm.effective_project () << '/' << sp->name << '-' << sp->version << ".tar.gz" << '\n'; #endif // Idiomatic epoch-version-release value. // os << '\n' << "%global evr %{?epoch:%{epoch}:}%{version}-%{release}" << '\n'; if (gen_main) { os << '\n' << "# " << st.main << '\n' << "#" << '\n'; if (!build_arch.empty ()) os << "BuildArch: " << build_arch << '\n'; os << "AutoReqProv: no" << '\n'; // Requires directives. // { bool first (true); if (!st.common.empty ()) add_requires (first, st.common + " = %{evr}"); for (const package_status& s: sdeps) { if (!s.main.empty ()) add_requires (first, s.main + " >= " + s.system_version); } add_lang_requires (first, "" /* suffix */, ops_->fedora_main_langreq ()); if (ops_->fedora_main_extrareq_specified ()) add_requires_list (first, ops_->fedora_main_extrareq ()); } } // Note that we need to add the %description directive regardless if // the main package is being generated or not. // if (!binless) { os << '\n' << "%description" << '\n' << "This package contains the runtime files." << '\n'; } else { os << '\n' << "%description" << '\n' << "This package contains the development files." << '\n'; } } // The -devel sub-package. // if (!st.devel.empty ()) { os << '\n' << "# " << st.devel << '\n' << "#" << '\n' << "%package -n " << st.devel << '\n' << "Summary: " << pm.summary << '\n'; // Feels like the architecture should be the same as for the main // package. // if (!build_arch.empty ()) os << "BuildArch: " << build_arch << '\n'; os << '\n' << "AutoReqProv: no" << '\n'; // Requires directives. // { bool first (true); // Dependency on the main package. // if (gen_main) add_requires (first, "%{name}" + isa + " = %{evr}"); for (const package_status& s: sdeps) { // Doesn't look like we can distinguish between interface and // implementation dependencies here. So better to over- than // under-specify. // // Note that if the -devel sub-package doesn't exist for a // dependency, then its potential content may be part of the main // package. If that's the case we, strictly speaking, should add // the dependency on the main package. Let's, however, skip that // since we already have this dependency implicitly via our own // main package, which the -devel sub-package depends on. // if (!s.devel.empty ()) add_requires (first, s.devel + " >= " + s.system_version); } add_lang_requires (first, "-devel", ops_->fedora_devel_langreq (), true /* intf_only */); if (ops_->fedora_devel_extrareq_specified ()) add_requires_list (first, ops_->fedora_devel_extrareq ()); } // If the -static sub-package is not being generated but there are // some static libraries installed, then they will be added to the // -devel sub-package. If that's the case, we add the // `Provides: %{name}-static` directive for the -devel sub-package, as // recommended. // // Should we do the same for the main package, where the static // libraries go if the -devel sub-package is not being generated // either? While it feels sensible, we've never seen such a practice // or recommendation. So let's not do it for now. // if (st.static_.empty ()) { for (auto p (ies.find_sub (libdir)); p.first != p.second; ++p.first) { const path& f (p.first->first); path l (f.leaf (libdir)); const string& n (l.string ()); if (l.simple () && n.size () > 3 && n.compare (0, 3, "lib") == 0 && l.extension () == "a") { os << '\n' << "Provides: %{name}-static" << isa << " = %{evr}" << '\n'; break; } } } os << '\n' << "%description -n " << st.devel << '\n' << "This package contains the development files." << '\n'; } // The -static sub-package. // if (!st.static_.empty ()) { os << '\n' << "# " << st.static_ << '\n' << "#" << '\n' << "%package -n " << st.static_ << '\n' << "Summary: " << pm.summary << '\n'; // Feels like the architecture should be the same as for the -devel // sub-package. // if (!build_arch.empty ()) os << "BuildArch: " << build_arch << '\n'; os << '\n' << "AutoReqProv: no" << '\n'; // Requires directives. // { bool first (true); // The static libraries without headers doesn't seem to be of any // use. Thus, add dependency on the -devel or main sub-package, if // not being generated. // // Note that if there is no -devel package, then this cannot be a // binless library and thus the main package is being generated. // add_requires ( first, (!st.devel.empty () ? st.devel : "%{name}") + isa + " = %{evr}"); // Add dependency on sub-packages that may contain static libraries. // Note that in the -devel case we can potentially over-specify the // dependency which is better than to under-specify. // for (const package_status& s: sdeps) { // Note that if the -static sub-package doesn't exist for a // dependency, then its potential content may be part of the // -devel sub-package, if exists, or the main package otherwise. // If that's the case we, strictly speaking, should add the // dependency on the -devel sub-package, if exists, or the main // package otherwise. Let's, however, also consider the implicit // dependencies via our own -devel and main (sub-)packages, which // we depend on, and simplify things similar to what we do for the // -devel sub-package above. // // Also note that we only refer to the dependency's -devel // sub-package if we don't have our own -devel sub-package // (unlikely, but possible), which would provide us with such an // implicit dependency. // const string& p (!s.static_.empty () ? s.static_ : st.devel.empty () ? s.devel : empty_string); if (!p.empty ()) add_requires (first, p + " >= " + st.system_version); } add_lang_requires (first, "-static", ops_->fedora_stat_langreq ()); if (ops_->fedora_stat_extrareq_specified ()) add_requires_list (first, ops_->fedora_stat_extrareq ()); } os << '\n' << "%description -n " << st.static_ << '\n' << "This package contains the static libraries." << '\n'; } // The -doc sub-package. // if (!st.doc.empty ()) { os << '\n' << "# " << st.doc << '\n' << "#" << '\n' << "%package -n " << st.doc << '\n' << "Summary: " << pm.summary << '\n' << "BuildArch: noarch" << '\n' << '\n' << "AutoReqProv: no" << '\n' << '\n' << "%description -n " << st.doc << '\n' << "This package contains the documentation." << '\n'; } // The -common sub-package. // if (!st.common.empty ()) { // Generally, this sub-package is not necessarily architecture- // independent (for example, it could contain something shared between // multiple binary packages produced from the same source package // rather than something shared between all the architectures of a // binary package). But seeing that we always generate one binary // package, for us it only makes sense as architecture-independent. // // It's also not clear what dependencies we can deduce for this // sub-package. Assuming that it depends on all the dependency -common // sub-packages is probably unreasonable. // os << '\n' << "# " << st.common << '\n' << "#" << '\n' << "%package -n " << st.common << '\n' << "Summary: " << pm.summary << '\n' << "BuildArch: noarch" << '\n' << '\n' << "AutoReqProv: no" << '\n' << '\n' << "%description -n " << st.common << '\n' << "This package contains the architecture-independent files." << '\n'; } // Build setup. // { bool lang_c (lang ("c")); bool lang_cxx (lang ("c++")); bool lang_cc (lang ("cc")); os << '\n' << "# Build setup." << '\n' << "#" << '\n'; // The -debuginfo and -debugsource sub-packages. // // Note that the -debuginfo and -debugsource sub-packages are defined // in the spec file by expanding the %{debug_package} macro (search // the macro definition in `rpm --showrc` stdout for details). This // expansion happens as part of the %install section processing but // only if the %{buildsubdir} macro is defined. This macro refers to // the package source subdirectory in the ~/rpmbuild/BUILD directory // and is normally set by the %setup macro expansion in the %prep // section which, in particular, extracts source files from an // archive, defines the %{buildsubdir} macro, and make this directory // current. Since we don't have an archive to extract, we will use the // %setup macro disabling sources extraction (-T) and creating an // empty source directory instead (-c). This directory is also used by // rpmbuild for saving debuginfo-related intermediate files // (debugfiles.list, etc). See "Fedora Debuginfo packages" and "Using // RPM build flags" documentation for better understanding what's // going on under the hood. There is also the "[Rpm-ecosystem] Trying // to understand %buildsubdir and debuginfo generation" mailing list // thread which provides some additional clarifications. // // Also note that we disable generating the -debugsource sub-packages // (see the generate() description above for the reasoning). // // For a binless library no -debug* packages are supposed to be // generated. Thus, we just drop their definitions by redefining the // %{debug_package} macro as an empty string. // if (!binless) { os << "%undefine _debugsource_packages" << '\n'; // Append the -ffile-prefix-map option which is normally used to // strip source file path prefix in debug information (besides other // places). By default it is not used since rpmbuild replaces the // recognized prefixes by using the debugedit program (see below for // details) and we cannot rely on that since in our case the prefix // (bpkg configuration directory) is not recognized. We need to // replace the bpkg configuration directory prefix in the source // file paths with the destination directory where the -debugsource // sub-package files would be installed (if we were to generate it). // For example: // // /usr/src/debug/foo-1.0.0-1.fc35.x86_64 // // There is just one complication: // // While the generation of the -debugsource sub-packages is // currently disabled, the executed by rpmbuild find-debuginfo // script still performs some preparations for them. It runs the // debugedit program, which, in particular, reads the source file // paths from the debug information in package binaries and saves // those which start with the ~/rpmbuild/BUILD/foo-1.0.0 or // /usr/src/debug/foo-1.0.0-1.fc35.x86_64 directory prefix into the // ~/rpmbuild/BUILD/foo-1.0.0/debugsources.list file, stripping the // prefixes. It also saves all the relative source file paths as is // (presumably assuming they are sub-entries of the // ~/rpmbuild/BUILD/foo-1.0.0 directory where the package archive // would normally be extracted). Afterwards, the content of the // debugsources.list file is piped as an input to the cpio program // executed in the ~/rpmbuild/BUILD/foo-1.0.0 directory as its // current working directory, which tries to copy these source files // to the // ~/rpmbuild/BUILDROOT/foo-1.0.0-1.fc35.x86_64/usr/src/debug/foo-1.0.0-1.fc35.x86_64 // directory. Given that these source files are actually located in // the bpkg configuration directory rather than in the // ~/rpmbuild/BUILD/foo-1.0.0 directory the cpio program fails to // stat them and complains. To work around that we need to change // the replacement directory path in the -ffile-prefix-map option // value with some other absolute (not necessarily existing) path // which is not a subdirectory of the directory prefixes recognized // by the debugedit program. This way debugedit won't recognize any // of the package source files, will create an empty // debugsources.list file, and thus the cpio program won't try to // copy anything. Not to confuse the user (who can potentially see // such paths in gdb while examining a core file produced by the // package binary), we will keep this replacement directory path // close to the desired one, but will also make it clear that the // path is bogus: // // /usr/src/debug/bogus/foo-1.0.0-1.fc35.x86_64 // // Note that this path mapping won't work for external packages with // source out of configuration (e.g., managed by bdep). // // @@ Supposedly this code won't be necessary when we add support // for -debugsource sub-packages somehow. Feels like one way // would be to make ~/rpmbuild/BUILD/foo-1.0.0 a symlink to the // bpkg configuration (or to the primary package inside, if not // --recursive). // if (ops_->fedora_buildflags () != "ignore") { const char* debugsource_dir ( "%{_usrsrc}/debug/bogus/%{name}-%{evr}.%{_arch}"); if (lang_c || lang_cc) os << "%global build_cflags %{?build_cflags} -ffile-prefix-map=" << cfg_dir.string () << '=' << debugsource_dir << '\n'; if (lang_cxx || lang_cc) os << "%global build_cxxflags %{?build_cxxflags} -ffile-prefix-map=" << cfg_dir.string () << '=' << debugsource_dir << '\n'; } } else os << "%global debug_package %{nil}" << '\n'; // Common arguments for build2 commands. // // Let's use absolute path to the build system driver in case we are // invoked with altered environment or some such. // // Note: should be consistent with the invocation in installed_entries() // above. // cstrings verb_args; string verb_arg; map_verb_b (*ops_, verb_b::normal, verb_args, verb_arg); os << '\n' << "%global build2 " << search_b (*ops_).effect_string (); for (const char* o: verb_args) os << ' ' << o; for (const string& o: ops_->build_option ()) os << ' ' << o; // Map the %{_smp_build_ncpus} macro value to the build2 --jobs or // --serial-stop options. // os << '\n' << '\n' << "%if %{defined _smp_build_ncpus}" << '\n' << " %if %{_smp_build_ncpus} == 1" << '\n' << " %global build2 %{build2} --serial-stop" << '\n' << " %else" << '\n' << " %global build2 %{build2} --jobs=%{_smp_build_ncpus}" << '\n' << " %endif" << '\n' << "%endif" << '\n'; // Configuration variables. // // Note: we need to quote values that contain `<>`, `[]`, since they // will be passed through shell. For simplicity, let's just quote // everything. // os << '\n' << "%global config_vars"; auto add_macro_line = [&os] (const auto& v) { os << " \\\\\\\n " << v; }; add_macro_line ("config.install.chroot='%{buildroot}/'"); add_macro_line ("config.install.sudo='[null]'"); // If this is a C-based language, add rpath for private installation. // if (priv && (lang_c || lang_cxx || lang_cc)) add_macro_line ("config.bin.rpath='%{_libdir}/" + pn.string () + "/'"); // Add build flags. // if (ops_->fedora_buildflags () != "ignore") { const string& m (ops_->fedora_buildflags ()); string o (m == "assign" ? "=" : m == "append" ? "+=" : m == "prepend" ? "=+" : ""); if (o.empty ()) fail << "unknown --fedora-buildflags option value '" << m << "'"; // Note that config.cc.* doesn't play well with the append/prepend // modes because the orders are: // // x.poptions cc.poptions // cc.coptions x.coptions // cc.loptions x.loptions // // Oh, well, hopefully it will be close enough for most cases. // // Note also that there are compiler mode options that are not // overridden. Also the preprocessor options are normally contained // in the %{build_cflags} and %{build_cxxflags} macro definitions // and have no separate macros associated at this level (see "Using // RPM build flags" documentation for details). For example: // // $ rpm --eval "%{build_cflags}" // -O2 -flto=auto -ffat-lto-objects -fexceptions -g // -grecord-gcc-switches -pipe -Wall -Werror=format-security // -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS // -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 // -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 // -m64 -mtune=generic -fasynchronous-unwind-tables // -fstack-clash-protection -fcf-protection // // Note the -Wp options above. Thus, we reset config.{c,cxx}.poptions // to [null] in the assign mode and, for simplicity, leave them as // configured otherwise. We could potentially fix that either by // extracting the -Wp,... options from %{build_cflags} and // %{build_cxxflags} macro values or using more lower level macros // instead (%{_preprocessor_defines}, %{_hardened_cflags}, etc), // which all feels quite hairy and brittle. // if (o == "=" && (lang_c || lang_cxx || lang_cc)) { add_macro_line ("config.cc.poptions='[null]'"); add_macro_line ("config.cc.coptions='[null]'"); add_macro_line ("config.cc.loptions='[null]'"); } if (lang_c || lang_cc) { if (o == "=") add_macro_line ("config.c.poptions='[null]'"); add_macro_line ("config.c.coptions" + o + "'%{?build_cflags}'"); add_macro_line ("config.c.loptions" + o + "'%{?build_ldflags}'"); } if (lang_cxx || lang_cc) { if (o == "=") add_macro_line ("config.cxx.poptions='[null]'"); add_macro_line ("config.cxx.coptions" + o + "'%{?build_cxxflags}'"); add_macro_line ("config.cxx.loptions" + o + "'%{?build_ldflags}'"); } } // Keep last to allow user-specified configuration variables to // override anything. // for (const string& c: config) { // Quote the value unless already quoted (see above). Presense of // potentially-quoted user variables complicates things a bit (can // be partially quoted, double-quoted, etc). // size_t p (c.find_first_of ("=+ \t")); // End of name. if (p != string::npos) { p = c.find_first_not_of ("=+ \t", p); // Beginning of value. if (p != string::npos) { if (c.find_first_of ("'\"", p) == string::npos) // Not quoted. { add_macro_line (string (c, 0, p) + '\'' + string (c, p) + '\''); continue; } } } add_macro_line (c); } os << '\n'; // Close the macro definition. // List of packages we need to install. // os << '\n' << "%global packages"; for (const package& p: pkgs) add_macro_line (p.out_root.representation ()); os << '\n'; // Close the macro definition. } // Build sections. // { os << '\n' << "# Build sections." << '\n' << "#" << '\n' << "%prep" << '\n' << "%setup -T -c" << '\n' << '\n' << "%build" << '\n' << "%{build2} %{config_vars} update-for-install: %{packages}" << '\n' << '\n' << "%install" << '\n' << "%{build2} %{config_vars} '!config.install.scope=" << scope << "' install: %{packages}" << '\n'; } // Files sections. // // Generate the %files section for each sub-package in order to sort out // which files belong where. // // For the details on the %files section directives see "Directives For // the %files list" documentation. But the summary is: // // - Supports only simple wildcards (?, *, [...]; no recursive/**). // - Includes directories recursively, unless the path is prefixed // with the %dir directive, in which case only includes this // directory entry, which will be created on install and removed on // uninstall, if empty. // - An entry that doesn't match anything is an error (say, // /usr/sbin/*). // // Keep in mind that wherever there is in the config.install.* // variable, we can end up with multiple different directories (bundled // packages). // { string main; string devel; string static_; string doc; string common; // Note that declaring package ownership for standard directories is // considered in Fedora a bad idea and is reported as an error by some // RPM package checking tools (rpmlint, etc). Thus, we generate, for // example, libexecdir/* entry rather than libexecdir/. However, if // the private directory is specified we generate libexecdir// // to own the directory. // // NOTE: use consistently with the above install directory expressions // (%{?_includedir}, etc) evaluation. // string pd (priv ? pn.string () + '/' : ""); // The main package contains everything that doesn't go to another // packages. // if (gen_main) { if (ies.contains_sub (bindir)) main += "%{_bindir}/*\n"; if (ies.contains_sub (sbindir)) main += "%{_sbindir}/*\n"; if (ies.contains_sub (libexecdir)) main += "%{_libexecdir}/" + (priv ? pd : "*") + '\n'; // This could potentially go to -common but it could also be target- // specific, who knows. So let's keep it in main for now. // // Let's also specify that the confdir/ sub-entries are // non-replacable configuration files. This, in particular, means // that if edited they will not be replaced/removed on the package // upgrade or uninstallation (see RPM Packaging Guide for more // details on the %config(noreplace) directive). Also note that the // binary package configuration files can later be queried by the // user via the `rpm --query --configfiles` command. // if (ies.contains_sub (confdir)) main += "%config(noreplace) %{_sysconfdir}/*\n"; } if (ies.contains_sub (incdir)) (!st.devel.empty () ? devel : main) += "%{_includedir}/" + (priv ? pd : "*") + '\n'; if (st.devel.empty () && st.static_.empty ()) { assert (gen_main); // Shouldn't be here otherwise. if (ies.contains_sub (libdir)) main += "%{_libdir}/" + (priv ? pd : "*") + '\n'; } else { // Ok, time for things to get hairy: we need to split the contents // of lib/ into the main, -devel, and/or -static sub-packages. The // -devel sub-package, if present, should contain three things: // // 1. Static libraries (.a), if no -static sub-package is present. // 2. Non-versioned shared library symlinks (.so). // 3. Contents of the pkgconfig/ subdirectory, except for *.static.pc // files if -static sub-package is present. // // The -static sub-package, if present, should contain two things: // // 1. Static libraries (.a). // 2. *.static.pc files in pkgconfig/ subdirectory. // // Everything else should go into the main package. // // The shared libraries are tricky. Here we can have three plausible // arrangements: // // A. Portably-versioned library: // // libfoo-1.2.so // libfoo.so -> libfoo-1.2.so // // B. Natively-versioned library: // // libfoo.so.1.2.3 // libfoo.so.1.2 -> libfoo.so.1.2.3 // libfoo.so.1 -> libfoo.so.1.2 // libfoo.so -> libfoo.so.1 // // C. Non-versioned library: // // libfoo.so // // Note that in the (C) case the library should go into the main // package. Based on this, the criteria appears to be // straightforward: the extension is .so and it's a symlink. For // good measure we also check that there is the `lib` prefix // (plugins, etc). // // Also note that if / is specified, then to establish // ownership of the libdir// directory we also need to add // it non-recursively to one of the potentially 3 sub-packages, // which all can contain some of its sub-entries. Not doing this // will result in an empty libdir// subdirectory after the // binary package uninstallation. Naturally, the owner should be the // right-most sub-package on the following diagram which contains // any of the libdir// sub-entries: // // -static -> -devel -> main // // The same reasoning applies to libdir//pkgconfig/. // string* owners[] = {&static_, &devel, &main}; // Indexes (in owners) of sub-packages which should own // libdir// and libdir//pkgconfig/. If nullopt, // then no additional directory ownership entry needs to be added // (installation is not private, recursive directory entry is // already added, etc). // optional private_owner; optional pkgconfig_owner; for (auto p (ies.find_sub (libdir)); p.first != p.second; ) { const path& f (p.first->first); const installed_entry& ie ((p.first++)->second); path l (f.leaf (libdir)); const string& n (l.string ()); string* fs (&main); // Go to the main package as a last resort. auto update_ownership = [&owners, &fs] (optional& pi) { size_t i (0); for (; owners[i] != fs; ++i) ; if (!pi || *pi < i) pi = i; }; if (l.simple ()) { if (n.size () > 3 && n.compare (0, 3, "lib") == 0) { string e (l.extension ()); if (e == "a") { fs = !st.static_.empty () ? &static_ : &devel; } else if (e == "so" && ie.target != nullptr && !st.devel.empty ()) { fs = &devel; } } *fs += "%{_libdir}/" + pd + n + '\n'; } else { // Let's keep things tidy and, when possible, use a // sub-directory rather than listing all its sub-entries // verbatim. // dir_path sd (*l.begin ()); dir_path d (libdir / sd); if (d == pkgdir) { // If the -static sub-package is not being generated, then the // whole directory goes into the -devel sub-package. // Otherwise, *.static.pc files go into the -static // sub-package and the rest into the -devel sub-package, // unless it is not being generated in which case it goes into // the main package. // if (!st.static_.empty ()) { if (n.size () > 10 && n.compare (n.size () - 10, 10, ".static.pc") == 0) fs = &static_; else if (!st.devel.empty ()) fs = &devel; *fs += "%{_libdir}/" + pd + n; // Update the index of a sub-package which should own // libdir//pkgconfig/. // if (priv) update_ownership (pkgconfig_owner); } else { fs = &devel; *fs += "%{_libdir}/" + pd + sd.string () + (priv ? "/" : "/*"); } } else *fs += "%{_libdir}/" + pd + sd.string () + '/'; // In the case of the directory (has the trailing slash) or // wildcard (has the trailing asterisk) skip all the other // entries in this subdirectory (in the prefix map they will all // be in a contiguous range). // char c (fs->back ()); if (c == '/' || c == '*') { while (p.first != p.second && p.first->first.sub (d)) ++p.first; } *fs += '\n'; } // We can only add files to the main package if we generate it. // assert (fs != &main || gen_main); // Update the index of a sub-package which should own // libdir//. // if (priv) update_ownership (private_owner); } // Add the directory ownership entries. // if (private_owner) *owners[*private_owner] += "%dir %{_libdir}/" + pd + '\n'; if (pkgconfig_owner) *owners[*pkgconfig_owner] += "%dir %{_libdir}/" + pd + "pkgconfig/" + '\n'; } // We cannot just do usr/share/* since it will clash with doc/, man/, // and licenses/ below. So we have to list all the top-level entries // in usr/share/ that are not doc/, man/, licenses/, or build2/. // if (gen_main) { // Note that if / is specified, then we also need to // establish ownership of the sharedir// directory (similar // to what we do for libdir// above). // string* private_owner (nullptr); string& fs (!st.common.empty () ? common : main); for (auto p (ies.find_sub (sharedir)); p.first != p.second; ) { const path& f ((p.first++)->first); if (f.sub (docdir) || f.sub (mandir) || f.sub (licensedir) || f.sub (build2dir)) continue; path l (f.leaf (sharedir)); if (l.simple ()) { fs += "%{_datadir}/" + pd + l.string () + '\n'; } else { // Let's keep things tidy and use a sub-directory rather than // listing all its sub-entries verbatim. // dir_path sd (*l.begin ()); fs += "%{_datadir}/" + pd + sd.string () + '/' + '\n'; // Skip all the other entries in this subdirectory (in the prefix // map they will all be in a contiguous range). // dir_path d (sharedir / sd); while (p.first != p.second && p.first->first.sub (d)) ++p.first; } // Indicate that we need to establish ownership of // sharedir//. // if (priv) private_owner = &fs; } // Add the directory ownership entry. // if (private_owner != nullptr) *private_owner += "%dir %{_datadir}/" + pd + '\n'; } // Note that we only consider the bfdir//* sub-entries, // adding the bfdir// subdirectories to the %files // section. This way no additional directory ownership entry needs to // be added. Any immediate sub-entries of bfdir/, if present, will be // ignored, which will end up with the 'unpackaged files' rpmbuild // error. // // Also note that the bfdir/ directory is not owned by any package. // if (gen_main) { for (auto p (ies.find_sub (bfdir)); p.first != p.second; ) { const path& f ((p.first++)->first); path l (f.leaf (bfdir)); if (!l.simple ()) { // Let's keep things tidy and use a sub-directory rather than // listing all its sub-entries verbatim. // dir_path sd (*l.begin ()); main += "%{_datadir}/build2/export/" + sd.string () + '/' + '\n'; // Skip all the other entries in this subdirectory (in the // prefix map they will all be in a contiguous range). // dir_path d (bfdir / sd); while (p.first != p.second && p.first->first.sub (d)) ++p.first; } } } // Should we put the documentation into -common if there is no -doc? // While there doesn't seem to be anything explicit in the policy, // there are packages that do it this way (e.g., mariadb-common). And // the same logic seems to apply to -devel (e.g., zlib-devel). // { string& fs (!st.doc.empty () ? doc : !st.common.empty () ? common : !st.devel.empty () ? devel : main); // We can only add doc files to the main or -common packages if we // generate the main package. // assert ((&fs != &main && &fs != &common) || gen_main); // Let's specify that the docdir/ sub-entries are documentation // files. Note that the binary package documentation files can later // be queried by the user via the `rpm --query --docfiles` command. // if (ies.contains_sub (docdir)) fs += "%doc %{_docdir}/" + (priv ? pd : "*") + '\n'; // Since the man file may not appear directly in the man/ // subdirectory we use the man/*/* wildcard rather than man/* not to // declare ownership for standard directories. // // As a side note, rpmbuild compresses the man files in the // installation directory, which needs to be taken into account if // writing more specific wildcards (e.g., %{_mandir}/man1/foo.1*). // if (ies.contains_sub (mandir)) fs += "%{_mandir}/*/*\n"; } // Let's specify that the licensedir/ sub-entries are license files. // Note that the binary package license files can later be queried by // the user via the `rpm --query --licensefiles` command. // if (ies.contains_sub (licensedir)) (gen_main ? main : devel) += "%license %{_licensedir}/" + (priv ? pd : "*") + '\n'; // Finally, write the %files sections. // if (!main.empty ()) { assert (gen_main); // Shouldn't be here otherwise. os << '\n' << "# " << st.main << " files." << '\n' << "#" << '\n' << "%files" << '\n' << main; } if (!devel.empty ()) { os << '\n' << "# " << st.devel << " files." << '\n' << "#" << '\n' << "%files -n " << st.devel << '\n' << devel; } if (!static_.empty ()) { os << '\n' << "# " << st.static_ << " files." << '\n' << "#" << '\n' << "%files -n " << st.static_ << '\n' << static_; } if (!doc.empty ()) { os << '\n' << "# " << st.doc << " files." << '\n' << "#" << '\n' << "%files -n " << st.doc << '\n' << doc; } if (!common.empty ()) { os << '\n' << "# " << st.common << " files." << '\n' << "#" << '\n' << "%files -n " << st.common << '\n' << common; } } // Changelog section. // // The section entry has the following format: // // * - - // - // - // ... // // For example: // // * Wed Feb 22 2023 John Doe - 2.3.4-1 // - New bpkg package release 2.3.4. // // We will use the Packager value for the ` ` // fields. Strictly speaking it may not exactly match the fields set but // it doesn't seem to break anything if that's the case. For good // measure, me will also use the English locale for the date. // // Note that the field doesn't contain the distribution tag. // { os << '\n' << "%changelog" << '\n' << "* "; // Given that we don't include the timezone there is no much sense to // print the current time as local. // std::locale l (os.imbue (std::locale ("C"))); to_stream (os, system_clock::now (), "%a %b %d %Y", false /* special */, false /* local */); os.imbue (l); os << ' ' << packager << " - "; if (!sys_version.epoch.empty ()) os << sys_version.epoch << ':'; os << sys_version.version << '-' << sys_version.release << '\n' << "- New bpkg package release " << pvs << '.' << '\n'; } os.close (); } catch (const io_error& e) { fail << "unable to write to " << spec << ": " << e; } // Run rpmbuild. // // Note that rpmbuild causes recompilation periodically by setting the // SOURCE_DATE_EPOCH environment variable (which we track for changes // since it affects GCC). Its value depends on the timestamp of the latest // change log entry and thus has a day resolution. Note that since we // don't have this SOURCE_DATE_EPOCH during dry-run caused by // installed_entries(), there would be a recompilation even if the value // weren't changing. // cstrings args {"rpmbuild", "-bb"}; // Only build binary packages. // Map our verbosity to rpmbuild --quiet and -vv options (-v is the // default). Note that there doesn't seem to be any way to control its // progress. // // Also note that even in the quiet mode rpmbuild may still print some // progress lines. // if (verb == 0) args.push_back ("--quiet"); else if (verb >= 4) // Note that -vv feels too verbose for level 3. args.push_back ("-vv"); // If requested, keep the installation directory, etc. // if (ops_->keep_output ()) args.push_back ("--noclean"); // Pass our --jobs value, if any. // string jobs_arg; if (size_t n = ops_->jobs_specified () ? ops_->jobs () : 0) { jobs_arg = "--define=_smp_build_ncpus " + to_string (n); args.push_back (jobs_arg.c_str ()); } // Pass the rpmbuild/rpm common options. // for (const string& o: common_opts) args.push_back (o.c_str ()); // Pass any additional options specified by the user. // for (const string& o: ops_->fedora_build_option ()) args.push_back (o.c_str ()); args.push_back (spec.string ().c_str ()); args.push_back (nullptr); if (ops_->fedora_prepare_only ()) { if (verb >= 1) { diag_record dr (text); dr << "prepared " << spec << text << "command line: "; print_process (dr, args); } return binary_files {}; } try { process_path pp (process::path_search (args[0])); process_env pe (pp); // There is going to be quite a bit of diagnostics so print the command // line unless quiet. // if (verb >= 1) print_process (pe, args); // Redirect stdout to stderr since some of rpmbuild diagnostics goes // there. For good measure also redirect stdin to /dev/null to make sure // there are no prompts of any kind. // process pr (pp, args, -2 /* stdin */, 2 /* stdout */, 2 /* stderr */); if (!pr.wait ()) { // Let's repeat the command line even if it was printed at the // beginning to save the user a rummage through the logs. // diag_record dr (fail); dr << args[0] << " exited with non-zero code" << 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 (); } // While it's tempting to always keep the spec file let's remove it, // unless requested not to, since it contains absolute paths to // configuration. // if (!ops_->keep_output ()) rm (spec); // Collect and return the binary sub-package paths. // // Here we will use `rpm --eval` to resolve the RPM sub-package paths. // binary_files r; r.system_version = st.system_version; { string expressions; auto add_macro = [&expressions] (const string& name, const string& value) { expressions += "%global " + name + ' ' + value + '\n'; }; add_macro ("VERSION", sys_version.version); add_macro ("RELEASE", sys_version.release + "%{?dist}"); const string& package_arch (!build_arch.empty () ? build_arch : arch); vector files; auto add_package = [&files, &expressions, &rpmfile, &add_macro] (const string& name, const string& arch, const char* type) -> size_t { add_macro ("NAME", name); add_macro ("ARCH", arch); expressions += rpmfile + '\n'; // Note: path is unknown yet. // files.push_back (binary_file {type, path (), name}); return files.size () - 1; }; if (gen_main) add_package (st.main, package_arch, "main.rpm"); if (!st.devel.empty ()) add_package (st.devel, package_arch, "devel.rpm"); if (!st.static_.empty ()) add_package (st.static_, package_arch, "static.rpm"); if (!st.doc.empty ()) add_package (st.doc, "noarch", "doc.rpm"); if (!st.common.empty ()) add_package (st.common, "noarch", "common.rpm"); optional di ( !binless ? add_package (st.main + "-debuginfo", arch, "debuginfo.rpm") : optional ()); // Strip the trailing newline since rpm adds one. // expressions.pop_back (); strings expansions (eval (cstrings ({expressions.c_str ()}))); if (expansions.size () != files.size ()) fail << "number of RPM file path expansions differs from number " << "of path expressions"; for (size_t i (0); i != files.size(); ++i) { try { path p (move (expansions[i])); if (p.empty ()) throw invalid_path (""); // Note that the -debuginfo sub-package may potentially not be // generated (no installed binaries to extract the debug info from, // etc). // if (exists (p)) { binary_file& f (files[i]); r.push_back ( binary_file {move (f.type), move (p), move (f.system_name)}); } else if (!di || i != *di) // Not a -debuginfo sub-package? fail << "expected output file " << p << " does not exist"; } catch (const invalid_path& e) { fail << "invalid path '" << e.path << "' in RPM file path expansions"; } } } return r; } }