OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
2 # Copyright 2014 The Chromium 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 # vim: set ts=2 sw=2 et sts=2 ai: | |
6 | |
7 """Download files for your platfrom and extract its contents into a directory. | |
8 | |
9 Uses download_from_google_storage for the actual download process. | |
10 Uses tar to extract the files from the archive. | |
11 | |
12 The input is a list of sha1 files suitable for download_from_google_storage. | |
13 """ | |
14 | |
15 """ | |
16 TODO(mithro): Replace the other download/extract scripts that exist in the | |
17 chrome tree. | |
18 | |
19 Replace chrome/installer/linux/sysroot_scripts/install-debian.wheezy.sysroot.py | |
20 download_and_extract.py \\ | |
21 --default-file-arch Linux \\ | |
22 --bucket chrome-linux-sysroot \\ | |
23 --extract ????/debian_wheezy_???-sysroot \\ | |
24 <sha1 files??> | |
25 | |
26 Replace build/linux/install-arm-sysroot.py | |
27 download_and_extract.py \\ | |
28 --bucket nativeclient-archive2 \\ | |
29 --extract build/linux/arm-sys-root \\ | |
30 <sha1 files??> | |
31 | |
32 Replace download part of tools/clang/scripts/update.(py|sh) | |
33 download_and_extract.py \\ | |
34 --bucket chromium-browser-clang \\ | |
35 --extract third_party/llvm-build/Release+Asserts \\ | |
36 <sha1 files??> | |
37 """ | |
38 | |
39 | |
40 import optparse | |
41 import os | |
42 import re | |
43 import shutil | |
44 import subprocess | |
45 import sys | |
46 import types | |
47 | |
48 | |
49 def MatchPatterns(mapping, string, error): | |
50 if not isinstance(string, types.StringTypes): | |
51 raise TypeError("Can't match %r (a %s)" % (string, type(string))) | |
52 | |
53 for i, (pattern, result) in enumerate(mapping): | |
54 if not isinstance(pattern, types.StringTypes): | |
55 raise TypeError( | |
56 "%r (%s) is not valid regex pattern (pattern %i - %s)." % ( | |
57 pattern, type(pattern), i, (pattern, result))) | |
58 if re.search(pattern, string, re.I): | |
59 return result | |
60 raise ValueError(error % string) | |
61 | |
62 | |
63 def ExtractAndNormalizeArch(string): | |
64 """Extract and normalize an architecture string. | |
65 >>> # Linux arch / uname output | |
66 >>> ExtractAndNormalizeArch('x86_64') | |
67 'amd64' | |
68 >>> ExtractAndNormalizeArch('i686') | |
69 'i386' | |
70 >>> ExtractAndNormalizeArch('i386') | |
71 'i386' | |
72 | |
73 >>> # autoconf "platform" tuple | |
74 >>> ExtractAndNormalizeArch('i686-pc-linux-gnu') | |
75 'i386' | |
76 >>> ExtractAndNormalizeArch('x86_64-unknown-linux-gnu') | |
77 'amd64' | |
78 | |
79 >>> # platform.machine() | |
80 >>> ExtractAndNormalizeArch('i386') | |
81 'i386' | |
82 >>> ExtractAndNormalizeArch('x86_64') | |
83 'amd64' | |
84 | |
85 >>> # GYP Defines | |
86 >>> ExtractAndNormalizeArch('target_arch=x64') | |
87 'amd64' | |
88 >>> ExtractAndNormalizeArch('target_arch=ia32') | |
89 'i386' | |
90 >>> ExtractAndNormalizeArch('x64') | |
91 'amd64' | |
92 >>> ExtractAndNormalizeArch('ia32') | |
93 'i386' | |
94 | |
95 >>> # Empty Arch | |
96 >>> ExtractAndNormalizeArch('') | |
97 '' | |
98 """ | |
99 ARCH_MAPPING = [ | |
100 # Linux 'arch' outputs | |
101 ('i[3456]?86', 'i386'), | |
102 ('i86pc', 'i386'), | |
103 ('x86_64', 'amd64'), | |
104 ('amd64', 'amd64'), | |
105 ('mips64', 'mips64'), # Must be before mips | |
106 ('mips', 'mips'), | |
107 ('arm', 'arm'), | |
108 ('aarch', 'arm64'), | |
109 # Windows | |
110 ('win32', 'i386'), | |
111 ('win64', 'amd64'), | |
112 # GYP defines | |
113 ('ia32', 'i386'), | |
114 ('x64', 'amd64'), | |
115 # Empty arch | |
116 ('^$', ''), | |
117 ] | |
118 return MatchPatterns( | |
119 ARCH_MAPPING, string, | |
120 'Was not able to extract architecture from %s') | |
121 | |
122 | |
123 def ExtractAndNormalizeOS(string): | |
124 """Extract and normalize an OS string. | |
125 | |
126 >>> # Used by download_from_storage | |
127 >>> # sys.platform | |
128 >>> ExtractAndNormalizeOS('linux2') | |
129 'Linux' | |
130 >>> ExtractAndNormalizeOS('darwin') | |
131 'Mac' | |
132 >>> ExtractAndNormalizeOS('cygwin') | |
133 'Win' | |
134 >>> ExtractAndNormalizeOS('win32') | |
135 'Win' | |
136 >>> ExtractAndNormalizeOS('win64') | |
137 'Win' | |
138 | |
139 >>> # platform.system() | |
140 >>> ExtractAndNormalizeOS('Linux') | |
141 'Linux' | |
142 >>> ExtractAndNormalizeOS('Windows') | |
143 'Win' | |
144 | |
145 >>> # Used by tools/clang/scripts | |
146 >>> # uname -s | |
147 >>> ExtractAndNormalizeOS('Linux') | |
148 'Linux' | |
149 >>> ExtractAndNormalizeOS('Darwin') | |
150 'Mac' | |
151 | |
152 >>> # GYP defines | |
153 >>> ExtractAndNormalizeOS('win') | |
154 'Win' | |
155 >>> ExtractAndNormalizeOS('linux') | |
156 'Linux' | |
157 >>> ExtractAndNormalizeOS('mac') | |
158 'Mac' | |
159 | |
160 >>> # GNU triplets | |
161 >>> ExtractAndNormalizeOS('i686-pc-linux-gnu') | |
162 'Linux' | |
163 >>> ExtractAndNormalizeOS('x86_64-unknown-linux-gnu') | |
164 'Linux' | |
165 >>> ExtractAndNormalizeOS('i586-pc-mingw32') | |
166 'Win' | |
167 >>> ExtractAndNormalizeOS('i386-pc-cygwin') | |
168 'Win' | |
169 >>> ExtractAndNormalizeOS('i386-pc-win32') | |
170 'Win' | |
171 >>> ExtractAndNormalizeOS('x86_64-apple-darwin10') | |
172 'Mac' | |
173 """ | |
174 PLATFORM_MAPPING = [ | |
175 # Mac | |
176 ('darwin', 'Mac'), | |
177 ('mac', 'Mac'), | |
178 # Linux | |
179 ('linux.*', 'Linux'), | |
180 # Windows | |
181 ('cygwin', 'Win'), | |
182 ('mingw', 'Win'), | |
Lei Zhang
2014/04/02 08:32:22
Is this in use anywhere? download_from_google_stor
| |
183 ('win', 'Win'), | |
184 ] | |
185 return MatchPatterns( | |
186 PLATFORM_MAPPING, string, | |
187 'Was not able to extract operating system from %s') | |
188 | |
189 | |
190 def GetSystemArch(os_str): | |
191 if os_str == 'Linux': | |
192 # Try calling arch first, then fall back to uname | |
193 try: | |
194 return subprocess.check_output(['arch']).strip() | |
195 except subprocess.CalledProcessError, e: | |
196 # We want the architecture, which is roughly the machine hardware name | |
197 # in uname. | |
198 # -m, --machine; print the machine hardware name | |
199 # These other two are possibilities? | |
200 # -p, --processor; print the processor type or 'unknown' | |
201 # -i, --hardware-platform; print the hardware platform or 'unknown' | |
202 return subprocess.check_output(['uname', '-m']).strip() | |
203 else: | |
204 # TODO(mithro): Make this work under Mac / Windows | |
205 return '' | |
206 | |
207 | |
208 def FilterFilesByPlatform(files, target_os, target_arch, | |
Lei Zhang
2014/04/02 08:32:22
Design concern: I feel this is trying too hard to
| |
209 file_os_default=None, file_arch_default=''): | |
210 """Filter input files to given platform. | |
211 | |
212 We assume the target arch is the host arch if not overridden. | |
213 | |
214 >>> clang_style = [ | |
215 ... 'abc/Linux_ia32/xxx', | |
216 ... 'abc/Linux_x64/xxx', | |
217 ... 'abc/Mac/xxx', | |
218 ... 'abc/Win/xxx', | |
219 ... ] | |
220 >>> FilterFilesByPlatform(clang_style, 'Linux', 'i386') | |
221 ['abc/Linux_ia32/xxx'] | |
222 >>> FilterFilesByPlatform(clang_style, 'Linux', 'amd64') | |
223 ['abc/Linux_x64/xxx'] | |
224 >>> FilterFilesByPlatform(clang_style, 'Win', '') | |
225 ['abc/Win/xxx'] | |
226 >>> FilterFilesByPlatform(clang_style, 'Mac', '') | |
227 ['abc/Mac/xxx'] | |
228 | |
229 >>> gnu_style = [ | |
230 ... 'XXX-i686-pc-linux-gnu.XXX', | |
231 ... 'XXX-x86_64-unknown-linux-gnu.XXX', | |
232 ... 'XXX-i586-pc-mingw32.XXX', | |
233 ... 'XXX-i386-pc-win32.XXX', | |
234 ... 'XXX-x86_64-apple-darwin10.XXX', | |
235 ... ] | |
236 >>> FilterFilesByPlatform(gnu_style, 'Linux', 'i386') | |
237 ['XXX-i686-pc-linux-gnu.XXX'] | |
238 >>> FilterFilesByPlatform(gnu_style, 'Linux', 'amd64') | |
239 ['XXX-x86_64-unknown-linux-gnu.XXX'] | |
240 >>> FilterFilesByPlatform(gnu_style, 'Win', '') | |
241 ['XXX-i586-pc-mingw32.XXX', 'XXX-i386-pc-win32.XXX'] | |
242 >>> FilterFilesByPlatform(gnu_style, 'Mac', '') | |
243 ['XXX-x86_64-apple-darwin10.XXX'] | |
244 | |
245 >>> simple_no_os = [ | |
246 ... 'XXXX_amd64_XXX', | |
247 ... 'XXXX_i386_XXX', | |
248 ... ] | |
249 >>> FilterFilesByPlatform( | |
250 ... simple_no_os, 'Linux', 'i386', file_os_default='Linux') | |
251 ['XXXX_i386_XXX'] | |
252 >>> FilterFilesByPlatform( | |
253 ... simple_no_os, 'Linux', 'amd64', file_os_default='Linux') | |
254 ['XXXX_amd64_XXX'] | |
255 >>> FilterFilesByPlatform( | |
256 ... simple_no_os, 'Win', '', file_os_default='Linux') | |
257 [] | |
258 >>> # Fails when no default is provided and can't extract from filename. | |
259 >>> FilterFilesByPlatform(simple_no_os, 'Linux', 'i386') | |
260 Traceback (most recent call last): | |
261 ... | |
262 ValueError: Was not able to extract operating system from XXXX_amd64_XXX | |
263 """ | |
264 files_for_platform = [] | |
265 for filename in files: | |
266 try: | |
267 file_os = ExtractAndNormalizeOS(filename) | |
268 except ValueError, e: | |
269 if file_os_default is None: | |
270 raise | |
271 file_os = file_os_default | |
272 | |
273 try: | |
274 file_arch = ExtractAndNormalizeArch(filename) | |
275 except ValueError: | |
276 if file_arch_default is None: | |
277 raise | |
278 file_arch = file_arch_default | |
279 | |
280 match = True | |
281 if target_os != '' and file_os != '': | |
282 match = target_os == file_os | |
283 | |
284 if target_arch != '' and file_arch != '': | |
285 match = match and target_arch == file_arch | |
286 | |
287 if match: | |
288 files_for_platform.append(filename) | |
289 | |
290 return files_for_platform | |
291 | |
292 | |
293 class StampFile(object): | |
294 """Stores a stamp for when an action occured. | |
295 | |
296 This is normally a revision number or checksum of the input files to the | |
297 process. Proper usage of stamping will mean a partial action is never | |
298 considered successful. | |
299 | |
300 Proper usage is; | |
301 * /Check/ -- Check the stamp file matches your revision/checksum. | |
302 * /Delete/ -- Delete the existing stamp file. | |
303 * /Clean up/ -- Clean up any partially failed previous attempt. | |
304 * /Do action/ -- Do your work. | |
305 * /Set/ -- Set the stamp file to your revision/checksum. | |
306 | |
307 For example: | |
308 >>> stamp = 'mySHA1sum' | |
309 >>> action_stamp = StampFile('stamp.action') | |
310 >>> # Check stamp | |
311 >>> if stamp != action_stamp.get(): # doctest: +SKIP | |
312 ... # Delete stamp | |
313 ... action_stamp.delete() | |
314 ... # Clean up | |
315 ... if os.path.exists(filename): | |
316 ... os.unlink(filename) | |
317 ... # Action | |
318 ... do_download(filename) | |
319 ... # Set stamp | |
320 ... action_stamp.set(stamp) | |
321 """ | |
322 | |
323 def __init__(self, filename): | |
324 self.filename = filename | |
325 | |
326 def get(self): | |
327 try: | |
328 return file(self.filename, 'r').read() | |
329 except IOError, e: | |
330 # We use NaN because it is not equal even to itself, so equality checks | |
331 # will always fail. | |
332 return float('NaN') | |
333 | |
334 def delete(self): | |
335 if os.path.exists(self.filename): | |
336 os.unlink(self.filename) | |
337 | |
338 def set(self, stamp): | |
339 if os.path.exists(self.filename): | |
340 raise IOError('Stamp file %r currently exist!' % filename) | |
341 | |
342 f = file(self.filename, 'w') | |
343 f.write(stamp) | |
344 # Force a fsync so this ends up on disk and other processes can see it. | |
345 os.fsync(f) | |
346 f.close() | |
347 | |
348 | |
349 def main(args): | |
350 usage = ( | |
351 'usage: %prog [options] targets\n\n' | |
352 'Downloads and extracts targets which match the platform.\n' | |
353 'Targets must be a list of a .sha1 file, containing a sha1 sum' | |
354 'used by download_from_google_storage tool.') | |
355 | |
356 parser = optparse.OptionParser(usage) | |
357 parser.add_option( | |
358 '-b', '--bucket', | |
359 help='Google Storage bucket to fetch from.') | |
360 parser.add_option( | |
361 '-e', '--extract', | |
362 help='Directory to extract the downloaded files into.') | |
363 parser.add_option( | |
364 '-a', '--target-arch', | |
365 help='Override architecture to given value.') | |
366 parser.add_option( | |
367 '', '--default-file-arch', | |
368 help='Override input file architecture to given value.') | |
369 parser.add_option( | |
370 '', '--default-file-os', | |
371 help='Override input file operating system to given value.') | |
372 parser.add_option( | |
373 '', '--self-test', default=False, action="store_true", | |
374 help='Run the internal tests.') | |
375 | |
376 (options, files) = parser.parse_args() | |
377 | |
378 if options.self_test: | |
379 import doctest | |
380 return doctest.testmod() | |
381 | |
382 errors = [] | |
383 if not options.bucket: | |
384 errors.append('--bucket (-b) is required option.') | |
385 | |
386 if not options.extract: | |
387 errors.append('--extract (-e) is required option.') | |
388 | |
389 if not files: | |
390 errors.append('Need to specify files to download.') | |
391 | |
392 for filename in files: | |
393 if not os.path.exists(filename): | |
394 errors.append('File %s does not exist.' % filename) | |
395 | |
396 if errors: | |
397 parser.error('\n '.join(errors)) | |
398 | |
399 # Figure out which files we want to download. Filter by the current platform | |
400 # the tool is being run on. | |
401 target_os = ExtractAndNormalizeOS(sys.platform) | |
402 | |
403 target_arch = None | |
404 if options.target_arch: | |
405 target_arch = options.target_arch | |
406 else: | |
407 # Try to get target_arch out of GYP | |
408 gyp_target_arch = re.search( | |
409 '(target_arch=[^ ]*)', os.environ.get('GYP_DEFINES', '')) | |
410 if gyp_target_arch: | |
411 target_arch = gyp_target_arch.groups(1) | |
412 | |
413 if target_arch is None: # '' is a valid target_arch | |
414 target_arch = GetSystemArch(target_os) | |
415 | |
416 target_arch = ExtractAndNormalizeArch(target_arch) | |
417 | |
418 todo = FilterFilesByPlatform( | |
419 files, | |
420 target_os, | |
421 target_arch, | |
422 file_os_default=options.default_file_os, | |
423 file_arch_default=options.default_file_arch) | |
424 | |
425 if len(todo) == 0: | |
426 print 'No files to download.' | |
427 return 0 | |
428 | |
429 elif len(todo) > 1: | |
430 # TODO(mithro): Support downloading and extracting multiple files. | |
431 parser.error( | |
432 'Matched multiple files on this platform!\n' + '\n'.join(todo)) | |
433 return 1 | |
434 | |
435 # Process the files | |
436 for filename in todo: | |
437 filename = os.path.abspath(filename) | |
438 | |
439 basename, ext = os.path.splitext(filename) | |
440 assert ext == '.sha1', 'Input filename %s does not end in .sha1' % filename | |
441 sha1 = file(filename, 'r').read().strip() | |
442 | |
443 # Download the tarball | |
444 download_stamp = StampFile('%s.stamp.download' % basename) | |
445 if sha1 != download_stamp.get(): | |
446 print "Downloading", basename | |
447 download_stamp.delete() | |
448 if os.path.exists(basename): | |
449 os.unlink(basename) | |
450 | |
451 subprocess.check_call([ | |
452 'download_from_google_storage', | |
453 '--no_resume', | |
454 '--no_auth', | |
455 '--bucket', options.bucket, | |
456 '-s', filename]) | |
457 | |
458 download_stamp.set(sha1) | |
459 | |
460 # Extract the tarball | |
461 extract_stamp = StampFile('%s.stamp.untar' % basename) | |
462 if sha1 != extract_stamp.get(): | |
463 print "Extracting", basename | |
464 extract_stamp.delete() | |
465 if os.path.exists(options.extract): | |
466 shutil.rmtree(options.extract) | |
467 | |
468 os.makedirs(options.extract) | |
469 | |
470 # TODO(mithro): Maybe use https://docs.python.org/2/library/tarfile.html | |
471 # rather than tar cmdline. | |
472 subprocess.check_call( | |
473 ['tar', 'axf', basename], cwd=options.extract) | |
474 | |
475 extract_stamp.set(sha1) | |
476 | |
477 return 0 | |
478 | |
479 | |
480 if __name__ == '__main__': | |
481 sys.exit(main(sys.argv)) | |
OLD | NEW |