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