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