OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. | |
3 # | |
4 # Use of this source code is governed by a BSD-style license | |
5 # that can be found in the LICENSE file in the root of the source | |
6 # tree. An additional intellectual property rights grant can be found | |
7 # in the file PATENTS. All contributing project authors may | |
8 # be found in the AUTHORS file in the root of the source tree. | |
9 | |
10 """Setup links to a Chromium checkout for WebRTC. | |
11 | |
12 WebRTC standalone shares a lot of dependencies and build tools with Chromium. | |
13 To do this, many of the paths of a Chromium checkout is emulated by creating | |
14 symlinks to files and directories. This script handles the setup of symlinks to | |
15 achieve this. | |
16 """ | |
17 | |
18 | |
19 import ctypes | |
20 import errno | |
21 import logging | |
22 import optparse | |
23 import os | |
24 import shelve | |
25 import shutil | |
26 import subprocess | |
27 import sys | |
28 import textwrap | |
29 | |
30 | |
31 DIRECTORIES = [ | |
32 'base', | |
33 'build', | |
34 'buildtools', | |
35 'testing', | |
36 'third_party/afl', | |
37 'third_party/binutils', | |
38 'third_party/boringssl', | |
39 'third_party/catapult', | |
40 'third_party/closure_compiler', | |
41 'third_party/colorama', | |
42 'third_party/expat', | |
43 'third_party/ffmpeg', | |
44 'third_party/instrumented_libraries', | |
45 'third_party/jsoncpp', | |
46 'third_party/libFuzzer', | |
47 'third_party/libjpeg', | |
48 'third_party/libjpeg_turbo', | |
49 'third_party/libsrtp', | |
50 'third_party/libvpx', | |
51 'third_party/libyuv', | |
52 'third_party/lss', | |
53 'third_party/ocmock', | |
54 'third_party/openh264', | |
55 'third_party/openmax_dl', | |
56 'third_party/opus', | |
57 'third_party/proguard', | |
58 'third_party/protobuf', | |
59 'third_party/sqlite', | |
60 'third_party/usrsctp', | |
61 'third_party/yasm', | |
62 'third_party/zlib', | |
63 'tools/clang', | |
64 'tools/clang_format_merge_driver', | |
65 'tools/determinism', | |
66 'tools/generate_library_loader', | |
67 'tools/generate_stubs', | |
68 'tools/gn', | |
69 'tools/grit', | |
70 'tools/gyp', | |
71 'tools/luci-go', | |
72 'tools/memory', | |
73 'tools/protoc_wrapper', | |
74 'tools/python', | |
75 'tools/swarming_client', | |
76 'tools/valgrind', | |
77 'tools/vim', | |
78 'tools/win', | |
79 ] | |
80 | |
81 from sync_chromium import get_target_os_list | |
82 target_os = get_target_os_list() | |
83 if 'android' in target_os: | |
84 DIRECTORIES += [ | |
85 'third_party/accessibility_test_framework', | |
86 'third_party/android_platform', | |
87 'third_party/android_support_test_runner', | |
88 'third_party/android_tools', | |
89 'third_party/apache_velocity', | |
90 'third_party/ashmem', | |
91 'third_party/bouncycastle', | |
92 'third_party/byte_buddy', | |
93 'third_party/ced', | |
94 'third_party/espresso', | |
95 'third_party/guava', | |
96 'third_party/hamcrest', | |
97 'third_party/icu', | |
98 'third_party/icu4j', | |
99 'third_party/ijar', | |
100 'third_party/intellij', | |
101 'third_party/javax_inject', | |
102 'third_party/jsr-305', | |
103 'third_party/junit', | |
104 'third_party/libxml', | |
105 'third_party/mockito', | |
106 'third_party/modp_b64', | |
107 'third_party/objenesis', | |
108 'third_party/ow2_asm', | |
109 'third_party/requests', | |
110 'third_party/robolectric', | |
111 'third_party/sqlite4java', | |
112 'third_party/tcmalloc', | |
113 'tools/android', | |
114 ] | |
115 | |
116 FILES = { | |
117 'third_party/BUILD.gn': None, | |
118 } | |
119 | |
120 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
121 CHROMIUM_CHECKOUT = os.path.join('chromium', 'src') | |
122 LINKS_DB = 'links' | |
123 | |
124 # Version management to make future upgrades/downgrades easier to support. | |
125 SCHEMA_VERSION = 1 | |
126 | |
127 | |
128 def query_yes_no(question, default=False): | |
129 """Ask a yes/no question via raw_input() and return their answer. | |
130 | |
131 Modified from http://stackoverflow.com/a/3041990. | |
132 """ | |
133 prompt = " [%s/%%s]: " | |
134 prompt = prompt % ('Y' if default is True else 'y') | |
135 prompt = prompt % ('N' if default is False else 'n') | |
136 | |
137 if default is None: | |
138 default = 'INVALID' | |
139 | |
140 while True: | |
141 sys.stdout.write(question + prompt) | |
142 choice = raw_input().lower() | |
143 if choice == '' and default != 'INVALID': | |
144 return default | |
145 | |
146 if 'yes'.startswith(choice): | |
147 return True | |
148 elif 'no'.startswith(choice): | |
149 return False | |
150 | |
151 print "Please respond with 'yes' or 'no' (or 'y' or 'n')." | |
152 | |
153 | |
154 # Actions | |
155 class Action(object): | |
156 def __init__(self, dangerous): | |
157 self.dangerous = dangerous | |
158 | |
159 def announce(self, planning): | |
160 """Log a description of this action. | |
161 | |
162 Args: | |
163 planning - True iff we're in the planning stage, False if we're in the | |
164 doit stage. | |
165 """ | |
166 pass | |
167 | |
168 def doit(self, links_db): | |
169 """Execute the action, recording what we did to links_db, if necessary.""" | |
170 pass | |
171 | |
172 | |
173 class Remove(Action): | |
174 def __init__(self, path, dangerous): | |
175 super(Remove, self).__init__(dangerous) | |
176 self._priority = 0 | |
177 self._path = path | |
178 | |
179 def announce(self, planning): | |
180 log = logging.warn | |
181 filesystem_type = 'file' | |
182 if not self.dangerous: | |
183 log = logging.info | |
184 filesystem_type = 'link' | |
185 if planning: | |
186 log('Planning to remove %s: %s', filesystem_type, self._path) | |
187 else: | |
188 log('Removing %s: %s', filesystem_type, self._path) | |
189 | |
190 def doit(self, _): | |
191 os.remove(self._path) | |
192 | |
193 | |
194 class Rmtree(Action): | |
195 def __init__(self, path): | |
196 super(Rmtree, self).__init__(dangerous=True) | |
197 self._priority = 0 | |
198 self._path = path | |
199 | |
200 def announce(self, planning): | |
201 if planning: | |
202 logging.warn('Planning to remove directory: %s', self._path) | |
203 else: | |
204 logging.warn('Removing directory: %s', self._path) | |
205 | |
206 def doit(self, _): | |
207 if sys.platform.startswith('win'): | |
208 # shutil.rmtree() doesn't work on Windows if any of the directories are | |
209 # read-only, which svn repositories are. | |
210 subprocess.check_call(['rd', '/q', '/s', self._path], shell=True) | |
211 else: | |
212 shutil.rmtree(self._path) | |
213 | |
214 | |
215 class Makedirs(Action): | |
216 def __init__(self, path): | |
217 super(Makedirs, self).__init__(dangerous=False) | |
218 self._priority = 1 | |
219 self._path = path | |
220 | |
221 def doit(self, _): | |
222 try: | |
223 os.makedirs(self._path) | |
224 except OSError as e: | |
225 if e.errno != errno.EEXIST: | |
226 raise | |
227 | |
228 | |
229 class Symlink(Action): | |
230 def __init__(self, source_path, link_path): | |
231 super(Symlink, self).__init__(dangerous=False) | |
232 self._priority = 2 | |
233 self._source_path = source_path | |
234 self._link_path = link_path | |
235 | |
236 def announce(self, planning): | |
237 if planning: | |
238 logging.info( | |
239 'Planning to create link from %s to %s', self._link_path, | |
240 self._source_path) | |
241 else: | |
242 logging.debug( | |
243 'Linking from %s to %s', self._link_path, self._source_path) | |
244 | |
245 def doit(self, links_db): | |
246 # Files not in the root directory need relative path calculation. | |
247 # On Windows, use absolute paths instead since NTFS doesn't seem to support | |
248 # relative paths for symlinks. | |
249 if sys.platform.startswith('win'): | |
250 source_path = os.path.abspath(self._source_path) | |
251 else: | |
252 if os.path.dirname(self._link_path) != self._link_path: | |
253 source_path = os.path.relpath(self._source_path, | |
254 os.path.dirname(self._link_path)) | |
255 | |
256 os.symlink(source_path, os.path.abspath(self._link_path)) | |
257 links_db[self._source_path] = self._link_path | |
258 | |
259 | |
260 class LinkError(IOError): | |
261 """Failed to create a link.""" | |
262 pass | |
263 | |
264 | |
265 # Use junctions instead of symlinks on the Windows platform. | |
266 if sys.platform.startswith('win'): | |
267 def symlink(source_path, link_path): | |
268 if os.path.isdir(source_path): | |
269 subprocess.check_call(['cmd.exe', '/c', 'mklink', '/J', link_path, | |
270 source_path]) | |
271 else: | |
272 # Don't create symlinks to files on Windows, just copy the file instead | |
273 # (there's no way to create a link without administrator's privileges). | |
274 shutil.copy(source_path, link_path) | |
275 os.symlink = symlink | |
276 | |
277 | |
278 class WebRTCLinkSetup(object): | |
279 def __init__(self, links_db, force=False, dry_run=False, prompt=False): | |
280 self._force = force | |
281 self._dry_run = dry_run | |
282 self._prompt = prompt | |
283 self._links_db = links_db | |
284 | |
285 def CreateLinks(self, on_bot): | |
286 logging.debug('CreateLinks') | |
287 # First, make a plan of action | |
288 actions = [] | |
289 | |
290 for source_path, link_path in FILES.iteritems(): | |
291 actions += self._ActionForPath( | |
292 source_path, link_path, check_fn=os.path.isfile, check_msg='files') | |
293 for source_dir in DIRECTORIES: | |
294 actions += self._ActionForPath( | |
295 source_dir, None, check_fn=os.path.isdir, | |
296 check_msg='directories') | |
297 | |
298 if not on_bot and self._force: | |
299 # When making the manual switch from legacy SVN checkouts to the new | |
300 # Git-based Chromium DEPS, the .gclient_entries file that contains cached | |
301 # URLs for all DEPS entries must be removed to avoid future sync problems. | |
302 entries_file = os.path.join(os.path.dirname(ROOT_DIR), '.gclient_entries') | |
303 if os.path.exists(entries_file): | |
304 actions.append(Remove(entries_file, dangerous=True)) | |
305 | |
306 actions.sort() | |
307 | |
308 if self._dry_run: | |
309 for action in actions: | |
310 action.announce(planning=True) | |
311 logging.info('Not doing anything because dry-run was specified.') | |
312 sys.exit(0) | |
313 | |
314 if any(a.dangerous for a in actions): | |
315 logging.warn('Dangerous actions:') | |
316 for action in (a for a in actions if a.dangerous): | |
317 action.announce(planning=True) | |
318 print | |
319 | |
320 if not self._force: | |
321 logging.error(textwrap.dedent("""\ | |
322 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
323 A C T I O N R E Q I R E D | |
324 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
325 | |
326 Setting up the checkout requires creating symlinks to directories in the | |
327 Chromium checkout inside chromium/src. | |
328 To avoid disrupting developers, we've chosen to not delete directories | |
329 forcibly, in case you have some work in progress in one of them :) | |
330 | |
331 ACTION REQUIRED: | |
332 Before running `gclient sync|runhooks` again, you must run: | |
333 %s%s --force | |
334 | |
335 Which will replace all directories which now must be symlinks, after | |
336 prompting with a summary of the work-to-be-done. | |
337 """), 'python ' if sys.platform.startswith('win') else '', __file__) | |
338 sys.exit(1) | |
339 elif self._prompt: | |
340 if not query_yes_no('Would you like to perform the above plan?'): | |
341 sys.exit(1) | |
342 | |
343 for action in actions: | |
344 action.announce(planning=False) | |
345 action.doit(self._links_db) | |
346 | |
347 if not on_bot and self._force: | |
348 logging.info('Completed!\n\nNow run `gclient sync|runhooks` again to ' | |
349 'let the remaining hooks (that probably were interrupted) ' | |
350 'execute.') | |
351 | |
352 def CleanupLinks(self): | |
353 logging.debug('CleanupLinks') | |
354 for source, link_path in self._links_db.iteritems(): | |
355 if source == 'SCHEMA_VERSION': | |
356 continue | |
357 if os.path.islink(link_path) or sys.platform.startswith('win'): | |
358 # os.path.islink() always returns false on Windows | |
359 # See http://bugs.python.org/issue13143. | |
360 logging.debug('Removing link to %s at %s', source, link_path) | |
361 if not self._dry_run: | |
362 if os.path.exists(link_path): | |
363 if sys.platform.startswith('win') and os.path.isdir(link_path): | |
364 subprocess.check_call(['rmdir', '/q', '/s', link_path], | |
365 shell=True) | |
366 else: | |
367 os.remove(link_path) | |
368 del self._links_db[source] | |
369 | |
370 @staticmethod | |
371 def _ActionForPath(source_path, link_path=None, check_fn=None, | |
372 check_msg=None): | |
373 """Create zero or more Actions to link to a file or directory. | |
374 | |
375 This will be a symlink on POSIX platforms. On Windows it will result in: | |
376 * a junction for directories | |
377 * a copied file for single files. | |
378 | |
379 Args: | |
380 source_path: Path relative to the Chromium checkout root. | |
381 For readability, the path may contain slashes, which will | |
382 automatically be converted to the right path delimiter on Windows. | |
383 link_path: The location for the link to create. If omitted it will be the | |
384 same path as source_path. | |
385 check_fn: A function returning true if the type of filesystem object is | |
386 correct for the attempted call. Otherwise an error message with | |
387 check_msg will be printed. | |
388 check_msg: String used to inform the user of an invalid attempt to create | |
389 a file. | |
390 Returns: | |
391 A list of Action objects. | |
392 """ | |
393 def fix_separators(path): | |
394 if sys.platform.startswith('win'): | |
395 return path.replace(os.altsep, os.sep) | |
396 else: | |
397 return path | |
398 | |
399 assert check_fn | |
400 assert check_msg | |
401 link_path = link_path or source_path | |
402 link_path = fix_separators(link_path) | |
403 | |
404 source_path = fix_separators(source_path) | |
405 source_path = os.path.join(CHROMIUM_CHECKOUT, source_path) | |
406 if os.path.exists(source_path) and not check_fn: | |
407 raise LinkError('Can only to link to %s: tried to link to: %s' % | |
408 (check_msg, source_path)) | |
409 | |
410 if not os.path.exists(source_path): | |
411 logging.debug('Silently ignoring missing source: %s. This is to avoid ' | |
412 'errors on platform-specific dependencies.', source_path) | |
413 return [] | |
414 | |
415 actions = [] | |
416 | |
417 if os.path.exists(link_path) or os.path.islink(link_path): | |
418 if os.path.islink(link_path): | |
419 actions.append(Remove(link_path, dangerous=False)) | |
420 elif os.path.isfile(link_path): | |
421 actions.append(Remove(link_path, dangerous=True)) | |
422 elif os.path.isdir(link_path): | |
423 actions.append(Rmtree(link_path)) | |
424 else: | |
425 raise LinkError('Don\'t know how to plan: %s' % link_path) | |
426 | |
427 # Create parent directories to the target link if needed. | |
428 target_parent_dirs = os.path.dirname(link_path) | |
429 if (target_parent_dirs and | |
430 target_parent_dirs != link_path and | |
431 not os.path.exists(target_parent_dirs)): | |
432 actions.append(Makedirs(target_parent_dirs)) | |
433 | |
434 actions.append(Symlink(source_path, link_path)) | |
435 | |
436 return actions | |
437 | |
438 def _initialize_database(filename): | |
439 links_database = shelve.open(filename) | |
440 | |
441 # Wipe the database if this version of the script ends up looking at a | |
442 # newer (future) version of the links db, just to be sure. | |
443 version = links_database.get('SCHEMA_VERSION') | |
444 if version and version != SCHEMA_VERSION: | |
445 logging.info('Found database with schema version %s while this script only ' | |
446 'supports %s. Wiping previous database contents.', version, | |
447 SCHEMA_VERSION) | |
448 links_database.clear() | |
449 links_database['SCHEMA_VERSION'] = SCHEMA_VERSION | |
450 return links_database | |
451 | |
452 | |
453 def main(): | |
454 on_bot = os.environ.get('CHROME_HEADLESS') == '1' | |
455 | |
456 parser = optparse.OptionParser() | |
457 parser.add_option('-d', '--dry-run', action='store_true', default=False, | |
458 help='Print what would be done, but don\'t perform any ' | |
459 'operations. This will automatically set logging to ' | |
460 'verbose.') | |
461 parser.add_option('-c', '--clean-only', action='store_true', default=False, | |
462 help='Only clean previously created links, don\'t create ' | |
463 'new ones. This will automatically set logging to ' | |
464 'verbose.') | |
465 parser.add_option('-f', '--force', action='store_true', default=on_bot, | |
466 help='Force link creation. CAUTION: This deletes existing ' | |
467 'folders and files in the locations where links are ' | |
468 'about to be created.') | |
469 parser.add_option('-n', '--no-prompt', action='store_false', dest='prompt', | |
470 default=(not on_bot), | |
471 help='Prompt if we\'re planning to do a dangerous action') | |
472 parser.add_option('-v', '--verbose', action='store_const', | |
473 const=logging.DEBUG, default=logging.INFO, | |
474 help='Print verbose output for debugging.') | |
475 options, _ = parser.parse_args() | |
476 | |
477 if options.dry_run or options.force or options.clean_only: | |
478 options.verbose = logging.DEBUG | |
479 logging.basicConfig(format='%(message)s', level=options.verbose) | |
480 | |
481 # Work from the root directory of the checkout. | |
482 script_dir = os.path.dirname(os.path.abspath(__file__)) | |
483 os.chdir(script_dir) | |
484 | |
485 if sys.platform.startswith('win'): | |
486 def is_admin(): | |
487 try: | |
488 return os.getuid() == 0 | |
489 except AttributeError: | |
490 return ctypes.windll.shell32.IsUserAnAdmin() != 0 | |
491 if is_admin(): | |
492 logging.warning('WARNING: On Windows, you no longer need run as ' | |
493 'administrator. Please run with user account privileges.') | |
494 | |
495 if not os.path.exists(CHROMIUM_CHECKOUT): | |
496 logging.warning('Cannot find a Chromium checkout at %s. Did you run ' | |
497 '"gclient sync" before running this script?', | |
498 CHROMIUM_CHECKOUT) | |
499 return 0 | |
500 | |
501 links_database = _initialize_database(LINKS_DB) | |
502 try: | |
503 symlink_creator = WebRTCLinkSetup(links_database, options.force, | |
504 options.dry_run, options.prompt) | |
505 symlink_creator.CleanupLinks() | |
506 if not options.clean_only: | |
507 symlink_creator.CreateLinks(on_bot) | |
508 except LinkError as e: | |
509 print >> sys.stderr, e.message | |
510 return 3 | |
511 finally: | |
512 links_database.close() | |
513 return 0 | |
514 | |
515 | |
516 if __name__ == '__main__': | |
517 sys.exit(main()) | |
OLD | NEW |