Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / native_client / build / download_toolchains.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 """Download all Native Client toolchains for this platform.
7
8 This module downloads multiple tgz's and expands them.
9 """
10
11 import cygtar
12 import optparse
13 import os
14 import re
15 import sys
16 import tempfile
17
18 sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
19 import pynacl.download_utils
20 import pynacl.file_tools
21 import pynacl.platform
22
23 import toolchainbinaries
24
25
26 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
27 PARENT_DIR = os.path.dirname(SCRIPT_DIR)
28
29 SHA1_IN_FILENAME = re.compile('.*_[0-9a-f]{40}$')
30
31
32 def LoadVersions(filepath):
33   """Load version data from specified filepath into a dictionary.
34
35   Arguments:
36     filepath: path to the file which will be loaded.
37   Returns:
38     A dictionary of KEY:VALUE pairs.
39   """
40   versions = {}
41   version_lines = open(filepath, 'r').readlines()
42   for line_num, line in enumerate(version_lines, start=1):
43     line = line.strip()
44     if line.startswith('#'):
45       continue
46
47     if line == '':
48       continue
49
50     if '=' not in line:
51       raise RuntimeError('Expecting KEY=VALUE in line %d:\n\t>>%s<<' %
52                          (line_num, line))
53
54     key, val = line.split('=', 1)
55     versions[key] = val
56   return versions
57
58
59 def VersionSelect(versions, flavor):
60   """Determine svn revision based on version data and flavor.
61
62   Arguments:
63     versions: version data loaded from file.
64     flavor: kind of tool.
65   Returns:
66     An svn version number (or other version string).
67   """
68
69   if isinstance(flavor, tuple):
70     ids = [versions[i] for i in flavor[1:]]
71     return ','.join(ids)
72   if toolchainbinaries.IsBionicFlavor(flavor):
73     return versions['BIONIC_VERSION']
74   if toolchainbinaries.IsPnaclFlavor(flavor):
75     return versions['PNACL_VERSION']
76   if toolchainbinaries.IsX86Flavor(flavor):
77     if toolchainbinaries.IsNotNaClNewlibFlavor(flavor):
78       return versions['GLIBC_VERSION']
79     else:
80       return versions['NEWLIB_VERSION']
81   if toolchainbinaries.IsArmTrustedFlavor(flavor):
82     return versions['ARM_TRUSTED_VERSION']
83   raise Exception('Unknown flavor "%s"' % flavor)
84
85
86 def HashKey(flavor):
87   """Generate the name of the key for this flavor's hash.
88
89   Arguments:
90     flavor: kind of tool.
91   Returns:
92     The string key for the hash.
93   """
94   return 'NACL_TOOL_%s_HASH' % flavor.upper()
95
96
97 def HashSelect(versions, flavor):
98   """Determine expected hash value based on version data and flavor.
99
100   Arguments:
101     versions: version data loaded from file.
102     flavor: kind of tool.
103   Returns:
104     A SHA1 hash.
105   """
106   return versions[HashKey(flavor)]
107
108
109 def IsFlavorNeeded(options, flavor):
110   if isinstance(flavor, tuple):
111     flavor = flavor[0]
112   if options.filter_out_predicates:
113     for predicate in options.filter_out_predicates:
114       if predicate(flavor):
115         return False
116   return True
117
118
119 def FlavorOutDir(options, flavor):
120   """Given a flavor, decide where it should be extracted."""
121   if isinstance(flavor, tuple):
122     return toolchainbinaries.GetStandardToolchainFlavorDir(
123         options.toolchain_dir,
124         flavor[0])
125   else:
126     return toolchainbinaries.GetStandardToolchainFlavorDir(
127         options.toolchain_dir,
128         flavor)
129
130
131 def FlavorName(flavor):
132   """Given a flavor, get a string name for it."""
133   if isinstance(flavor, tuple):
134     return flavor[0]
135   else:
136     return flavor
137
138
139 def FlavorComponentNames(flavor):
140   if isinstance(flavor, tuple):
141     return flavor[1:]
142   else:
143     return [flavor]
144
145
146 def FlavorUrls(options, versions, flavor):
147   """Given a flavor, get a list of the URLs of its components."""
148   if isinstance(flavor, tuple):
149     ids = [versions[i] for i in flavor[1:]]
150     return [toolchainbinaries.EncodeToolchainUrl(
151             options.base_once_url, i, 'new') for i in ids]
152   else:
153     return [toolchainbinaries.EncodeToolchainUrl(
154             options.base_url, VersionSelect(versions, flavor), flavor)]
155
156
157 def FlavorHashes(versions, flavor):
158   """Given a flavor, get the list of hashes of its components."""
159   if isinstance(flavor, tuple):
160     return [HashSelect(versions, i) for i in flavor[1:]]
161   else:
162     return [HashSelect(versions, flavor)]
163
164
165 def GetUpdatedDEPS(options, versions):
166   """Return a suggested DEPS toolchain hash update for all platforms.
167
168   Arguments:
169     options: options from the command line.
170   """
171   flavors = set()
172   for pm in toolchainbinaries.PLATFORM_MAPPING.itervalues():
173     for flavorlist in pm.itervalues():
174       for flavor in flavorlist:
175         if IsFlavorNeeded(options, flavor):
176           flavors.add(flavor)
177   new_deps = {}
178   for flavor in flavors:
179     names = FlavorComponentNames(flavor)
180     urls = FlavorUrls(options, versions, flavor)
181     for name, url in zip(names, urls):
182       new_deps[name] = pynacl.download_utils.HashUrl(url)
183   return new_deps
184
185
186 def ShowUpdatedDEPS(options, versions):
187   """Print a suggested DEPS toolchain hash update for all platforms.
188
189   Arguments:
190     options: options from the command line.
191   """
192   for flavor, value in sorted(GetUpdatedDEPS(options, versions).iteritems()):
193     keyname = HashKey(flavor)
194     print '%s=%s' % (keyname, value)
195     sys.stdout.flush()
196
197
198 def SyncFlavor(flavor, urls, dst, hashes, min_time, keep=False, force=False,
199                verbose=False):
200   """Sync a flavor of the nacl toolchain
201
202   Arguments:
203     flavor: short directory name of the toolchain flavor.
204     urls: urls to download the toolchain flavor from.
205     dst: destination directory for the toolchain.
206     hashes: expected hashes of the toolchain.
207   """
208   if isinstance(flavor, tuple):
209     flavor_name = flavor[0]
210   else:
211     flavor_name = flavor
212
213   toolchain_dir = os.path.join(PARENT_DIR, 'toolchain')
214   if not os.path.exists(toolchain_dir):
215     os.makedirs(toolchain_dir)
216   download_dir = os.path.join(toolchain_dir, '.tars')
217
218   prefix = 'tmp_unpacked_toolchain_'
219   suffix = '.tmp'
220
221   # Attempt to cleanup.
222   for path in os.listdir(toolchain_dir):
223     if path.startswith(prefix) and path.endswith(suffix):
224       full_path = os.path.join(toolchain_dir, path)
225       try:
226         print 'Cleaning up %s...' % full_path
227         pynacl.file_tools.RemoveDir(full_path)
228       except Exception, e:
229         print 'Failed cleanup with: ' + str(e)
230
231   # If we are forcing a sync, then ignore stamp
232   if force:
233     stamp_dir = None
234   else:
235     stamp_dir = dst
236
237   filepaths = []
238   need_sync = False
239
240   index = 0
241   for url, hash_val in zip(urls, hashes):
242     # Build the tarfile name from the url
243     # http://foo..../bar.tar.gz -> bar
244     filepath, ext = url.split('/')[-1].split('.', 1)
245     # For filenames containing _SHA1s, drop the sha1 part.
246     if SHA1_IN_FILENAME.match(filepath) is not None:
247       filepath = filepath.rsplit('_', 1)[0]
248     # Put it in the download dir and add back extension.
249     filepath = os.path.join(download_dir, '.'.join([filepath, ext]))
250     filepaths.append(filepath)
251     # If we did not need to synchronize, then we are done
252     if pynacl.download_utils.SyncURL(url, filepath, stamp_dir=stamp_dir,
253                                      min_time=min_time, hash_val=hash_val,
254                                      stamp_index=index,
255                                      keep=keep, verbose=verbose):
256       need_sync = True
257     index += 1
258
259   if not need_sync:
260     return False
261
262   # Compute the new hashes for each file.
263   new_hashes = []
264   for filepath in filepaths:
265     new_hashes.append(pynacl.download_utils.HashFile(filepath))
266
267   untar_dir = tempfile.mkdtemp(
268       suffix=suffix, prefix=prefix, dir=toolchain_dir)
269   try:
270     if verbose:
271       tar_file = os.path.basename(filepath)
272       rel_dest = os.path.relpath(dst, toolchain_dir)
273       print '%s: Extracting "%s" -> "%s"...' % (flavor_name, tar_file, rel_dest)
274
275     for filepath in filepaths:
276       tar = cygtar.CygTar(filepath, 'r:*', verbose=verbose)
277       curdir = os.getcwd()
278       os.chdir(untar_dir)
279       try:
280         tar.Extract()
281         tar.Close()
282       finally:
283         os.chdir(curdir)
284
285       if not keep:
286         os.remove(filepath)
287
288     # TODO(bradnelson_): get rid of this when toolchain tarballs flattened.
289     if 'bionic' in flavor:
290       src = os.path.join(untar_dir, flavor)
291     elif isinstance(flavor, tuple) or 'arm' in flavor or 'pnacl' in flavor:
292       src = os.path.join(untar_dir)
293     elif 'newlib' in flavor:
294       src = os.path.join(untar_dir, 'sdk', 'nacl-sdk')
295     else:
296       src = os.path.join(untar_dir, 'toolchain', flavor)
297     pynacl.file_tools.MoveDirCleanly(src, dst)
298   finally:
299     try:
300       pynacl.file_tools.RemoveDir(untar_dir)
301     except Exception, e:
302       print 'Failed cleanup with: ' + str(e)
303       print 'Continuing on original exception...'
304   pynacl.download_utils.WriteSourceStamp(dst, '\n'.join(urls))
305   pynacl.download_utils.WriteHashStamp(dst, '\n'.join(new_hashes))
306   return True
307
308
309 def ParseArgs(args):
310   parser = optparse.OptionParser()
311   parser.add_option(
312       '-b', '--base-url', dest='base_url',
313       default=toolchainbinaries.BASE_DOWNLOAD_URL,
314       help='base url to download from')
315   parser.add_option(
316       '--base-once-url', dest='base_once_url',
317       default=toolchainbinaries.BASE_ONCE_DOWNLOAD_URL,
318       help='base url to download new toolchain artifacts from')
319   parser.add_option(
320       '-c', '--hashes', dest='hashes',
321       default=False,
322       action='store_true',
323       help='Calculate hashes.')
324   parser.add_option(
325       '-k', '--keep', dest='keep',
326       default=False,
327       action='store_true',
328       help='Keep the downloaded tarballs.')
329   parser.add_option(
330       '-q', '--quiet', dest='verbose',
331       default=True,
332       action='store_false',
333       help='Produce less output.')
334   parser.add_option(
335       '--toolchain-dir', dest='toolchain_dir',
336       default=os.path.join(PARENT_DIR, 'toolchain'),
337       help='(optional) location of toolchain directory')
338   parser.add_option(
339       '--nacl-newlib-only', dest='filter_out_predicates',
340       action='append_const', const=toolchainbinaries.IsNotNaClNewlibFlavor,
341       help='download only the non-pnacl newlib toolchain')
342   parser.add_option(
343       '--allow-bionic', dest='allow_bionic', action='store_true',
344       default=False,
345       help='Allow download of bionic toolchain.')
346   parser.add_option(
347       '--no-pnacl', dest='filter_out_predicates', action='append_const',
348       const=toolchainbinaries.IsPnaclFlavor,
349       help='Filter out PNaCl toolchains.')
350   parser.add_option(
351       '--no-x86', dest='filter_out_predicates', action='append_const',
352       const=toolchainbinaries.IsX86Flavor,
353       help='Filter out x86 toolchains.')
354   parser.add_option(
355       '--arm-untrusted', dest='arm_untrusted', action='store_true',
356       default=False, help='Add arm untrusted toolchains.')
357   parser.add_option(
358       '--no-pnacl-translator', dest='filter_out_predicates',
359       action='append_const',
360       const=toolchainbinaries.IsSandboxedTranslatorFlavor,
361       help='Filter out PNaCl sandboxed translator.')
362   parser.add_option(
363       '--no-arm-trusted', dest='filter_out_predicates', action='append_const',
364       const=toolchainbinaries.IsArmTrustedFlavor,
365       help='Filter out trusted arm toolchains.')
366   options, args = parser.parse_args(args)
367
368   if not options.arm_untrusted:
369     if options.filter_out_predicates is None:
370       options.filter_out_predicates = []
371     options.filter_out_predicates.append(toolchainbinaries.IsArmUntrustedFlavor)
372
373   if not options.allow_bionic:
374     if options.filter_out_predicates is None:
375       options.filter_out_predicates = []
376     options.filter_out_predicates.append(toolchainbinaries.IsBionicFlavor)
377
378   if len(args) > 1:
379     parser.error('Expecting only one version file.')
380   return options, args
381
382
383 def ScriptDependencyTimestamp():
384   """Determine the timestamp for the most recently changed script."""
385   src_list = ['download_toolchains.py', '../pynacl/download_utils.py',
386               '../pynacl/file_tools.py', '../pynacl/platform.py',
387               'cygtar.py', '../pynacl/http_download.py']
388   srcs = [os.path.join(SCRIPT_DIR, src) for src in src_list]
389   src_times = []
390
391   for src in srcs:
392     src_times.append(os.stat(src).st_mtime)
393   return sorted(src_times)[-1]
394
395
396 def main(args):
397   script_time = ScriptDependencyTimestamp()
398   options, version_files = ParseArgs(args)
399
400   # If not provided, default to native_client/toolchain_versions.txt
401   if not version_files:
402     version_files = [os.path.join(PARENT_DIR, 'TOOL_REVISIONS')]
403   versions = LoadVersions(version_files[0])
404
405   if options.hashes:
406     print '  (Calculating, may take a second...)'
407     print '-' * 70
408     sys.stdout.flush()
409     ShowUpdatedDEPS(options, versions)
410     print '-' * 70
411     return 0
412
413   host_os = pynacl.platform.GetOS()
414   arch = pynacl.platform.GetArch()
415   platform_mapping = toolchainbinaries.PLATFORM_MAPPING
416   flavors = [flavor
417              for flavor in platform_mapping[host_os][arch]
418              if IsFlavorNeeded(options, flavor)]
419
420   for flavor in flavors:
421     version = VersionSelect(versions, flavor)
422     urls = FlavorUrls(options, versions, flavor)
423     dst = FlavorOutDir(options, flavor)
424     hashes = FlavorHashes(versions, flavor)
425     flavor_name = FlavorName(flavor)
426
427     if version == 'latest':
428       print flavor + ': downloading latest version...'
429       force = True
430     else:
431       force = False
432
433     try:
434       if SyncFlavor(flavor, urls, dst, hashes, script_time, force=force,
435                     keep=options.keep, verbose=options.verbose):
436         print flavor_name + ': updated to version ' + version + '.'
437       else:
438         print flavor_name + ': already up to date.'
439     except pynacl.download_utils.HashError, e:
440       print str(e)
441       print '-' * 70
442       print 'You probably want to update the %s hashes to:' % version_files[0]
443       print '  (Calculating, may take a second...)'
444       print '-' * 70
445       sys.stdout.flush()
446       ShowUpdatedDEPS(options, versions)
447       print '-' * 70
448       return 1
449   return 0
450
451
452 if __name__ == '__main__':
453   sys.exit(main(sys.argv[1:]))