| #!/usr/bin/env python |
| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import argparse |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| |
| |
| SUPPORTED_ARCHES = ['i386', 'x86_64', 'armv7', 'arm64'] |
| |
| |
| class SubprocessError(Exception): |
| pass |
| |
| |
| class ConfigurationError(Exception): |
| pass |
| |
| |
| def out_directories(root): |
| """Returns all output directories containing crnet objects under root. |
| |
| Currently this list is just hardcoded. |
| |
| Args: |
| root: prefix for output directories. |
| """ |
| out_dirs = ['Release-iphoneos', 'Release-iphonesimulator'] |
| return map(lambda x: os.path.join(root, 'out', x), out_dirs) |
| |
| |
| def check_command(command): |
| """Runs a command, raising an exception if it fails. |
| |
| If the command returns a nonzero exit code, prints any data the command |
| emitted on stdout and stderr. |
| |
| Args: |
| command: command to execute, in argv format. |
| |
| Raises: |
| SubprocessError: the specified command returned nonzero exit status. |
| """ |
| p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| (stdout, stderr) = p.communicate() |
| if p.returncode == 0: |
| return |
| message = 'Command failed: {0} (status {1})'.format(command, p.returncode) |
| print message |
| print 'stdout: {0}'.format(stdout) |
| print 'stderr: {0}'.format(stderr) |
| raise SubprocessError(message) |
| |
| |
| def file_contains_string(path, string): |
| """Returns whether the file named by path contains string. |
| |
| Args: |
| path: path of the file to search. |
| string: string to search the file for. |
| |
| Returns: |
| True if file contains string, False otherwise. |
| """ |
| with open(path, 'r') as f: |
| for line in f: |
| if string in line: |
| return True |
| return False |
| |
| |
| def is_object_filename(filename): |
| """Returns whether the given filename names an object file. |
| |
| Args: |
| filename: filename to inspect. |
| |
| Returns: |
| True if filename names an object file, false otherwise. |
| """ |
| (_, ext) = os.path.splitext(filename) |
| return ext in ('.a', '.o') |
| |
| |
| class Step(object): |
| """Represents a single step of the crnet build process. |
| |
| This parent class exists only to define the interface Steps present and keep |
| track of elapsed time for each step. Subclasses of Step should override the |
| run() method, which is called internally by start(). |
| |
| Attributes: |
| name: human-readable name of this step, used in debug output. |
| started_at: seconds since epoch that this step started running at. |
| """ |
| def __init__(self, name): |
| self._name = name |
| self._started_at = None |
| self._ended_at = None |
| |
| @property |
| def name(self): |
| return self._name |
| |
| def start(self): |
| """Start running this step. |
| |
| This method keeps track of how long the run() method takes to run and emits |
| the elapsed time after run() returns. |
| """ |
| self._started_at = time.time() |
| print '{0}: '.format(self._name), |
| sys.stdout.flush() |
| self._run() |
| self._ended_at = time.time() |
| print '{0:.2f}s'.format(self._ended_at - self._started_at) |
| |
| def _run(self): |
| """Actually run this step. |
| |
| Subclasses should override this method to implement their own step logic. |
| """ |
| raise NotImplementedError |
| |
| |
| class CleanStep(Step): |
| """Clean the build output directories. |
| |
| This step deletes intermediates generated by the build process. Some of these |
| intermediates (crnet_consumer.app and crnet_resources.bundle) are directories, |
| which contain files ninja doesn't know and hence won't remove, so the run() |
| method here explicitly deletes those directories before running 'ninja -t |
| clean'. |
| |
| Attributes: |
| dirs: list of output directories to clean. |
| """ |
| def __init__(self, root): |
| super(CleanStep, self).__init__('clean') |
| self._dirs = out_directories(root) |
| |
| def _run(self): |
| """Runs the clean step. |
| |
| Deletes crnet_consumer.app and crnet_resources.bundle in each output |
| directory and runs 'ninja -t clean' in each output directory. |
| """ |
| for d in self._dirs: |
| if os.path.exists(os.path.join(d, 'crnet_consumer.app')): |
| shutil.rmtree(os.path.join(d, 'crnet_consumer.app')) |
| if os.path.exists(os.path.join(d, 'crnet_resources.bundle')): |
| shutil.rmtree(os.path.join(d, 'crnet_resources.bundle')) |
| check_command(['ninja', '-C', d, '-t', 'clean']) |
| |
| |
| class HooksStep(Step): |
| """Validates the gyp config and reruns gclient hooks. |
| |
| Attributes: |
| root: directory to find gyp config under. |
| """ |
| def __init__(self, root): |
| super(HooksStep, self).__init__('hooks') |
| self._root = root |
| |
| def _run(self): |
| """Runs the hooks step. |
| |
| Checks that root/build/common.gypi contains target_subarch = both in a crude |
| way, then calls 'gclient runhooks'. TODO(ellyjones): parse common.gypi in a |
| more robust way. |
| |
| Raises: |
| ConfigurationError: if target_subarch != both |
| """ |
| common_gypi = os.path.join(self._root, 'build', 'common.gypi') |
| if not file_contains_string(common_gypi, "'target_subarch%': 'both'"): |
| raise ConfigurationError('target_subarch must be both in {0}'.format( |
| common_gypi)) |
| check_command(['gclient', 'runhooks']) |
| |
| |
| class BuildStep(Step): |
| """Builds all the intermediate crnet binaries. |
| |
| All the hard work of this step is done by ninja; this step just shells out to |
| ninja to build the crnet_pack target. |
| |
| Attributes: |
| dirs: output directories to run ninja in. |
| """ |
| def __init__(self, root): |
| super(BuildStep, self).__init__('build') |
| self._dirs = out_directories(root) |
| |
| def _run(self): |
| """Runs the build step. |
| |
| For each output directory, run ninja to build the crnet_pack target in that |
| directory. |
| """ |
| for d in self._dirs: |
| check_command(['ninja', '-C', d, 'crnet_pack']) |
| |
| |
| class PackageStep(Step): |
| """Packages the built object files for release. |
| |
| The release format is a tarball, containing one gzipped tarball per |
| architecture and a manifest file, which lists metadata about the build. |
| |
| Attributes: |
| outdirs: directories containing built object files. |
| workdir: temporary working directory. Deleted at end of the step. |
| archdir: temporary directory under workdir. Used for collecting per-arch |
| binaries. |
| proddir: temporary directory under workdir. Used for intermediate per-arch |
| tarballs. |
| """ |
| def __init__(self, root, outfile): |
| super(PackageStep, self).__init__('package') |
| self._outdirs = out_directories(root) |
| self._outfile = outfile |
| |
| def _run(self): |
| """Runs the package step. |
| |
| Packages each architecture from |root| into an individual .tar.gz file, then |
| packages all the .tar.gz files into one .tar file, which is written to |
| |outfile|. |
| """ |
| (workdir, archdir, proddir) = self.create_work_dirs() |
| for arch in SUPPORTED_ARCHES: |
| self.package_arch(archdir, proddir, arch) |
| self.package(proddir) |
| shutil.rmtree(workdir) |
| |
| def create_work_dirs(self): |
| """Creates working directories and returns their paths.""" |
| workdir = tempfile.mkdtemp() |
| archdir = os.path.join(workdir, 'arch') |
| proddir = os.path.join(workdir, 'prod') |
| os.mkdir(archdir) |
| os.mkdir(proddir) |
| return (workdir, archdir, proddir) |
| |
| def object_files_for_arch(self, arch): |
| """Returns a list of object files for the given architecture. |
| |
| Under each outdir d, per-arch files are stored in d/arch, and object files |
| for a given arch contain the arch's name as a substring. |
| |
| Args: |
| arch: architecture name. Must be in SUPPORTED_ARCHES. |
| |
| Returns: |
| List of full pathnames to object files in outdirs for the named arch. |
| """ |
| arch_files = [] |
| for d in self._outdirs: |
| files = os.listdir(os.path.join(d, 'arch')) |
| for f in filter(is_object_filename, files): |
| if arch in f: |
| arch_files.append(os.path.join(d, 'arch', f)) |
| return arch_files |
| |
| def package_arch(self, archdir, proddir, arch): |
| """Packages an individual architecture. |
| |
| Copies all the object files for the specified arch into a working directory |
| under self.archdir, then tars them up into a gzipped tarball under |
| self.proddir. |
| |
| Args: |
| archdir: directory to stage architecture files in. |
| proddir: directory to stage result tarballs in. |
| arch: architecture name to package. Must be in SUPPORTED_ARCHES. |
| """ |
| arch_files = self.object_files_for_arch(arch) |
| os.mkdir(os.path.join(archdir, arch)) |
| for f in arch_files: |
| shutil.copy(f, os.path.join(archdir, arch)) |
| out_filename = os.path.join(proddir, '{0}.tar.gz'.format(arch)) |
| check_command(['tar', '-C', archdir, '-czf', out_filename, arch]) |
| |
| def package(self, proddir): |
| """Final packaging step. Packages all the arch tarballs into one tarball.""" |
| arch_tarballs = [] |
| for a in SUPPORTED_ARCHES: |
| arch_tarballs.append('{0}.tar.gz'.format(a)) |
| check_command(['tar', '-C', proddir, '-cf', self._outfile] + |
| arch_tarballs) |
| |
| |
| def main(): |
| step_classes = { |
| 'clean': lambda: CleanStep(args.rootdir), |
| 'hooks': lambda: HooksStep(args.rootdir), |
| 'build': lambda: BuildStep(args.rootdir), |
| 'package': lambda: PackageStep(args.rootdir, args.outfile) |
| } |
| parser = argparse.ArgumentParser(description='Build and package crnet.') |
| parser.add_argument('--outfile', dest='outfile', default='crnet.tar', |
| help='Output file to generate (default: crnet.tar)') |
| parser.add_argument('--rootdir', dest='rootdir', default='../..', |
| help='Root directory to build from (default: ../..)') |
| parser.add_argument('steps', metavar='step', nargs='*') |
| args = parser.parse_args() |
| step_names = args.steps or ['clean', 'hooks', 'build', 'package'] |
| steps = [step_classes[x]() for x in step_names] |
| for step in steps: |
| step.start() |
| |
| |
| if __name__ == '__main__': |
| main() |