setup_links.py 16 KB

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