4e9ac1b1929a31beb70d3689071527113536a7d1
[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 OldFlavorOutDir(options, flavor):
132   """Given a flavor, returns old flavor directory (transitional)."""
133   if isinstance(flavor, tuple):
134     return os.path.join(options.toolchain_dir, flavor[0])
135   else:
136     return os.path.join(options.toolchain_dir, flavor)
137
138
139 def FlavorName(flavor):
140   """Given a flavor, get a string name for it."""
141   if isinstance(flavor, tuple):
142     return flavor[0]
143   else:
144     return flavor
145
146
147 def FlavorComponentNames(flavor):
148   if isinstance(flavor, tuple):
149     return flavor[1:]
150   else:
151     return [flavor]
152
153
154 def FlavorUrls(options, versions, flavor):
155   """Given a flavor, get a list of the URLs of its components."""
156   if isinstance(flavor, tuple):
157     ids = [versions[i] for i in flavor[1:]]
158     return [toolchainbinaries.EncodeToolchainUrl(
159             options.base_once_url, i, 'new') for i in ids]
160   else:
161     return [toolchainbinaries.EncodeToolchainUrl(
162             options.base_url, VersionSelect(versions, flavor), flavor)]
163
164
165 def FlavorHashes(versions, flavor):
166   """Given a flavor, get the list of hashes of its components."""
167   if isinstance(flavor, tuple):
168     return [HashSelect(versions, i) for i in flavor[1:]]
169   else:
170     return [HashSelect(versions, flavor)]
171
172
173 def GetUpdatedDEPS(options, versions):
174   """Return a suggested DEPS toolchain hash update for all platforms.
175
176   Arguments:
177     options: options from the command line.
178   """
179   flavors = set()
180   for pm in toolchainbinaries.PLATFORM_MAPPING.itervalues():
181     for flavorlist in pm.itervalues():
182       for flavor in flavorlist:
183         if IsFlavorNeeded(options, flavor):
184           flavors.add(flavor)
185   new_deps = {}
186   for flavor in flavors:
187     names = FlavorComponentNames(flavor)
188     urls = FlavorUrls(options, versions, flavor)
189     for name, url in zip(names, urls):
190       new_deps[name] = pynacl.download_utils.HashUrl(url)
191   return new_deps
192
193
194 def ShowUpdatedDEPS(options, versions):
195   """Print a suggested DEPS toolchain hash update for all platforms.
196
197   Arguments:
198     options: options from the command line.
199   """
200   for flavor, value in sorted(GetUpdatedDEPS(options, versions).iteritems()):
201     keyname = HashKey(flavor)
202     print '%s=%s' % (keyname, value)
203     sys.stdout.flush()
204
205
206 def SyncFlavor(flavor, urls, dst, hashes, min_time, keep=False, force=False,
207                verbose=False):
208   """Sync a flavor of the nacl toolchain
209
210   Arguments:
211     flavor: short directory name of the toolchain flavor.
212     urls: urls to download the toolchain flavor from.
213     dst: destination directory for the toolchain.
214     hashes: expected hashes of the toolchain.
215   """
216
217   toolchain_dir = os.path.join(PARENT_DIR, 'toolchain')
218   if not os.path.exists(toolchain_dir):
219     os.makedirs(toolchain_dir)
220   download_dir = os.path.join(toolchain_dir, '.tars')
221
222   prefix = 'tmp_unpacked_toolchain_'
223   suffix = '.tmp'
224
225   # Attempt to cleanup.
226   for path in os.listdir(toolchain_dir):
227     if path.startswith(prefix) and path.endswith(suffix):
228       full_path = os.path.join(toolchain_dir, path)
229       try:
230         print 'Cleaning up %s...' % full_path
231         pynacl.file_tools.RemoveDir(full_path)
232       except Exception, e:
233         print 'Failed cleanup with: ' + str(e)
234
235   # If we are forcing a sync, then ignore stamp
236   if force:
237     stamp_dir = None
238   else:
239     stamp_dir = dst
240
241   filepaths = []
242   need_sync = False
243
244   index = 0
245   for url, hash_val in zip(urls, hashes):
246     # Build the tarfile name from the url
247     # http://foo..../bar.tar.gz -> bar
248     filepath, ext = url.split('/')[-1].split('.', 1)
249     # For filenames containing _SHA1s, drop the sha1 part.
250     if SHA1_IN_FILENAME.match(filepath) is not None:
251       filepath = filepath.rsplit('_', 1)[0]
252     # Put it in the download dir and add back extension.
253     filepath = os.path.join(download_dir, '.'.join([filepath, ext]))
254     filepaths.append(filepath)
255     # If we did not need to synchronize, then we are done
256     if pynacl.download_utils.SyncURL(url, filepath, stamp_dir=stamp_dir,
257                                      min_time=min_time, hash_val=hash_val,
258                                      stamp_index=index,
259                                      keep=keep, verbose=verbose):
260       need_sync = True
261     index += 1
262
263   if not need_sync:
264     return False
265
266   # Compute the new hashes for each file.
267   new_hashes = []
268   for filepath in filepaths:
269     new_hashes.append(pynacl.download_utils.HashFile(filepath))
270
271   untar_dir = tempfile.mkdtemp(
272       suffix=suffix, prefix=prefix, dir=toolchain_dir)
273   try:
274     for filepath in filepaths:
275       tar = cygtar.CygTar(filepath, 'r:*', verbose=verbose)
276       curdir = os.getcwd()
277       os.chdir(untar_dir)
278       try:
279         tar.Extract()
280         tar.Close()
281       finally:
282         os.chdir(curdir)
283
284       if not keep:
285         os.remove(filepath)
286
287     # TODO(bradnelson_): get rid of this when toolchain tarballs flattened.
288     if 'bionic' in flavor:
289       src = os.path.join(untar_dir, flavor)
290     elif isinstance(flavor, tuple) or 'arm' in flavor or 'pnacl' in flavor:
291       src = os.path.join(untar_dir)
292     elif 'newlib' in flavor:
293       src = os.path.join(untar_dir, 'sdk', 'nacl-sdk')
294     else:
295       src = os.path.join(untar_dir, 'toolchain', flavor)
296     pynacl.file_tools.MoveDirCleanly(src, dst)
297   finally:
298     try:
299       pynacl.file_tools.RemoveDir(untar_dir)
300     except Exception, e:
301       print 'Failed cleanup with: ' + str(e)
302       print 'Continuing on original exception...'
303   pynacl.download_utils.WriteSourceStamp(dst, '\n'.join(urls))
304   pynacl.download_utils.WriteHashStamp(dst, '\n'.join(new_hashes))
305   return True
306
307
308 def ParseArgs(args):
309   parser = optparse.OptionParser()
310   parser.add_option(
311       '-b', '--base-url', dest='base_url',
312       default=toolchainbinaries.BASE_DOWNLOAD_URL,
313       help='base url to download from')
314   parser.add_option(
315       '--base-once-url', dest='base_once_url',
316       default=toolchainbinaries.BASE_ONCE_DOWNLOAD_URL,
317       help='base url to download new toolchain artifacts from')
318   parser.add_option(
319       '-c', '--hashes', dest='hashes',
320       default=False,
321       action='store_true',
322       help='Calculate hashes.')
323   parser.add_option(
324       '-k', '--keep', dest='keep',
325       default=False,
326       action='store_true',
327       help='Keep the downloaded tarballs.')
328   parser.add_option(
329       '-q', '--quiet', dest='verbose',
330       default=True,
331       action='store_false',
332       help='Produce less output.')
333   parser.add_option(
334       '--toolchain-dir', dest='toolchain_dir',
335       default=os.path.join(PARENT_DIR, 'toolchain'),
336       help='(optional) location of toolchain directory')
337   parser.add_option(
338       '--nacl-newlib-only', dest='filter_out_predicates',
339       action='append_const', const=toolchainbinaries.IsNotNaClNewlibFlavor,
340       help='download only the non-pnacl newlib toolchain')
341   parser.add_option(
342       '--allow-bionic', dest='allow_bionic', action='store_true',
343       default=False,
344       help='Allow download of bionic toolchain.')
345   parser.add_option(
346       '--no-pnacl', dest='filter_out_predicates', action='append_const',
347       const=toolchainbinaries.IsPnaclFlavor,
348       help='Filter out PNaCl toolchains.')
349   parser.add_option(
350       '--no-x86', dest='filter_out_predicates', action='append_const',
351       const=toolchainbinaries.IsX86Flavor,
352       help='Filter out x86 toolchains.')
353   parser.add_option(
354       '--arm-untrusted', dest='arm_untrusted', action='store_true',
355       default=False, help='Add arm untrusted toolchains.')
356   parser.add_option(
357       '--no-pnacl-translator', dest='filter_out_predicates',
358       action='append_const',
359       const=toolchainbinaries.IsSandboxedTranslatorFlavor,
360       help='Filter out PNaCl sandboxed translator.')
361   parser.add_option(
362       '--no-arm-trusted', dest='filter_out_predicates', action='append_const',
363       const=toolchainbinaries.IsArmTrustedFlavor,
364       help='Filter out trusted arm toolchains.')
365   options, args = parser.parse_args(args)
366
367   if not options.arm_untrusted:
368     if options.filter_out_predicates is None:
369       options.filter_out_predicates = []
370     options.filter_out_predicates.append(toolchainbinaries.IsArmUntrustedFlavor)
371
372   if not options.allow_bionic:
373     if options.filter_out_predicates is None:
374       options.filter_out_predicates = []
375     options.filter_out_predicates.append(toolchainbinaries.IsBionicFlavor)
376
377   if len(args) > 1:
378     parser.error('Expecting only one version file.')
379   return options, args
380
381
382 def ScriptDependencyTimestamp():
383   """Determine the timestamp for the most recently changed script."""
384   src_list = ['download_toolchains.py', '../pynacl/download_utils.py',
385               '../pynacl/file_tools.py', '../pynacl/platform.py',
386               'cygtar.py', '../pynacl/http_download.py']
387   srcs = [os.path.join(SCRIPT_DIR, src) for src in src_list]
388   src_times = []
389
390   for src in srcs:
391     src_times.append(os.stat(src).st_mtime)
392   return sorted(src_times)[-1]
393
394
395 def main(args):
396   script_time = ScriptDependencyTimestamp()
397   options, version_files = ParseArgs(args)
398
399   # If not provided, default to native_client/toolchain_versions.txt
400   if not version_files:
401     version_files = [os.path.join(PARENT_DIR, 'TOOL_REVISIONS')]
402   versions = LoadVersions(version_files[0])
403
404   if options.hashes:
405     print '  (Calculating, may take a second...)'
406     print '-' * 70
407     sys.stdout.flush()
408     ShowUpdatedDEPS(options, versions)
409     print '-' * 70
410     return 0
411
412   host_os = pynacl.platform.GetOS()
413   arch = pynacl.platform.GetArch()
414   platform_mapping = toolchainbinaries.PLATFORM_MAPPING
415   flavors = [flavor
416              for flavor in platform_mapping[host_os][arch]
417              if IsFlavorNeeded(options, flavor)]
418
419   for flavor in flavors:
420     version = VersionSelect(versions, flavor)
421     urls = FlavorUrls(options, versions, flavor)
422     dst = FlavorOutDir(options, flavor)
423     old_dst = OldFlavorOutDir(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       # While we are still transitioning to the new toolchain directory format,
435       # keep on syncing to the old destination. Eventually we will remove this.
436       SyncFlavor(flavor, urls, old_dst, hashes, script_time, force=force,
437                  keep=options.keep, verbose=options.verbose)
438
439       if SyncFlavor(flavor, urls, dst, hashes, script_time, force=force,
440                     keep=options.keep, verbose=options.verbose):
441         print flavor_name + ': updated to version ' + version + '.'
442       else:
443         print flavor_name + ': already up to date.'
444     except pynacl.download_utils.HashError, e:
445       print str(e)
446       print '-' * 70
447       print 'You probably want to update the %s hashes to:' % version_files[0]
448       print '  (Calculating, may take a second...)'
449       print '-' * 70
450       sys.stdout.flush()
451       ShowUpdatedDEPS(options, versions)
452       print '-' * 70
453       return 1
454   return 0
455
456
457 if __name__ == '__main__':
458   sys.exit(main(sys.argv[1:]))