Index: infra/scripts/legacy/scripts/common/env.py |
diff --git a/infra/scripts/legacy/scripts/common/env.py b/infra/scripts/legacy/scripts/common/env.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..3e497ff48bf188938178b9eaa5334e59b3e48273 |
--- /dev/null |
+++ b/infra/scripts/legacy/scripts/common/env.py |
@@ -0,0 +1,439 @@ |
+#!/usr/bin/env python |
+# Copyright 2015 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. |
+ |
+"""Implements a standard mechanism for Chrome Infra Python environment setup. |
+ |
+This library provides a central location to define Chrome Infra environment |
+setup. It also provides several faculties to install this environment. |
+ |
+Within a cooperating script, the environment can be setup by importing this |
+module and running its 'Install' method: |
+ |
+ # Install Chrome-Infra environment (replaces 'sys.path'). |
+ sys.path.insert(0, |
+ os.path.join(os.path.dirname(__file__), os.pardir, ...)) |
+ # (/path/to/build/scripts) |
+ import common.env |
+ common.env.Install() |
+ |
+When attempting to export the Chrome Infra path to external scripts, this |
+script can be invoked as an executable with various subcommands to emit a valid |
+PYTHONPATH clause. |
+ |
+In addition, this module has several functions to construct the path. |
+ |
+The goal is to deploy this module universally among Chrome-Infra scripts, |
+BuildBot configurations, tool invocations, and tests to ensure that they all |
+execute with the same centrally-defined environment. |
+""" |
+ |
+import argparse |
+import collections |
+import contextlib |
+import imp |
+import itertools |
+import os |
+import sys |
+import traceback |
+ |
+ |
+# Export for bootstrapping. |
+__all__ = [ |
+ 'Install', |
+ 'PythonPath', |
+ ] |
+ |
+ |
+# Name of enviornment extension file to seek. |
+ENV_EXTENSION_NAME = 'environment.cfg.py' |
+ |
+# Standard directories (based on this file's location in the <build> tree). |
+def path_if(*args): |
+ if not all(args): |
+ return None |
+ path = os.path.abspath(os.path.join(*args)) |
+ return (path) if os.path.exists(path) else (None) |
+ |
+# The path to the <build> directory in which this script resides. |
+Build = path_if(os.path.dirname(__file__), os.pardir, os.pardir) |
+# The path to the <build_internal> directory. |
+BuildInternal = path_if(Build, os.pardir, 'build_internal') |
+ |
+ |
+def SetPythonPathEnv(value): |
+ """Sets the system's PYTHONPATH environemnt variable. |
+ |
+ Args: |
+ value (str): The value to use. If this is empty/None, the system's |
+ PYTHONPATH will be cleared. |
+ """ |
+ # Since we can't assign None to the environment "dictionary", we have to |
+ # either set or delete the key depending on the original value. |
+ if value is not None: |
+ os.environ['PYTHONPATH'] = str(value) |
+ else: |
+ os.environ.pop('PYTHONPATH', None) |
+ |
+ |
+def Install(**kwargs): |
+ """Replaces the current 'sys.path' with a hermetic Chrome-Infra path. |
+ |
+ Args: |
+ kwargs (dict): See GetInfraPythonPath arguments. |
+ |
+ Returns (PythonPath): The PythonPath object that was installed. |
+ """ |
+ infra_python_path = GetInfraPythonPath(**kwargs) |
+ infra_python_path.Install() |
+ return infra_python_path |
+ |
+ |
+def SplitPath(path): |
+ """Returns (list): A list of path elements. |
+ |
+ Splits a path into path elements. For example (assuming '/' is the local |
+ system path separator): |
+ >>> print SplitPath('/a/b/c/d') |
+ ['/', 'a', 'b', 'c', 'd'] |
+ >>> print SplitPath('a/b/c') |
+ ['a', 'b,' 'c'] |
+ """ |
+ parts = [] |
+ while True: |
+ path, component = os.path.split(path) |
+ if not component: |
+ if path: |
+ parts.append(path) |
+ break |
+ parts.append(component) |
+ parts.reverse() |
+ return parts |
+ |
+ |
+def ExtendPath(base, root_dir): |
+ """Returns (PythonPath): The extended python path. |
+ |
+ This method looks for the ENV_EXTENSION_NAME file within "root_dir". If |
+ present, it will be loaded as a Python module and have its "Extend" method |
+ called. |
+ |
+ If no extension is found, the base PythonPath will be returned. |
+ |
+ Args: |
+ base (PythonPath): The base python path. |
+ root_dir (str): The path to check for an extension. |
+ """ |
+ extension_path = os.path.join(root_dir, ENV_EXTENSION_NAME) |
+ if not os.path.isfile(extension_path): |
+ return base |
+ with open(extension_path, 'r') as fd: |
+ extension = fd.read() |
+ extension_module = imp.new_module('env-extension') |
+ |
+ # Execute the enviornment extension. |
+ try: |
+ exec extension in extension_module.__dict__ |
+ |
+ extend_func = getattr(extension_module, 'Extend', None) |
+ assert extend_func, ( |
+ "The environment extension module is missing the 'Extend()' method.") |
+ base = extend_func(base, root_dir) |
+ if not isinstance(base, PythonPath): |
+ raise TypeError("Extension module returned non-PythonPath object (%s)" % ( |
+ type(base).__name__,)) |
+ except Exception: |
+ # Re-raise the exception, but include the configuration file name. |
+ tb = traceback.format_exc() |
+ raise RuntimeError("Environment extension [%s] raised exception: %s" % ( |
+ extension_path, tb)) |
+ return base |
+ |
+ |
+def IsSystemPythonPath(path): |
+ """Returns (bool): If a python path is user-installed. |
+ |
+ Paths that are known to be user-installed paths can be ignored when setting |
+ up a hermetic Python path environment to avoid user libraries that would not |
+ be present in other environments falsely affecting code. |
+ |
+ This function can be updated as-needed to exclude other non-system paths |
+ encountered on bots and in the wild. |
+ """ |
+ components = SplitPath(path) |
+ for component in components: |
+ if component in ('dist-packages', 'site-packages'): |
+ return False |
+ return True |
+ |
+ |
+class PythonPath(collections.Sequence): |
+ """An immutable set of Python path elements. |
+ |
+ All paths represented in this structure are absolute. If a relative path |
+ is passed into this structure, it will be converted to absolute based on |
+ the current working directory (via os.path.abspath). |
+ """ |
+ |
+ def __init__(self, components=None): |
+ """Initializes a new PythonPath instance. |
+ |
+ Args: |
+ components (list): A list of path component strings. |
+ """ |
+ seen = set() |
+ self._components = [] |
+ for component in (components or ()): |
+ component = os.path.abspath(component) |
+ assert isinstance(component, basestring), ( |
+ "Path component '%s' is not a string (%s)" % ( |
+ component, type(component).__name__)) |
+ if component in seen: |
+ continue |
+ seen.add(component) |
+ self._components.append(component) |
+ |
+ def __getitem__(self, value): |
+ return self._components[value] |
+ |
+ def __len__(self): |
+ return len(self._components) |
+ |
+ def __iadd__(self, other): |
+ return self.Append(other) |
+ |
+ def __repr__(self): |
+ return self.pathstr |
+ |
+ def __eq__(self, other): |
+ assert isinstance(other, type(self)) |
+ return self._components == other._components |
+ |
+ @classmethod |
+ def Flatten(cls, *paths): |
+ """Returns (list): A single-level list containing flattened path elements. |
+ |
+ >>> print PythonPath.Flatten('a', ['b', ['c', 'd']]) |
+ ['a', 'b', 'c', 'd'] |
+ """ |
+ result = [] |
+ for path in paths: |
+ if not isinstance(path, basestring): |
+ # Assume it's an iterable of paths. |
+ result += cls.Flatten(*path) |
+ else: |
+ result.append(path) |
+ return result |
+ |
+ @classmethod |
+ def FromPaths(cls, *paths): |
+ """Returns (PythonPath): A PythonPath instantiated from path elements. |
+ |
+ Args: |
+ paths (tuple): A tuple of path elements or iterables containing path |
+ elements (e.g., PythonPath instances). |
+ """ |
+ return cls(cls.Flatten(*paths)) |
+ |
+ @classmethod |
+ def FromPathStr(cls, pathstr): |
+ """Returns (PythonPath): A PythonPath instantiated from the path string. |
+ |
+ Args: |
+ pathstr (str): An os.pathsep()-delimited path string. |
+ """ |
+ return cls(pathstr.split(os.pathsep)) |
+ |
+ @property |
+ def pathstr(self): |
+ """Returns (str): A path string for the instance's path elements.""" |
+ return os.pathsep.join(self) |
+ |
+ def IsHermetic(self): |
+ """Returns (bool): True if this instance contains only system paths.""" |
+ return all(IsSystemPythonPath(p) for p in self) |
+ |
+ def GetHermetic(self): |
+ """Returns (PythonPath): derivative PythonPath containing only system paths. |
+ """ |
+ return type(self).FromPaths(*(p for p in self if IsSystemPythonPath(p))) |
+ |
+ def Append(self, *paths): |
+ """Returns (PythonPath): derivative PythonPath with paths added to the end. |
+ |
+ Args: |
+ paths (tuple): A tuple of path elements to append to the current instance. |
+ """ |
+ return type(self)(itertools.chain(self, self.FromPaths(*paths))) |
+ |
+ def Override(self, *paths): |
+ """Returns (PythonPath): derivative PythonPath with paths prepended. |
+ |
+ Args: |
+ paths (tuple): A tuple of path elements to prepend to the current |
+ instance. |
+ """ |
+ return self.FromPaths(*paths).Append(self) |
+ |
+ def Install(self): |
+ """Overwrites Python runtime variables based on the current instance. |
+ |
+ Performs the following operations: |
+ - Replaces sys.path with the current instance's path. |
+ - Replaces os.environ['PYTHONPATH'] with the current instance's path |
+ string. |
+ """ |
+ sys.path = list(self) |
+ SetPythonPathEnv(self.pathstr) |
+ |
+ @contextlib.contextmanager |
+ def Enter(self): |
+ """Context manager wrapper for Install. |
+ |
+ On exit, the context manager will restore the original environment. |
+ """ |
+ orig_sys_path = sys.path[:] |
+ orig_pythonpath = os.environ.get('PYTHONPATH') |
+ |
+ try: |
+ self.Install() |
+ yield |
+ finally: |
+ sys.path = orig_sys_path |
+ SetPythonPathEnv(orig_pythonpath) |
+ |
+ |
+def GetSysPythonPath(hermetic=True): |
+ """Returns (PythonPath): A path based on 'sys.path'. |
+ |
+ Args: |
+ hermetic (bool): If True, prune any non-system path. |
+ """ |
+ path = PythonPath.FromPaths(*sys.path) |
+ if hermetic: |
+ path = path.GetHermetic() |
+ return path |
+ |
+ |
+def GetEnvPythonPath(): |
+ """Returns (PythonPath): A path based on the PYTHONPATH environment variable. |
+ """ |
+ pythonpath = os.environ.get('PYTHONPATH') |
+ if not pythonpath: |
+ return PythonPath.FromPaths() |
+ return PythonPath.FromPathStr(pythonpath) |
+ |
+ |
+def GetMasterPythonPath(master_dir): |
+ """Returns (PythonPath): A path including a BuildBot master's directory. |
+ |
+ Args: |
+ master_dir (str): The BuildBot master root directory. |
+ """ |
+ return PythonPath.FromPaths(master_dir) |
+ |
+ |
+def GetBuildPythonPath(): |
+ """Returns (PythonPath): The Chrome Infra build path.""" |
+ build_path = PythonPath.FromPaths() |
+ for extension_dir in ( |
+ Build, |
+ BuildInternal, |
+ ): |
+ if extension_dir: |
+ build_path = ExtendPath(build_path, extension_dir) |
+ return build_path |
+ |
+ |
+def GetInfraPythonPath(hermetic=True, master_dir=None): |
+ """Returns (PythonPath): The full working Chrome Infra utility path. |
+ |
+ This path is consistent for master, slave, and tool usage. It includes (in |
+ this order): |
+ - Any environment PYTHONPATH overrides. |
+ - If 'master_dir' is supplied, the master's python path component. |
+ - The Chrome Infra build path. |
+ - The system python path. |
+ |
+ Args: |
+ hermetic (bool): True, prune any non-system path from the system path. |
+ master_dir (str): If not None, include a master path component. |
+ """ |
+ path = GetEnvPythonPath() |
+ if master_dir: |
+ path += GetMasterPythonPath(master_dir) |
+ path += GetBuildPythonPath() |
+ path += GetSysPythonPath(hermetic=hermetic) |
+ return path |
+ |
+ |
+def _InfraPathFromArgs(args): |
+ """Returns (PythonPath): A PythonPath populated from command-line arguments. |
+ |
+ Args: |
+ args (argparse.Namespace): The command-line arguments constructed by 'main'. |
+ """ |
+ return GetInfraPythonPath( |
+ master_dir=args.master_dir, |
+ ) |
+ |
+ |
+def _Command_Echo(args, path): |
+ """Returns (int): Return code. |
+ |
+ Command function for the 'echo' subcommand. Outputs the path string for |
+ 'path'. |
+ |
+ Args: |
+ args (argparse.Namespace): The command-line arguments constructed by 'main'. |
+ path (PythonPath): The python path to use. |
+ """ |
+ args.output.write(path.pathstr) |
+ return 0 |
+ |
+ |
+def _Command_Print(args, path): |
+ """Returns (int): Return code. |
+ |
+ Command function for the 'print' subcommand. Outputs each path component in |
+ path on a separate line. |
+ |
+ Args: |
+ args (argparse.Namespace): The command-line arguments constructed by 'main'. |
+ path (PythonPath): The python path to use. |
+ """ |
+ for component in path: |
+ print >>args.output, component |
+ return 0 |
+ |
+ |
+def main(): |
+ """Main execution function.""" |
+ parser = argparse.ArgumentParser() |
+ parser.add_argument('-M', '--master_dir', |
+ help="Augment the path with the master's directory.") |
+ parser.add_argument('-o', '--output', metavar='PATH', |
+ type=argparse.FileType('w'), default='-', |
+ help="File to output to (use '-' for STDOUT).") |
+ |
+ subparsers = parser.add_subparsers() |
+ |
+ # 'echo' |
+ subparser = subparsers.add_parser('echo') |
+ subparser.set_defaults(func=_Command_Echo) |
+ |
+ # 'print' |
+ subparser = subparsers.add_parser('print') |
+ subparser.set_defaults(func=_Command_Print) |
+ |
+ # Parse |
+ args = parser.parse_args() |
+ |
+ # Execute our subcommand function, which will return the exit code. |
+ path = _InfraPathFromArgs(args) |
+ return args.func(args, path) |
+ |
+ |
+if __name__ == '__main__': |
+ sys.exit(main()) |