Move gclient path access to separate module.

gclient_utils.py is a kitchen sink and is for that reason expensive
to import. Move the comparatively cheap and simple path routines
to a new gclient_paths module and use that in gn.py, clang_format.py,
dart_format.py.

(To be able to move FindGclientRoot() to gclient_paths.py,
make it use io.open() instead of FileRead(). FileRead() tries
to paper over invalid utf-8, but that was added for presubmits,
not for .gclient files, so this is hopefully fine.)

Cuts gn.py overhead in half (on my Windows laptop from 0.6s to 0.25s,
still high; on my Mac laptop from 0.1s to 0.05s), and probably helps
the other two too.

Completely remove PathDifference() since it's unused.

Bug: 939959
Change-Id: I6a70f6e4c16062b622fb2df8778e8a598d4cc956
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1512058
Commit-Queue: Nico Weber <thakis@chromium.org>
Reviewed-by: Andrii Shyshkalov <tandrii@chromium.org>
diff --git a/gclient_paths.py b/gclient_paths.py
new file mode 100644
index 0000000..68a95fa
--- /dev/null
+++ b/gclient_paths.py
@@ -0,0 +1,135 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This file is imported by various thin wrappers (around gn, clang-format, ...),
+# so it's meant to import very quickly. To keep it that way don't add more
+# code, and even more importantly don't add more toplevel import statements.
+import os
+
+
+def FindGclientRoot(from_dir, filename='.gclient'):
+  """Tries to find the gclient root."""
+  real_from_dir = os.path.realpath(from_dir)
+  path = real_from_dir
+  while not os.path.exists(os.path.join(path, filename)):
+    split_path = os.path.split(path)
+    if not split_path[1]:
+      return None
+    path = split_path[0]
+
+  # If we did not find the file in the current directory, make sure we are in a
+  # sub directory that is controlled by this configuration.
+  if path != real_from_dir:
+    entries_filename = os.path.join(path, filename + '_entries')
+    if not os.path.exists(entries_filename):
+      import sys
+      # If .gclient_entries does not exist, a previous call to gclient sync
+      # might have failed. In that case, we cannot verify that the .gclient
+      # is the one we want to use. In order to not to cause too much trouble,
+      # just issue a warning and return the path anyway.
+      print >> sys.stderr, ("%s missing, %s file in parent directory %s might "
+          "not be the file you want to use." %
+          (entries_filename, filename, path))
+      return path
+    scope = {}
+    try:
+      import io
+      with io.open(entries_filename, encoding='utf-8') as f:
+        exec(f.read(), scope)
+    except SyntaxError, e:
+      SyntaxErrorToError(filename, e)
+    all_directories = scope['entries'].keys()
+    path_to_check = real_from_dir[len(path)+1:]
+    while path_to_check:
+      if path_to_check in all_directories:
+        return path
+      path_to_check = os.path.dirname(path_to_check)
+    return None
+
+  import logging
+  logging.info('Found gclient root at ' + path)
+  return path
+
+
+def GetPrimarySolutionPath():
+  """Returns the full path to the primary solution. (gclient_root + src)"""
+
+  gclient_root = FindGclientRoot(os.getcwd())
+  if not gclient_root:
+    # Some projects might not use .gclient. Try to see whether we're in a git
+    # checkout.
+    top_dir = [os.getcwd()]
+    def filter_fn(line):
+      repo_root_path = os.path.normpath(line.rstrip('\n'))
+      if os.path.exists(repo_root_path):
+        top_dir[0] = repo_root_path
+    try:
+      CheckCallAndFilter(["git", "rev-parse", "--show-toplevel"],
+                         print_stdout=False, filter_fn=filter_fn)
+    except Exception:
+      pass
+    top_dir = top_dir[0]
+    if os.path.exists(os.path.join(top_dir, 'buildtools')):
+      return top_dir
+    return None
+
+  # Some projects' top directory is not named 'src'.
+  source_dir_name = GetGClientPrimarySolutionName(gclient_root) or 'src'
+  return os.path.join(gclient_root, source_dir_name)
+
+
+def GetBuildtoolsPath():
+  """Returns the full path to the buildtools directory.
+  This is based on the root of the checkout containing the current directory."""
+
+  # Overriding the build tools path by environment is highly unsupported and may
+  # break without warning.  Do not rely on this for anything important.
+  override = os.environ.get('CHROMIUM_BUILDTOOLS_PATH')
+  if override is not None:
+    return override
+
+  primary_solution = GetPrimarySolutionPath()
+  if not primary_solution:
+    return None
+  buildtools_path = os.path.join(primary_solution, 'buildtools')
+  if not os.path.exists(buildtools_path):
+    # Buildtools may be in the gclient root.
+    gclient_root = FindGclientRoot(os.getcwd())
+    buildtools_path = os.path.join(gclient_root, 'buildtools')
+  return buildtools_path
+
+
+def GetBuildtoolsPlatformBinaryPath():
+  """Returns the full path to the binary directory for the current platform."""
+  buildtools_path = GetBuildtoolsPath()
+  if not buildtools_path:
+    return None
+
+  if sys.platform.startswith(('cygwin', 'win')):
+    subdir = 'win'
+  elif sys.platform == 'darwin':
+    subdir = 'mac'
+  elif sys.platform.startswith('linux'):
+      subdir = 'linux64'
+  else:
+    raise Error('Unknown platform: ' + sys.platform)
+  return os.path.join(buildtools_path, subdir)
+
+
+def GetExeSuffix():
+  """Returns '' or '.exe' depending on how executables work on this platform."""
+  if sys.platform.startswith(('cygwin', 'win')):
+    return '.exe'
+  return ''
+
+
+def GetGClientPrimarySolutionName(gclient_root_dir_path):
+  """Returns the name of the primary solution in the .gclient file specified."""
+  gclient_config_file = os.path.join(gclient_root_dir_path, '.gclient')
+  env = {}
+  execfile(gclient_config_file, env)
+  solutions = env.get('solutions', [])
+  if solutions:
+    return solutions[0].get('name')
+  return None