Browse Source

[win-bootstrap] Bootstrap Git config global-level instead of system

Modifying the system-level Git config may require admin privileges,
depending on where Git was installed.

This CL makes the bootstrap.py script target a user's Git config
at the global level, instead of system level.

Checks the global Git config for postprocess version and explicit
enabling of global config changing. Logs a warning if out of date
and not enabled so the user knows how to agree to global config
changes.

Bug: b/379888465, b/360206460
Change-Id: I9be395b64d5429f85514397918f4a6140dfe2a1a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/6034204
Reviewed-by: Allen Li <ayatane@chromium.org>
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
Commit-Queue: Anne Redulla <aredulla@google.com>
Anne Redulla 8 months ago
parent
commit
c35fbb2ecb
2 changed files with 104 additions and 31 deletions
  1. 5 2
      bootstrap/README.md
  2. 99 29
      bootstrap/bootstrap.py

+ 5 - 2
bootstrap/README.md

@@ -8,12 +8,15 @@ from replacing it outright without breaking running code. To
 ommodate this, and Python cleanup, we handle Python in two stages:
 
 1. Use CIPD to install both Git and Python at once.
-2. Use "win_tools.py" as a post-processor to install generated files and
+2. Use "bootstrap.py" as a post-processor to install generated files and
    fix-ups.
 
 ## Software bootstrapped
   * Python 3 (https://www.python.org/)
   * Git for Windows (https://git-for-windows.github.io/)
+    * This will soon be unbundled from depot_tools. To prepare for this change,
+    please install Git directly. See the instructions in the
+    [Windows build docs](https://chromium.googlesource.com/chromium/src/+/main/docs/windows_build_instructions.md#Install-git).
 
 ## Mechanism
 
@@ -29,7 +32,7 @@ cases is not true, it will download and unpack the respective binary.
 Installation of Git and Python is done by the [win_tools.bat](./win_tools.bat)
 script, which uses CIPD (via the [cipd](/cipd.bat) bootstrap) to acquire and
 install each package into the root of the `depot_tools` repository. Afterwards,
-the [win_tools.py](./win_tools.py) Python script is invoked to install stubs,
+the [bootstrap.py](./bootstrap.py) Python script is invoked to install stubs,
 wrappers, and support scripts into `depot_tools` for end-users.
 
 ### Manifest

+ 99 - 29
bootstrap/bootstrap.py

@@ -34,6 +34,19 @@ WIN_GIT_STUBS = {
     'ssh-keygen.bat': 'usr\\bin\\ssh-keygen.exe',
 }
 
+# The global git config which should be applied by |git_postprocess|.
+# The following set of parameters is versioned by "GIT_POSTPROCESS_VERSION".
+# If they change, update "GIT_POSTPROCESS_VERSION" accordingly.
+GIT_GLOBAL_CONFIG = {
+    'core.autocrlf': 'false',
+    'core.filemode': 'false',
+    'core.preloadindex': 'true',
+    'core.fscache': 'true',
+    'protocol.version': '2',
+}
+# Version of GIT_GLOBAL_CONFIG above.
+GIT_POSTPROCESS_VERSION = '2'
+
 
 # Accumulated template parameters for generated stubs.
 class Template(
@@ -185,14 +198,15 @@ def _toolchain_in_use(toolchain_path):
 
 
 def _check_call(argv, stdin_input=None, **kwargs):
-    """Wrapper for subprocess.check_call that adds logging."""
+    """Wrapper for subprocess.Popen that adds logging."""
     logging.info('running %r', argv)
     if stdin_input is not None:
         kwargs['stdin'] = subprocess.PIPE
     proc = subprocess.Popen(argv, **kwargs)
-    proc.communicate(input=stdin_input)
+    stdout, stderr = proc.communicate(input=stdin_input)
     if proc.returncode:
         raise subprocess.CalledProcessError(proc.returncode, argv, None)
+    return stdout, stderr
 
 
 def _safe_rmtree(path):
@@ -241,10 +255,6 @@ def clean_up_old_installations(skip_dir):
                 logging.info('Toolchain at %r is in-use; skipping', full_entry)
 
 
-# Version of "git_postprocess" system configuration (see |git_postprocess|).
-GIT_POSTPROCESS_VERSION = '2'
-
-
 def _within_depot_tools(path):
     """Returns whether the given path is within depot_tools."""
     try:
@@ -307,7 +317,85 @@ def git_get_mingw_dir(git_directory):
     return None
 
 
-def git_postprocess(template, bootstrap_git_dir, add_docs):
+def get_git_global_config_value(git_path, key):
+    """Helper to get the value from the global git config.
+
+    Returns:
+      - if present in the global git config, the associated value for the key;
+      - empty string if the key is not in the global git config.
+
+    Raises:
+        subprocess.CalledProcessError if there was an error reading the config
+        which resulted in an exit code other than 1.
+    """
+    try:
+        stdout, _ = _check_call([git_path, 'config', '--global', key],
+                                stdout=subprocess.PIPE,
+                                encoding='utf-8')
+        return stdout.strip()
+    except subprocess.CalledProcessError as e:
+        # Exit code is 1 if the key is invalid (i.e. not in the config).
+        if e.returncode == 1:
+            logging.info('{} was not in the global git config.'.format(key))
+            return ''
+
+        raise e
+
+
+def _win_git_bootstrap_config():
+    """Bootstraps the global git config, if enabled by the user.
+
+    To allow depot_tools to update your global git config, run:
+        git config --global depot-tools.allowGlobalGitConfig true
+
+    To prevent depot_tools updating your global git config and silence the
+    warning, run:
+        git config --global depot-tools.allowGlobalGitConfig false
+    """
+    git_bat_path = os.path.join(ROOT_DIR, 'git.bat')
+
+    postprocess_key = 'depot-tools.gitPostprocessVersion'
+    postprocess_version = get_git_global_config_value(git_path=git_bat_path,
+                                                      key=postprocess_key)
+    if postprocess_version == GIT_POSTPROCESS_VERSION:
+        # Already configured. Nothing to do.
+        return
+
+    # Check whether the user has authorized depot_tools to update their global
+    # git config.
+    allow_global_key = 'depot-tools.allowGlobalGitConfig'
+    allow_global = get_git_global_config_value(git_path=git_bat_path,
+                                               key=allow_global_key).lower()
+
+    if allow_global in ('false', '0', 'no', 'off'):
+        # The user has explicitly disabled this.
+        return
+
+    if allow_global not in ('true', '1', 'yes', 'on'):
+        logging.warning(
+            'depot_tools would like to update your global Git config\n'
+            'to have the optimal settings for Chromium development.\n'
+            'Allow this by running:\n'
+            f'    git config --global {allow_global_key} true\n'
+            'Then, get depot_tools to update your config by running:\n'
+            '    gclient sync\n'
+            'To deny and silence this warning, run:\n'
+            f'    git config --global {allow_global_key} false\n')
+        return
+
+    # Global git config changes have been authorized - do the update.
+    for k, v in GIT_GLOBAL_CONFIG.items():
+        _check_call([git_bat_path, 'config', '--global', k, v])
+
+    # Update the postprocess version to denote this version of the config has
+    # been applied.
+    _check_call([
+        git_bat_path, 'config', '--global', postprocess_key,
+        GIT_POSTPROCESS_VERSION
+    ])
+
+
+def git_postprocess(template, add_docs):
     if add_docs:
         # Update depot_tools files for "git help <command>".
         mingw_dir = git_get_mingw_dir(template.GIT_BIN_ABSDIR)
@@ -327,25 +415,8 @@ def git_postprocess(template, bootstrap_git_dir, add_docs):
         stub_template.maybe_install('git.template.bat',
                                     os.path.join(ROOT_DIR, stub_name))
 
-    # Set-up our system configuration environment. The following set of
-    # parameters is versioned by "GIT_POSTPROCESS_VERSION". If they change,
-    # update "GIT_POSTPROCESS_VERSION" accordingly.
-    def configure_git_system():
-        git_bat_path = os.path.join(ROOT_DIR, 'git.bat')
-        _check_call(
-            [git_bat_path, 'config', '--system', 'core.autocrlf', 'false'])
-        _check_call(
-            [git_bat_path, 'config', '--system', 'core.filemode', 'false'])
-        _check_call(
-            [git_bat_path, 'config', '--system', 'core.preloadindex', 'true'])
-        _check_call(
-            [git_bat_path, 'config', '--system', 'core.fscache', 'true'])
-        _check_call(
-            [git_bat_path, 'config', '--system', 'protocol.version', '2'])
-
-    os.makedirs(bootstrap_git_dir, exist_ok=True)
-    call_if_outdated(os.path.join(bootstrap_git_dir, '.git_postprocess'),
-                     GIT_POSTPROCESS_VERSION, configure_git_system)
+    # Bootstrap the git global config.
+    _win_git_bootstrap_config()
 
 
 def main(argv):
@@ -374,7 +445,6 @@ def main(argv):
     clean_up_old_installations(bootstrap_dir)
 
     if IS_WIN:
-        bootstrap_git_dir = os.path.join(bootstrap_dir, 'git')
         # Avoid messing with system git docs.
         add_docs = False
         git_dir = None
@@ -384,10 +454,10 @@ def main(argv):
             # Either using system git was not enabled
             # or git was not found in PATH.
             # Fall back to depot_tools bundled git.
-            git_dir = bootstrap_git_dir
+            git_dir = os.path.join(bootstrap_dir, 'git')
             add_docs = True
         template = template._replace(GIT_BIN_ABSDIR=git_dir)
-        git_postprocess(template, bootstrap_git_dir, add_docs)
+        git_postprocess(template, add_docs)
         templates = [
             ('git-bash.template.sh', 'git-bash', ROOT_DIR),
             ('python3.bat', 'python3.bat', ROOT_DIR),