bootstrap.py 13 KB

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