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