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