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