• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2020 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Parses config file and provides various ways of using it."""
15
16import xml.etree.ElementTree as ET
17import collections
18
19# The config file must be in XML with a structure as descibed below.
20#
21# The top level config element shall contain one or more "target" child
22# elements. Each of these may contain one or more build_config child elements.
23# The build_config child elements will inherit the properties of the target
24# parent.
25#
26# Each "target" and "build_config" may contain the following:
27#
28# Attributes:
29#
30#   name: The name of the target.
31#
32#   android_target: The name of the android target used with lunch
33#
34#   allow_readwrite_all: "true" if the full source folder shall be mounted as
35#   read/write. It should be accompanied by a comment with the bug describing
36#   why it was required.
37#
38#   tags: A comma-separated list of strings to be associated with the target
39#     and any of its nested build_targets. You can use a tag to associate
40#     information with a target in your configuration file, and retrieve that
41#     information using the get_tags API or the has_tag API.
42#
43# Child elements:
44#
45#   config: A generic name-value configuration element.
46#
47#     Attributes:
48#       name: Name of the configuration
49#       value: Value of the configuration
50#
51#   overlay: An overlay to be mounted while building the target.
52#
53#     Attributes:
54#
55#       name: The name of the overlay.
56#
57#     Child elements:
58#
59#       replacement_path:  An overlay path that supersedes any conflicts
60#         after it.
61#
62#         Properties:
63#
64#           name: The name of the replacement path. This path will will
65#             superced the same path for any subsequent conflicts. If two
66#             overlays have the same replacement path an error will occur.
67#
68#
69#   view: A map (optionally) specifying a filesystem view mapping for each
70#     target.
71#
72#     Attributes:
73#
74#       name: The name of the view.
75#
76#   allow_readwrite: A folder to mount read/write
77#   inside the Android build nsjail. Each allowed read-write entry should be
78#   accompanied by a bug that indicates why it was required and tracks the
79#   progress to a fix.
80#
81#     Attributes:
82#
83#       path: The path to be allowed read-write mounting.
84#
85#   build_config: A list of goals to be used while building the target.
86#
87#     Attributes:
88#
89#       name: The name of the build config. Defaults to the target name
90#         if not set.
91#
92#     Child elements:
93#
94#       goal: A build goal.
95#
96#         Properties:
97#
98#           name: The name of the build goal. The build tools pass the name
99#             attribute as a parameter to make. This can have a value like
100#             "droid" or "VAR=value".
101#
102#           contexts: A comma-separated list of the contexts in which this
103#             goal applies. If this attribute is missing or blank, the goal
104#             applies to all contexts. Otherwise, it applies only in the
105#             requested contexts (see get_build_goals).
106
107Overlay = collections.namedtuple('Overlay', ['name', 'replacement_paths'])
108
109
110class BuildConfig(object):
111  """Represents configuration of a build_target.
112
113  Attributes:
114    name: name of the build_target used to pull the configuration.
115    android_target: The name of the android target used with lunch.
116    tags: List of tags associated with the build target config
117    build_goals: List of goals to be used while building the target.
118    overlays: List of overlays to be mounted.
119    views: A list of (source, destination) string path tuple to be mounted. See
120      view nodes in XML.
121    allow_readwrite_all: If true, mount source tree as rw.
122    allow_readwrite: List of directories to be mounted as rw.
123    allowed_projects_file: a string path name of a file with a containing
124      allowed projects.
125    configurations: a map of name to value configurations
126  """
127
128  def __init__(self,
129               name,
130               android_target,
131               tags=frozenset(),
132               build_goals=(),
133               build_flags=(),
134               overlays=(),
135               views=(),
136               allow_readwrite_all=False,
137               allow_readwrite=(),
138               allowed_projects_file=None,
139               configurations=None):
140    super().__init__()
141    self.name = name
142    self.android_target = android_target
143    self.tags = tags
144    self.build_goals = list(build_goals)
145    self.build_flags = list(build_flags)
146    self.overlays = list(overlays)
147    self.views = list(views)
148    self.allow_readwrite_all = allow_readwrite_all
149    self.allow_readwrite = list(allow_readwrite)
150    self.allowed_projects_file = allowed_projects_file
151    self.configurations = configurations or {}
152
153  def validate(self):
154    """Run tests to validate build configuration"""
155    if not self.name:
156      raise ValueError('Error build_config must have a name.')
157    # Validate that a build config does not contain an overlay with
158    # conflicting replacement paths.
159    if len(self.overlays) > 1 and set.intersection(
160        *[o.replacement_paths for o in self.overlays]):
161      raise ValueError(
162          'Error build_config overlays have conflicting replacement_paths.')
163
164  @classmethod
165  def from_config(cls, config_elem, fs_view_map, base_config=None):
166    """Creates a BuildConfig from a config XML element and an optional base_config.
167
168    Args:
169      config_elem: the config XML node element to build the configuration
170      fs_view_map: A map of view names to list of tuple(source, destination)
171        paths.
172      base_config: the base BuildConfig to use
173
174    Returns:
175      A build config generated from the config element and the base
176      configuration if provided.
177    """
178    if base_config is None:
179      # Build a base_config with required elements from the new config_elem
180      name = config_elem.get('name')
181      base_config = cls(
182          name=name, android_target=config_elem.get('android_target', name))
183
184    return cls(
185        android_target=config_elem.get('android_target',
186                                       base_config.android_target),
187        name=config_elem.get('name', base_config.name),
188        allowed_projects_file=config_elem.get(
189            'allowed_projects_file', base_config.allowed_projects_file),
190        build_goals=_get_build_config_goals(config_elem,
191                                            base_config.build_goals),
192        build_flags=_get_build_config_flags(config_elem,
193                                            base_config.build_flags),
194        tags=_get_config_tags(config_elem, base_config.tags),
195        overlays=_get_overlays(config_elem, base_config.overlays),
196        allow_readwrite=_get_allow_readwrite(config_elem,
197                                             base_config.allow_readwrite),
198        views=_get_views(config_elem, fs_view_map, base_config.views),
199        allow_readwrite_all=_get_allowed_readwrite_all(
200            config_elem, base_config.allow_readwrite_all),
201        configurations=_get_configurations(config_elem,
202                                           base_config.configurations))
203
204
205def _get_configurations(config_elem, base):
206  configs = dict(base)
207  configs.update({
208      config.get('name'): config.get('value')
209      for config in config_elem.findall('config')
210  })
211  return configs
212
213
214def _get_build_config_goals(config_elem, base=None):
215  """Retrieves goals from build_config or target.
216
217  Args:
218    config_elem: A build_config or target xml element.
219    base: Initial list of goals to prepend to the list
220
221  Returns:
222    A list of tuples where the first element of the tuple is the build goal
223    name, and the second is a list of the contexts to which this goal applies.
224  """
225
226  return base + [(goal.get('name'), set(goal.get('contexts').split(','))
227                  if goal.get('contexts') else None)
228                 for goal in config_elem.findall('goal')]
229
230
231def _get_build_config_flags(config_elem, base=None):
232  """See _get_build_config_goals. Gets 'flag' instead of 'goal'."""
233  return base + [(goal.get('name'), set(goal.get('contexts').split(','))
234                  if goal.get('contexts') else None)
235                 for goal in config_elem.findall('flag')]
236
237
238def _get_config_tags(config_elem, base=frozenset()):
239  """Retrieves tags from build_config or target.
240
241  Args:
242    config_elem: A build_config or target xml element.
243    base: Initial list of tags to seed the set
244
245  Returns:
246    A set of tags for a build_config.
247  """
248  tags = config_elem.get('tags')
249  return base.union(set(tags.split(',')) if tags else set())
250
251
252def _get_allowed_readwrite_all(config_elem, default=False):
253  """Determines if build_config or target is set to allow readwrite for all source paths.
254
255  Args:
256    config_elem: A build_config or target xml element.
257    default: Value to use if element doesn't contain the allow_readwrite_all
258      attribute.
259
260  Returns:
261    True if build config is set to allow readwrite for all sorce paths
262  """
263  value = config_elem.get('allow_readwrite_all')
264  return value == 'true' if value else default
265
266
267def _get_overlays(config_elem, base=None):
268  """Retrieves list of overlays from build_config or target.
269
270  Args:
271    config_elem: A build_config or target xml element.
272    base: Initial list of overlays to prepend to the list
273
274  Returns:
275    A list of tuples of overlays and replacement paths to mount for a
276    build_config or target.
277  """
278  overlays = []
279  for overlay in config_elem.findall('overlay'):
280    overlays.append(
281        Overlay(
282            name=overlay.get('name'),
283            replacement_paths=set([
284                path.get('path') for path in overlay.findall('replacement_path')
285            ])))
286  return base + overlays
287
288
289def _get_views(config_elem, fs_view_map, base=None):
290  """Retrieves list of views from build_config or target.
291
292  Args:
293    config_elem: A build_config or target xml element.
294    base: Initial list of views to prepend to the list
295
296  Returns:
297    A list of (source, destination) string path tuple to be mounted. See view
298      nodes in XML.
299  """
300  return base + [
301      fs for o in config_elem.findall('view')
302      for fs in fs_view_map[o.get('name')]
303  ]
304
305
306def _get_allow_readwrite(config_elem, base=None):
307  """Retrieves list of directories to be mounted rw from build_config or target.
308
309  Args:
310    config_elem: A build_config or target xml element.
311    base: Initial list of rw directories to prepend to the list
312
313  Returns:
314    A list of directories to be mounted rw.
315  """
316  return (base +
317          [o.get('path') for o in config_elem.findall('allow_readwrite')])
318
319
320def _get_fs_view_map(config):
321  """Retrieves the map of filesystem views.
322
323  Args:
324    config: An XML Element that is the root of the config XML tree.
325
326  Returns:
327    A dict of filesystem views keyed by view name. A filesystem view is a
328    list of (source, destination) string path tuples.
329  """
330  # A valid config file is not required to include FS Views, only overlay
331  # targets.
332  return {
333      view.get('name'): [(path.get('source'), path.get('destination'))
334                         for path in view.findall('path')
335                        ] for view in config.findall('view')
336  }
337
338
339def _get_build_config_map(config):
340  """Retrieves a map of all build config.
341
342  Args:
343    config: An XML Element that is the root of the config XML tree.
344
345  Returns:
346    A dict of BuildConfig keyed by build_target.
347  """
348  fs_view_map = _get_fs_view_map(config)
349  build_config_map = {}
350  for target_config in config.findall('target'):
351    base_target = BuildConfig.from_config(target_config, fs_view_map)
352
353    for build_config in target_config.findall('build_config'):
354      build_target = BuildConfig.from_config(build_config, fs_view_map,
355                                             base_target)
356      build_target.validate()
357      build_config_map[build_target.name] = build_target
358
359  return build_config_map
360
361
362class Config:
363  """Presents an API to the static XML configuration."""
364
365  def __init__(self, config_filename):
366    """Initializes a Config instance from the specificed filename
367
368    This method parses the XML content of the file named by config_filename
369    into internal data structures. You can then use various methods to query
370    the static config.
371
372    Args:
373      config_filename: The name of the file from which to load the config.
374    """
375
376    tree = ET.parse(config_filename)
377    config = tree.getroot()
378    self._build_config_map = _get_build_config_map(config)
379
380  def get_available_build_targets(self):
381    """Return a list of available build targets."""
382    return sorted(self._build_config_map.keys())
383
384  def get_tags(self, build_target):
385    """Given a build_target, return the (possibly empty) set of tags."""
386    return self._build_config_map[build_target].tags
387
388  def has_tag(self, build_target, tag):
389    """Return true if build_target has tag.
390
391    Args:
392      build_target: A string build_target to be queried.
393      tag: A string tag that this target may have.
394
395    Returns:
396      If the build_target has the tag, True. Otherwise, False.
397    """
398    return tag in self._build_config_map[build_target].tags
399
400  def get_allowed_projects_file(self, build_target):
401    """Given a build_target, return a string with the allowed projects file."""
402    return self._build_config_map[build_target].allowed_projects_file
403
404  def get_build_config_android_target(self, build_target):
405    """Given a build_target, return an android_target.
406
407    Generally a build_target maps directory to the android_target of the same
408    name, but they can differ. In a config.xml file, the name attribute of a
409    target element is the android_target (which is used for lunch). The name
410    attribute (if any) of a build_config element is the build_target. If a
411    build_config element does not have a name attribute, then the build_target
412    is the android_target.
413
414    Args:
415      build_target: A string build_target to be queried.
416
417    Returns:
418      A string android_target that can be used for lunch.
419    """
420    return self._build_config_map[build_target].android_target
421
422  def get_build_goals(self, build_target, contexts=frozenset()):
423    """Given a build_target and a context, return a list of build goals.
424
425    For a given build_target, we may build in a variety of contexts. For
426    example we might build in continuous integration, or we might build
427    locally, or other contexts defined by the configuration file and scripts
428    that use it. The contexts parameter is a set of strings that specify the
429    contexts for which this function should retrieve goals.
430
431    In the configuration file, each goal has a contexts attribute, which
432    specifies the contexts to which the goal applies. We treat a goal with no
433    contexts attribute as applying to all contexts.
434
435    Example:
436
437      <build_config>
438        <goal name="droid"/>
439        <goal name="dist" contexts="ota"/>
440      </build_config>
441
442      Here we have the goal "droid", which matches all contexts, and the goal
443      "dist", which matches the "ota" context. Invoking this method with the
444      set(['ota']) would return ['droid', 'dist'].
445
446    Args:
447      build_target: A string build_target to be queried.
448      context: A set of contexts for which to retrieve goals.
449
450    Returns:
451      A list of strings, where each string is a goal to be passed to make.
452    """
453
454    build_goals = []
455    for goal, build_contexts in self._build_config_map[
456        build_target].build_goals:
457      if not build_contexts:
458        build_goals.append(goal)
459      elif build_contexts.intersection(contexts):
460        build_goals.append(goal)
461
462    return build_goals
463
464  def get_build_flags(self, build_target, contexts=frozenset()):
465    """See get_build_goals. Gets flags instead of goals."""
466    build_flags = []
467    for flag, build_contexts in self._build_config_map[
468        build_target].build_flags:
469      if not build_contexts:
470        build_flags.append(flag)
471      elif build_contexts.intersection(contexts):
472        build_flags.append(flag)
473
474    return build_flags
475
476  def get_rw_allowlist_map(self):
477    """Return read-write allowlist map.
478
479    Returns:
480      A dict of string lists of keyed by target name. Each value in the dict is
481      a list of allowed read-write paths corresponding to the target.
482    """
483    return {b.name: b.allow_readwrite for b in self._build_config_map.values()}
484
485  def get_allow_readwrite_all(self, build_target):
486    """Return True if the target should mount all its source as read-write.
487
488    Args:
489      build_target: A string build_target to be queried.
490
491    Returns:
492      True if the target should mount all its source as read-write.
493    """
494    return self._build_config_map[build_target].allow_readwrite_all
495
496  def get_overlay_map(self):
497    """Return the overlay map.
498
499    Returns:
500      A dict of keyed by target name. Each value in the dict is a list of
501      overlay names corresponding to the target.
502    """
503    return {
504        b.name: [o.name for o in b.overlays
505                ] for b in self._build_config_map.values()
506    }
507
508  def get_fs_view_map(self):
509    """Return the filesystem view map.
510
511    Returns:
512      A dict of filesystem views keyed by target name. A filesystem view is a
513      list of (source, destination) string path tuples.
514    """
515    return {b.name: b.views for b in self._build_config_map.values()}
516
517  def get_build_config(self, build_target):
518    return self._build_config_map[build_target]
519
520
521def factory(config_filename):
522  """Create an instance of a Config class.
523
524  Args:
525    config_filename: The name of the file from which to load the config. This
526      can be None, which results in this function returning None.
527
528  Returns:
529    If config_filename is None, returns None. Otherwise, a new instance of a
530    Config class containing the configuration parsed from config_filename.
531  """
532  if config_filename is None:
533    return None
534
535  return Config(config_filename)
536