aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2023-03-09 17:36:31 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2023-03-17 14:22:43 +0300
commita6f636f74cb931b881275e930567ad18eb975a55 (patch)
tree663bd5f1fb1b87dc870b83ceee740f5a79af3e83
parente6587668fa1f74bf32e0017666bdf2152434dbfb (diff)
Add pkg-bindist implementation for Fedora
-rw-r--r--bpkg/buildfile1
-rw-r--r--bpkg/pkg-bindist.cli190
-rw-r--r--bpkg/pkg-bindist.cxx2
-rw-r--r--bpkg/system-package-manager-debian.cxx25
-rw-r--r--bpkg/system-package-manager-fedora.cxx2355
-rw-r--r--bpkg/system-package-manager-fedora.hxx22
-rw-r--r--bpkg/system-package-manager-fedora.test.cxx29
-rw-r--r--bpkg/system-package-manager-fedora.test.testscript163
-rw-r--r--bpkg/system-package-manager.cxx2
-rwxr-xr-xdoc/cli.sh3
-rw-r--r--doc/manual.cli123
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.
diff --git a/doc/cli.sh b/doc/cli.sh
index 6e50bed..fa6e2f3 100755
--- a/doc/cli.sh
+++ b/doc/cli.sh
@@ -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).