blob: 12874bf43bee59b0963ec34b40db4813364f1f15 [file] [log] [blame]
#!/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()