From a6f636f74cb931b881275e930567ad18eb975a55 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Thu, 9 Mar 2023 17:36:31 +0300 Subject: Add pkg-bindist implementation for Fedora --- bpkg/system-package-manager-fedora.cxx | 2357 +++++++++++++++++++++++++++++++- 1 file changed, 2343 insertions(+), 14 deletions(-) (limited to 'bpkg/system-package-manager-fedora.cxx') diff --git a/bpkg/system-package-manager-fedora.cxx b/bpkg/system-package-manager-fedora.cxx index 8ea1866..6ace1d2 100644 --- a/bpkg/system-package-manager-fedora.cxx +++ b/bpkg/system-package-manager-fedora.cxx @@ -3,8 +3,12 @@ #include +#include + #include +#include + using namespace butl; namespace bpkg @@ -158,7 +162,7 @@ namespace bpkg // just . // // Note that the order is important since for a mixed package we need to - // end up with the -libs subpackage rather than with the base package as, + // 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 -> @@ -1141,7 +1145,8 @@ namespace bpkg // 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. + // 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. @@ -1643,7 +1648,9 @@ namespace bpkg if (p != string::npos) sv.erase (0, p + 1); - // @@ Do the same for Debian? + // 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) @@ -1829,20 +1836,2342 @@ namespace bpkg } } + // 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. + // paths system_package_manager_fedora:: - generate (const packages&, - const packages&, - const strings&, - const dir_path&, - const package_manifest&, - const string&, - const small_vector&, - optional) + 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 recur) { - // @@ TODO: make sure --output-root is not specified or matched the - // rpm standard directory. + 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); + + 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. + // + // 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) + { + 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); + + sdeps.push_back (move (s)); + } + + if (verb >= 3) + { + auto print_status = [] (diag_record& dr, const package_status& s) + { + dr << s.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); + } + + for (const package_status& st: sdeps) + { + diag_record dr (trace); + dr << "dependency: "; + print_status (dr, st); + } + } + + // 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 ()) + { + // 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.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; + + // These are used for sorting out the installed files into the %files + // sections of the sub-packages. + // + dir_path bindir; + dir_path sbindir; + dir_path libexecdir; + dir_path confdir; + dir_path incdir; + dir_path libdir; + dir_path pkgdir; // Not queried, set as libdir/pkgconfig/. + dir_path sharedir; + dir_path docdir; + dir_path mandir; + dir_path licensedir; + + // 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 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_dir = [&expansions, &expressions] () + { + assert (!expansions.empty ()); + + try + { + dir_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; + } + }; + + // 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 << "'"; + + pop_string (); // We only need %{?_arch} expansion for the verification. + + 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 () / 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 (!recur || *recur == recursive_mode::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; + } + } + + // 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. + // + 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. + // + + // 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 (!build_arch.empty ()) + os << "BuildArch: " << build_arch << '\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'; + + os << '\n' + << "# " << st.main << '\n' + << "#" << '\n' + << "AutoReqProv: no" << '\n'; + + // Requires directives. + // + { + bool first (true); + if (!st.common.empty ()) + add_requires (first, st.common + " = %{evr}"); + + for (const package_status& s: sdeps) + 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 ()); + } + + os << '\n' + << "%description" << '\n' + << "This package contains the runtime 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. + // + 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. + // + 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). + // + os << "%undefine _debugsource_packages" << '\n'; + + // Append the -ffile-prefix-map option (if specified) which is used to + // strip source file path prefix in debug information (besides other + // places). By default it is not used, presumably since we disable + // generating the -debugsource sub-packages. We change it to point to + // the bpkg configuration directory. Note that this 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. + // + // Note that adding this option may also result in notification + // messages for probably all the source files as following: + // + // cpio: libfoo-1.2.3/foo.cxx: Cannot stat: No such file or directory + // + // This bloats the rpmbuild output but doesn't seem to break + // anything. + // + if (ops_->fedora_buildflags () != "ignore") + { + if (lang_c || lang_cc) + os << "%global build_cflags %{?build_cflags} -ffile-prefix-map=" + << cfg_dir.string () << "=." << '\n'; + + if (lang_cxx || lang_cc) + os << "%global build_cxxflags %{?build_cxxflags} -ffile-prefix-map=" + << cfg_dir.string () << "=." << '\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 (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 ()) + { + 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 () + '/'; + } + } + else + *fs += "%{_libdir}/" + pd + sd.string () + '/'; + + // In the case of the directory (has the trailing slash) skip + // all the other entries in this subdirectory (in the prefix map + // they will all be in a contiguous range). + // + if (fs->back () == '/') + { + while (p.first != p.second && p.first->first.sub (d)) + ++p.first; + } + + *fs += '\n'; + } + + // 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/, or licenses/. + // + { + // 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)) + 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'; + } + + // 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); + + // 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)) + main += "%license %{_licensedir}/" + (priv ? pd : "*") + '\n'; + + // Finally, write the %files sections. + // + if (!main.empty ()) + { + 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 " << pv.string () << '.' << '\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 paths {}; + } + + 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. + // + paths r; + { + 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); + + size_t np (0); + auto add_package = [&expressions, &rpmfile, &np, &add_macro] + (const string& name, const string& arch) -> size_t + { + add_macro ("NAME", name); + add_macro ("ARCH", arch); + expressions += rpmfile + '\n'; + return np++; + }; + + add_package (st.main, package_arch); + + if (!st.devel.empty ()) + add_package (st.devel, package_arch); + + if (!st.static_.empty ()) + add_package (st.static_, package_arch); + + if (!st.doc.empty ()) + add_package (st.doc, "noarch"); + + if (!st.common.empty ()) + add_package (st.common, "noarch"); + + size_t di (add_package (st.main + "-debuginfo", arch)); + + // Strip the trailing newline since rpm adds one. + // + expressions.pop_back (); + + strings expansions (eval (cstrings ({expressions.c_str ()}))); + + if (expansions.size () != np) + fail << "number of RPM file path expansions differs from number " + << "of path expressions"; + + r.reserve (np); + + for (size_t i (0); i != expansions.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)) + r.push_back (move (p)); + else if (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"; + } + } + } - paths r; return r; } } -- cgit v1.1