#!/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()