NaCl: Update revision in DEPS, b3d4cc1 -> a0efd24, update UMA test
[chromium-blink-merge.git] / ios / crnet / build.py
blob12874bf43bee59b0963ec34b40db4813364f1f15
1 #!/usr/bin/env python
2 # Copyright 2014 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 import argparse
7 import os
8 import shutil
9 import subprocess
10 import sys
11 import tempfile
12 import time
15 SUPPORTED_ARCHES = ['i386', 'x86_64', 'armv7', 'arm64']
18 class SubprocessError(Exception):
19 pass
22 class ConfigurationError(Exception):
23 pass
26 def out_directories(root):
27 """Returns all output directories containing crnet objects under root.
29 Currently this list is just hardcoded.
31 Args:
32 root: prefix for output directories.
33 """
34 out_dirs = ['Release-iphoneos', 'Release-iphonesimulator']
35 return map(lambda x: os.path.join(root, 'out', x), out_dirs)
38 def check_command(command):
39 """Runs a command, raising an exception if it fails.
41 If the command returns a nonzero exit code, prints any data the command
42 emitted on stdout and stderr.
44 Args:
45 command: command to execute, in argv format.
47 Raises:
48 SubprocessError: the specified command returned nonzero exit status.
49 """
50 p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
51 (stdout, stderr) = p.communicate()
52 if p.returncode == 0:
53 return
54 message = 'Command failed: {0} (status {1})'.format(command, p.returncode)
55 print message
56 print 'stdout: {0}'.format(stdout)
57 print 'stderr: {0}'.format(stderr)
58 raise SubprocessError(message)
61 def file_contains_string(path, string):
62 """Returns whether the file named by path contains string.
64 Args:
65 path: path of the file to search.
66 string: string to search the file for.
68 Returns:
69 True if file contains string, False otherwise.
70 """
71 with open(path, 'r') as f:
72 for line in f:
73 if string in line:
74 return True
75 return False
78 def is_object_filename(filename):
79 """Returns whether the given filename names an object file.
81 Args:
82 filename: filename to inspect.
84 Returns:
85 True if filename names an object file, false otherwise.
86 """
87 (_, ext) = os.path.splitext(filename)
88 return ext in ('.a', '.o')
91 class Step(object):
92 """Represents a single step of the crnet build process.
94 This parent class exists only to define the interface Steps present and keep
95 track of elapsed time for each step. Subclasses of Step should override the
96 run() method, which is called internally by start().
98 Attributes:
99 name: human-readable name of this step, used in debug output.
100 started_at: seconds since epoch that this step started running at.
102 def __init__(self, name):
103 self._name = name
104 self._started_at = None
105 self._ended_at = None
107 @property
108 def name(self):
109 return self._name
111 def start(self):
112 """Start running this step.
114 This method keeps track of how long the run() method takes to run and emits
115 the elapsed time after run() returns.
117 self._started_at = time.time()
118 print '{0}: '.format(self._name),
119 sys.stdout.flush()
120 self._run()
121 self._ended_at = time.time()
122 print '{0:.2f}s'.format(self._ended_at - self._started_at)
124 def _run(self):
125 """Actually run this step.
127 Subclasses should override this method to implement their own step logic.
129 raise NotImplementedError
132 class CleanStep(Step):
133 """Clean the build output directories.
135 This step deletes intermediates generated by the build process. Some of these
136 intermediates (crnet_consumer.app and crnet_resources.bundle) are directories,
137 which contain files ninja doesn't know and hence won't remove, so the run()
138 method here explicitly deletes those directories before running 'ninja -t
139 clean'.
141 Attributes:
142 dirs: list of output directories to clean.
144 def __init__(self, root):
145 super(CleanStep, self).__init__('clean')
146 self._dirs = out_directories(root)
148 def _run(self):
149 """Runs the clean step.
151 Deletes crnet_consumer.app and crnet_resources.bundle in each output
152 directory and runs 'ninja -t clean' in each output directory.
154 for d in self._dirs:
155 if os.path.exists(os.path.join(d, 'crnet_consumer.app')):
156 shutil.rmtree(os.path.join(d, 'crnet_consumer.app'))
157 if os.path.exists(os.path.join(d, 'crnet_resources.bundle')):
158 shutil.rmtree(os.path.join(d, 'crnet_resources.bundle'))
159 check_command(['ninja', '-C', d, '-t', 'clean'])
162 class HooksStep(Step):
163 """Validates the gyp config and reruns gclient hooks.
165 Attributes:
166 root: directory to find gyp config under.
168 def __init__(self, root):
169 super(HooksStep, self).__init__('hooks')
170 self._root = root
172 def _run(self):
173 """Runs the hooks step.
175 Checks that root/build/common.gypi contains target_subarch = both in a crude
176 way, then calls 'gclient runhooks'. TODO(ellyjones): parse common.gypi in a
177 more robust way.
179 Raises:
180 ConfigurationError: if target_subarch != both
182 common_gypi = os.path.join(self._root, 'build', 'common.gypi')
183 if not file_contains_string(common_gypi, "'target_subarch%': 'both'"):
184 raise ConfigurationError('target_subarch must be both in {0}'.format(
185 common_gypi))
186 check_command(['gclient', 'runhooks'])
189 class BuildStep(Step):
190 """Builds all the intermediate crnet binaries.
192 All the hard work of this step is done by ninja; this step just shells out to
193 ninja to build the crnet_pack target.
195 Attributes:
196 dirs: output directories to run ninja in.
198 def __init__(self, root):
199 super(BuildStep, self).__init__('build')
200 self._dirs = out_directories(root)
202 def _run(self):
203 """Runs the build step.
205 For each output directory, run ninja to build the crnet_pack target in that
206 directory.
208 for d in self._dirs:
209 check_command(['ninja', '-C', d, 'crnet_pack'])
212 class PackageStep(Step):
213 """Packages the built object files for release.
215 The release format is a tarball, containing one gzipped tarball per
216 architecture and a manifest file, which lists metadata about the build.
218 Attributes:
219 outdirs: directories containing built object files.
220 workdir: temporary working directory. Deleted at end of the step.
221 archdir: temporary directory under workdir. Used for collecting per-arch
222 binaries.
223 proddir: temporary directory under workdir. Used for intermediate per-arch
224 tarballs.
226 def __init__(self, root, outfile):
227 super(PackageStep, self).__init__('package')
228 self._outdirs = out_directories(root)
229 self._outfile = outfile
231 def _run(self):
232 """Runs the package step.
234 Packages each architecture from |root| into an individual .tar.gz file, then
235 packages all the .tar.gz files into one .tar file, which is written to
236 |outfile|.
238 (workdir, archdir, proddir) = self.create_work_dirs()
239 for arch in SUPPORTED_ARCHES:
240 self.package_arch(archdir, proddir, arch)
241 self.package(proddir)
242 shutil.rmtree(workdir)
244 def create_work_dirs(self):
245 """Creates working directories and returns their paths."""
246 workdir = tempfile.mkdtemp()
247 archdir = os.path.join(workdir, 'arch')
248 proddir = os.path.join(workdir, 'prod')
249 os.mkdir(archdir)
250 os.mkdir(proddir)
251 return (workdir, archdir, proddir)
253 def object_files_for_arch(self, arch):
254 """Returns a list of object files for the given architecture.
256 Under each outdir d, per-arch files are stored in d/arch, and object files
257 for a given arch contain the arch's name as a substring.
259 Args:
260 arch: architecture name. Must be in SUPPORTED_ARCHES.
262 Returns:
263 List of full pathnames to object files in outdirs for the named arch.
265 arch_files = []
266 for d in self._outdirs:
267 files = os.listdir(os.path.join(d, 'arch'))
268 for f in filter(is_object_filename, files):
269 if arch in f:
270 arch_files.append(os.path.join(d, 'arch', f))
271 return arch_files
273 def package_arch(self, archdir, proddir, arch):
274 """Packages an individual architecture.
276 Copies all the object files for the specified arch into a working directory
277 under self.archdir, then tars them up into a gzipped tarball under
278 self.proddir.
280 Args:
281 archdir: directory to stage architecture files in.
282 proddir: directory to stage result tarballs in.
283 arch: architecture name to package. Must be in SUPPORTED_ARCHES.
285 arch_files = self.object_files_for_arch(arch)
286 os.mkdir(os.path.join(archdir, arch))
287 for f in arch_files:
288 shutil.copy(f, os.path.join(archdir, arch))
289 out_filename = os.path.join(proddir, '{0}.tar.gz'.format(arch))
290 check_command(['tar', '-C', archdir, '-czf', out_filename, arch])
292 def package(self, proddir):
293 """Final packaging step. Packages all the arch tarballs into one tarball."""
294 arch_tarballs = []
295 for a in SUPPORTED_ARCHES:
296 arch_tarballs.append('{0}.tar.gz'.format(a))
297 check_command(['tar', '-C', proddir, '-cf', self._outfile] +
298 arch_tarballs)
301 def main():
302 step_classes = {
303 'clean': lambda: CleanStep(args.rootdir),
304 'hooks': lambda: HooksStep(args.rootdir),
305 'build': lambda: BuildStep(args.rootdir),
306 'package': lambda: PackageStep(args.rootdir, args.outfile)
308 parser = argparse.ArgumentParser(description='Build and package crnet.')
309 parser.add_argument('--outfile', dest='outfile', default='crnet.tar',
310 help='Output file to generate (default: crnet.tar)')
311 parser.add_argument('--rootdir', dest='rootdir', default='../..',
312 help='Root directory to build from (default: ../..)')
313 parser.add_argument('steps', metavar='step', nargs='*')
314 args = parser.parse_args()
315 step_names = args.steps or ['clean', 'hooks', 'build', 'package']
316 steps = [step_classes[x]() for x in step_names]
317 for step in steps:
318 step.start()
321 if __name__ == '__main__':
322 main()