Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / native_client / toolchain_build / toolchain_main.py
1 #!/usr/bin/python
2 # Copyright (c) 2012 The Native Client 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 """Build NativeClient toolchain packages."""
7
8 import logging
9 import optparse
10 import os
11 import sys
12 import textwrap
13
14 sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
15 import pynacl.file_tools
16 import pynacl.gsd_storage
17 import pynacl.log_tools
18 import pynacl.local_storage_cache
19
20 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
21 NACL_DIR = os.path.dirname(SCRIPT_DIR)
22 ROOT_DIR = os.path.dirname(NACL_DIR)
23 BUILD_DIR = os.path.join(NACL_DIR, 'build')
24 PKG_VER_DIR = os.path.join(BUILD_DIR, 'package_version')
25 sys.path.append(PKG_VER_DIR)
26 import archive_info
27 import package_info
28
29 import once
30
31 DEFAULT_CACHE_DIR = os.path.join(SCRIPT_DIR, 'cache')
32 DEFAULT_SRC_DIR = os.path.join(SCRIPT_DIR, 'src')
33 DEFAULT_OUT_DIR = os.path.join(SCRIPT_DIR, 'out')
34
35 def PrintFlush(message):
36   """Flush stdout and print a message to stderr.
37
38   Buildbot annotator messages must be at the beginning of a line, and we want to
39   ensure that any output from the script or from subprocesses appears in the
40   correct order wrt BUILD_STEP messages. So we flush stdout before printing all
41   buildbot messages here.
42   """
43   sys.stdout.flush()
44   print >>sys.stderr, message
45
46 def PrintAnnotatorURL(url):
47   """Print an URL in buildbot annotator form.
48
49   Args:
50     url: A URL to print.
51   """
52   PrintFlush('@@@STEP_LINK@download@%s@@@' % url)
53
54
55 class PackageBuilder(object):
56   """Module to build a setup of packages."""
57
58   def __init__(self, packages, package_targets, args):
59     """Constructor.
60
61     Args:
62       packages: A dictionary with the following format. There are two types of
63                 packages: source and build (described below).
64         {
65           '<package name>': {
66             'type': 'source',
67                 # Source packages are for sources; in particular remote sources
68                 # where it is not known whether they have changed until they are
69                 # synced (it can also or for tarballs which need to be
70                 # unpacked). Source package commands are run unconditionally
71                 # unless sync is skipped via the command-line option. Source
72                 # package contents are not memoized.
73             'dependencies':  # optional
74               [<list of package depdenencies>],
75             'output_dirname': # optional
76               '<directory name>', # Name of the directory to checkout sources
77               # into (a subdirectory of the global source directory); defaults
78               # to the package name.
79             'commands':
80               [<list of command.Runnable objects to run>],
81             'inputs': # optional
82               {<mapping whose keys are names, and whose values are files or
83                 directories (e.g. checked-in tarballs) used as input. Since
84                 source targets are unconditional, this is only useful as a
85                 convenience for commands, which may refer to the inputs by their
86                 key name>},
87            },
88           '<package name>': {
89             'type': 'build',
90                 # Build packages are memoized, and will build only if their
91                 # inputs have changed. Their inputs consist of the output of
92                 # their package dependencies plus any file or directory inputs
93                 # given by their 'inputs' member
94             'dependencies':  # optional
95               [<list of package depdenencies>],
96             'inputs': # optional
97               {<mapping whose keys are names, and whose values are files or
98                 directories (e.g. checked-in tarballs) used as input>},
99             'output_subdir': # optional
100               '<directory name>', # Name of a subdir to be created in the output
101                # directory, into which all output will be placed. If not present
102                # output will go into the root of the output directory.
103             'commands':
104               [<list of command.Command objects to run>],
105               # Objects that have a 'skip_for_incremental' attribute that
106               # evaluates to True will not be run on incremental builds unless
107               # the working directory is empty.
108           },
109         }
110       package_targets: A dictionary with the following format. This is a
111                        description of output package targets the packages are
112                        built for. Each output package should contain a list of
113                        <package_name> referenced in the previous "packages"
114                        dictionary. This list of targets is expected to stay
115                        the same from build to build, so it should include
116                        package names even if they aren't being built. A package
117                        target is usually the platform, such as "$OS_$ARCH",
118                        while the output package is usually the toolchain name,
119                        such as "nacl_arm_newlib".
120         {
121           '<package_target>': {
122             '<output_package>':
123               [<list of package names included in output package>]
124           }
125         }
126       args: sys.argv[1:] or equivalent.
127     """
128     self._packages = packages
129     self._package_targets = package_targets
130     self.DecodeArgs(packages, args)
131     self._build_once = once.Once(
132         use_cached_results=self._options.use_cached_results,
133         cache_results=self._options.cache_results,
134         print_url=PrintAnnotatorURL,
135         storage=self.CreateStorage())
136     self._signature_file = None
137     if self._options.emit_signatures is not None:
138       if self._options.emit_signatures == '-':
139         self._signature_file = sys.stdout
140       else:
141         self._signature_file = open(self._options.emit_signatures, 'w')
142
143   def Main(self):
144     """Main entry point."""
145     pynacl.file_tools.MakeDirectoryIfAbsent(self._options.source)
146     pynacl.file_tools.MakeDirectoryIfAbsent(self._options.output)
147     pynacl.log_tools.SetupLogging(self._options.verbose,
148                                   open(os.path.join(self._options.output,
149                                                    'toolchain_build.log'), 'w'))
150     self.BuildAll()
151     self.OutputPackagesInformation()
152
153   def GetOutputDir(self, package, use_subdir):
154     # The output dir of source packages is in the source directory, and can be
155     # overridden.
156     if self._packages[package]['type'] == 'source':
157       dirname = self._packages[package].get('output_dirname', package)
158       return os.path.join(self._options.source, dirname)
159     else:
160        root = os.path.join(self._options.output, package + '_install')
161        if use_subdir and 'output_subdir' in self._packages[package]:
162          return os.path.join(root, self._packages[package]['output_subdir'])
163        return root
164
165   def BuildPackage(self, package):
166     """Build a single package.
167
168     Assumes dependencies of the package have been built.
169     Args:
170       package: Package to build.
171     """
172
173     package_info = self._packages[package]
174
175     # Validate the package description.
176     if 'type' not in package_info:
177       raise Exception('package %s does not have a type' % package)
178     type_text = package_info['type']
179     if type_text not in ('source', 'build'):
180       raise Execption('package %s has unrecognized type: %s' %
181                       (package, type_text))
182     is_source_target = type_text == 'source'
183
184     if 'commands' not in package_info:
185       raise Exception('package %s does not have any commands' % package)
186
187     # Source targets are the only ones to run when doing sync-only.
188     if not is_source_target and self._options.sync_sources_only:
189       logging.debug('Build skipped: not running commands for %s' % package)
190       return
191
192     # Source targets do not run when skipping sync.
193     if is_source_target and not (
194         self._options.sync_sources or self._options.sync_sources_only):
195       logging.debug('Sync skipped: not running commands for %s' % package)
196       return
197
198     PrintFlush('@@@BUILD_STEP %s (%s)@@@' % (package, type_text))
199     logging.debug('Building %s package %s' % (type_text, package))
200
201     dependencies = package_info.get('dependencies', [])
202
203     # Collect a dict of all the inputs.
204     inputs = {}
205     # Add in explicit inputs.
206     if 'inputs' in package_info:
207       for key, value in package_info['inputs'].iteritems():
208         if key in dependencies:
209           raise Exception('key "%s" found in both dependencies and inputs of '
210                           'package "%s"' % (key, package))
211         inputs[key] = value
212     else:
213       inputs['src'] = os.path.join(self._options.source, package)
214     # Add in each dependency by package name.
215     for dependency in dependencies:
216       inputs[dependency] = self.GetOutputDir(dependency, True)
217
218     # Each package generates intermediate into output/<PACKAGE>_work.
219     # Clobbered here explicitly.
220     work_dir = os.path.join(self._options.output, package + '_work')
221     if self._options.clobber:
222       logging.debug('Clobbering working directory %s' % work_dir)
223       pynacl.file_tools.RemoveDirectoryIfPresent(work_dir)
224     pynacl.file_tools.MakeDirectoryIfAbsent(work_dir)
225
226     output = self.GetOutputDir(package, False)
227     output_subdir = self.GetOutputDir(package, True)
228
229     if not is_source_target or self._options.clobber_source:
230       logging.debug('Clobbering output directory %s' % output)
231       pynacl.file_tools.RemoveDirectoryIfPresent(output)
232       os.makedirs(output_subdir)
233
234     commands = package_info.get('commands', [])
235     if not self._options.clobber and len(os.listdir(work_dir)) > 0:
236       commands = [cmd for cmd in commands if
237                   not (hasattr(cmd, 'skip_for_incremental') and
238                        cmd.skip_for_incremental)]
239     # Do it.
240     self._build_once.Run(
241         package, inputs, output,
242         commands=commands,
243         working_dir=work_dir,
244         memoize=not is_source_target,
245         signature_file=self._signature_file,
246         subdir=output_subdir)
247
248     if not is_source_target and self._options.install:
249       logging.debug('Installing output to %s' % self._options.install)
250       pynacl.file_tools.CopyTree(output, self._options.install)
251
252   def BuildOrder(self, targets):
253     """Find what needs to be built in what order to build all targets.
254
255     Args:
256       targets: A list of target packages to build.
257     Returns:
258       A topologically sorted list of the targets plus their transitive
259       dependencies, in an order that will allow things to be built.
260     """
261     order = []
262     order_set = set()
263     if self._options.ignore_dependencies:
264       return targets
265     def Add(target, target_path):
266       if target in order_set:
267         return
268       if target not in self._packages:
269         raise Exception('Unknown package %s' % target)
270       next_target_path = target_path + [target]
271       if target in target_path:
272         raise Exception('Dependency cycle: %s' % ' -> '.join(next_target_path))
273       for dependency in self._packages[target].get('dependencies', []):
274         Add(dependency, next_target_path)
275       order.append(target)
276       order_set.add(target)
277     for target in targets:
278       Add(target, [])
279     return order
280
281   def BuildAll(self):
282     """Build all packages selected and their dependencies."""
283     for target in self._targets:
284       self.BuildPackage(target)
285
286   def OutputPackagesInformation(self):
287     """Outputs packages information for the built data."""
288     packages_dir = os.path.join(self._options.output, 'packages')
289     pynacl.file_tools.RemoveDirectoryIfPresent(packages_dir)
290     os.makedirs(packages_dir)
291
292     built_packages = []
293     for target, target_dict in self._package_targets.iteritems():
294       target_dir = os.path.join(packages_dir, target)
295       pynacl.file_tools.MakeDirectoryIfAbsent(target_dir)
296       for output_package, components in target_dict.iteritems():
297         package_desc = package_info.PackageInfo()
298
299         include_package = False
300         for component in components:
301           if '.' in component:
302             archive_name = component
303           else:
304             archive_name = component + '.tgz'
305           cache_item = self._build_once.GetCachedDirItemForPackage(component)
306           if cache_item is None:
307             archive_desc = archive_info.ArchiveInfo(archive_name)
308           else:
309             include_package = True
310             archive_desc = archive_info.ArchiveInfo(archive_name,
311                                                     cache_item.hash,
312                                                     url=cache_item.url)
313
314           package_desc.AppendArchive(archive_desc)
315
316         # Only output package file if an archive was actually included.
317         if include_package:
318           package_file = os.path.join(target_dir, output_package + '.json')
319           package_desc.SavePackageFile(package_file)
320
321           built_packages.append(package_file)
322
323     if self._options.packages_file:
324       pynacl.file_tools.MakeParentDirectoryIfAbsent(self._options.packages_file)
325       with open(self._options.packages_file, 'wt') as f:
326         f.write('\n'.join(built_packages))
327
328   def DecodeArgs(self, packages, args):
329     """Decode command line arguments to this build.
330
331     Populated self._options and self._targets.
332     Args:
333       packages: A list of package names to build.
334       args: sys.argv[1:] or equivalent.
335     """
336     package_list = sorted(packages.keys())
337     parser = optparse.OptionParser(
338         usage='USAGE: %prog [options] [targets...]\n\n'
339               'Available targets:\n' +
340               '\n'.join(textwrap.wrap(' '.join(package_list))))
341     parser.add_option(
342         '-v', '--verbose', dest='verbose',
343         default=False, action='store_true',
344         help='Produce more output.')
345     parser.add_option(
346         '-c', '--clobber', dest='clobber',
347         default=False, action='store_true',
348         help='Clobber working directories before building.')
349     parser.add_option(
350         '--cache', dest='cache',
351         default=DEFAULT_CACHE_DIR,
352         help='Select directory containing local storage cache.')
353     parser.add_option(
354         '-s', '--source', dest='source',
355         default=DEFAULT_SRC_DIR,
356         help='Select directory containing source checkouts.')
357     parser.add_option(
358         '-o', '--output', dest='output',
359         default=DEFAULT_OUT_DIR,
360         help='Select directory containing build output.')
361     parser.add_option(
362         '--packages-file', dest='packages_file',
363         default=None,
364         help='Output packages file describing list of package files built.')
365     parser.add_option(
366         '--no-use-cached-results', dest='use_cached_results',
367         default=True, action='store_false',
368         help='Do not rely on cached results.')
369     parser.add_option(
370         '--no-use-remote-cache', dest='use_remote_cache',
371         default=True, action='store_false',
372         help='Do not rely on non-local cached results.')
373     parser.add_option(
374         '--no-cache-results', dest='cache_results',
375         default=True, action='store_false',
376         help='Do not cache results.')
377     parser.add_option(
378         '--no-pinned', dest='pinned',
379         default=True, action='store_false',
380         help='Do not use pinned revisions.')
381     parser.add_option(
382         '--trybot', dest='trybot',
383         default=False, action='store_true',
384         help='Run and cache as if on trybot.')
385     parser.add_option(
386         '--buildbot', dest='buildbot',
387         default=False, action='store_true',
388         help='Run and cache as if on a non-trybot buildbot.')
389     parser.add_option(
390         '--clobber-source', dest='clobber_source',
391         default=False, action='store_true',
392         help='Clobber source directories before building')
393     parser.add_option(
394         '-y', '--sync', dest='sync_sources',
395         default=False, action='store_true',
396         help='Run source target commands')
397     parser.add_option(
398         '--sync-only', dest='sync_sources_only',
399         default=False, action='store_true',
400         help='Run source target commands only')
401     parser.add_option(
402         '--emit-signatures', dest='emit_signatures',
403         help='Write human readable build signature for each step to FILE.',
404         metavar='FILE')
405     parser.add_option(
406         '-i', '--ignore-dependencies', dest='ignore_dependencies',
407         default=False, action='store_true',
408         help='Ignore target dependencies and build only the specified target.')
409     parser.add_option('--install', dest='install',
410                       help='After building, copy contents of build packages' +
411                       ' to the specified directory')
412     options, targets = parser.parse_args(args)
413     if options.trybot and options.buildbot:
414       print >>sys.stderr, (
415           'ERROR: Tried to run with both --trybot and --buildbot.')
416       sys.exit(1)
417     if options.trybot or options.buildbot:
418       options.verbose = True
419       options.sync_sources = True
420       options.clobber = True
421       options.emit_signatures = '-'
422     self._options = options
423     if not targets:
424       if self._options.ignore_dependencies:
425         print >>sys.stderr, (
426             'ERROR: A target must be specified if ignoring target dependencies')
427         sys.exit(1)
428       targets = sorted(packages.keys())
429     targets = self.BuildOrder(targets)
430     self._targets = targets
431
432   def CreateStorage(self):
433     """Create a storage object for this build.
434
435     Returns:
436       A storage object (GSDStorage).
437     """
438     if self._options.buildbot:
439       return pynacl.gsd_storage.GSDStorage(
440           write_bucket='nativeclient-once',
441           read_buckets=['nativeclient-once'])
442     elif self._options.trybot:
443       return pynacl.gsd_storage.GSDStorage(
444           write_bucket='nativeclient-once-try',
445           read_buckets=['nativeclient-once', 'nativeclient-once-try'])
446     else:
447       read_buckets = []
448       if self._options.use_remote_cache:
449         read_buckets += ['nativeclient-once']
450       return pynacl.local_storage_cache.LocalStorageCache(
451           cache_path=self._options.cache,
452           storage=pynacl.gsd_storage.GSDStorage(
453               write_bucket=None,
454               read_buckets=read_buckets))