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