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