| # 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 contextlib |
| import fileinput |
| import os |
| import re |
| import shutil |
| import subprocess |
| import sys |
| from tempfile import mkdtemp |
| |
| # pylint: disable=g-direct-third-party-import |
| from third_party.py import gflags |
| |
| gflags.DEFINE_string('name', '', 'The name of the software being packaged.') |
| gflags.DEFINE_string('version', '', |
| 'The version of the software being packaged.') |
| gflags.DEFINE_string('release', '', |
| 'The release of the software being packaged.') |
| gflags.DEFINE_string('arch', '', |
| 'The CPU architecture of the software being packaged.') |
| |
| gflags.DEFINE_string('spec_file', '', |
| 'The file containing the RPM specification.') |
| gflags.DEFINE_string('out_file', '', |
| 'The destination to save the resulting RPM file to.') |
| gflags.DEFINE_boolean('debug', False, 'Print debug messages.') |
| |
| |
| # 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 = mkdtemp() |
| |
| def Cleanup(): |
| shutil.rmtree(dirpath) |
| |
| with Cd(dirpath, Cleanup): |
| yield dirpath |
| |
| |
| def GetFlagValue(flagvalue, strip=True): |
| if flagvalue: |
| if flagvalue[0] == '@': |
| with open(flagvalue[1:], 'r') as f: |
| flagvalue = f.read() |
| if strip: |
| return flagvalue.strip() |
| return flagvalue |
| |
| |
| 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.search(log) |
| if m: |
| return m.group('rpm_path') |
| return None |
| |
| |
| def CopyAndRewrite(input_file, output_file, 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. |
| """ |
| 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 |
| output.write(line) |
| |
| |
| 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. |
| """ |
| |
| def IsExe(fpath): |
| return os.path.isfile(fpath) and os.access(fpath, os.X_OK) |
| |
| for path in os.environ['PATH'].split(os.pathsep): |
| filename = os.path.join(path, program) |
| if IsExe(filename): |
| return filename |
| |
| return None |
| |
| |
| class NoRpmbuildFound(Exception): |
| pass |
| |
| |
| def FindRpmbuild(): |
| path = Which('rpmbuild') |
| if path: |
| return path |
| else: |
| raise NoRpmbuildFound() |
| |
| |
| class RpmBuilder(object): |
| """A helper class to manage building the RPM file.""" |
| |
| SOURCE_DIR = 'SOURCES' |
| BUILD_DIR = 'BUILD' |
| TEMP_DIR = 'TMP' |
| DIRS = [SOURCE_DIR, BUILD_DIR, TEMP_DIR] |
| |
| def __init__(self, name, version, release, arch, debug): |
| self.name = name |
| self.version = GetFlagValue(version) |
| self.release = GetFlagValue(release) |
| self.arch = arch |
| self.debug = debug |
| self.files = [] |
| self.rpmbuild_path = FindRpmbuild() |
| self.rpm_path = 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): |
| """Create the needed structure in the workdir.""" |
| |
| # Create directory structure. |
| for name in RpmBuilder.DIRS: |
| if not os.path.exists(name): |
| os.makedirs(name, 0o777) |
| |
| # Copy the files. |
| 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) |
| |
| # Copy the spec file, updating with the correct version. |
| 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) |
| |
| def CallRpmBuild(self, dirname): |
| """Call rpmbuild with the correct arguments.""" |
| |
| args = [ |
| self.rpmbuild_path, |
| '--define', |
| '_topdir %s' % dirname, |
| '--define', |
| '_tmppath %s/TMP' % dirname, |
| '--bb', |
| '--buildroot', |
| os.path.join(dirname, 'BUILDROOT'), |
| self.spec_file, |
| ] |
| p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| output = p.communicate()[0] |
| |
| if p.returncode == 0: |
| # Find the created file. |
| self.rpm_path = FindOutputFile(output) |
| |
| if p.returncode != 0 or not self.rpm_path: |
| print('Error calling rpmbuild:') |
| print(output) |
| |
| # Return the status. |
| return p.returncode |
| |
| def SaveResult(self, out_file): |
| """Save the result RPM out of the temporary working directory.""" |
| |
| if self.rpm_path: |
| shutil.copy(self.rpm_path, 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): |
| """Build the RPM described by the spec_file.""" |
| 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) |
| with Tempdir() as dirname: |
| self.SetupWorkdir(spec_file, original_dir) |
| status = self.CallRpmBuild(dirname) |
| self.SaveResult(out_file) |
| |
| return status |
| |
| |
| def main(argv=()): |
| try: |
| builder = RpmBuilder(FLAGS.name, FLAGS.version, FLAGS.release, FLAGS.arch, |
| FLAGS.debug) |
| builder.AddFiles(argv[1:]) |
| return builder.Build(FLAGS.spec_file, FLAGS.out_file) |
| except NoRpmbuildFound: |
| print('ERROR: rpmbuild is required but is not present in PATH') |
| return 1 |
| |
| |
| if __name__ == '__main__': |
| FLAGS = gflags.FLAGS |
| main(FLAGS(sys.argv)) |