1# Copyright 2013 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
5"""Client configuration management.
6
7This module holds the code for detecting and configuring the current client and
8it's output directories.
9It is responsible for writing out the client specific plugins that tell the
10rest of the cr tool what the client is capable of.
11"""
12
13import os
14import pprint
15import sys
16
17import cr
18import cr.auto.build
19import cr.auto.client
20
21# The config version currently supported.
22VERSION = 0.5
23# The default directory name to store config inside
24CLIENT_CONFIG_PATH = '.cr'
25# The partial filename to add to a directory to get it's config file.
26CLIENT_CONFIG_FILE = os.path.join(CLIENT_CONFIG_PATH, 'config.py')
27# The format string for the header of a config file.
28CONFIG_FILE_PREFIX = """
29# This is an autogenerated file
30# it *will* be overwritten, and changes may lost
31# The system will autoload any other python file in the same folder.
32
33import cr
34
35OVERRIDES = cr.Config.From("""
36# The format string for each value in a config file.
37CONFIG_VAR_LINE = '\n  {0} = {1!r},'
38# The format string for the tail of a config file.
39CONFIG_FILE_SUFFIX = '\n)\n'
40# The name of the gclient config file
41GCLIENT_FILENAME = '.gclient'
42
43# The default config values installed by this module.
44DEFAULT = cr.Config.From(
45    CR_ROOT_PATH=os.path.join('{GOOGLE_CODE}'),
46    CR_CLIENT_NAME='chromium',
47    CR_CLIENT_PATH=os.path.join('{CR_ROOT_PATH}', '{CR_CLIENT_NAME}'),
48    CR_SRC=os.path.join('{CR_CLIENT_PATH}', 'src'),
49    CR_BUILD_DIR=os.path.join('{CR_SRC}', '{CR_OUT_FULL}'),
50)
51
52
53def DetectClient():
54  # Attempt to detect the current client from the cwd
55  # See if we can detect the source tree root
56  client_path = os.getcwd()
57  while (client_path and
58         not os.path.exists(os.path.join(client_path, GCLIENT_FILENAME))):
59    old = client_path
60    client_path = os.path.dirname(client_path)
61    if client_path == old:
62      client_path = None
63  if client_path is not None:
64    dirname, basename = os.path.split(client_path)
65    if basename == 'src':
66      # we have the src path, base is one level up
67      client_path = dirname
68  if client_path is not None:
69    cr.context.derived['CR_CLIENT_PATH'] = client_path
70  # now get the value from it may be different
71  client_path = cr.context.Get('CR_CLIENT_PATH')
72  if client_path is not None:
73    cr.context.derived['CR_CLIENT_NAME'] = os.path.basename(client_path)
74
75
76def _GetConfigFilename(path):
77  return os.path.realpath(os.path.join(path, CLIENT_CONFIG_FILE))
78
79
80def _IsOutputDir(path):
81  return os.path.isfile(_GetConfigFilename(path))
82
83
84def _WriteConfig(writer, data):
85  writer.write(CONFIG_FILE_PREFIX)
86  for key, value in data.items():
87    writer.write(CONFIG_VAR_LINE.format(key, value))
88  writer.write(CONFIG_FILE_SUFFIX)
89
90
91def AddArguments(parser):
92  parser.add_argument(
93      '-o', '--out', dest='_out', metavar='name',
94      default=None,
95      help='The name of the out directory to use. Overrides CR_OUT.'
96  )
97
98
99def GetOutArgument():
100  return getattr(cr.context.args, '_out', None)
101
102
103def ApplyOutArgument():
104  # TODO(iancottrell): be flexible, allow out to do approximate match...
105  out = GetOutArgument()
106  if out:
107    cr.context.derived.Set(CR_OUT_FULL=out)
108
109
110def ReadGClient():
111  """Loads the .gclient configuration for the current client.
112
113  This will load from CR_CLIENT_PATH.
114
115  Returns:
116    The dict of values set in the .gclient file.
117
118  """
119  # Now attempt to load and parse the .gclient file
120  result = {}
121  try:
122    gclient_file = cr.context.Substitute(
123        os.path.join('{CR_CLIENT_PATH}', GCLIENT_FILENAME))
124    with open(gclient_file, 'r') as spec_file:
125      # matching the behaviour of gclient, so pylint: disable=exec-used
126      exec(spec_file.read(), {}, result)
127  except IOError:
128    # no .gclient file, skip it
129    pass
130  return result
131
132
133def WriteGClient():
134  """Writes the .gclient configuration for the current client.
135
136  This will write to CR_CLIENT_PATH.
137
138  """
139  gclient_file = cr.context.Substitute(
140      os.path.join('{CR_CLIENT_PATH}', GCLIENT_FILENAME))
141  spec = '\n'.join('%s = %s' % (key, pprint.pformat(value))
142      for key,value in cr.context.gclient.items())
143  if cr.context.dry_run:
144    print 'Write the following spec to', gclient_file
145    print spec
146  else:
147    with open(gclient_file, 'w') as spec_file:
148      spec_file.write(spec)
149
150def LoadConfig():
151  """Loads the client configuration for the given context.
152
153  This will load configuration if present from CR_CLIENT_PATH and then
154  CR_BUILD_DIR.
155
156  Returns:
157    True if configuration was fully loaded.
158
159  """
160  # Load the root config, will help set default build dir
161  client_path = cr.context.Find('CR_CLIENT_PATH')
162  if not client_path:
163    return False
164  cr.auto.client.__path__.append(os.path.join(client_path, CLIENT_CONFIG_PATH))
165  cr.loader.Scan()
166  # Now load build dir config
167  build_dir = cr.context.Find('CR_BUILD_DIR')
168  if not build_dir:
169    return False
170  cr.auto.build.__path__.append(os.path.join(build_dir, CLIENT_CONFIG_PATH))
171  cr.loader.Scan()
172  return hasattr(cr.auto.build, 'config')
173
174
175def WriteConfig(path, data):
176  """Writes a configuration out to a file.
177
178  This writes all the key value pairs in data out to a config file below path.
179
180  Args:
181    path: The base path to write the config plugin into.
182    data: The key value pairs to write.
183  """
184  filename = _GetConfigFilename(path)
185  config_dir = os.path.dirname(filename)
186  if cr.context.dry_run:
187    print 'makedirs', config_dir
188    print 'Write config to', filename
189    _WriteConfig(sys.stdout, data)
190  else:
191    try:
192      os.makedirs(config_dir)
193    except OSError:
194      if not os.path.isdir(config_dir):
195        raise
196    with open(filename, 'w') as writer:
197      _WriteConfig(writer, data)
198
199
200def PrintInfo():
201  print 'Selected output directory is', cr.context.Find('CR_BUILD_DIR')
202  try:
203    for name in cr.auto.build.config.OVERRIDES.exported.keys():
204      print ' ', name, '=', cr.context.Get(name)
205  except AttributeError:
206    pass
207
208
209class InitHook(cr.Plugin, cr.Plugin.Type):
210  """Base class for output directory initialization hooks.
211
212  Implementations used to fix from old version to new ones live in the
213  cr.fixups package.
214  """
215
216  def Run(self, old_version, config):
217    """Run the initialization hook.
218
219    This is invoked once per init invocation.
220    Args:
221      old_version: The old version,
222          0.0 if the old version was bad or missing,
223          None if building a new output direcory.
224      config: The mutable config that will be written.
225    """
226    raise NotImplementedError('Must be overridden.')
227
228