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