diff options
author | Karen Arutyunov <karen@codesynthesis.com> | 2023-03-09 17:36:31 +0300 |
---|---|---|
committer | Karen Arutyunov <karen@codesynthesis.com> | 2023-03-17 14:22:43 +0300 |
commit | a6f636f74cb931b881275e930567ad18eb975a55 (patch) | |
tree | 663bd5f1fb1b87dc870b83ceee740f5a79af3e83 | |
parent | e6587668fa1f74bf32e0017666bdf2152434dbfb (diff) |
Add pkg-bindist implementation for Fedora
-rw-r--r-- | bpkg/buildfile | 1 | ||||
-rw-r--r-- | bpkg/pkg-bindist.cli | 190 | ||||
-rw-r--r-- | bpkg/pkg-bindist.cxx | 2 | ||||
-rw-r--r-- | bpkg/system-package-manager-debian.cxx | 25 | ||||
-rw-r--r-- | bpkg/system-package-manager-fedora.cxx | 2355 | ||||
-rw-r--r-- | bpkg/system-package-manager-fedora.hxx | 22 | ||||
-rw-r--r-- | bpkg/system-package-manager-fedora.test.cxx | 29 | ||||
-rw-r--r-- | bpkg/system-package-manager-fedora.test.testscript | 163 | ||||
-rw-r--r-- | bpkg/system-package-manager.cxx | 2 | ||||
-rwxr-xr-x | doc/cli.sh | 3 | ||||
-rw-r--r-- | doc/manual.cli | 123 |
11 files changed, 2880 insertions, 35 deletions
diff --git a/bpkg/buildfile b/bpkg/buildfile index 05ded7e..4782180 100644 --- a/bpkg/buildfile +++ b/bpkg/buildfile @@ -217,6 +217,7 @@ if $cli.configured cli.cxx{pkg-bindist-options}: cli.options += \ --class-doc bpkg::pkg_bindist_debian_options=exclude-base \ +--class-doc bpkg::pkg_bindist_fedora_options=exclude-base \ --class-doc bpkg::pkg_bindist_archive_options=exclude-base # Avoid generating CLI runtime and empty inline file for help topics. diff --git a/bpkg/pkg-bindist.cli b/bpkg/pkg-bindist.cli index cdbb10c..843fb09 100644 --- a/bpkg/pkg-bindist.cli +++ b/bpkg/pkg-bindist.cli @@ -35,8 +35,8 @@ namespace bpkg \cb{config.install.*} variables) to match the distribution's layout. Then it generates any necessary distribution package metadata files based on the information from the package \cb{manifest} files. Finally, it invokes - the distribution-specified command to produce the binary package. Unless - overrident with the \cb{--architecture} and \cb{--distribution} options, + the distribution-specific command to produce the binary package. Unless + overridden with the \cb{--architecture} and \cb{--distribution} options, the binary package is generated for the host architecture using the host's standard system package manager. Additional command line variables (<vars>, normally \cb{config.*}) can be passed to the build system during @@ -117,7 +117,7 @@ namespace bpkg bool --debian-prepare-only { "Prepare all the package metadata files (\cb{control}, \cb{rules}, etc) - but do not invoke \cb{bpkg-buildpackage} to generate the binary + but do not invoke \cb{dpkg-buildpackage} to generate the binary package, printing its command line instead unless requested to be quiet. Implies \cb{--keep-output}." } @@ -186,7 +186,8 @@ namespace bpkg { "<v>", "Alternative \cb{Architecture} \cb{control} file field value for - the main binary package. The default is \cb{any}." + the main binary package, normally \cb{all} (architecture-independent). + The default is \cb{any} (architecture-dependent)." } string --debian-main-langdep @@ -220,6 +221,186 @@ namespace bpkg } }; + class pkg_bindist_fedora_options + { + "\h|FEDORA DESCRIPTION| + + The Fedora binary packages are generated by producing the standard RPM + spec file and then invoking \cb{rpmbuild(8)} to build the binary package + from that. While this approach is normally used to build packages from + source, this implementation \"pretends\" that this is what's happening by + overriding a number of RPM spec file sections to invoke the \cb{build2} + build system on the required packages directly in their \cb{bpkg} + configuration locations. + + The \cb{rpmdevtools} Fedora package must be installed before invocation. + Typical invocation: + + \ + bpkg build libhello + bpkg test libhello + bpkg bindist libhello + \ + + The resulting binary packages are placed into the standard \cb{rpmbuild} + output directory (normally \c{\b{~/rpmbuild/RPMS/}\i{arch}\b{/}}). + + Unless the \cb{--recursive} option is specified, dependencies of the + specified package are translated to dependencies in the resulting binary + package using names and versions that refer to packages that would be + generated by the \cb{pkg-bindist} command (called \"non-native\" + packages). If instead you would like certain dependencies to refer to + binary packages provided by the distribution (called \"native\" + packages), then you need to arrange for them to be built as system (see + \l{bpkg-pkg-build(1)} for details). For example, if our \cb{libhello} has + a dependency on \cb{libsqlite3} and we would like the binary package for + \cb{libhello} to refer to \cb{sqlite-libs} from Fedora (or alike), then + the \cb{pkg-build} command would need to be (\cb{--sys-install} is + optional): + + \ + bpkg build --sys-install libhello ?sys:libsqlite3 + \ + + Such a package with native dependencies can then be installed (including + any missing native dependencies) using the \cb{dnf install} command. + For example: + + \ + dnf install libhello-1.2.3-1.fc35.x86_64.rpm \ + libhello-devel-1.2.3-1.fc35.x86_64.rpm + \ + + See \l{bpkg#bindist-mapping-fedora-produce Fedora Package Mapping for + Production} for details on \cb{bpkg} to Fedora package name and version + mapping. + " + + "\h|PKG-BINDIST FEDORA OPTIONS|" + + bool --fedora-prepare-only + { + "Prepare the RPM spec file but do not invoke \cb{rpmbuild} to generate + the binary package, printing its command line instead unless requested + to be quiet." + } + + string --fedora-buildflags = "assign" + { + "<mode>", + "Package build flags (\cb{%{build_*flags\}} macros) usage mode. Valid + <mode> values are \cb{assign} (use the build flags instead of + configured), \cb{append} (use the build flags in addition to + configured, putting them last), \cb{prepend} (use the build flags in + addition to configured, putting them first), and \cb{ignore} (ignore + build flags). The default mode is \cb{assign}. Note that compiler mode + options, if any, are used as configured." + } + + strings --fedora-build-option + { + "<o>", + "Additional option to pass to the \cb{rpmbuild} program. If specified, + these options must be consistent with the query options + (\cb{--fedora-query-option}) to result in identical macro + expansions. Repeat this option to specify multiple build options." + } + + strings --fedora-query-option + { + "<o>", + "Additional option to pass to the \cb{rpm} program. This program is used + to query RPM macro values which affect the binary package. If + specified, these options must be consistent with the build options + (\cb{--fedora-build-option}) to result in identical macro expansions. + Repeat this option to specify multiple query options." + } + + string --fedora-dist-tag + { + "<tag>", + "Alternative distribution tag to use in the binary package release. If + empty value is specified, then no distribution tag is included. The + default is a value that identifies the distribution being used to build + the package, for example, \cb{fc35} for Fedora 35 or \cb{el8} for RHEL + 8." + } + + string --fedora-packager + { + "<v>", + "Alternative \cb{Packager} RPM spec file directive value. The default is + the \cb{package-email} value from package \cb{manifest}. If empty value + is specified, then the \cb{Packager} directive is omitted from the spec + file." + } + + string --fedora-build-arch + { + "<v>", + "\cb{BuildArch} RPM spec file directive value for the main binary + package, normally \cb{noarch} (architecture-independent). By default + the directive is omitted, assuming that the package is + architecture-dependent." + } + + strings --fedora-main-langreq + { + "<v>", + "Override the language runtime dependencies (such as \cb{glibc}, + \cb{libstdc++}, etc) of the main binary package by replacing the + corresponding \cb{Requires} RPM spec file directives. If empty value is + specified then no language runtime dependencies are specified. Repeat + this option to specify multiple language runtime dependencies." + } + + strings --fedora-devel-langreq + { + "<v>", + "Override the language runtime dependencies (such as \cb{glibc-devel}, + \cb{libstdc++-devel}, etc) of the development (\cb{-devel}) binary + package by replacing the corresponding \cb{Requires} RPM spec file + directives. If empty value is specified then no language runtime + dependencies are specified. Repeat this option to specify multiple + language runtime dependencies." + } + + strings --fedora-stat-langreq + { + "<v>", + "Override the language runtime dependencies (such as \cb{glibc-static}, + \cb{libstdc++-static}, etc) of the static libraries (\cb{-static}) + binary package by replacing the corresponding \cb{Requires} RPM spec + file directives. If empty value is specified then no language runtime + dependencies are specified. Repeat this option to specify multiple + language runtime dependencies." + } + + strings --fedora-main-extrareq + { + "<v>", + "Extra dependency to add to the main binary package as an additional + \cb{Requires} RPM spec file directive. Repeat this option to specify + multiple extra dependencies." + } + + strings --fedora-devel-extrareq + { + "<v>", + "Extra dependency to add to the development (\cb{-devel}) binary package + as an additional \cb{Requires} RPM spec file directive. Repeat this + option to specify multiple extra dependencies." + } + + strings --fedora-stat-extrareq + { + "<v>", + "Extra dependency to add to the static libraries (\cb{-static}) binary + package as an additional \cb{Requires} RPM spec file directive. Repeat + this option to specify multiple extra dependencies." + } + }; + class pkg_bindist_archive_options { "\h|ARCHIVE DESCRIPTION| @@ -407,6 +588,7 @@ namespace bpkg // class pkg_bindist_options: configuration_options, pkg_bindist_debian_options, + pkg_bindist_fedora_options, pkg_bindist_archive_options { "\h|PKG-BINDIST COMMON OPTIONS|" diff --git a/bpkg/pkg-bindist.cxx b/bpkg/pkg-bindist.cxx index 32be5dd..8c2163b 100644 --- a/bpkg/pkg-bindist.cxx +++ b/bpkg/pkg-bindist.cxx @@ -155,7 +155,7 @@ namespace bpkg shared_ptr<selected_package> d (ld.load ()); // Packaging stuff that is spread over multiple configurations is just - // to hairy so we don't support it. Specifically, it becomes tricky to + // too hairy so we don't support it. Specifically, it becomes tricky to // override build options since using a global override will also affect // host/build2 configurations. // diff --git a/bpkg/system-package-manager-debian.cxx b/bpkg/system-package-manager-debian.cxx index 9370f37..f0a34a4 100644 --- a/bpkg/system-package-manager-debian.cxx +++ b/bpkg/system-package-manager-debian.cxx @@ -1326,12 +1326,21 @@ namespace bpkg if (!v) { // Fallback to using system version as downstream version. But first - // strip the epoch, if any. + // 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); @@ -1513,7 +1522,7 @@ namespace bpkg // while native names are libsqlite3-0 and libsqlite3-dev. 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 - // exception (e.g., our package is "better" in some way, such as configured + // 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 @@ -2052,7 +2061,7 @@ namespace bpkg // Note: we need to quote values that contain `$` so that they don't get // expanded as build2 variables in the installed_entries() call. // - // NOTE: make sure to update .install files below if changing anyting + // NOTE: make sure to update .install files below if changing anything // here. // strings config { @@ -2265,7 +2274,7 @@ namespace bpkg // The Multi-Arch hint can be `same` or `foreign`. The former means that // a separate copy of the package may be installed for each architecture // (e.g., library) while the latter -- that a single copy may be used by - // all architectures (e.g., executable, -doc, -common). Not that for + // all architectures (e.g., executable, -doc, -common). Note that for // some murky reasons Multi-Arch:foreign needs to be explicitly // specified for Architecture:all. // @@ -2518,7 +2527,6 @@ namespace bpkg // // <day-of-week>, <dd> <month> <yyyy> <hh>:<mm>:<ss> +<zzzz> // - timestamp now (system_clock::now ()); os << " -- " << maintainer << " "; std::locale l (os.imbue (std::locale ("C"))); to_stream (os, @@ -2892,11 +2900,6 @@ namespace bpkg } os << '\n'; - // Disable synchronization hooks for good measure. - // - os << "export BDEP_SYNC := 0\n" - << '\n'; - // Default to the dh command sequencer. // // Note that passing --buildsystem=none doesn't seem to make any @@ -3113,7 +3116,7 @@ namespace bpkg // libfoo.so // // Note that in the (C) case the library should go into the main - // package. Based on this, the criteria appears to be straightforwrad: + // 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). // 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 <bpkg/system-package-manager-fedora.hxx> +#include <locale> + #include <bpkg/diagnostics.hxx> +#include <bpkg/pkg-bindist-options.hxx> + using namespace butl; namespace bpkg @@ -158,7 +162,7 @@ namespace bpkg // just <devel-stem>. // // 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<available_package>& ap (aps.front ().first); + const lazy_shared_ptr<repository_fragment>& 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: + // + // [<epoch>:]<version>-<release> + // + // Where <release> has the following form: + // + // <release-number>[.<distribution-tag>] + // + // 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: + // + // [+<epoch>-]<upstream>[-<prerel>][+<revision>] + // + // Let's start with the case where neither distribution nor upstream + // version is specified and we need to derive everything from the bpkg + // version. + // + // <epoch> + // + // 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. + // + // + // <upstream>[-<prerel>] + // + // Our upstream version maps naturally to Fedora's <version>. That is, + // our upstream version format/semantics is a subset of Fedora's + // <version>. + // + // 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 <prerel> as is since its format is the same as + // <upstream> and thus should map naturally. + // + // + // <revision> + // + // 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 <epoch>-<upstream> 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 <epoch> and <version> + // 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<string> 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 [<epoch>:]<version> 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 <version> 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 <expr1> --eval <expr2>...` 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<language, 1>&, - optional<recursive_mode>) + 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<language, 1>& langs, + optional<recursive_mode> 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<selected_package>& 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<package_status> sdeps; + sdeps.reserve (deps.size ()); + for (const package& p: deps) + { + const shared_ptr<selected_package>& sp (p.selected); + const available_packages& aps (p.available); + + package_status s; + if (sp->substate == package_substate::system) + { + optional<package_status> 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 <project> substitution since in the recursive mode + // we may be installing multiple projects. Note that the <private> + // directory component is automatically removed if this functionality is + // not enabled. One side-effect of using <project> 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 <project> to be sqlite-libs). To keep + // things consistent we use the bpkg package name for <private> 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}/<private>/", + "config.install.lib.mode=755", + "config.install.libexec=%{_libexecdir}/<private>/<project>/", + "config.install.pkgconfig=lib/pkgconfig/", + + "config.install.etc=%{_sysconfdir}/", + "config.install.include=%{_includedir}/<private>/", + "config.install.include_arch=include/", + "config.install.share=%{_datadir}/", + "config.install.data=share/<private>/<project>/", + + "config.install.doc=%{_docdir}/<private>/<project>/", + "config.install.legal=%{_licensedir}/<private>/<project>/", + "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 + // <private>/ (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/<package-dir>/ + // 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 <john@example.org>` 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++<suffix> and glibc<suffix> 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 <project> 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/<private>/ + // 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 <private>/ is specified, then to establish + // ownership of the libdir/<private>/ 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/<private>/ 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/<private>/ sub-entries: + // + // -static -> -devel -> main + // + // The same reasoning applies to libdir/<private>/pkgconfig/. + // + string* owners[] = {&static_, &devel, &main}; + + // Indexes (in owners) of sub-packages which should own + // libdir/<private>/ and libdir/<private>/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<size_t> private_owner; + optional<size_t> 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<size_t>& 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/<private>/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/<private>/. + // + 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 <private>/ is specified, then we also need to + // establish ownership of the sharedir/<private>/ directory (similar + // to what we do for libdir/<private>/ 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/<private>/. + // + 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: + // + // * <day-of-week> <month> <day> <year> <name> <surname> <email> - <version>-<release> + // - <change1-description> + // - <change2-description> + // ... + // + // For example: + // + // * Wed Feb 22 2023 John Doe <john@example.com> - 2.3.4-1 + // - New bpkg package release 2.3.4. + // + // We will use the Packager value for the `<name> <surname> <email>` + // 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 <release> 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"; + } + } + } + return r; } } diff --git a/bpkg/system-package-manager-fedora.hxx b/bpkg/system-package-manager-fedora.hxx index 28aff96..53106d8 100644 --- a/bpkg/system-package-manager-fedora.hxx +++ b/bpkg/system-package-manager-fedora.hxx @@ -71,6 +71,9 @@ namespace bpkg // // keyutils keyutils-libs keyutils-libs-devel // + // Note that while we support arbitrary -debug* sub-package names for + // consumption, we only generate <main-package>-debug*. + // // Based on that, it seems our best bet when trying to automatically map our // library package name to Fedora package names is to go for the -devel // package first and figure out the shared library package from that based @@ -229,14 +232,19 @@ namespace bpkg yes, move (sudo)) {} + // Note: options can only be NULL when testing functions that don't need + // them. + // system_package_manager_fedora (bpkg::os_release&& osr, const target_triplet& h, string a, - optional<bool> progress) + optional<bool> progress, + const pkg_bindist_options* ops) : system_package_manager (move (osr), h, a.empty () ? arch_from_target (h) : move (a), - progress) {} + progress), + ops_ (ops) {} // Implementation details exposed for testing (see definitions for // documentation). @@ -276,6 +284,14 @@ namespace bpkg static string arch_from_target (const target_triplet&); + package_status + map_package (const package_name&, + const version&, + const available_packages&) const; + + static strings + rpm_eval (const cstrings& opts, const cstrings& expressions); + // If simulate is not NULL, then instead of executing the actual dnf // commands simulate their execution: (1) for `dnf list` and `dnf // repoquery --requires` by printing their command lines and reading the @@ -343,6 +359,8 @@ namespace bpkg bool installed_ = false; // True if already installed. std::map<package_name, optional<system_package_status_fedora>> status_cache_; + + const pkg_bindist_options* ops_ = nullptr; // Only for production. }; } diff --git a/bpkg/system-package-manager-fedora.test.cxx b/bpkg/system-package-manager-fedora.test.cxx index 893d938..d2120e7 100644 --- a/bpkg/system-package-manager-fedora.test.cxx +++ b/bpkg/system-package-manager-fedora.test.cxx @@ -39,6 +39,8 @@ namespace bpkg // the `<dep-pkg> <dep-ver>` // per line form // + // map-package manifest comes from stdin + // // build <query-pkg>... [--install [--no-fetch] <install-pkg>...] // // The stdin of the build command is used to read the simulation description @@ -202,6 +204,33 @@ namespace bpkg cout << system_package_manager_fedora::main_from_devel (n, v, ds) << '\n'; } + else if (cmd == "map-package") + { + assert (argc == 2); + + available_packages aps; + aps.push_back (make_available_from_manifest ("", "-")); + + const package_name& n (aps.front ().first->id.name); + const version& v (aps.front ().first->version); + + system_package_manager_fedora m (move (osr), + host_triplet, + "" /* arch */, + nullopt /* progress */, + nullptr /* options */); + + package_status s (m.map_package (n, v, aps)); + + cout << "version: " << s.system_version << '\n' + << "main: " << s.main << '\n'; + if (!s.devel.empty ()) cout << "devel: " << s.devel << '\n'; + if (!s.static_.empty ()) cout << "static: " << s.static_ << '\n'; + if (!s.doc.empty ()) cout << "doc: " << s.doc << '\n'; + if (!s.debuginfo.empty ()) cout << "debuginfo: " << s.debuginfo << '\n'; + if (!s.debugsource.empty ()) cout << "debugsource: " << s.debugsource << '\n'; + if (!s.common.empty ()) cout << "common: " << s.common << '\n'; + } else if (cmd == "build") { assert (argc >= 3); // <query-pkg>... diff --git a/bpkg/system-package-manager-fedora.test.testscript b/bpkg/system-package-manager-fedora.test.testscript index ab3590a..95d357f 100644 --- a/bpkg/system-package-manager-fedora.test.testscript +++ b/bpkg/system-package-manager-fedora.test.testscript @@ -260,6 +260,169 @@ $* boost-http-server-devel 0-1.20220116gitcd5245f.fc35 <:'' >'' } +: map-package +: +{ + test.arguments += map-package + + : default-name + : + $* <<EOI >>EOO + : 1 + name: byacc + version: 20210808 + summary: yacc parser generator + license: other: public domain + EOI + version: 20210808-1 + main: byacc + EOO + + : default-name-lib + : + $* <<EOI >>EOO + : 1 + name: libsqlite3 + version: 3.40.1 + summary: database library + license: other: public domain + EOI + version: 3.40.1-1 + main: libsqlite3 + devel: libsqlite3-devel + EOO + + : custom-name + : + $* <<EOI >>EOO + : 1 + name: libsqlite3 + fedora_35-name: libsqlite3 libsqlite3-devel + version: 3.40.1 + summary: database library + license: other: public domain + EOI + version: 3.40.1-1 + main: libsqlite3 + devel: libsqlite3-devel + EOO + + : custom-name-dev-only + : + $* <<EOI >>EOO + : 1 + name: libsqlite3 + fedora_35-name: libsqlite3-devel + version: 3.40.1 + summary: database library + license: other: public domain + EOI + version: 3.40.1-1 + main: libsqlite3 + devel: libsqlite3-devel + EOO + + : custom-name-non-native + : + $* <<EOI >>EOO + : 1 + name: libsqlite3 + fedora_0-name: libsqlite libsqlite-devel + fedor_35-name: libsqlite3 libsqlite3-devel + version: 3.40.1 + summary: database library + license: other: public domain + EOI + version: 3.40.1-1 + main: libsqlite + devel: libsqlite-devel + EOO + + : version-upstream + : + $* <<EOI >>EOO + : 1 + name: byacc + version: +2-1.2.3-beta.1+3 + upstream-version: 20210808 + summary: yacc parser generator + license: other: public domain + EOI + version: 20210808~beta.1-4 + main: byacc + EOO + + : version-distribution + : + $* <<EOI >>EOO + : 1 + name: byacc + version: +2-1.2.3-beta.1+3 + fedora-version: 20210808~beta.1 + summary: yacc parser generator + license: other: public domain + EOI + version: 20210808~beta.1-1 + main: byacc + EOO + + : version-distribution-epoch-revision + : + $* <<EOI >>EOO + : 1 + name: byacc + version: +2-1.2.3-beta.1+3 + fedora-version: 1:1.2.3-2 + summary: yacc parser generator + license: other: public domain + EOI + version: 1:1.2.3-2 + main: byacc + EOO + + : version-distribution-empty-prerelease + : + $* <<EOI >>EOO + : 1 + name: byacc + version: +2-1.2.3-beta.1+3 + fedora-version: 20210808~-4 + summary: yacc parser generator + license: other: public domain + EOI + version: 20210808~beta.1-4 + main: byacc + EOO + + : version-distribution-empty-revision + : + $* <<EOI >>EOO + : 1 + name: byacc + version: +2-1.2.3-beta.1+3 + fedora-version: 20210808~b.1- + summary: yacc parser generator + license: other: public domain + EOI + version: 20210808~b.1-4 + main: byacc + EOO + + : version-distribution-empty-release-revision + : + $* <<EOI >>EOO + : 1 + name: byacc + version: +2-1.2.3-beta.1+3 + fedora-version: 20210808~- + summary: yacc parser generator + license: other: public domain + EOI + version: 20210808~beta.1-4 + main: byacc + EOO +} + : build : { diff --git a/bpkg/system-package-manager.cxx b/bpkg/system-package-manager.cxx index c4ebe5a..2ffb1bb 100644 --- a/bpkg/system-package-manager.cxx +++ b/bpkg/system-package-manager.cxx @@ -205,7 +205,7 @@ namespace bpkg os.like_ids.push_back ("fedora"); r.reset (new system_package_manager_fedora ( - move (os), host, arch, progress)); + move (os), host, arch, progress, &o)); } // NOTE: remember to update the --distribution pkg-bindist option // documentation if adding support for another package manager. @@ -76,10 +76,11 @@ o="--suppress-undocumented --output-prefix bpkg- --class-doc bpkg::common_option compile "common" $o --output-suffix "-options" --class-doc bpkg::common_options=long compile "bpkg" $o --output-prefix "" --class-doc bpkg::commands=short --class-doc bpkg::topics=short -compile "pkg-build" $o --class-doc bpkg::pkg_build_pkg_options=exclude-base +compile "pkg-build" $o --class-doc bpkg::pkg_build_pkg_options=exclude-base compile "pkg-bindist" $o \ --class-doc bpkg::pkg_bindist_debian_options=exclude-base \ + --class-doc bpkg::pkg_bindist_fedora_options=exclude-base \ --class-doc bpkg::pkg_bindist_archive_options=exclude-base # NOTE: remember to update a similar list in buildfile and bpkg.cli as well as diff --git a/doc/manual.cli b/doc/manual.cli index 48592fd..29e5535 100644 --- a/doc/manual.cli +++ b/doc/manual.cli @@ -3187,7 +3187,7 @@ Finally, we have the distribution version. The Debian \c{<epoch>} and distribution version as required. This leaves pre-release and revision. It feels like in most cases we would want these copied over from the \c{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 +manually. However, we want the user to have the full override ability. So instead, if empty revision is specified, as in \c{1.2.3-}, then we automatically add the \c{bpkg} revision. Similarly, if empty pre-release is specified, as in \c{1.2.3~}, then we add the \c{bpkg} pre-release. To add both @@ -3262,6 +3262,9 @@ ncurses ncurses-libs ncurses-c++-libs ncurses-devel ncurses-static keyutils keyutils-libs keyutils-libs-devel \ +Note that while we support arbitrary \c{-debug*} sub-package names for +consumption, we only generate \c{<main-package>-debug*}. + Based on that, our approach when trying to automatically map a \c{bpkg} library package name to Fedora package names is to go for the \c{-devel} package first and figure out the shared library package from that based on the @@ -3313,10 +3316,126 @@ to the \c{bpkg} version is specified with the \c{fedora-to-downstream-version} then we match it against the \c{[<epoch>:]<version>} parts ignoring \c{<release>}. + \h2#bindist-mapping-fedora-produce|Fedora Package Mapping for Production| -@@ TODO +The same \c{fedora-name} (or alike) manifest values as used for consumption +are also used to derive the package names for production except here we have +the option to specify alternative non-native package names using the special +\c{fedora_0-name} (or alike) value. If only the \c{-devel} package is +specified, then the main package name is derived from that by removing the +\c{-devel} suffix. + +The generated binary package version can be specified with the +\c{fedora-version} (or alike) manifest value. If it's not specified, then the +\c{upstream-version} is used if specified. Otherwise, the \c{bpkg} version +is translated to the Fedora version as described next. + +To recap, a Fedora package version has the following form: + +\ +[<epoch>:]<version>-<release> +\ + +Where <release> has the following form: + +\ +<release-number>[.<distribution-tag>] +\ + +For details on the ordering semantics, see the Fedora Versioning Guidelines. +While overall unsurprising, the only notable exceptions are \c{~}, which sorts +before anything else and is commonly used for upstream pre-releases, and +\c{^}, which sorts after anything else and is supposedly used for upstream +post-release snapshots. For example, \c{0.1.0~alpha.1-1.fc35} sorts earlier +than \c{0.1.0-1.fc35}. + +To recap, the bpkg version has the following form (see +\l{#package-version Package Version} for details): + +\ +[+<epoch>-]<upstream>[-<prerel>][+<revision>] +\ + +Let's start with the case where neither distribution (\c{fedora-version}) nor +upstream version (\c{upstream-version}) is specified and we need to derive +everything from the \c{bpkg} version (what follows is as much description as +rationale). + +\dl| + +\li|\c{<epoch>} + + On one hand, if we keep our (as in, \c{bpkg}) 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 (see below), we 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 shift this value range.| + +\li|\c{<upstream>[-<prerel>]} + + Our upstream version maps naturally to Fedora's \c{<version>}. That is, our + upstream version format/semantics is a subset of Fedora's \c{<version>}. + + 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 (\c{~}) which is + essentially meant for this, so we use it. We will use \c{<prerel>} as is + since its format is the same as \c{<upstream>} and thus should map + naturally.| + +\li|\c{<revision>} + + 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), we use it. + + Note that the Fedora start release number is 1 and our revision is 0. So we + shift this value range. + + Also we automatically add the trailing distribution tag (\c{.fc35}, + \c{.el8}, etc) to the Fedora release. The tag is deduced automatically + unless overridden on the command line (see \l{bpkg-pkg-bindist(1)} command + for details). +|| + +The next case to consider is when we have the upstream version +(\c{upstream-version} manifest value). After some rumination it feels correct +to use it in place of the \c{<epoch>-<upstream>} 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 \c{bpkg} version. If this +is not the desired semantics, then it can always be overridden with the +distribution version (see below). + +Finally, we have the distribution version. The Fedora \c{<epoch>} and +\c{<version>} 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 \c{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 \c{1.2.3-}, then we +automatically add the \c{bpkg} revision. Similarly, if empty pre-release is +specified, as in \c{1.2.3~}, then we add the \c{bpkg} pre-release. To add both +automatically, we would specify \c{1.2.3~-} (other combinations are +\c{1.2.3~b.1-} and \c{1.2.3~-1}). If specified, the release must not contain +the distribution tag, since it is deduced automatically unless overridden on +the command line (see \l{bpkg-pkg-bindist(1)} command for details). 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 \c{:} or \c{-}. Note that the \c{bpkg} upstream +version may not contain either. " //@@ TODO items (grep). |