diff options
author | John Cater <jcater@google.com> | 2017-02-03 16:31:32 +0000 |
---|---|---|
committer | Kristina Chodorow <kchodorow@google.com> | 2017-02-06 19:35:37 +0000 |
commit | e77ad90fb6289a39d644ca4699976285922ff49d (patch) | |
tree | 64cd48d423837f679fd4234e2093bd17854da640 | |
parent | cc48e5bd10d1f518295f16a4f661f3a1eadcd6fd (diff) |
Add pkg_rpm build rule to help generating RPM packages.
RELNOTES: Adds pkg_rpm rule for generating RPM packages.
--
Change-Id: I2e83161e29218700bbe7e62186b0b1667e555a7c
Reviewed-on: https://cr.bazel.build/8590
PiperOrigin-RevId: 146477490
MOS_MIGRATED_REVID=146477490
-rw-r--r-- | scripts/packages/fedora/BUILD | 30 | ||||
-rw-r--r-- | scripts/packages/fedora/bazel.spec | 6 | ||||
-rwxr-xr-x | scripts/packages/fedora/build_rpm.sh | 56 | ||||
-rw-r--r-- | tools/build_defs/pkg/BUILD | 18 | ||||
-rw-r--r-- | tools/build_defs/pkg/README.md | 70 | ||||
-rw-r--r-- | tools/build_defs/pkg/make_rpm.py | 228 | ||||
-rw-r--r-- | tools/build_defs/pkg/make_rpm_test.py | 105 | ||||
-rw-r--r-- | tools/build_defs/pkg/rpm.bzl | 109 |
8 files changed, 545 insertions, 77 deletions
diff --git a/scripts/packages/fedora/BUILD b/scripts/packages/fedora/BUILD index 36d8690ebf..2ee91d01ab 100644 --- a/scripts/packages/fedora/BUILD +++ b/scripts/packages/fedora/BUILD @@ -5,26 +5,18 @@ filegroup( srcs = glob(["**"]), ) -genrule( - name = "bazel-fedora", - srcs = [ - "bazel.spec", +load("//tools/build_defs/pkg:rpm.bzl", "pkg_rpm") + +pkg_rpm( + name = "bazel", + architecture = "x86_64", + changelog = "//:changelog-file", + data = [ + "//scripts:bash_completion", "//scripts/packages:bazel", + "//scripts/packages:bazel.bazelrc", "//scripts/packages:bazel-real", - "//scripts/packages:debian/bazel.bazelrc", - "//scripts:bash_completion", - ], - outs = [ - "bazel-fedora.rpm", ], - cmd = """ - $(location :build_rpm.sh) \ - $(location :bazel.spec) \ - $(location bazel-fedora.rpm) \ - $(location //scripts/packages:bazel) \ - $(location //scripts/packages:bazel-real) \ - $(location //scripts/packages:debian/bazel.bazelrc) \ - $(location //scripts:bash_completion) - """, - tools = ["build_rpm.sh"], + spec_file = "bazel.spec", + version_file = "//scripts/packages:version.txt", ) diff --git a/scripts/packages/fedora/bazel.spec b/scripts/packages/fedora/bazel.spec index 0d69e61e5d..14cabfc5c1 100644 --- a/scripts/packages/fedora/bazel.spec +++ b/scripts/packages/fedora/bazel.spec @@ -30,7 +30,7 @@ install -m 755 bazel %{buildroot}%{_bindir}/bazel install -m 755 bazel-real %{buildroot}%{_bindir}/bazel-real mkdir -p %{buildroot}%{_sysconfdir}/ install -m 644 bazel.bazelrc %{buildroot}%{_sysconfdir}/bazel.bazelrc -mkdir -p %{buildroot}%{_sysconfdir}/bash_completion.d +mkdir -p %{buildroot}%{_sysconfdir}/bash_completion.d/ install -m 644 bazel-complete.bash %{buildroot}%{_sysconfdir}/bash_completion.d/bazel %files @@ -40,4 +40,6 @@ install -m 644 bazel-complete.bash %{buildroot}%{_sysconfdir}/bash_completion.d/ %{_sysconfdir}/bash_completion.d/bazel %changelog -# TODO: Include changelog. +* Fri Jan 27 2017 jcater@google.com - devel-1 +- Initial package version released. + diff --git a/scripts/packages/fedora/build_rpm.sh b/scripts/packages/fedora/build_rpm.sh deleted file mode 100755 index 525c858b13..0000000000 --- a/scripts/packages/fedora/build_rpm.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/sh - -# Copyright 2016 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. - -set -eu - -# Build a RPM archive from the Bazel sources. - -# Usage: build_rpm.sh spec_file dest_file [other files] - -spec_file=$1 -shift -dest_file=$1 -shift - -echo "Building ${dest_file} from ${spec_file}." -WORK_DIR="${PWD}/bazel-fedora" - -# Copy needed sources. -rm -rf ${WORK_DIR} -mkdir -p ${WORK_DIR}/SOURCES -mkdir -p ${WORK_DIR}/BUILD -cp $spec_file ${WORK_DIR} -for i in "$@"; do - cp $i $WORK_DIR/BUILD -done - -# Build the RPM. -rpmbuild \ - --define "_topdir ${WORK_DIR}" \ - --define "_tmppath /tmp" \ - -bb ${spec_file} > rpmbuild.log 2>&1 -if [ $? -ne 0 ]; then - err=$? - echo "Error in rpmbuild:" - cat rpmbuild.log - exit $err -fi -out_file=$(grep '^Wrote:' rpmbuild.log | cut -d ' ' -f 2) - -# Copy output back to the destination. -cp $out_file $dest_file -echo "Created $dest_file" - diff --git a/tools/build_defs/pkg/BUILD b/tools/build_defs/pkg/BUILD index 1bc4786034..266a2050ba 100644 --- a/tools/build_defs/pkg/BUILD +++ b/tools/build_defs/pkg/BUILD @@ -66,6 +66,24 @@ py_binary( ], ) +# Used by pkg_rpm in rpm.bzl. +py_binary( + name = "make_rpm", + srcs = ["make_rpm.py"], + visibility = ["//visibility:public"], + deps = [ + "@bazel_tools//third_party/py/gflags", + ], +) + +py_test( + name = "make_rpm_test", + srcs = ["make_rpm_test.py"], + deps = [ + ":make_rpm", + ], +) + # tests load("//tools/build_defs/pkg:pkg.bzl", "pkg_deb", "pkg_tar") diff --git a/tools/build_defs/pkg/README.md b/tools/build_defs/pkg/README.md index 8fda66636f..4369777cd7 100644 --- a/tools/build_defs/pkg/README.md +++ b/tools/build_defs/pkg/README.md @@ -5,6 +5,7 @@ <ul> <li><a href="#pkg_tar">pkg_tar</a></li> <li><a href="#pkg_deb">pkg_deb</a></li> + <li><a href="#pkg_rpm">pkg_rpm</a></li> </ul> </div> @@ -386,3 +387,72 @@ for more details on this. </tbody> </tbody> </table> + +<a name="pkg_rpm"></a> +### pkg_rpm + +```python +pkg_rpm(name, spec_file, architecture, version, version_file, changelog, data) +``` + +Create an RPM package. See <a +href="http://rpm.org/documentation.html">http://rpm.org/documentation.html</a> +for more details on this. + +<table class="table table-condensed table-bordered table-params"> + <colgroup> + <col class="col-param" /> + <col class="param-description" /> + </colgroup> + <thead> + <tr> + <th colspan="2">Attributes</th> + </tr> + </thead> + <tbody> + <tr> + <td><code>name</code></td> + <td> + <code>Name, required</code> + <p>A unique name for this rule. Used to name the output package.</p> + </td> + </tr> + <tr> + <td><code>spec_file</code></td> + <td> + <code>File, required</code> + <p>The RPM specification file used to generate the package.</p> + <p> + See <a href="http://rpm.org/max-rpm-snapshot/s1-rpm-build-creating-spec-file.html">http://rpm.org/max-rpm-snapshot/s1-rpm-build-creating-spec-file.html</a>. + </p> + </td> + </tr> + <tr> + <td><code>architecture</code></td> + <td> + <code>String, default to 'all'</code> + <p>The architecture that this package target.</p> + </td> + </tr> + <tr> + <td><code>version</code>, <code>version_file</code></td> + <td> + <code>String or File, required</code> + <p> + The package version provided either inline (with <code>version</code>) + or from a file (with <code>version_file</code>). + </p> + </td> + </tr> + <tr> + <td><code>data</code></td> + <td> + <code>Files, required</code> + <p> + Files to include in the generated package. + </p> + </td> + </tr> + </tbody> + </tbody> +</table> diff --git a/tools/build_defs/pkg/make_rpm.py b/tools/build_defs/pkg/make_rpm.py new file mode 100644 index 0000000000..9f0f40a23b --- /dev/null +++ b/tools/build_defs/pkg/make_rpm.py @@ -0,0 +1,228 @@ +# 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 + +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('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.') + + +# 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.iteritems(): + if line.startswith(prefix): + line = prefix + ' ' + text + '\n' + break + output.write(line) + + +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, arch): + self.name = name + self.version = GetFlagValue(version) + self.arch = arch + self.files = [] + self.rpm_path = None + + def AddFiles(self, files): + """Add a set of files to the current RPM.""" + self.files += files + + 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, 0777) + + shutil.copy(os.path.join(original_dir, spec_file), os.getcwd()) + + # Copy the files. + for f in self.files: + shutil.copy(os.path.join(original_dir, f), RpmBuilder.BUILD_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 + CopyAndRewrite(spec_origin, self.spec_file, replacements) + + def CallRpmBuild(self, dirname): + """Call rpmbuild with the correct arguments.""" + + args = [ + 'rpmbuild', + '--define', + '_topdir %s' % dirname, + '--define', + '_tmppath %s/TMP' % dirname, + '--bb', + 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) + 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.""" + 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=()): + builder = RpmBuilder(FLAGS.name, FLAGS.version, FLAGS.arch) + builder.AddFiles(argv[1:]) + return builder.Build(FLAGS.spec_file, FLAGS.out_file) + + +if __name__ == '__main__': + FLAGS = gflags.FLAGS + main(FLAGS(sys.argv)) diff --git a/tools/build_defs/pkg/make_rpm_test.py b/tools/build_defs/pkg/make_rpm_test.py new file mode 100644 index 0000000000..f8f771038a --- /dev/null +++ b/tools/build_defs/pkg/make_rpm_test.py @@ -0,0 +1,105 @@ +# 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. +"""Tests for make_rpm.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import unittest + +from tools.build_defs.pkg import make_rpm + + +def WriteFile(filename, *contents): + with open(filename, 'w') as text_file: + text_file.write('\n'.join(contents)) + + +def DirExists(dirname): + return os.path.exists(dirname) and os.path.isdir(dirname) + + +def FileExists(filename): + return os.path.exists(filename) and not os.path.isdir(filename) + + +def FileContents(filename): + with open(filename, 'r') as text_file: + return [s.strip() for s in text_file.readlines()] + + +class MakeRpmTest(unittest.TestCase): + + def testFindOutputFile(self): + log = """ + Lots of data. + Wrote: /path/to/file/here.rpm + More data present. + """ + + result = make_rpm.FindOutputFile(log) + self.assertEquals('/path/to/file/here.rpm', result) + + def testFindOutputFile_missing(self): + log = """ + Lots of data. + More data present. + """ + + result = make_rpm.FindOutputFile(log) + self.assertEquals(None, result) + + def testCopyAndRewrite(self): + with make_rpm.Tempdir(): + WriteFile('test.txt', 'Some: data1', 'Other: data2', 'More: data3') + make_rpm.CopyAndRewrite('test.txt', 'out.txt', { + 'Some:': 'data1a', + 'More:': 'data3a', + }) + + self.assertTrue(FileExists('out.txt')) + self.assertItemsEqual(['Some: data1a', 'Other: data2', 'More: data3a'], + FileContents('out.txt')) + + def testSetupWorkdir(self): + builder = make_rpm.RpmBuilder('test', '1.0', 'x86') + with make_rpm.Tempdir() as outer: + # Create spec_file, test files. + WriteFile('test.spec', 'Name: test', 'Version: 0.1', 'Summary: test data') + WriteFile('file1.txt', 'Hello') + WriteFile('file2.txt', 'Goodbye') + builder.AddFiles(['file1.txt', 'file2.txt']) + + with make_rpm.Tempdir(): + # Call RpmBuilder. + builder.SetupWorkdir('test.spec', outer) + + # Make sure files exist. + self.assertTrue(DirExists('SOURCES')) + self.assertTrue(DirExists('BUILD')) + self.assertTrue(DirExists('TMP')) + self.assertTrue(FileExists('test.spec')) + self.assertItemsEqual( + ['Name: test', 'Version: 1.0', 'Summary: test data'], + FileContents('test.spec')) + self.assertTrue(FileExists('BUILD/file1.txt')) + self.assertItemsEqual(['Hello'], FileContents('BUILD/file1.txt')) + self.assertTrue(FileExists('BUILD/file2.txt')) + self.assertItemsEqual(['Goodbye'], FileContents('BUILD/file2.txt')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/build_defs/pkg/rpm.bzl b/tools/build_defs/pkg/rpm.bzl new file mode 100644 index 0000000000..2322dfae77 --- /dev/null +++ b/tools/build_defs/pkg/rpm.bzl @@ -0,0 +1,109 @@ +# 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. +"""Rules to create RPM archives.""" + +rpm_filetype = [".rpm"] +spec_filetype = [".spec"] + +def _pkg_rpm_impl(ctx): + """Implements to pkg_rpm rule.""" + + files = [] + args = ["--name=" + ctx.label.name] + + # Version can be specified by a file or inlined + if ctx.attr.version_file: + if ctx.attr.version: + fail("Both version and version_file attributes were specified") + args += ["--version=@" + ctx.file.version_file.path] + files += [ctx.file.version_file] + elif ctx.attr.version: + args += ["--version=" + ctx.attr.version] + else: + fail("Neither version_file nor version attribute was specified") + + if ctx.attr.architecture: + args += ["--arch=" + ctx.attr.architecture] + + if ctx.attr.spec_file: + args += ["--spec_file=" + ctx.file.spec_file.path] + files += [ctx.file.spec_file] + else: + fail("spec_file was not specified") + + args += ["--out_file=" + ctx.outputs.rpm.path] + + # Add data files. + files += [ctx.file.changelog] + ctx.files.data + args += [ctx.file.changelog.path] + for f in ctx.files.data: + args += [f.path] + + # Call the generator script. + # TODO(katre): Generate a source RPM. + ctx.action( + executable = ctx.executable._make_rpm, + arguments = args, + inputs = files, + outputs = [ctx.outputs.rpm], + mnemonic = "MakeRpm") + + # Link the RPM to the expected output name. + ctx.action( + command = "ln -s %s %s" % (ctx.outputs.rpm.basename, ctx.outputs.out.path), + inputs = [ctx.outputs.rpm], + outputs = [ctx.outputs.out]) + +# Define the rule. +pkg_rpm = rule( + implementation = _pkg_rpm_impl, + attrs = { + "spec_file" : attr.label(mandatory=True, allow_files=spec_filetype, single_file=True), + "architecture": attr.string(default="all"), + "version_file": attr.label(allow_files=True, single_file=True), + "version": attr.string(), + "changelog" : attr.label(mandatory=True, allow_files=True, single_file=True), + "data": attr.label_list(mandatory=True, allow_files=True), + + # Implicit dependencies. + "_make_rpm": attr.label( + default=Label("//tools/build_defs/pkg:make_rpm"), + cfg="host", + executable=True, + allow_files=True), + }, + outputs = { + "out": "%{name}.rpm", + "rpm": "%{name}-%{architecture}.rpm", + }, + executable = False) +"""Creates an RPM format package from the data files. + +This runs rpmbuild (and requires it to be installed beforehand) to generate +an RPM package based on the spec_file and data attributes. + +Args: + spec_file: The RPM spec file to use. If the version or version_file + attributes are provided, the Version in the spec will be overwritten. + Any Sources listed in the spec file must be provided as data dependencies. + version: The version of the package to generate. This will overwrite any + Version provided in the spec file. Only specify one of version and + version_file. + version_file: A file containing the version of the package to generate. This + will overwrite any Version provided in the spec file. Only specify one of + version and version_file. + changelog: A changelog file to include. This will not be written to the spec + file, which should only list changes to the packaging, not the software itself. + data: List all files to be included in the package here. +""" |