OLD | NEW |
(Empty) | |
| 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. |
| 5 |
| 6 import argparse |
| 7 import os |
| 8 import shutil |
| 9 import subprocess |
| 10 import sys |
| 11 import tempfile |
| 12 import time |
| 13 |
| 14 |
| 15 SUPPORTED_ARCHES = ['i386', 'x86_64', 'armv7', 'arm64'] |
| 16 |
| 17 |
| 18 class SubprocessError(Exception): |
| 19 pass |
| 20 |
| 21 |
| 22 class ConfigurationError(Exception): |
| 23 pass |
| 24 |
| 25 |
| 26 def out_directories(root): |
| 27 """Returns all output directories containing crnet objects under root. |
| 28 |
| 29 Currently this list is just hardcoded. |
| 30 |
| 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) |
| 36 |
| 37 |
| 38 def check_command(command): |
| 39 """Runs a command, raising an exception if it fails. |
| 40 |
| 41 If the command returns a nonzero exit code, prints any data the command |
| 42 emitted on stdout and stderr. |
| 43 |
| 44 Args: |
| 45 command: command to execute, in argv format. |
| 46 |
| 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) |
| 59 |
| 60 |
| 61 def file_contains_string(path, string): |
| 62 """Returns whether the file named by path contains string. |
| 63 |
| 64 Args: |
| 65 path: path of the file to search. |
| 66 string: string to search the file for. |
| 67 |
| 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 |
| 76 |
| 77 |
| 78 def is_object_filename(filename): |
| 79 """Returns whether the given filename names an object file. |
| 80 |
| 81 Args: |
| 82 filename: filename to inspect. |
| 83 |
| 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') |
| 89 |
| 90 |
| 91 class Step(object): |
| 92 """Represents a single step of the crnet build process. |
| 93 |
| 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(). |
| 97 |
| 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. |
| 101 """ |
| 102 def __init__(self, name): |
| 103 self._name = name |
| 104 self._started_at = None |
| 105 self._ended_at = None |
| 106 |
| 107 @property |
| 108 def name(self): |
| 109 return self._name |
| 110 |
| 111 def start(self): |
| 112 """Start running this step. |
| 113 |
| 114 This method keeps track of how long the run() method takes to run and emits |
| 115 the elapsed time after run() returns. |
| 116 """ |
| 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) |
| 123 |
| 124 def _run(self): |
| 125 """Actually run this step. |
| 126 |
| 127 Subclasses should override this method to implement their own step logic. |
| 128 """ |
| 129 raise NotImplementedError |
| 130 |
| 131 |
| 132 class CleanStep(Step): |
| 133 """Clean the build output directories. |
| 134 |
| 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'. |
| 140 |
| 141 Attributes: |
| 142 dirs: list of output directories to clean. |
| 143 """ |
| 144 def __init__(self, root): |
| 145 super(CleanStep, self).__init__('clean') |
| 146 self._dirs = out_directories(root) |
| 147 |
| 148 def _run(self): |
| 149 """Runs the clean step. |
| 150 |
| 151 Deletes crnet_consumer.app and crnet_resources.bundle in each output |
| 152 directory and runs 'ninja -t clean' in each output directory. |
| 153 """ |
| 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']) |
| 160 |
| 161 |
| 162 class HooksStep(Step): |
| 163 """Validates the gyp config and reruns gclient hooks. |
| 164 |
| 165 Attributes: |
| 166 root: directory to find gyp config under. |
| 167 """ |
| 168 def __init__(self, root): |
| 169 super(HooksStep, self).__init__('hooks') |
| 170 self._root = root |
| 171 |
| 172 def _run(self): |
| 173 """Runs the hooks step. |
| 174 |
| 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. |
| 178 |
| 179 Raises: |
| 180 ConfigurationError: if target_subarch != both |
| 181 """ |
| 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']) |
| 187 |
| 188 |
| 189 class BuildStep(Step): |
| 190 """Builds all the intermediate crnet binaries. |
| 191 |
| 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. |
| 194 |
| 195 Attributes: |
| 196 dirs: output directories to run ninja in. |
| 197 """ |
| 198 def __init__(self, root): |
| 199 super(BuildStep, self).__init__('build') |
| 200 self._dirs = out_directories(root) |
| 201 |
| 202 def _run(self): |
| 203 """Runs the build step. |
| 204 |
| 205 For each output directory, run ninja to build the crnet_pack target in that |
| 206 directory. |
| 207 """ |
| 208 for d in self._dirs: |
| 209 check_command(['ninja', '-C', d, 'crnet_pack']) |
| 210 |
| 211 |
| 212 class PackageStep(Step): |
| 213 """Packages the built object files for release. |
| 214 |
| 215 The release format is a tarball, containing one gzipped tarball per |
| 216 architecture and a manifest file, which lists metadata about the build. |
| 217 |
| 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. |
| 225 """ |
| 226 def __init__(self, root, outfile): |
| 227 super(PackageStep, self).__init__('package') |
| 228 self._outdirs = out_directories(root) |
| 229 self._outfile = outfile |
| 230 |
| 231 def _run(self): |
| 232 """Runs the package step. |
| 233 |
| 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|. |
| 237 """ |
| 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) |
| 243 |
| 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) |
| 252 |
| 253 def object_files_for_arch(self, arch): |
| 254 """Returns a list of object files for the given architecture. |
| 255 |
| 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. |
| 258 |
| 259 Args: |
| 260 arch: architecture name. Must be in SUPPORTED_ARCHES. |
| 261 |
| 262 Returns: |
| 263 List of full pathnames to object files in outdirs for the named arch. |
| 264 """ |
| 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 |
| 272 |
| 273 def package_arch(self, archdir, proddir, arch): |
| 274 """Packages an individual architecture. |
| 275 |
| 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. |
| 279 |
| 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. |
| 284 """ |
| 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]) |
| 291 |
| 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) |
| 299 |
| 300 |
| 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) |
| 307 } |
| 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() |
| 319 |
| 320 |
| 321 if __name__ == '__main__': |
| 322 main() |
OLD | NEW |