1# Copyright 2015 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 contextlib
6import json
7import logging
8import os
9import platform
10import sys
11import tempfile
12import threading
13
14CATAPULT_ROOT_PATH = os.path.abspath(
15    os.path.join(os.path.dirname(__file__), '..', '..'))
16DEPENDENCY_MANAGER_PATH = os.path.join(CATAPULT_ROOT_PATH, 'dependency_manager')
17PYMOCK_PATH = os.path.join(CATAPULT_ROOT_PATH, 'third_party', 'mock')
18PY_UTILS_PATH = os.path.join(CATAPULT_ROOT_PATH, 'common', 'py_utils')
19SIX_PATH = os.path.join(CATAPULT_ROOT_PATH, 'third_party', 'six')
20
21
22@contextlib.contextmanager
23def SysPath(path):
24  sys.path.append(path)
25  yield
26  if sys.path[-1] != path:
27    sys.path.remove(path)
28  else:
29    sys.path.pop()
30
31
32with SysPath(DEPENDENCY_MANAGER_PATH):
33  import dependency_manager  # pylint: disable=import-error
34
35with SysPath(SIX_PATH):
36  import six
37
38_ANDROID_BUILD_TOOLS = {'aapt', 'dexdump', 'split-select'}
39
40_DEVIL_DEFAULT_CONFIG = os.path.abspath(
41    os.path.join(os.path.dirname(__file__), 'devil_dependencies.json'))
42
43_LEGACY_ENVIRONMENT_VARIABLES = {
44    'ADB_PATH': {
45        'dependency_name': 'adb',
46        'platform': 'linux2_x86_64',
47    },
48    'ANDROID_SDK_ROOT': {
49        'dependency_name': 'android_sdk',
50        'platform': 'linux2_x86_64',
51    },
52}
53
54
55def EmptyConfig():
56  return {'config_type': 'BaseConfig', 'dependencies': {}}
57
58
59def LocalConfigItem(dependency_name, dependency_platform, dependency_path):
60  if isinstance(dependency_path, six.string_types):
61    dependency_path = [dependency_path]
62  return {
63      dependency_name: {
64          'file_info': {
65              dependency_platform: {
66                  'local_paths': dependency_path
67              },
68          },
69      },
70  }
71
72
73def _GetEnvironmentVariableConfig():
74  env_config = EmptyConfig()
75  path_config = ((os.environ.get(k), v)
76                 for k, v in six.iteritems(_LEGACY_ENVIRONMENT_VARIABLES))
77  path_config = ((p, c) for p, c in path_config if p)
78  for p, c in path_config:
79    env_config['dependencies'].update(
80        LocalConfigItem(c['dependency_name'], c['platform'], p))
81  return env_config
82
83
84class _Environment(object):
85  def __init__(self):
86    self._dm_init_lock = threading.Lock()
87    self._dm = None
88    self._logging_init_lock = threading.Lock()
89    self._logging_initialized = False
90
91  def Initialize(self, configs=None, config_files=None):
92    """Initialize devil's environment from configuration files.
93
94    This uses all configurations provided via |configs| and |config_files|
95    to determine the locations of devil's dependencies. Configurations should
96    all take the form described by py_utils.dependency_manager.BaseConfig.
97    If no configurations are provided, a default one will be used if available.
98
99    Args:
100      configs: An optional list of dict configurations.
101      config_files: An optional list of files to load
102    """
103
104    # Make sure we only initialize self._dm once.
105    with self._dm_init_lock:
106      if self._dm is None:
107        if configs is None:
108          configs = []
109
110        env_config = _GetEnvironmentVariableConfig()
111        if env_config:
112          configs.insert(0, env_config)
113        self._InitializeRecursive(configs=configs, config_files=config_files)
114        assert self._dm is not None, 'Failed to create dependency manager.'
115
116  def _InitializeRecursive(self, configs=None, config_files=None):
117    # This recurses through configs to create temporary files for each and
118    # take advantage of context managers to appropriately close those files.
119    # TODO(jbudorick): Remove this recursion if/when dependency_manager
120    # supports loading configurations directly from a dict.
121    if configs:
122      with tempfile.NamedTemporaryFile(mode='w',
123                                       delete=False) as next_config_file:
124        try:
125          next_config_file.write(json.dumps(configs[0]))
126          next_config_file.close()
127          self._InitializeRecursive(
128              configs=configs[1:],
129              config_files=[next_config_file.name] + (config_files or []))
130        finally:
131          if os.path.exists(next_config_file.name):
132            os.remove(next_config_file.name)
133    else:
134      config_files = config_files or []
135      if 'DEVIL_ENV_CONFIG' in os.environ:
136        config_files.append(os.environ.get('DEVIL_ENV_CONFIG'))
137      config_files.append(_DEVIL_DEFAULT_CONFIG)
138
139      self._dm = dependency_manager.DependencyManager(
140          [dependency_manager.BaseConfig(c) for c in config_files])
141
142  def InitializeLogging(self, log_level, formatter=None, handler=None):
143    if self._logging_initialized:
144      return
145
146    with self._logging_init_lock:
147      if self._logging_initialized:
148        return
149
150      formatter = formatter or logging.Formatter(
151          '%(threadName)-4s  %(message)s')
152      handler = handler or logging.StreamHandler(sys.stdout)
153      handler.setFormatter(formatter)
154
155      devil_logger = logging.getLogger('devil')
156      devil_logger.setLevel(log_level)
157      devil_logger.propagate = False
158      devil_logger.addHandler(handler)
159
160      with SysPath(PY_UTILS_PATH):
161        import py_utils.cloud_storage
162
163      lock_logger = py_utils.cloud_storage.logger
164      lock_logger.setLevel(log_level)
165      lock_logger.propagate = False
166      lock_logger.addHandler(handler)
167
168      self._logging_initialized = True
169
170  def FetchPath(self, dependency, arch=None, device=None):
171    if self._dm is None:
172      self.Initialize()
173    if dependency in _ANDROID_BUILD_TOOLS:
174      self.FetchPath('android_build_tools_libc++', arch=arch, device=device)
175    return self._dm.FetchPath(dependency, GetPlatform(arch, device))
176
177  def LocalPath(self, dependency, arch=None, device=None):
178    if self._dm is None:
179      self.Initialize()
180    return self._dm.LocalPath(dependency, GetPlatform(arch, device))
181
182  def PrefetchPaths(self, dependencies=None, arch=None, device=None):
183    return self._dm.PrefetchPaths(
184        GetPlatform(arch, device), dependencies=dependencies)
185
186
187def GetPlatform(arch=None, device=None):
188  if arch or device:
189    return 'android_%s' % (arch or device.product_cpu_abi)
190  # use 'linux2' for linux as this is already used in json file
191  return '%s_%s' % (
192      sys.platform if not sys.platform.startswith('linux') else 'linux2',
193      platform.machine())
194
195
196config = _Environment()
197