123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- # -*- 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 OptionParser, OptionGroup
- 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: %s', args)
- proc = subprocess.Popen(args,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- stdout, stderr = proc.communicate()
- if stdout:
- log.debug('Command output: \n%s', stdout)
- if stderr:
- log.error(stderr)
- log.info('Return code: %s', 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 %s', 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: %s', 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: %s', salt_pkgver)
- log.info('salt_srcver: %s', 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 \'%s\', falling back to \'warning\'',
- 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 %s to artifact directory', artifact)
- log.info('Done!')
|