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.
6 """Build NativeClient toolchain packages."""
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
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)
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')
35 def PrintFlush(message):
36 """Flush stdout and print a message to stderr.
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.
44 print >>sys.stderr, message
46 def PrintAnnotatorURL(url):
47 """Print an URL in buildbot annotator form.
52 PrintFlush('@@@STEP_LINK@download@%s@@@' % url)
55 class PackageBuilder(object):
56 """Module to build a setup of packages."""
58 def __init__(self, packages, package_targets, args):
62 packages: A dictionary with the following format. There are two types of
63 packages: source and build (described below).
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.
80 [<list of command.Runnable objects to run>],
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
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>],
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.
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.
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".
121 '<package_target>': {
123 [<list of package names included in output package>]
126 args: sys.argv[1:] or equivalent.
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
141 self._signature_file = open(self._options.emit_signatures, 'w')
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'))
151 self.OutputPackagesInformation()
153 def GetOutputDir(self, package, use_subdir):
154 # The output dir of source packages is in the source directory, and can be
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)
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'])
165 def BuildPackage(self, package):
166 """Build a single package.
168 Assumes dependencies of the package have been built.
170 package: Package to build.
173 package_info = self._packages[package]
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'
184 if 'commands' not in package_info:
185 raise Exception('package %s does not have any commands' % package)
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)
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)
198 PrintFlush('@@@BUILD_STEP %s (%s)@@@' % (package, type_text))
199 logging.debug('Building %s package %s' % (type_text, package))
201 dependencies = package_info.get('dependencies', [])
203 # Collect a dict of all the 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))
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)
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)
226 output = self.GetOutputDir(package, False)
227 output_subdir = self.GetOutputDir(package, True)
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)
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)]
240 self._build_once.Run(
241 package, inputs, output,
243 working_dir=work_dir,
244 memoize=not is_source_target,
245 signature_file=self._signature_file,
246 subdir=output_subdir)
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)
252 def BuildOrder(self, targets):
253 """Find what needs to be built in what order to build all targets.
256 targets: A list of target packages to build.
258 A topologically sorted list of the targets plus their transitive
259 dependencies, in an order that will allow things to be built.
263 if self._options.ignore_dependencies:
265 def Add(target, target_path):
266 if target in order_set:
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)
276 order_set.add(target)
277 for target in targets:
282 """Build all packages selected and their dependencies."""
283 for target in self._targets:
284 self.BuildPackage(target)
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)
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()
299 include_package = False
300 for component in components:
302 archive_name = component
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)
309 include_package = True
310 archive_desc = archive_info.ArchiveInfo(archive_name,
314 package_desc.AppendArchive(archive_desc)
316 # Only output package file if an archive was actually included.
318 package_file = os.path.join(target_dir, output_package + '.json')
319 package_desc.SavePackageFile(package_file)
321 built_packages.append(package_file)
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))
328 def DecodeArgs(self, packages, args):
329 """Decode command line arguments to this build.
331 Populated self._options and self._targets.
333 packages: A list of package names to build.
334 args: sys.argv[1:] or equivalent.
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))))
342 '-v', '--verbose', dest='verbose',
343 default=False, action='store_true',
344 help='Produce more output.')
346 '-c', '--clobber', dest='clobber',
347 default=False, action='store_true',
348 help='Clobber working directories before building.')
350 '--cache', dest='cache',
351 default=DEFAULT_CACHE_DIR,
352 help='Select directory containing local storage cache.')
354 '-s', '--source', dest='source',
355 default=DEFAULT_SRC_DIR,
356 help='Select directory containing source checkouts.')
358 '-o', '--output', dest='output',
359 default=DEFAULT_OUT_DIR,
360 help='Select directory containing build output.')
362 '--packages-file', dest='packages_file',
364 help='Output packages file describing list of package files built.')
366 '--no-use-cached-results', dest='use_cached_results',
367 default=True, action='store_false',
368 help='Do not rely on cached results.')
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.')
374 '--no-cache-results', dest='cache_results',
375 default=True, action='store_false',
376 help='Do not cache results.')
378 '--no-pinned', dest='pinned',
379 default=True, action='store_false',
380 help='Do not use pinned revisions.')
382 '--trybot', dest='trybot',
383 default=False, action='store_true',
384 help='Run and cache as if on trybot.')
386 '--buildbot', dest='buildbot',
387 default=False, action='store_true',
388 help='Run and cache as if on a non-trybot buildbot.')
390 '--clobber-source', dest='clobber_source',
391 default=False, action='store_true',
392 help='Clobber source directories before building')
394 '-y', '--sync', dest='sync_sources',
395 default=False, action='store_true',
396 help='Run source target commands')
398 '--sync-only', dest='sync_sources_only',
399 default=False, action='store_true',
400 help='Run source target commands only')
402 '--emit-signatures', dest='emit_signatures',
403 help='Write human readable build signature for each step to FILE.',
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.')
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
424 if self._options.ignore_dependencies:
425 print >>sys.stderr, (
426 'ERROR: A target must be specified if ignoring target dependencies')
428 targets = sorted(packages.keys())
429 targets = self.BuildOrder(targets)
430 self._targets = targets
432 def CreateStorage(self):
433 """Create a storage object for this build.
436 A storage object (GSDStorage).
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'])
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(
454 read_buckets=read_buckets))