blob: 7b44ed59d4c038cb619660798db80eeb154d72be [file] [log] [blame]
# Copyright 2017 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.
"""A simple cross-platform helper to create an RPM package."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import argparse
import contextlib
import fileinput
import os
import pprint
import re
import shutil
import subprocess
import sys
import tempfile
from string import Template
from pkg.private import helpers
# Setup to safely create a temporary directory and clean it up when done.
@contextlib.contextmanager
def Cd(newdir, cleanup=lambda: True):
"""Change the current working directory.
This will run the provided cleanup function when the context exits and the
previous working directory is restored.
Args:
newdir: The directory to change to. This must already exist.
cleanup: An optional cleanup function to be executed when the context exits.
Yields:
Nothing.
"""
prevdir = os.getcwd()
os.chdir(os.path.expanduser(newdir))
try:
yield
finally:
os.chdir(prevdir)
cleanup()
@contextlib.contextmanager
def Tempdir():
"""Create a new temporary directory and change to it.
The temporary directory will be removed when the context exits.
Yields:
The full path of the temporary directory.
"""
dirpath = tempfile.mkdtemp()
def Cleanup():
shutil.rmtree(dirpath)
with Cd(dirpath, Cleanup):
yield dirpath
WROTE_FILE_RE = re.compile(r'Wrote: (?P<rpm_path>.+)', re.MULTILINE)
def FindOutputFile(log):
"""Find the written file from the log information."""
m = WROTE_FILE_RE.findall(log)
if m:
return m
return None
def SlurpFile(input_path):
with open(input_path, 'r') as input:
return input.read()
def CopyAndRewrite(input_file, output_file, replacements=None, template_replacements=None):
"""Copies the given file and optionally rewrites with replacements.
Args:
input_file: The file to copy.
output_file: The file to write to.
replacements: A dictionary of replacements.
Keys are prefixes scan for, values are the replacements to write after
the prefix.
template_replacements: A dictionary of in-place replacements.
Keys are variable names, values are replacements. Used with
string.Template.
"""
with open(output_file, 'w') as output:
for line in fileinput.input(input_file):
if replacements:
for prefix, text in replacements.items():
if line.startswith(prefix):
line = prefix + ' ' + text + '\n'
break
if template_replacements:
template = Template(line)
line = template.safe_substitute(template_replacements)
output.write(line)
def IsExe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
def Which(program):
"""Search for the given program in the PATH.
Args:
program: The program to search for.
Returns:
The full path to the program.
"""
for path in os.environ['PATH'].split(os.pathsep):
filename = os.path.join(path, program)
if IsExe(filename):
return filename
return None
class NoRpmbuildFoundError(Exception):
pass
class InvalidRpmbuildError(Exception):
pass
def FindRpmbuild(rpmbuild_path):
"""Finds absolute path to rpmbuild.
Args:
rpmbuild_path: path to the rpmbuild_binary. If None, find 'rpmbuild' by
walking $PATH.
"""
if rpmbuild_path:
if not rpmbuild_path.startswith(os.path.sep):
return os.path.join(os.getcwd(), rpmbuild_path)
return rpmbuild_path
path = Which('rpmbuild')
if path:
return path
raise NoRpmbuildFoundError()
class RpmBuilder(object):
"""A helper class to manage building the RPM file."""
SOURCE_DIR = 'SOURCES'
BUILD_DIR = 'BUILD'
BUILD_SUBDIR = 'BUILD_SUB'
BUILDROOT_DIR = 'BUILDROOT'
TEMP_DIR = 'TMP'
RPMS_DIR = 'RPMS'
DIRS = [SOURCE_DIR, BUILD_DIR, RPMS_DIR, TEMP_DIR]
# `debuginfo` RPM types as defined in `toolchains/rpm/rpmbuild_configure.bzl`
DEBUGINFO_TYPE_NONE = "none"
DEBUGINFO_TYPE_CENTOS = "centos"
DEBUGINFO_TYPE_FEDORA = "fedora"
SUPPORTED_DEBUGINFO_TYPES = {
DEBUGINFO_TYPE_CENTOS,
DEBUGINFO_TYPE_FEDORA,
}
def __init__(self, name, version, release, arch, rpmbuild_path,
source_date_epoch=None,
debug=False):
self.name = name
self.version = helpers.GetFlagValue(version)
self.release = helpers.GetFlagValue(release)
self.arch = arch
self.files = []
self.rpmbuild_path = FindRpmbuild(rpmbuild_path)
self.rpm_paths = None
self.source_date_epoch = helpers.GetFlagValue(source_date_epoch)
self.debug = debug
# The below are initialized in SetupWorkdir()
self.spec_file = None
self.preamble_file = None
self.description_file = None
self.install_script_file = None
self.file_list_path = None
self.changelog = None
self.pre_scriptlet = None
self.post_scriptlet = None
self.preun_scriptlet = None
self.postun_scriptlet = None
self.subrpms = None
def AddFiles(self, paths, root=''):
"""Add a set of files to the current RPM.
If an item in paths is a directory, its files are recursively added.
Args:
paths: The files to add.
root: The root of the filesystem to search for files. Defaults to ''.
"""
for path in paths:
full_path = os.path.join(root, path)
if os.path.isdir(full_path):
self.AddFiles(os.listdir(full_path), full_path)
else:
self.files.append(full_path)
def SetupWorkdir(self,
spec_file,
original_dir,
preamble_file=None,
description_file=None,
install_script_file=None,
subrpms_file=None,
pre_scriptlet_path=None,
post_scriptlet_path=None,
preun_scriptlet_path=None,
postun_scriptlet_path=None,
posttrans_scriptlet_path=None,
changelog_file=None,
file_list_path=None):
"""Create the needed structure in the workdir."""
# Create the rpmbuild-expected directory structure.
for name in RpmBuilder.DIRS:
if not os.path.exists(name):
os.makedirs(name, 0o777)
# Copy the to-be-packaged files into the BUILD directory
for f in self.files:
dst_dir = os.path.join(RpmBuilder.BUILD_DIR, os.path.dirname(f))
if not os.path.exists(dst_dir):
os.makedirs(dst_dir, 0o777)
shutil.copy(os.path.join(original_dir, f), dst_dir)
# The code below is related to assembling the RPM spec template and
# everything else it needs to produce a valid RPM package.
#
# There two different types of substitution going on here: textual, directly
# into the spec file, and macro; done when we call rpmbuild(8).
#
# Plans to clean this up are tracked in #209.
# Slurp in the scriptlets...
self.pre_scriptlet = \
SlurpFile(os.path.join(original_dir, pre_scriptlet_path)) if pre_scriptlet_path else ''
self.post_scriptlet = \
SlurpFile(os.path.join(original_dir, post_scriptlet_path)) if post_scriptlet_path else ''
self.preun_scriptlet = \
SlurpFile(os.path.join(original_dir, preun_scriptlet_path)) if preun_scriptlet_path else ''
self.postun_scriptlet = \
SlurpFile(os.path.join(original_dir, postun_scriptlet_path)) if postun_scriptlet_path else ''
self.posttrans_scriptlet = \
SlurpFile(os.path.join(original_dir, posttrans_scriptlet_path)) if posttrans_scriptlet_path else ''
self.subrpms = \
SlurpFile(os.path.join(original_dir, subrpms_file)) if subrpms_file else ''
# Then prepare for textual substitution. This is typically only the case for the
# experimental `pkg_rpm`.
tpl_replacements = {
'PRE_SCRIPTLET': ("%pre\n" + self.pre_scriptlet) if self.pre_scriptlet else "",
'POST_SCRIPTLET': ("%post\n" + self.post_scriptlet) if self.post_scriptlet else "",
'PREUN_SCRIPTLET': ("%preun\n" + self.preun_scriptlet) if self.preun_scriptlet else "",
'POSTUN_SCRIPTLET': ("%postun\n" + self.postun_scriptlet) if self.postun_scriptlet else "",
'POSTTRANS_SCRIPTLET': ("%posttrans\n" + self.posttrans_scriptlet) if self.posttrans_scriptlet else "",
'SUBRPMS' : self.subrpms,
'CHANGELOG': ""
}
if changelog_file:
self.changelog = SlurpFile(os.path.join(original_dir, changelog_file))
tpl_replacements["CHANGELOG"] = "%changelog\n" + self.changelog
# If the spec file has "Version" and "Release" tags specified in the spec
# file's preamble, the values are filled in immediately afterward. These go
# into "replacements". This is typically only the case for the "original"
# `pkg_rpm`.
#
# The "tpl_replacements" are used for direct text substitution of scriptlets
# into the spec file, typically only for the "experimental" `pkg_rpm`.
spec_origin = os.path.join(original_dir, spec_file)
self.spec_file = os.path.basename(spec_file)
replacements = {}
if self.version:
replacements['Version:'] = self.version
if self.release:
replacements['Release:'] = self.release
CopyAndRewrite(spec_origin, self.spec_file,
replacements=replacements,
template_replacements=tpl_replacements)
# "Preamble" template substitutions. Currently only support values for the
# "Version" and "Release" tags.
#
# This is only the case for `pkg_rpm` in experimental/rpm.bzl.
#
# This is substituted by rpmbuild(8) via macro expansion.
if preamble_file:
# Copy in the various other files needed to build the RPM
self.preamble_file = os.path.basename(preamble_file)
tpl_replacements = {}
if self.version:
tpl_replacements['VERSION_FROM_FILE'] = self.version
if self.release:
tpl_replacements['RELEASE_FROM_FILE'] = self.release
CopyAndRewrite(os.path.join(original_dir, preamble_file),
self.preamble_file,
template_replacements=tpl_replacements)
# The below are all copied into place within the RPM spec root. It may be
# possible to directly some, if not all, of these out of the Bazel build
# root instead. "file_list_path" may be the problematic one here,
# as it must be there.
#
# These are substituted by rpmbuild(8) via macro expansion.
# Used in %description
if description_file:
shutil.copy(os.path.join(original_dir, description_file), os.getcwd())
self.description_file = os.path.basename(description_file)
# Used in %install
if install_script_file:
shutil.copy(os.path.join(original_dir, install_script_file), os.getcwd())
self.install_script_file = os.path.basename(install_script_file)
# Used in %files -f
if file_list_path:
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, debuginfo_type):
"""Call rpmbuild with the correct arguments."""
buildroot = os.path.join(dirname, RpmBuilder.BUILDROOT_DIR)
# For reference, E121 is a hanging indent flake8 issue. It really wants
# four space indents, but properly fixing that will require re-indenting the
# entire file.
# Further, the use of disabling yapf and friends is to allow argument names
# to be associated with their values neatly.
args = [
self.rpmbuild_path, # noqa: E121
]
if self.debug:
args.append('-vv')
if debuginfo_type == RpmBuilder.DEBUGINFO_TYPE_FEDORA:
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,
]
if debuginfo_type in RpmBuilder.SUPPORTED_DEBUGINFO_TYPES:
args += ['--undefine', '_debugsource_packages']
if debuginfo_type == RpmBuilder.DEBUGINFO_TYPE_CENTOS:
args += ['--define', 'buildsubdir .']
if debuginfo_type == RpmBuilder.DEBUGINFO_TYPE_FEDORA:
args += ['--define', f'buildsubdir {RpmBuilder.BUILD_SUBDIR}']
args += [
'--bb',
'--buildroot=%s' % buildroot,
] # yapf: disable
# Macro-based RPM parameter substitution, if necessary inputs provided.
if self.preamble_file:
args += ['--define', 'build_rpm_options %s' % self.preamble_file]
if self.description_file:
args += ['--define', 'build_rpm_description %s' % self.description_file]
if self.install_script_file:
args += ['--define', 'build_rpm_install %s' % self.install_script_file]
if self.file_list_path:
# %files -f is taken relative to the package root
base_path = os.path.basename(self.file_list_path)
if debuginfo_type == RpmBuilder.DEBUGINFO_TYPE_FEDORA:
base_path = os.path.join("..", base_path)
args += ['--define', 'build_rpm_files %s' % base_path]
args.extend(rpmbuild_args)
args.append(self.spec_file)
env = {
'LANG': 'C',
'RPM_BUILD_ROOT': buildroot,
}
if self.source_date_epoch is not None:
env['SOURCE_DATE_EPOCH'] = self.source_date_epoch
args += ["--define", "clamp_mtime_to_source_date_epoch 1"]
args += ["--define", "use_source_date_epoch_as_buildtime 1"]
if self.debug:
print('Running rpmbuild as:', ' '.join(["'" + a + "'" for a in args]))
print('With environment:')
pprint.pprint(env)
p = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env)
output = p.communicate()[0].decode()
if p.returncode == 0:
# Find the created file.
self.rpm_paths = FindOutputFile(output)
if p.returncode != 0 or not self.rpm_paths:
print('Error calling rpmbuild:')
print(output)
elif self.debug:
print(output)
# Return the status.
return p.returncode
def SaveResult(self, out_file, subrpm_out_files):
"""Save the result RPM out of the temporary working directory."""
if self.rpm_paths:
subrpms_seen = set()
sorted_subrpm_out_files = sorted(
subrpm_out_files, key=lambda n: len(n[1]), reverse=True)
for p in self.rpm_paths:
is_subrpm = False
for subrpm_name, subrpm_out_file in sorted_subrpm_out_files:
if subrpm_name in subrpms_seen:
continue
subrpm_prefix = self.name + '-' + subrpm_name
if os.path.basename(p).startswith(subrpm_prefix):
subrpms_seen.add(subrpm_name)
shutil.copy(p, subrpm_out_file)
is_subrpm = True
if self.debug:
print('Saved %s sub RPM file to %s' % (
subrpm_name, subrpm_out_file))
break
if not is_subrpm:
shutil.copy(p, out_file)
if self.debug:
print('Saved RPM file to %s' % out_file)
else:
print('No RPM file created.')
def Build(self, spec_file, out_file, subrpm_out_files=None,
preamble_file=None,
description_file=None,
install_script_file=None,
subrpms_file=None,
pre_scriptlet_path=None,
post_scriptlet_path=None,
preun_scriptlet_path=None,
postun_scriptlet_path=None,
posttrans_scriptlet_path=None,
file_list_path=None,
changelog_file=None,
rpmbuild_args=None,
debuginfo_type=None):
"""Build the RPM described by the spec_file, with other metadata in keyword arguments"""
if self.debug:
print('Building RPM for %s at %s' % (self.name, out_file))
original_dir = os.getcwd()
spec_file = os.path.join(original_dir, spec_file)
out_file = os.path.join(original_dir, out_file)
if subrpm_out_files:
subrpm_out_files = (s.split(':') for s in subrpm_out_files)
subrpm_out_files = [
(s[0], os.path.join(original_dir, s[1])) for s in subrpm_out_files]
else:
subrpm_out_files = []
with Tempdir() as dirname:
self.SetupWorkdir(spec_file,
original_dir,
preamble_file=preamble_file,
description_file=description_file,
install_script_file=install_script_file,
subrpms_file=subrpms_file,
file_list_path=file_list_path,
pre_scriptlet_path=pre_scriptlet_path,
post_scriptlet_path=post_scriptlet_path,
preun_scriptlet_path=preun_scriptlet_path,
postun_scriptlet_path=postun_scriptlet_path,
posttrans_scriptlet_path=posttrans_scriptlet_path,
changelog_file=changelog_file)
status = self.CallRpmBuild(dirname, rpmbuild_args or [], debuginfo_type)
self.SaveResult(out_file, subrpm_out_files)
return status
def main(argv):
parser = argparse.ArgumentParser(
description='Helper for building rpm packages',
fromfile_prefix_chars='@')
parser.add_argument('--name',
help='The name of the software being packaged.')
parser.add_argument('--version',
help='The version of the software being packaged.')
parser.add_argument('--release',
help='The release of the software being packaged.')
parser.add_argument(
'--arch',
help='The CPU architecture of the software being packaged.')
parser.add_argument('--spec_file', required=True,
help='The file containing the RPM specification.')
parser.add_argument('--out_file', required=True,
help='The destination to save the resulting RPM file to.')
parser.add_argument('--subrpm_out_file', action='append',
help='List of destinations to save resulting ' +
'Sub RPMs to in the form of name:destination')
parser.add_argument('--rpmbuild', help='Path to rpmbuild executable.')
parser.add_argument('--source_date_epoch',
help='Value for the SOURCE_DATE_EPOCH rpmbuild '
'environment variable')
parser.add_argument('--debug', action='store_true', default=False,
help='Print debug messages.')
# Options currently used experimental/rpm.bzl:
parser.add_argument('--install_script',
help='Installer script')
parser.add_argument('--file_list',
help='File containing a list of files to include with rpm spec %%files -f')
parser.add_argument('--preamble',
help='File containing the RPM Preamble')
parser.add_argument('--description',
help='File containing the RPM %%description text')
parser.add_argument('--subrpms',
help='File containing the RPM subrpm details')
parser.add_argument('--pre_scriptlet',
help='File containing the RPM %%pre scriptlet, if to be substituted')
parser.add_argument('--post_scriptlet',
help='File containing the RPM %%post scriptlet, if to be substituted')
parser.add_argument('--preun_scriptlet',
help='File containing the RPM %%preun scriptlet, if to be substituted')
parser.add_argument('--postun_scriptlet',
help='File containing the RPM %%postun scriptlet, if to be substituted')
parser.add_argument('--posttrans_scriptlet',
help='File containing the RPM %%posttrans scriptlet, if to be substituted')
parser.add_argument('--changelog',
help='File containing the RPM changelog text')
parser.add_argument('--rpmbuild_arg', dest='rpmbuild_args', action='append',
help='Any additional arguments to pass to rpmbuild')
parser.add_argument('--debuginfo_type', default=RpmBuilder.DEBUGINFO_TYPE_NONE,
choices=sorted(RpmBuilder.SUPPORTED_DEBUGINFO_TYPES) + [RpmBuilder.DEBUGINFO_TYPE_NONE],
help='debuginfo type to use')
parser.add_argument('files', nargs='*')
options = parser.parse_args(argv or ())
try:
builder = RpmBuilder(options.name,
options.version, options.release,
options.arch, options.rpmbuild,
source_date_epoch=options.source_date_epoch,
debug=options.debug)
builder.AddFiles(options.files)
return builder.Build(options.spec_file, options.out_file,
options.subrpm_out_file,
preamble_file=options.preamble,
description_file=options.description,
install_script_file=options.install_script,
subrpms_file=options.subrpms,
file_list_path=options.file_list,
pre_scriptlet_path=options.pre_scriptlet,
post_scriptlet_path=options.post_scriptlet,
preun_scriptlet_path=options.preun_scriptlet,
postun_scriptlet_path=options.postun_scriptlet,
posttrans_scriptlet_path=options.posttrans_scriptlet,
changelog_file=options.changelog,
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
if __name__ == '__main__':
main(sys.argv[1:])
# vim: ts=2:sw=2: