Index: ios/crnet/build.py |
diff --git a/ios/crnet/build.py b/ios/crnet/build.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..12874bf43bee59b0963ec34b40db4813364f1f15 |
--- /dev/null |
+++ b/ios/crnet/build.py |
@@ -0,0 +1,322 @@ |
+#!/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() |