Add support for generating debuginfo RPMs (#842)

* Enable creation and capture of debuginfo RPMs

This change enables the creation and capture of debuginfo RPMs on
Fedora40 and CentOS7.

See:
https://docs.fedoraproject.org/en-US/packaging-guidelines/Debuginfo/

Fedora 40 expects the RPM contents to be located in a subdirectory
which is specified using the `buildsubdir` variable.  In order to
account for this, we need to tweak some of the location details if
debuginfo is enabled.

CentOS expects `buildsubdir` to have a value like `.` instead.

In both cases, we disable debugsource packages by ensuring that we
undefine `_debugsource_packages`, otherwise we'll try to generate them
alongside the debuginfo packages and will fail.

We only want this method of producing debuginfo to apply when we're
using the system `rpmbuild` because the underlying behaviour is
controlled by a combination of the rpmbuild version, macro
definitions, find-debuginfo.sh, and debugedit.  If we were to expand
this to use a hermetic debuginfo then a different approach might be
desirable.

* Add an RPM example that generates debuginfo

This provides a basic example that generates a debuginfo RPM
configured to run on CentOS7.

* Upgrade rules_python to 0.31.0

rules_python seems to fail us when we're generating debuginfo RPMs
unless we upgrade to a version more recent than 0.24.0.

* Only generate debuginfo RPM when pkg_rpm() asks for it

In lieu of enabling this behaviour by default on the supported
platforms, we add an additional argument to the pkg_rpm() rule that
will allow us to enable it for pkg_rpm() targets.  This prevents us
from enabling it in cases where it's not desired.

* Add test for building debuginfo RPM

This test is modelled on the subrpm test.  In lieu of using a simple
text file as an input it instead generates a binary that includes
debug symbosl from a C source file and includes that in the RPM.

The baseline comparison strips out the `.build-id` paths because the
hashes that are generated may not be stable.x

* Remove architecture and size from debuginfo test output

These values may vary depending on the platform that this is being run
on and we don't really care about them.

* Add period to docstring

* Enable debuginfo support for CentOS Stream 9

CentOS Stream 9 appears to work more or less the same for debuginfo
generation as CentOS 7.  `os-release` describes it as os == `centos`
and version == `9`.  This change creates an extra token for `centos9`
and sticks it in the places where we currently have controls for
`centos7`.
This commit is contained in:
Mike Kelly 2024-04-24 13:03:11 -07:00 committed by GitHub
parent a811e7f44f
commit 581a86a294
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 354 additions and 21 deletions

View File

@ -7,7 +7,7 @@ module(
# Do not update to newer versions until you need a specific new feature.
bazel_dep(name = "rules_license", version = "0.0.4")
bazel_dep(name = "rules_python", version = "0.24.0")
bazel_dep(name = "rules_python", version = "0.31.0")
bazel_dep(name = "bazel_skylib", version = "1.2.0")
# Only for development

View File

@ -0,0 +1,55 @@
# Copyright 2020 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -*- coding: utf-8 -*-
load("@rules_pkg//pkg:mappings.bzl", "pkg_files")
load("@rules_pkg//pkg:rpm.bzl", "pkg_rpm")
cc_binary(
name = "test",
copts = ["-g"],
srcs = [
"test.c",
],
)
pkg_files(
name = "rpm_files",
srcs = [
":test",
],
)
pkg_rpm(
name = "test-rpm",
srcs = [
":rpm_files",
],
release = "0",
version = "1",
license = "Some license",
summary = "Summary",
description = "Description",
debuginfo = True,
)
# If you have rpmbuild, you probably have rpm2cpio too.
# Feature idea: Add rpm2cpio and cpio to the rpmbuild toolchain
genrule(
name = "inspect_content",
srcs = [":test-rpm"],
outs = ["content.txt"],
cmd = "rpm2cpio $(locations :test-rpm) | cpio -ivt >$@",
)

View File

@ -0,0 +1,32 @@
# Copyright 2024 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module(name = "rules_pkg_example_rpm_system_rpmbuild_bzlmod")
bazel_dep(name = "rules_pkg")
local_path_override(
module_name = "rules_pkg",
path = "../../..",
)
find_rpmbuild = use_extension(
"@rules_pkg//toolchains/rpm:rpmbuild_configure.bzl",
"find_system_rpmbuild_bzlmod",
)
use_repo(find_rpmbuild, "rules_pkg_rpmbuild")
register_toolchains(
"@rules_pkg_rpmbuild//:all",
)

View File

@ -0,0 +1,17 @@
# Using system rpmbuild with bzlmod and generating debuginfo
## Summary
This example uses the `find_system_rpmbuild_bzlmod` module extension to help
us register the system rpmbuild as a toolchain in a bzlmod environment.
It configures the system toolchain to be aware of which debuginfo configuration
to use (defaults to "none", the example uses "centos7").
## To use
```
bazel build :*
rpm2cpio bazel-bin/test-rpm.rpm | cpio -ivt
cat bazel-bin/content.txt
```

View File

@ -0,0 +1,3 @@
int main() {
return 0;
}

View File

@ -172,6 +172,7 @@ class RpmBuilder(object):
SOURCE_DIR = 'SOURCES'
BUILD_DIR = 'BUILD'
BUILD_SUBDIR = 'BUILD_SUB'
BUILDROOT_DIR = 'BUILDROOT'
TEMP_DIR = 'TMP'
RPMS_DIR = 'RPMS'
@ -345,7 +346,7 @@ class RpmBuilder(object):
shutil.copy(os.path.join(original_dir, file_list_path), RpmBuilder.BUILD_DIR)
self.file_list_path = os.path.join(RpmBuilder.BUILD_DIR, os.path.basename(file_list_path))
def CallRpmBuild(self, dirname, rpmbuild_args):
def CallRpmBuild(self, dirname, rpmbuild_args, debuginfo_type):
"""Call rpmbuild with the correct arguments."""
buildroot = os.path.join(dirname, RpmBuilder.BUILDROOT_DIR)
@ -361,16 +362,31 @@ class RpmBuilder(object):
if self.debug:
args.append('-vv')
if debuginfo_type == "fedora40":
os.makedirs(f'{dirname}/{RpmBuilder.BUILD_DIR}/{RpmBuilder.BUILD_SUBDIR}')
# Common options
# NOTE: There may be a need to add '--define', 'buildsubdir .' for some
# rpmbuild versions. But that breaks other rpmbuild versions, so before
# adding it back in, add extensive tests.
args += [
'--define', '_topdir %s' % dirname,
'--define', '_tmppath %s/TMP' % dirname,
'--define', '_builddir %s/BUILD' % dirname,
'--bb',
'--buildroot=%s' % buildroot,
'--define', '_topdir %s' % dirname,
'--define', '_tmppath %s/TMP' % dirname,
'--define', '_builddir %s/BUILD' % dirname,
]
if debuginfo_type in ["fedora40", "centos7", "centos9"]:
args += ['--undefine', '_debugsource_packages']
if debuginfo_type in ["centos7", "centos9"]:
args += ['--define', 'buildsubdir .']
if debuginfo_type == "fedora40":
args += ['--define', f'buildsubdir {RpmBuilder.BUILD_SUBDIR}']
args += [
'--bb',
'--buildroot=%s' % buildroot,
] # yapf: disable
# Macro-based RPM parameter substitution, if necessary inputs provided.
@ -382,7 +398,11 @@ class RpmBuilder(object):
args += ['--define', 'build_rpm_install %s' % self.install_script_file]
if self.file_list_path:
# %files -f is taken relative to the package root
args += ['--define', 'build_rpm_files %s' % os.path.basename(self.file_list_path)]
base_path = os.path.basename(self.file_list_path)
if debuginfo_type == "fedora40":
base_path = os.path.join("..", base_path)
args += ['--define', 'build_rpm_files %s' % base_path]
args.extend(rpmbuild_args)
@ -459,7 +479,8 @@ class RpmBuilder(object):
posttrans_scriptlet_path=None,
file_list_path=None,
changelog_file=None,
rpmbuild_args=None):
rpmbuild_args=None,
debuginfo_type=None):
"""Build the RPM described by the spec_file, with other metadata in keyword arguments"""
if self.debug:
@ -490,7 +511,7 @@ class RpmBuilder(object):
postun_scriptlet_path=postun_scriptlet_path,
posttrans_scriptlet_path=posttrans_scriptlet_path,
changelog_file=changelog_file)
status = self.CallRpmBuild(dirname, rpmbuild_args or [])
status = self.CallRpmBuild(dirname, rpmbuild_args or [], debuginfo_type)
self.SaveResult(out_file, subrpm_out_files)
return status
@ -550,6 +571,8 @@ def main(argv):
parser.add_argument('--rpmbuild_arg', dest='rpmbuild_args', action='append',
help='Any additional arguments to pass to rpmbuild')
parser.add_argument('--debuginfo_type', dest='debuginfo_type', default='none',
help='debuginfo type to use (centos7, fedora40, or none)')
parser.add_argument('files', nargs='*')
options = parser.parse_args(argv or ())
@ -574,7 +597,8 @@ def main(argv):
postun_scriptlet_path=options.postun_scriptlet,
posttrans_scriptlet_path=options.posttrans_scriptlet,
changelog_file=options.changelog,
rpmbuild_args=options.rpmbuild_args)
rpmbuild_args=options.rpmbuild_args,
debuginfo_type=options.debuginfo_type)
except NoRpmbuildFoundError:
print('ERROR: rpmbuild is required but is not present in PATH')
return 1

View File

@ -67,6 +67,13 @@ DEFAULT_FILE_MODE = "%defattr(-,root,root)"
_INSTALL_FILE_STANZA_FMT = """
install -d "%{{buildroot}}/$(dirname '{1}')"
cp '{0}' '%{{buildroot}}/{1}'
chmod +w '%{{buildroot}}/{1}'
""".strip()
_INSTALL_FILE_STANZA_FMT_FEDORA40_DEBUGINFO = """
install -d "%{{buildroot}}/$(dirname '{1}')"
cp '../{0}' '%{{buildroot}}/{1}'
chmod +w '%{{buildroot}}/{1}'
""".strip()
# TODO(nacl): __install
@ -172,7 +179,7 @@ def _make_absolute_if_not_already_or_is_macro(path):
# TODO(nacl, #459): These are redundant with functions and structures in
# pkg/private/pkg_files.bzl. We should really use the infrastructure provided
# there, but as of writing, it's not quite ready.
def _process_files(pfi, origin_label, grouping_label, file_base, rpm_ctx):
def _process_files(pfi, origin_label, grouping_label, file_base, rpm_ctx, debuginfo_type):
for dest, src in pfi.dest_src_map.items():
metadata = _package_contents_metadata(origin_label, grouping_label)
if dest in rpm_ctx.dest_check_map:
@ -196,7 +203,12 @@ def _process_files(pfi, origin_label, grouping_label, file_base, rpm_ctx):
else:
# Files are well-known. Take care of them right here.
rpm_ctx.rpm_files_list.append(_FILE_MODE_STANZA_FMT.format(file_base, abs_dest))
rpm_ctx.install_script_pieces.append(_INSTALL_FILE_STANZA_FMT.format(
install_stanza_fmt = _INSTALL_FILE_STANZA_FMT
if debuginfo_type == "fedora40":
install_stanza_fmt = _INSTALL_FILE_STANZA_FMT_FEDORA40_DEBUGINFO
rpm_ctx.install_script_pieces.append(install_stanza_fmt.format(
src.path,
abs_dest,
))
@ -231,7 +243,7 @@ def _process_symlink(psi, origin_label, grouping_label, file_base, rpm_ctx):
psi.attributes["mode"],
))
def _process_dep(dep, rpm_ctx):
def _process_dep(dep, rpm_ctx, debuginfo_type):
# NOTE: This does not detect cases where directories are not named
# consistently. For example, all of these may collide in reality, but
# won't be detected by the below:
@ -255,6 +267,7 @@ def _process_dep(dep, rpm_ctx):
None, # group label
_make_filetags(dep[PackageFilesInfo].attributes), # file_base
rpm_ctx,
debuginfo_type,
)
if PackageDirsInfo in dep:
@ -285,6 +298,7 @@ def _process_dep(dep, rpm_ctx):
dep.label,
file_base,
rpm_ctx,
debuginfo_type
)
for entry, origin in pfg_info.pkg_dirs:
file_base = _make_filetags(entry.attributes, "%dir")
@ -306,7 +320,7 @@ def _process_dep(dep, rpm_ctx):
rpm_ctx,
)
def _process_subrpm(ctx, rpm_name, rpm_info, rpm_ctx):
def _process_subrpm(ctx, rpm_name, rpm_info, rpm_ctx, debuginfo_type):
sub_rpm_ctx = struct(
dest_check_map = {},
install_script_pieces = [],
@ -356,7 +370,7 @@ def _process_subrpm(ctx, rpm_name, rpm_info, rpm_ctx):
]
for dep in rpm_info.srcs:
_process_dep(dep, sub_rpm_ctx)
_process_dep(dep, sub_rpm_ctx, debuginfo_type)
# rpmbuild will be unhappy if we have no files so we stick
# default file mode in for that scenario
@ -424,6 +438,7 @@ def _pkg_rpm_impl(ctx):
files = []
tools = []
debuginfo_type = "none"
name = ctx.attr.package_name if ctx.attr.package_name else ctx.label.name
rpm_ctx.make_rpm_args.append("--name=" + name)
@ -448,6 +463,10 @@ def _pkg_rpm_impl(ctx):
tools.append(executable_files)
rpm_ctx.make_rpm_args.append("--rpmbuild=%s" % executable_files.executable.path)
if ctx.attr.debuginfo:
debuginfo_type = toolchain.debuginfo_type
rpm_ctx.make_rpm_args.append("--debuginfo_type=%s" % debuginfo_type)
#### Calculate output file name
# rpm_name takes precedence over name if provided
if ctx.attr.package_name:
@ -678,13 +697,14 @@ def _pkg_rpm_impl(ctx):
# they aren't unnecessarily recreated.
for dep in ctx.attr.srcs:
_process_dep(dep, rpm_ctx)
_process_dep(dep, rpm_ctx, debuginfo_type)
#### subrpms
if ctx.attr.subrpms:
subrpm_lines = []
for s in ctx.attr.subrpms:
subrpm_lines.extend(_process_subrpm(ctx, rpm_name, s[PackageSubRPMInfo], rpm_ctx))
subrpm_lines.extend(_process_subrpm(
ctx, rpm_name, s[PackageSubRPMInfo], rpm_ctx, debuginfo_type))
subrpm_file = ctx.actions.declare_file(
"{}.spec.subrpms".format(rpm_name),
@ -696,6 +716,27 @@ def _pkg_rpm_impl(ctx):
files.append(subrpm_file)
rpm_ctx.make_rpm_args.append("--subrpms=" + subrpm_file.path)
if debuginfo_type != "none":
debuginfo_default_file = ctx.actions.declare_file(
"{}-debuginfo.rpm".format(rpm_name))
debuginfo_package_file_name = "%s-%s-%s-%s.%s.rpm" % (
rpm_name,
"debuginfo",
ctx.attr.version,
ctx.attr.release,
ctx.attr.architecture,
)
_, debuginfo_output_file, _ = setup_output_files(
ctx,
debuginfo_package_file_name,
default_output_file = debuginfo_default_file,
)
rpm_ctx.output_rpm_files.append(debuginfo_output_file)
rpm_ctx.make_rpm_args.append(
"--subrpm_out_file=debuginfo:%s" % debuginfo_output_file.path )
#### Procedurally-generated scripts/lists (%install, %files)
# We need to write these out regardless of whether we are using
@ -1177,6 +1218,15 @@ pkg_rpm = rule(
[PackageSubRPMInfo],
],
),
"debuginfo": attr.bool(
doc = """Enable generation of debuginfo RPMs
For supported platforms this will enable the generation of debuginfo RPMs adjacent
to the regular RPMs. Currently this is supported by Fedora 40, CentOS7 and
CentOS Stream 9.
""",
default = False,
),
"rpmbuild_path": attr.string(
doc = """Path to a `rpmbuild` binary. Deprecated in favor of the rpmbuild toolchain""",
),

View File

@ -531,7 +531,7 @@ genrule(
grep -v 'Build Date' | grep -v 'Build Host' | grep -v 'Relocations' >> $@
echo "===== sub RPM ======" >> $@
rpm -qpi --list $${RPMS[1]} | \
grep -v 'Build Date' | grep -v 'Build Host' | grep -v 'Relocations'>> $@
grep -v 'Build Date' | grep -v 'Build Host' | grep -v 'Relocations' >> $@
""",
)
@ -541,6 +541,59 @@ diff_test(
file2 = "test_sub_rpm_contents.txt.golden",
)
############################################################################
# debuginfo tests
############################################################################
cc_binary(
name = "test_debuginfo",
copts = ["-g"],
srcs = [
"test.c",
],
)
pkg_files(
name = "test_debuginfo_rpm_files",
srcs = [
":test_debuginfo",
],
)
pkg_rpm(
name = "test_debuginfo_rpm",
srcs = [
":test_debuginfo_rpm_files",
],
release = "0",
version = "1",
license = "Some license",
summary = "Summary",
description = "Description",
debuginfo = True,
)
genrule(
name = "test_debuginfo_rpm_contents",
srcs = [":test_debuginfo_rpm"],
outs = [":test_debuginfo_rpm_contents.txt"],
cmd = """
# pkg_rpm emits two outputs
RPMS=($(SRCS))
echo "===== main RPM =====" > $@
rpm -qpi --list $${RPMS[0]} | \
grep -v 'Build Date' | grep -v 'Build Host' | grep -v 'Relocations' | grep -v 'Architecture' | grep -v 'Size' | grep -v '.build-id' >> $@
echo "===== sub RPM ======" >> $@
rpm -qpi --list $${RPMS[1]} | \
grep -v 'Build Date' | grep -v 'Build Host' | grep -v 'Relocations' | grep -v 'Architecture' | grep -v 'Size' | grep -v '.build-id' >> $@
""",
)
diff_test(
name = "test_golden_debuginfo_rpm_contents",
file1 = ":test_debuginfo_rpm_contents",
file2 = "test_debuginfo_rpm_contents.txt.golden",
)
############################################################################
# Common tests
############################################################################

3
tests/rpm/test.c Normal file
View File

@ -0,0 +1,3 @@
int main() {
return 0;
}

View File

@ -0,0 +1,29 @@
===== main RPM =====
Name : test_debuginfo_rpm
Version : 1
Release : 0
Install Date: (not installed)
Group : Unspecified
License : Some license
Signature : (none)
Source RPM : test_debuginfo_rpm-1-0.src.rpm
Summary : Summary
Description :
Description
/test_debuginfo
===== sub RPM ======
Name : test_debuginfo_rpm-debuginfo
Version : 1
Release : 0
Install Date: (not installed)
Group : Development/Debug
License : Some license
Signature : (none)
Source RPM : test_debuginfo_rpm-1-0.src.rpm
Summary : Debug information for package test_debuginfo_rpm
Description :
This package provides debug information for package test_debuginfo_rpm.
Debug information is useful when developing applications that use this
package or when debugging this package.
/usr/lib/debug
/usr/lib/debug/test_debuginfo.debug

View File

@ -5,6 +5,7 @@ rpmbuild_toolchain(
name = "rpmbuild_auto",
path = "{RPMBUILD_PATH}",
version = "{RPMBUILD_VERSION}",
debuginfo_type = "{RPMBUILD_DEBUGINFO_TYPE}",
)
toolchain(

View File

@ -21,6 +21,7 @@ RpmbuildInfo = provider(
"label": "The path to a target I will build",
"path": "The path to a pre-built rpmbuild",
"version": "The version string of rpmbuild",
"debuginfo_type": "The variant of the underlying debuginfo config",
},
)
@ -35,6 +36,7 @@ def _rpmbuild_toolchain_impl(ctx):
label = ctx.attr.label,
path = ctx.attr.path,
version = ctx.attr.version,
debuginfo_type = ctx.attr.debuginfo_type,
),
)
return [toolchain_info]
@ -54,6 +56,14 @@ rpmbuild_toolchain = rule(
"version": attr.string(
doc = "The version string of the rpmbuild executable. This should be manually set.",
),
"debuginfo_type": attr.string(
doc = """
The underlying debuginfo configuration for the system rpmbuild.
One of centos7, fedora40, or none
""",
default = "none",
),
},
)

View File

@ -17,8 +17,9 @@
# MODULE.bazel files. It seems like we should have a better interface that
# allows for this module name to be specified from a single point.
NAME = "rules_pkg_rpmbuild"
RELEASE_PATH = "/etc/os-release"
def _write_build(rctx, path, version):
def _write_build(rctx, path, version, debuginfo_type):
if not path:
path = ""
rctx.template(
@ -28,17 +29,58 @@ def _write_build(rctx, path, version):
"{GENERATOR}": "@rules_pkg//toolchains/rpm/rpmbuild_configure.bzl%find_system_rpmbuild",
"{RPMBUILD_PATH}": str(path),
"{RPMBUILD_VERSION}": version,
"{RPMBUILD_DEBUGINFO_TYPE}": debuginfo_type,
},
executable = False,
)
def _strip_quote(s):
if s.startswith("\"") and s.endswith("\"") and len(s) > 1:
return s[1:-1]
return s
def _parse_release_info(release_info):
os_name = "unknown"
os_version = "unknown"
for line in release_info.splitlines():
if "=" not in line:
continue
key, value = line.split("=")
if key == "ID":
os_name = _strip_quote(value)
if key == "VERSION_ID":
os_version = _strip_quote(value)
return os_name, os_version
def _build_repo_for_rpmbuild_toolchain_impl(rctx):
debuginfo_type = "none"
if rctx.path(RELEASE_PATH).exists:
os_name, os_version = _parse_release_info(rctx.read(RELEASE_PATH))
if os_name == "centos":
if os_version == "7":
debuginfo_type = "centos7"
elif os_version == "9":
debuginfo_type = "centos9"
if os_name == "fedora" and os_version == "40":
debuginfo_type = "fedora40"
rpmbuild_path = rctx.which("rpmbuild")
if rctx.attr.verbose:
if rpmbuild_path:
print("Found rpmbuild at '%s'" % rpmbuild_path) # buildifier: disable=print
else:
print("No system rpmbuild found.") # buildifier: disable=print
if rctx.attr.debuginfo_type not in ["centos7", "fedora40", "none"]:
fail("debuginfo_type must be one of centos7, fedora40, or none")
version = "unknown"
if rpmbuild_path:
res = rctx.execute([rpmbuild_path, "--version"])
@ -47,7 +89,13 @@ def _build_repo_for_rpmbuild_toolchain_impl(rctx):
parts = res.stdout.strip().split(" ")
if parts[0] == "RPM" and parts[1] == "version":
version = parts[2]
_write_build(rctx = rctx, path = rpmbuild_path, version = version)
_write_build(
rctx = rctx,
path = rpmbuild_path,
version = version,
debuginfo_type = debuginfo_type,
)
build_repo_for_rpmbuild_toolchain = repository_rule(
implementation = _build_repo_for_rpmbuild_toolchain_impl,
@ -58,6 +106,14 @@ build_repo_for_rpmbuild_toolchain = repository_rule(
"verbose": attr.bool(
doc = "If true, print status messages.",
),
"debuginfo_type": attr.string(
doc = """
The underlying debuginfo configuration for the system rpmbuild.
One of centos7, fedora40, or none
""",
default = "none",
),
},
)