bootstrap.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. # Copyright 2016 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. import argparse
  5. import collections
  6. import contextlib
  7. import fnmatch
  8. import hashlib
  9. import logging
  10. import os
  11. import platform
  12. import posixpath
  13. import shutil
  14. import string
  15. import subprocess
  16. import sys
  17. import tempfile
  18. THIS_DIR = os.path.abspath(os.path.dirname(__file__))
  19. ROOT_DIR = os.path.abspath(os.path.join(THIS_DIR, '..'))
  20. DEVNULL = open(os.devnull, 'w')
  21. IS_WIN = sys.platform.startswith('win')
  22. BAT_EXT = '.bat' if IS_WIN else ''
  23. # Top-level stubs to generate that fall through to executables within the Git
  24. # directory.
  25. WIN_GIT_STUBS = {
  26. 'git.bat': 'cmd\\git.exe',
  27. 'gitk.bat': 'cmd\\gitk.exe',
  28. 'ssh.bat': 'usr\\bin\\ssh.exe',
  29. 'ssh-keygen.bat': 'usr\\bin\\ssh-keygen.exe',
  30. }
  31. # Accumulated template parameters for generated stubs.
  32. class Template(collections.namedtuple('Template', (
  33. 'PYTHON_RELDIR', 'PYTHON_BIN_RELDIR', 'PYTHON_BIN_RELDIR_UNIX',
  34. 'PYTHON3_BIN_RELDIR', 'PYTHON3_BIN_RELDIR_UNIX', 'GIT_BIN_RELDIR',
  35. 'GIT_BIN_RELDIR_UNIX', 'GIT_PROGRAM',
  36. ))):
  37. @classmethod
  38. def empty(cls):
  39. return cls(**{k: None for k in cls._fields})
  40. def maybe_install(self, name, dst_path):
  41. """Installs template |name| to |dst_path| if it has changed.
  42. This loads the template |name| from THIS_DIR, resolves template parameters,
  43. and installs it to |dst_path|. See `maybe_update` for more information.
  44. Args:
  45. name (str): The name of the template to install.
  46. dst_path (str): The destination filesystem path.
  47. Returns (bool): True if |dst_path| was updated, False otherwise.
  48. """
  49. template_path = os.path.join(THIS_DIR, name)
  50. with open(template_path, 'r', encoding='utf8') as fd:
  51. t = string.Template(fd.read())
  52. return maybe_update(t.safe_substitute(self._asdict()), dst_path)
  53. def maybe_update(content, dst_path):
  54. """Writes |content| to |dst_path| if |dst_path| does not already match.
  55. This function will ensure that there is a file at |dst_path| containing
  56. |content|. If |dst_path| already exists and contains |content|, no operation
  57. will be performed, preserving filesystem modification times and avoiding
  58. potential write contention.
  59. Args:
  60. content (str): The file content.
  61. dst_path (str): The destination filesystem path.
  62. Returns (bool): True if |dst_path| was updated, False otherwise.
  63. """
  64. # If the path already exists and matches the new content, refrain from writing
  65. # a new one.
  66. if os.path.exists(dst_path):
  67. with open(dst_path, 'r', encoding='utf-8') as fd:
  68. if fd.read() == content:
  69. return False
  70. logging.debug('Updating %r', dst_path)
  71. with open(dst_path, 'w', encoding='utf-8') as fd:
  72. fd.write(content)
  73. os.chmod(dst_path, 0o755)
  74. return True
  75. def maybe_copy(src_path, dst_path):
  76. """Writes the content of |src_path| to |dst_path| if needed.
  77. See `maybe_update` for more information.
  78. Args:
  79. src_path (str): The content source filesystem path.
  80. dst_path (str): The destination filesystem path.
  81. Returns (bool): True if |dst_path| was updated, False otherwise.
  82. """
  83. with open(src_path, 'r', encoding='utf-8') as fd:
  84. content = fd.read()
  85. return maybe_update(content, dst_path)
  86. def call_if_outdated(stamp_path, stamp_version, fn):
  87. """Invokes |fn| if the stamp at |stamp_path| doesn't match |stamp_version|.
  88. This can be used to keep a filesystem record of whether an operation has been
  89. performed. The record is stored at |stamp_path|. To invalidate a record,
  90. change the value of |stamp_version|.
  91. After |fn| completes successfully, |stamp_path| will be updated to match
  92. |stamp_version|, preventing the same update from happening in the future.
  93. Args:
  94. stamp_path (str): The filesystem path of the stamp file.
  95. stamp_version (str): The desired stamp version.
  96. fn (callable): A callable to invoke if the current stamp version doesn't
  97. match |stamp_version|.
  98. Returns (bool): True if an update occurred.
  99. """
  100. stamp_version = stamp_version.strip()
  101. if os.path.isfile(stamp_path):
  102. with open(stamp_path, 'r', encoding='utf-8') as fd:
  103. current_version = fd.read().strip()
  104. if current_version == stamp_version:
  105. return False
  106. fn()
  107. with open(stamp_path, 'w', encoding='utf-8') as fd:
  108. fd.write(stamp_version)
  109. return True
  110. def _in_use(path):
  111. """Checks if a Windows file is in use.
  112. When Windows is using an executable, it prevents other writers from
  113. modifying or deleting that executable. We can safely test for an in-use
  114. file by opening it in write mode and checking whether or not there was
  115. an error.
  116. Returns (bool): True if the file was in use, False if not.
  117. """
  118. try:
  119. with open(path, 'r+'):
  120. return False
  121. except IOError:
  122. return True
  123. def _toolchain_in_use(toolchain_path):
  124. """Returns (bool): True if a toolchain rooted at |path| is in use.
  125. """
  126. # Look for Python files that may be in use.
  127. for python_dir in (
  128. os.path.join(toolchain_path, 'python', 'bin'), # CIPD
  129. toolchain_path, # Legacy ZIP distributions.
  130. ):
  131. for component in (
  132. os.path.join(python_dir, 'python.exe'),
  133. os.path.join(python_dir, 'DLLs', 'unicodedata.pyd'),
  134. ):
  135. if os.path.isfile(component) and _in_use(component):
  136. return True
  137. # Look for Pytho:n 3 files that may be in use.
  138. python_dir = os.path.join(toolchain_path, 'python3', 'bin')
  139. for component in (
  140. os.path.join(python_dir, 'python3.exe'),
  141. os.path.join(python_dir, 'DLLs', 'unicodedata.pyd'),
  142. ):
  143. if os.path.isfile(component) and _in_use(component):
  144. return True
  145. return False
  146. def _check_call(argv, stdin_input=None, **kwargs):
  147. """Wrapper for subprocess.check_call that adds logging."""
  148. logging.info('running %r', argv)
  149. if stdin_input is not None:
  150. kwargs['stdin'] = subprocess.PIPE
  151. proc = subprocess.Popen(argv, **kwargs)
  152. proc.communicate(input=stdin_input)
  153. if proc.returncode:
  154. raise subprocess.CalledProcessError(proc.returncode, argv, None)
  155. def _safe_rmtree(path):
  156. if not os.path.exists(path):
  157. return
  158. def _make_writable_and_remove(path):
  159. st = os.stat(path)
  160. new_mode = st.st_mode | 0o200
  161. if st.st_mode == new_mode:
  162. return False
  163. try:
  164. os.chmod(path, new_mode)
  165. os.remove(path)
  166. return True
  167. except Exception:
  168. return False
  169. def _on_error(function, path, excinfo):
  170. if not _make_writable_and_remove(path):
  171. logging.warning('Failed to %s: %s (%s)', function, path, excinfo)
  172. shutil.rmtree(path, onerror=_on_error)
  173. def clean_up_old_installations(skip_dir):
  174. """Removes Python installations other than |skip_dir|.
  175. This includes an "in-use" check against the "python.exe" in a given directory
  176. to avoid removing Python executables that are currently ruinning. We need
  177. this because our Python bootstrap may be run after (and by) other software
  178. that is using the bootstrapped Python!
  179. """
  180. root_contents = os.listdir(ROOT_DIR)
  181. for f in ('win_tools-*_bin', 'python27*_bin', 'git-*_bin', 'bootstrap-*_bin'):
  182. for entry in fnmatch.filter(root_contents, f):
  183. full_entry = os.path.join(ROOT_DIR, entry)
  184. if full_entry == skip_dir or not os.path.isdir(full_entry):
  185. continue
  186. logging.info('Cleaning up old installation %r', entry)
  187. if not _toolchain_in_use(full_entry):
  188. _safe_rmtree(full_entry)
  189. else:
  190. logging.info('Toolchain at %r is in-use; skipping', full_entry)
  191. # Version of "git_postprocess" system configuration (see |git_postprocess|).
  192. GIT_POSTPROCESS_VERSION = '2'
  193. def git_get_mingw_dir(git_directory):
  194. """Returns (str) The "mingw" directory in a Git installation, or None."""
  195. for candidate in ('mingw64', 'mingw32'):
  196. mingw_dir = os.path.join(git_directory, candidate)
  197. if os.path.isdir(mingw_dir):
  198. return mingw_dir
  199. return None
  200. def git_postprocess(template, git_directory):
  201. # Update depot_tools files for "git help <command>"
  202. mingw_dir = git_get_mingw_dir(git_directory)
  203. if mingw_dir:
  204. docsrc = os.path.join(ROOT_DIR, 'man', 'html')
  205. git_docs_dir = os.path.join(mingw_dir, 'share', 'doc', 'git-doc')
  206. for name in os.listdir(docsrc):
  207. maybe_copy(
  208. os.path.join(docsrc, name),
  209. os.path.join(git_docs_dir, name))
  210. else:
  211. logging.info('Could not find mingw directory for %r.', git_directory)
  212. # Create Git templates and configure its base layout.
  213. for stub_name, relpath in WIN_GIT_STUBS.items():
  214. stub_template = template._replace(GIT_PROGRAM=relpath)
  215. stub_template.maybe_install(
  216. 'git.template.bat',
  217. os.path.join(ROOT_DIR, stub_name))
  218. # Set-up our system configuration environment. The following set of
  219. # parameters is versioned by "GIT_POSTPROCESS_VERSION". If they change,
  220. # update "GIT_POSTPROCESS_VERSION" accordingly.
  221. def configure_git_system():
  222. git_bat_path = os.path.join(ROOT_DIR, 'git.bat')
  223. _check_call([git_bat_path, 'config', '--system', 'core.autocrlf', 'false'])
  224. _check_call([git_bat_path, 'config', '--system', 'core.filemode', 'false'])
  225. _check_call([git_bat_path, 'config', '--system', 'core.preloadindex',
  226. 'true'])
  227. _check_call([git_bat_path, 'config', '--system', 'core.fscache', 'true'])
  228. _check_call([git_bat_path, 'config', '--system', 'protocol.version', '2'])
  229. call_if_outdated(
  230. os.path.join(git_directory, '.git_postprocess'),
  231. GIT_POSTPROCESS_VERSION,
  232. configure_git_system)
  233. def main(argv):
  234. parser = argparse.ArgumentParser()
  235. parser.add_argument('--verbose', action='store_true')
  236. parser.add_argument('--bootstrap-name', required=True,
  237. help='The directory of the Python installation.')
  238. args = parser.parse_args(argv)
  239. logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARN)
  240. template = Template.empty()._replace(
  241. PYTHON_RELDIR=os.path.join(args.bootstrap_name, 'python'),
  242. PYTHON_BIN_RELDIR=os.path.join(args.bootstrap_name, 'python', 'bin'),
  243. PYTHON_BIN_RELDIR_UNIX=posixpath.join(
  244. args.bootstrap_name, 'python', 'bin'),
  245. PYTHON3_BIN_RELDIR=os.path.join(args.bootstrap_name, 'python3', 'bin'),
  246. PYTHON3_BIN_RELDIR_UNIX=posixpath.join(
  247. args.bootstrap_name, 'python3', 'bin'),
  248. GIT_BIN_RELDIR=os.path.join(args.bootstrap_name, 'git'),
  249. GIT_BIN_RELDIR_UNIX=posixpath.join(args.bootstrap_name, 'git'))
  250. bootstrap_dir = os.path.join(ROOT_DIR, args.bootstrap_name)
  251. # Clean up any old Python and Git installations.
  252. clean_up_old_installations(bootstrap_dir)
  253. if IS_WIN:
  254. git_postprocess(template, os.path.join(bootstrap_dir, 'git'))
  255. templates = [
  256. ('git-bash.template.sh', 'git-bash', ROOT_DIR),
  257. ('python27.bat', 'python.bat', ROOT_DIR),
  258. ('python3.bat', 'python3.bat', ROOT_DIR),
  259. ]
  260. for src_name, dst_name, dst_dir in templates:
  261. # Re-evaluate and regenerate our root templated files.
  262. template.maybe_install(src_name, os.path.join(dst_dir, dst_name))
  263. # Emit our Python bin depot-tools-relative directory. This is read by
  264. # python.bat, python3.bat, vpython[.bat] and vpython3[.bat] to identify the
  265. # path of the current Python installation.
  266. #
  267. # We use this indirection so that upgrades can change this pointer to
  268. # redirect "python.bat" to a new Python installation. We can't just update
  269. # "python.bat" because batch file executions reload the batch file and seek
  270. # to the previous cursor in between every command, so changing the batch
  271. # file contents could invalidate any existing executions.
  272. #
  273. # The intention is that the batch file itself never needs to change when
  274. # switching Python versions.
  275. maybe_update(
  276. template.PYTHON_BIN_RELDIR,
  277. os.path.join(ROOT_DIR, 'python_bin_reldir.txt'))
  278. maybe_update(
  279. template.PYTHON3_BIN_RELDIR,
  280. os.path.join(ROOT_DIR, 'python3_bin_reldir.txt'))
  281. return 0
  282. if __name__ == '__main__':
  283. sys.exit(main(sys.argv[1:]))