123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- # -*- coding: utf-8 -*-
- # Maintainer: Erik Johnson (https://github.com/terminalmage)
- #
- # WARNING: This script will recursively remove the build and artifact
- # directories.
- #
- # This script is designed for speed, therefore it does not use mock and does not
- # run tests. It *will* install the build deps on the machine running the script.
- #
- # pylint: disable=file-perms,resource-leakage
- from __future__ import absolute_import, print_function
- import errno
- import glob
- import logging
- import os
- import re
- import shutil
- import subprocess
- import sys
- from optparse import OptionGroup, OptionParser
- logging.QUIET = 0
- logging.GARBAGE = 1
- logging.TRACE = 5
- logging.addLevelName(logging.QUIET, "QUIET")
- logging.addLevelName(logging.TRACE, "TRACE")
- logging.addLevelName(logging.GARBAGE, "GARBAGE")
- LOG_LEVELS = {
- "all": logging.NOTSET,
- "debug": logging.DEBUG,
- "error": logging.ERROR,
- "critical": logging.CRITICAL,
- "garbage": logging.GARBAGE,
- "info": logging.INFO,
- "quiet": logging.QUIET,
- "trace": logging.TRACE,
- "warning": logging.WARNING,
- }
- log = logging.getLogger(__name__)
- # FUNCTIONS
- def _abort(msgs):
- """
- Unrecoverable error, pull the plug
- """
- if not isinstance(msgs, list):
- msgs = [msgs]
- for msg in msgs:
- log.error(msg)
- sys.stderr.write(msg + "\n\n")
- sys.stderr.write("Build failed. See log file for further details.\n")
- sys.exit(1)
- # HELPER FUNCTIONS
- def _init():
- """
- Parse CLI options.
- """
- parser = OptionParser()
- parser.add_option("--platform", dest="platform", help="Platform ('os' grain)")
- parser.add_option(
- "--log-level",
- dest="log_level",
- default="warning",
- help="Control verbosity of logging. Default: %default",
- )
- # All arguments dealing with file paths (except for platform-specific ones
- # like those for SPEC files) should be placed in this group so that
- # relative paths are properly expanded.
- path_group = OptionGroup(parser, "File/Directory Options")
- path_group.add_option(
- "--source-dir",
- default="/testing",
- help="Source directory. Must be a git checkout. " "(default: %default)",
- )
- path_group.add_option(
- "--build-dir",
- default="/tmp/salt-buildpackage",
- help="Build root, will be removed if it exists "
- "prior to running script. (default: %default)",
- )
- path_group.add_option(
- "--artifact-dir",
- default="/tmp/salt-packages",
- help="Location where build artifacts should be "
- "placed for Jenkins to retrieve them "
- "(default: %default)",
- )
- parser.add_option_group(path_group)
- # This group should also consist of nothing but file paths, which will be
- # normalized below.
- rpm_group = OptionGroup(parser, "RPM-specific File/Directory Options")
- rpm_group.add_option(
- "--spec",
- dest="spec_file",
- default="/tmp/salt.spec",
- help="Spec file to use as a template to build RPM. " "(default: %default)",
- )
- parser.add_option_group(rpm_group)
- opts = parser.parse_args()[0]
- # Expand any relative paths
- for group in (path_group, rpm_group):
- for path_opt in [opt.dest for opt in group.option_list]:
- path = getattr(opts, path_opt)
- if not os.path.isabs(path):
- # Expand ~ or ~user
- path = os.path.expanduser(path)
- if not os.path.isabs(path):
- # Still not absolute, resolve '..'
- path = os.path.realpath(path)
- # Update attribute with absolute path
- setattr(opts, path_opt, path)
- # Sanity checks
- problems = []
- if not opts.platform:
- problems.append("Platform ('os' grain) required")
- if not os.path.isdir(opts.source_dir):
- problems.append("Source directory {0} not found".format(opts.source_dir))
- try:
- shutil.rmtree(opts.build_dir)
- except OSError as exc:
- if exc.errno not in (errno.ENOENT, errno.ENOTDIR):
- problems.append(
- "Unable to remove pre-existing destination "
- "directory {0}: {1}".format(opts.build_dir, exc)
- )
- finally:
- try:
- os.makedirs(opts.build_dir)
- except OSError as exc:
- problems.append(
- "Unable to create destination directory {0}: {1}".format(
- opts.build_dir, exc
- )
- )
- try:
- shutil.rmtree(opts.artifact_dir)
- except OSError as exc:
- if exc.errno not in (errno.ENOENT, errno.ENOTDIR):
- problems.append(
- "Unable to remove pre-existing artifact directory "
- "{0}: {1}".format(opts.artifact_dir, exc)
- )
- finally:
- try:
- os.makedirs(opts.artifact_dir)
- except OSError as exc:
- problems.append(
- "Unable to create artifact directory {0}: {1}".format(
- opts.artifact_dir, exc
- )
- )
- # Create log file in the artifact dir so it is sent back to master if the
- # job fails
- opts.log_file = os.path.join(opts.artifact_dir, "salt-buildpackage.log")
- if problems:
- _abort(problems)
- return opts
- def _move(src, dst):
- """
- Wrapper around shutil.move()
- """
- try:
- os.remove(os.path.join(dst, os.path.basename(src)))
- except OSError as exc:
- if exc.errno != errno.ENOENT:
- _abort(exc)
- try:
- shutil.move(src, dst)
- except shutil.Error as exc:
- _abort(exc)
- def _run_command(args):
- log.info("Running command: {0}".format(args))
- proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- stdout, stderr = proc.communicate()
- if stdout:
- log.debug("Command output: \n{0}".format(stdout))
- if stderr:
- log.error(stderr)
- log.info("Return code: {0}".format(proc.returncode))
- return stdout, stderr, proc.returncode
- def _make_sdist(opts, python_bin="python"):
- os.chdir(opts.source_dir)
- stdout, stderr, rcode = _run_command([python_bin, "setup.py", "sdist"])
- if rcode == 0:
- # Find the sdist with the most recently-modified metadata
- sdist_path = max(
- glob.iglob(os.path.join(opts.source_dir, "dist", "salt-*.tar.gz")),
- key=os.path.getctime,
- )
- log.info("sdist is located at {0}".format(sdist_path))
- return sdist_path
- else:
- _abort("Failed to create sdist")
- # BUILDER FUNCTIONS
- def build_centos(opts):
- """
- Build an RPM
- """
- log.info("Building CentOS RPM")
- log.info("Detecting major release")
- try:
- with open("/etc/redhat-release", "r") as fp_:
- redhat_release = fp_.read().strip()
- major_release = int(redhat_release.split()[2].split(".")[0])
- except (ValueError, IndexError):
- _abort(
- "Unable to determine major release from /etc/redhat-release "
- "contents: '{0}'".format(redhat_release)
- )
- except IOError as exc:
- _abort("{0}".format(exc))
- log.info("major_release: {0}".format(major_release))
- define_opts = ["--define", "_topdir {0}".format(os.path.join(opts.build_dir))]
- build_reqs = ["rpm-build"]
- if major_release == 5:
- python_bin = "python26"
- define_opts.extend(["--define", "dist .el5"])
- if os.path.exists("/etc/yum.repos.d/saltstack.repo"):
- build_reqs.extend(["--enablerepo=saltstack"])
- build_reqs.extend(["python26-devel"])
- elif major_release == 6:
- build_reqs.extend(["python-devel"])
- elif major_release == 7:
- build_reqs.extend(["python-devel", "systemd-units"])
- else:
- _abort("Unsupported major release: {0}".format(major_release))
- # Install build deps
- _run_command(["yum", "-y", "install"] + build_reqs)
- # Make the sdist
- try:
- sdist = _make_sdist(opts, python_bin=python_bin)
- except NameError:
- sdist = _make_sdist(opts)
- # Example tarball names:
- # - Git checkout: salt-2014.7.0rc1-1584-g666602e.tar.gz
- # - Tagged release: salt-2014.7.0.tar.gz
- tarball_re = re.compile(r"^salt-([^-]+)(?:-(\d+)-(g[0-9a-f]+))?\.tar\.gz$")
- try:
- base, offset, oid = tarball_re.match(os.path.basename(sdist)).groups()
- except AttributeError:
- _abort("Unable to extract version info from sdist filename '{0}'".format(sdist))
- if offset is None:
- salt_pkgver = salt_srcver = base
- else:
- salt_pkgver = ".".join((base, offset, oid))
- salt_srcver = "-".join((base, offset, oid))
- log.info("salt_pkgver: {0}".format(salt_pkgver))
- log.info("salt_srcver: {0}".format(salt_srcver))
- # Setup build environment
- for build_dir in "BUILD BUILDROOT RPMS SOURCES SPECS SRPMS".split():
- path = os.path.join(opts.build_dir, build_dir)
- try:
- os.makedirs(path)
- except OSError:
- pass
- if not os.path.isdir(path):
- _abort("Unable to make directory: {0}".format(path))
- # Get sources into place
- build_sources_path = os.path.join(opts.build_dir, "SOURCES")
- rpm_sources_path = os.path.join(opts.source_dir, "pkg", "rpm")
- _move(sdist, build_sources_path)
- for src in (
- "salt-master",
- "salt-syndic",
- "salt-minion",
- "salt-api",
- "salt-master.service",
- "salt-syndic.service",
- "salt-minion.service",
- "salt-api.service",
- "README.fedora",
- "logrotate.salt",
- "salt.bash",
- ):
- shutil.copy(os.path.join(rpm_sources_path, src), build_sources_path)
- # Prepare SPEC file
- spec_path = os.path.join(opts.build_dir, "SPECS", "salt.spec")
- with open(opts.spec_file, "r") as spec:
- spec_lines = spec.read().splitlines()
- with open(spec_path, "w") as fp_:
- for line in spec_lines:
- if line.startswith("%global srcver "):
- line = "%global srcver {0}".format(salt_srcver)
- elif line.startswith("Version: "):
- line = "Version: {0}".format(salt_pkgver)
- fp_.write(line + "\n")
- # Do the thing
- cmd = ["rpmbuild", "-ba"]
- cmd.extend(define_opts)
- cmd.append(spec_path)
- stdout, stderr, rcode = _run_command(cmd)
- if rcode != 0:
- _abort("Build failed.")
- packages = glob.glob(
- os.path.join(
- opts.build_dir,
- "RPMS",
- "noarch",
- "salt-*{0}*.noarch.rpm".format(salt_pkgver),
- )
- )
- packages.extend(
- glob.glob(
- os.path.join(
- opts.build_dir, "SRPMS", "salt-{0}*.src.rpm".format(salt_pkgver)
- )
- )
- )
- return packages
- # MAIN
- if __name__ == "__main__":
- opts = _init()
- print(
- "Starting {0} build. Progress will be logged to {1}.".format(
- opts.platform, opts.log_file
- )
- )
- # Setup logging
- log_format = "%(asctime)s.%(msecs)03d %(levelname)s: %(message)s"
- log_datefmt = "%H:%M:%S"
- log_level = (
- LOG_LEVELS[opts.log_level]
- if opts.log_level in LOG_LEVELS
- else LOG_LEVELS["warning"]
- )
- logging.basicConfig(
- filename=opts.log_file,
- format=log_format,
- datefmt=log_datefmt,
- level=LOG_LEVELS[opts.log_level],
- )
- if opts.log_level not in LOG_LEVELS:
- log.error(
- "Invalid log level '{0}', falling back to 'warning'".format(opts.log_level)
- )
- # Build for the specified platform
- if not opts.platform:
- _abort("Platform required")
- elif opts.platform.lower() == "centos":
- artifacts = build_centos(opts)
- else:
- _abort("Unsupported platform '{0}'".format(opts.platform))
- msg = "Build complete. Artifacts will be stored in {0}".format(opts.artifact_dir)
- log.info(msg)
- print(msg) # pylint: disable=C0325
- for artifact in artifacts:
- shutil.copy(artifact, opts.artifact_dir)
- log.info("Copied {0} to artifact directory".format(artifact))
- log.info("Done!")
|