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