1# Copyright 2021, The Android Open Source Project 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# http://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"""Implementation of Atest's Bazel mode. 16 17Bazel mode runs tests using Bazel by generating a synthetic workspace that 18contains test targets. Using Bazel allows Atest to leverage features such as 19sandboxing, caching, and remote execution. 20""" 21# pylint: disable=missing-function-docstring 22# pylint: disable=missing-class-docstring 23# pylint: disable=too-many-lines 24 25from __future__ import annotations 26 27from abc import ABC, abstractmethod 28import argparse 29import atexit 30from collections import OrderedDict, defaultdict, deque 31from collections.abc import Iterable 32import contextlib 33import dataclasses 34import enum 35import functools 36import importlib.resources 37import logging 38import os 39import pathlib 40import re 41import shlex 42import shutil 43import subprocess 44import tempfile 45import time 46from types import MappingProxyType 47from typing import Any, Callable, Dict, IO, List, Set, Tuple 48import warnings 49from xml.etree import ElementTree as ET 50 51from atest import atest_configs 52from atest import atest_utils 53from atest import constants 54from atest import module_info 55from atest.atest_enum import DetectType, ExitCode 56from atest.metrics import metrics 57from atest.proto import file_md5_pb2 58from atest.test_finders import test_finder_base 59from atest.test_finders import test_info 60from atest.test_runners import atest_tf_test_runner as tfr 61from atest.test_runners import test_runner_base as trb 62from google.protobuf.message import DecodeError 63 64 65JDK_PACKAGE_NAME = 'prebuilts/robolectric_jdk' 66JDK_NAME = 'jdk' 67ROBOLECTRIC_CONFIG = 'build/make/core/robolectric_test_config_template.xml' 68 69BAZEL_TEST_LOGS_DIR_NAME = 'bazel-testlogs' 70TEST_OUTPUT_DIR_NAME = 'test.outputs' 71TEST_OUTPUT_ZIP_NAME = 'outputs.zip' 72 73_BAZEL_WORKSPACE_DIR = 'atest_bazel_workspace' 74_SUPPORTED_BAZEL_ARGS = MappingProxyType({ 75 # https://docs.bazel.build/versions/main/command-line-reference.html#flag--runs_per_test 76 constants.ITERATIONS: lambda arg_value: [ 77 f'--runs_per_test={str(arg_value)}' 78 ], 79 # https://docs.bazel.build/versions/main/command-line-reference.html#flag--flaky_test_attempts 80 constants.RETRY_ANY_FAILURE: lambda arg_value: [ 81 f'--flaky_test_attempts={str(arg_value)}' 82 ], 83 # https://docs.bazel.build/versions/main/command-line-reference.html#flag--test_output 84 constants.VERBOSE: ( 85 lambda arg_value: ['--test_output=all'] if arg_value else [] 86 ), 87 constants.BAZEL_ARG: lambda arg_value: [ 88 item for sublist in arg_value for item in sublist 89 ], 90}) 91 92# Maps Bazel configuration names to Soong variant names. 93_CONFIG_TO_VARIANT = { 94 'host': 'host', 95 'device': 'target', 96} 97 98 99class AbortRunException(Exception): 100 pass 101 102 103@enum.unique 104class Features(enum.Enum): 105 NULL_FEATURE = ('--null-feature', 'Enables a no-action feature.', True) 106 EXPERIMENTAL_DEVICE_DRIVEN_TEST = ( 107 '--experimental-device-driven-test', 108 'Enables running device-driven tests in Bazel mode.', 109 True, 110 ) 111 EXPERIMENTAL_REMOTE_AVD = ( 112 '--experimental-remote-avd', 113 'Enables running device-driven tests in remote AVD.', 114 False, 115 ) 116 EXPERIMENTAL_BES_PUBLISH = ( 117 '--experimental-bes-publish', 118 'Upload test results via BES in Bazel mode.', 119 False, 120 ) 121 EXPERIMENTAL_JAVA_RUNTIME_DEPENDENCIES = ( 122 '--experimental-java-runtime-dependencies', 123 ( 124 'Mirrors Soong Java `libs` and `static_libs` as Bazel target ' 125 'dependencies in the generated workspace. Tradefed test rules use ' 126 'these dependencies to set up the execution environment and ensure ' 127 'that all transitive runtime dependencies are present.' 128 ), 129 True, 130 ) 131 EXPERIMENTAL_REMOTE = ( 132 '--experimental-remote', 133 'Use Bazel remote execution and caching where supported.', 134 False, 135 ) 136 EXPERIMENTAL_HOST_DRIVEN_TEST = ( 137 '--experimental-host-driven-test', 138 'Enables running host-driven device tests in Bazel mode.', 139 True, 140 ) 141 EXPERIMENTAL_ROBOLECTRIC_TEST = ( 142 '--experimental-robolectric-test', 143 'Enables running Robolectric tests in Bazel mode.', 144 True, 145 ) 146 NO_BAZEL_DETAILED_SUMMARY = ( 147 '--no-bazel-detailed-summary', 148 'Disables printing detailed summary of Bazel test results.', 149 False, 150 ) 151 152 def __init__(self, arg_flag, description, affects_workspace): 153 self._arg_flag = arg_flag 154 self._description = description 155 self.affects_workspace = affects_workspace 156 157 @property 158 def arg_flag(self): 159 return self._arg_flag 160 161 @property 162 def description(self): 163 return self._description 164 165 166def add_parser_arguments(parser: argparse.ArgumentParser, dest: str): 167 for _, member in Features.__members__.items(): 168 parser.add_argument( 169 member.arg_flag, 170 action='append_const', 171 const=member, 172 dest=dest, 173 help=member.description, 174 ) 175 176 177def get_bazel_workspace_dir() -> pathlib.Path: 178 return atest_utils.get_build_out_dir(_BAZEL_WORKSPACE_DIR) 179 180 181def generate_bazel_workspace( 182 mod_info: module_info.ModuleInfo, enabled_features: Set[Features] = None 183): 184 """Generate or update the Bazel workspace used for running tests.""" 185 186 start = time.time() 187 src_root_path = pathlib.Path(os.environ.get(constants.ANDROID_BUILD_TOP)) 188 workspace_path = get_bazel_workspace_dir() 189 resource_manager = ResourceManager( 190 src_root_path=src_root_path, 191 resource_root_path=_get_resource_root(), 192 product_out_path=pathlib.Path( 193 os.environ.get(constants.ANDROID_PRODUCT_OUT) 194 ), 195 md5_checksum_file_path=workspace_path.joinpath('workspace_md5_checksum'), 196 ) 197 jdk_path = _read_robolectric_jdk_path( 198 resource_manager.get_src_file_path(ROBOLECTRIC_CONFIG, True) 199 ) 200 201 workspace_generator = WorkspaceGenerator( 202 resource_manager=resource_manager, 203 workspace_out_path=workspace_path, 204 host_out_path=pathlib.Path(os.environ.get(constants.ANDROID_HOST_OUT)), 205 build_out_dir=atest_utils.get_build_out_dir(), 206 mod_info=mod_info, 207 jdk_path=jdk_path, 208 enabled_features=enabled_features, 209 ) 210 workspace_generator.generate() 211 212 metrics.LocalDetectEvent( 213 detect_type=DetectType.BAZEL_WORKSPACE_GENERATE_TIME, 214 result=int(time.time() - start), 215 ) 216 217 218def get_default_build_metadata(): 219 return BuildMetadata( 220 atest_utils.get_manifest_branch(), atest_utils.get_build_target() 221 ) 222 223 224class ResourceManager: 225 """Class for managing files required to generate a Bazel Workspace.""" 226 227 def __init__( 228 self, 229 src_root_path: pathlib.Path, 230 resource_root_path: pathlib.Path, 231 product_out_path: pathlib.Path, 232 md5_checksum_file_path: pathlib.Path, 233 ): 234 self._root_type_to_path = { 235 file_md5_pb2.RootType.SRC_ROOT: src_root_path, 236 file_md5_pb2.RootType.RESOURCE_ROOT: resource_root_path, 237 file_md5_pb2.RootType.ABS_PATH: pathlib.Path(), 238 file_md5_pb2.RootType.PRODUCT_OUT: product_out_path, 239 } 240 self._md5_checksum_file = md5_checksum_file_path 241 self._file_checksum_list = file_md5_pb2.FileChecksumList() 242 243 def get_src_file_path( 244 self, rel_path: pathlib.Path = None, affects_workspace: bool = False 245 ) -> pathlib.Path: 246 """Get the abs file path from the relative path of source_root. 247 248 Args: 249 rel_path: A relative path of the source_root. 250 affects_workspace: A boolean of whether the file affects the workspace. 251 252 Returns: 253 A abs path of the file. 254 """ 255 return self._get_file_path( 256 file_md5_pb2.RootType.SRC_ROOT, rel_path, affects_workspace 257 ) 258 259 def get_resource_file_path( 260 self, 261 rel_path: pathlib.Path = None, 262 affects_workspace: bool = False, 263 ) -> pathlib.Path: 264 """Get the abs file path from the relative path of resource_root. 265 266 Args: 267 rel_path: A relative path of the resource_root. 268 affects_workspace: A boolean of whether the file affects the workspace. 269 270 Returns: 271 A abs path of the file. 272 """ 273 return self._get_file_path( 274 file_md5_pb2.RootType.RESOURCE_ROOT, rel_path, affects_workspace 275 ) 276 277 def get_product_out_file_path( 278 self, rel_path: pathlib.Path = None, affects_workspace: bool = False 279 ) -> pathlib.Path: 280 """Get the abs file path from the relative path of product out. 281 282 Args: 283 rel_path: A relative path to the product out. 284 affects_workspace: A boolean of whether the file affects the workspace. 285 286 Returns: 287 An abs path of the file. 288 """ 289 return self._get_file_path( 290 file_md5_pb2.RootType.PRODUCT_OUT, rel_path, affects_workspace 291 ) 292 293 def _get_file_path( 294 self, 295 root_type: file_md5_pb2.RootType, 296 rel_path: pathlib.Path, 297 affects_workspace: bool = True, 298 ) -> pathlib.Path: 299 abs_path = self._root_type_to_path[root_type].joinpath( 300 rel_path or pathlib.Path() 301 ) 302 303 if not affects_workspace: 304 return abs_path 305 306 if abs_path.is_dir(): 307 for file in abs_path.glob('**/*'): 308 self._register_file(root_type, file) 309 else: 310 self._register_file(root_type, abs_path) 311 return abs_path 312 313 def _register_file( 314 self, root_type: file_md5_pb2.RootType, abs_path: pathlib.Path 315 ): 316 if not abs_path.is_file(): 317 logging.debug(' ignore %s: not a file.', abs_path) 318 return 319 320 rel_path = abs_path 321 if abs_path.is_relative_to(self._root_type_to_path[root_type]): 322 rel_path = abs_path.relative_to(self._root_type_to_path[root_type]) 323 324 self._file_checksum_list.file_checksums.append( 325 file_md5_pb2.FileChecksum( 326 root_type=root_type, 327 rel_path=str(rel_path), 328 md5sum=atest_utils.md5sum(abs_path), 329 ) 330 ) 331 332 def register_file_with_abs_path(self, abs_path: pathlib.Path): 333 """Register a file which affects the workspace. 334 335 Args: 336 abs_path: A abs path of the file. 337 """ 338 self._register_file(file_md5_pb2.RootType.ABS_PATH, abs_path) 339 340 def save_affects_files_md5(self): 341 with open(self._md5_checksum_file, 'wb') as f: 342 f.write(self._file_checksum_list.SerializeToString()) 343 344 def check_affects_files_md5(self): 345 """Check all affect files are consistent with the actual MD5.""" 346 if not self._md5_checksum_file.is_file(): 347 return False 348 349 with open(self._md5_checksum_file, 'rb') as f: 350 file_md5_list = file_md5_pb2.FileChecksumList() 351 352 try: 353 file_md5_list.ParseFromString(f.read()) 354 except DecodeError: 355 atest_utils.print_and_log_warning( 356 'Failed to parse the workspace md5 checksum file.' 357 ) 358 return False 359 360 for file_md5 in file_md5_list.file_checksums: 361 abs_path = pathlib.Path( 362 self._root_type_to_path[file_md5.root_type] 363 ).joinpath(file_md5.rel_path) 364 if not abs_path.is_file(): 365 return False 366 if atest_utils.md5sum(abs_path) != file_md5.md5sum: 367 return False 368 return True 369 370 371class WorkspaceGenerator: 372 """Class for generating a Bazel workspace.""" 373 374 # pylint: disable=too-many-arguments 375 def __init__( 376 self, 377 resource_manager: ResourceManager, 378 workspace_out_path: pathlib.Path, 379 host_out_path: pathlib.Path, 380 build_out_dir: pathlib.Path, 381 mod_info: module_info.ModuleInfo, 382 jdk_path: pathlib.Path = None, 383 enabled_features: Set[Features] = None, 384 ): 385 """Initializes the generator. 386 387 Args: 388 workspace_out_path: Path where the workspace will be output. 389 host_out_path: Path of the ANDROID_HOST_OUT. 390 build_out_dir: Path of OUT_DIR 391 mod_info: ModuleInfo object. 392 enabled_features: Set of enabled features. 393 """ 394 if ( 395 enabled_features 396 and Features.EXPERIMENTAL_REMOTE_AVD in enabled_features 397 and Features.EXPERIMENTAL_DEVICE_DRIVEN_TEST not in enabled_features 398 ): 399 raise ValueError( 400 'Cannot run remote device test because ' 401 '"--experimental-device-driven-test" flag is' 402 ' not set.' 403 ) 404 self.enabled_features = enabled_features or set() 405 self.resource_manager = resource_manager 406 self.workspace_out_path = workspace_out_path 407 self.host_out_path = host_out_path 408 self.build_out_dir = build_out_dir 409 self.mod_info = mod_info 410 self.path_to_package = {} 411 self.jdk_path = jdk_path 412 413 def generate(self): 414 """Generate a Bazel workspace. 415 416 If the workspace md5 checksum file doesn't exist or is stale, a new 417 workspace will be generated. Otherwise, the existing workspace will be 418 reused. 419 """ 420 start = time.time() 421 enabled_features_file = self.workspace_out_path.joinpath( 422 'atest_bazel_mode_enabled_features' 423 ) 424 enabled_features_file_contents = '\n'.join( 425 sorted(f.name for f in self.enabled_features if f.affects_workspace) 426 ) 427 428 if self.workspace_out_path.exists(): 429 # Update the file with the set of the currently enabled features to 430 # make sure that changes are detected in the workspace checksum. 431 enabled_features_file.write_text(enabled_features_file_contents) 432 if self.resource_manager.check_affects_files_md5(): 433 return 434 435 # We raise an exception if rmtree fails to avoid leaving stale 436 # files in the workspace that could interfere with execution. 437 shutil.rmtree(self.workspace_out_path) 438 439 atest_utils.colorful_print('Generating Bazel workspace.\n', constants.RED) 440 441 self._add_test_module_targets() 442 443 self.workspace_out_path.mkdir(parents=True) 444 self._generate_artifacts() 445 446 # Note that we write the set of enabled features despite having written 447 # it above since the workspace no longer exists at this point. 448 enabled_features_file.write_text(enabled_features_file_contents) 449 450 self.resource_manager.get_product_out_file_path( 451 self.mod_info.mod_info_file_path.relative_to( 452 self.resource_manager.get_product_out_file_path() 453 ), 454 True, 455 ) 456 self.resource_manager.register_file_with_abs_path(enabled_features_file) 457 self.resource_manager.save_affects_files_md5() 458 metrics.LocalDetectEvent( 459 detect_type=DetectType.FULL_GENERATE_BAZEL_WORKSPACE_TIME, 460 result=int(time.time() - start), 461 ) 462 463 def _add_test_module_targets(self): 464 seen = set() 465 466 for name, info in self.mod_info.name_to_module_info.items(): 467 # Ignore modules that have a 'host_cross_' prefix since they are 468 # duplicates of existing modules. For example, 469 # 'host_cross_aapt2_tests' is a duplicate of 'aapt2_tests'. We also 470 # ignore modules with a '_32' suffix since these also are redundant 471 # given that modules have both 32 and 64-bit variants built by 472 # default. See b/77288544#comment6 and b/23566667 for more context. 473 if name.endswith('_32') or name.startswith('host_cross_'): 474 continue 475 476 if ( 477 Features.EXPERIMENTAL_DEVICE_DRIVEN_TEST in self.enabled_features 478 and self.mod_info.is_device_driven_test(info) 479 ): 480 self._resolve_dependencies( 481 self._add_device_test_target(info, False), seen 482 ) 483 484 if self.mod_info.is_host_unit_test(info): 485 self._resolve_dependencies(self._add_deviceless_test_target(info), seen) 486 elif ( 487 Features.EXPERIMENTAL_ROBOLECTRIC_TEST in self.enabled_features 488 and self.mod_info.is_modern_robolectric_test(info) 489 ): 490 self._resolve_dependencies( 491 self._add_tradefed_robolectric_test_target(info), seen 492 ) 493 elif ( 494 Features.EXPERIMENTAL_HOST_DRIVEN_TEST in self.enabled_features 495 and self.mod_info.is_host_driven_test(info) 496 ): 497 self._resolve_dependencies( 498 self._add_device_test_target(info, True), seen 499 ) 500 501 def _resolve_dependencies(self, top_level_target: Target, seen: Set[Target]): 502 503 stack = [deque([top_level_target])] 504 505 while stack: 506 top = stack[-1] 507 508 if not top: 509 stack.pop() 510 continue 511 512 target = top.popleft() 513 514 # Note that we're relying on Python's default identity-based hash 515 # and equality methods. This is fine since we actually DO want 516 # reference-equality semantics for Target objects in this context. 517 if target in seen: 518 continue 519 520 seen.add(target) 521 522 next_top = deque() 523 524 for ref in target.dependencies(): 525 info = ref.info or self._get_module_info(ref.name) 526 ref.set(self._add_prebuilt_target(info)) 527 next_top.append(ref.target()) 528 529 stack.append(next_top) 530 531 def _add_device_test_target( 532 self, info: Dict[str, Any], is_host_driven: bool 533 ) -> Target: 534 package_name = self._get_module_path(info) 535 name_suffix = 'host' if is_host_driven else 'device' 536 name = f'{info[constants.MODULE_INFO_ID]}_{name_suffix}' 537 538 def create(): 539 return TestTarget.create_device_test_target( 540 name, 541 package_name, 542 info, 543 is_host_driven, 544 ) 545 546 return self._add_target(package_name, name, create) 547 548 def _add_deviceless_test_target(self, info: Dict[str, Any]) -> Target: 549 package_name = self._get_module_path(info) 550 name = f'{info[constants.MODULE_INFO_ID]}_host' 551 552 def create(): 553 return TestTarget.create_deviceless_test_target( 554 name, 555 package_name, 556 info, 557 ) 558 559 return self._add_target(package_name, name, create) 560 561 def _add_tradefed_robolectric_test_target( 562 self, info: Dict[str, Any] 563 ) -> Target: 564 package_name = self._get_module_path(info) 565 name = f'{info[constants.MODULE_INFO_ID]}_host' 566 567 return self._add_target( 568 package_name, 569 name, 570 lambda: TestTarget.create_tradefed_robolectric_test_target( 571 name, package_name, info, f'//{JDK_PACKAGE_NAME}:{JDK_NAME}' 572 ), 573 ) 574 575 def _add_prebuilt_target(self, info: Dict[str, Any]) -> Target: 576 package_name = self._get_module_path(info) 577 name = info[constants.MODULE_INFO_ID] 578 579 def create(): 580 return SoongPrebuiltTarget.create( 581 self, 582 info, 583 package_name, 584 ) 585 586 return self._add_target(package_name, name, create) 587 588 def _add_target( 589 self, package_path: str, target_name: str, create_fn: Callable 590 ) -> Target: 591 592 package = self.path_to_package.get(package_path) 593 594 if not package: 595 package = Package(package_path) 596 self.path_to_package[package_path] = package 597 598 target = package.get_target(target_name) 599 600 if target: 601 return target 602 603 target = create_fn() 604 package.add_target(target) 605 606 return target 607 608 def _get_module_info(self, module_name: str) -> Dict[str, Any]: 609 info = self.mod_info.get_module_info(module_name) 610 611 if not info: 612 raise LookupError( 613 f'Could not find module `{module_name}` in module_info file' 614 ) 615 616 return info 617 618 def _get_module_path(self, info: Dict[str, Any]) -> str: 619 mod_path = info.get(constants.MODULE_PATH) 620 621 if len(mod_path) < 1: 622 module_name = info['module_name'] 623 raise ValueError(f'Module `{module_name}` does not have any path') 624 625 if len(mod_path) > 1: 626 module_name = info['module_name'] 627 # We usually have a single path but there are a few exceptions for 628 # modules like libLLVM_android and libclang_android. 629 # TODO(yangbill): Raise an exception for multiple paths once 630 # b/233581382 is resolved. 631 warnings.formatwarning = lambda msg, *args, **kwargs: f'{msg}\n' 632 warnings.warn( 633 f'Module `{module_name}` has more than one path: `{mod_path}`' 634 ) 635 636 return mod_path[0] 637 638 def _generate_artifacts(self): 639 """Generate workspace files on disk.""" 640 641 self._create_base_files() 642 643 self._add_workspace_resource(src='rules', dst='bazel/rules') 644 self._add_workspace_resource(src='configs', dst='bazel/configs') 645 646 if Features.EXPERIMENTAL_DEVICE_DRIVEN_TEST in self.enabled_features: 647 self._add_workspace_resource(src='device_def', dst='device_def') 648 649 self._add_bazel_bootstrap_files() 650 651 # Symlink to package with toolchain definitions. 652 self._symlink(src='prebuilts/build-tools', target='prebuilts/build-tools') 653 654 device_infra_path = 'vendor/google/tools/atest/device_infra' 655 if self.resource_manager.get_src_file_path(device_infra_path).exists(): 656 self._symlink(src=device_infra_path, target=device_infra_path) 657 658 self._link_required_src_file_path('external/bazelbuild-rules_python') 659 self._link_required_src_file_path('external/bazelbuild-rules_java') 660 661 self._create_constants_file() 662 663 self._generate_robolectric_resources() 664 665 for package in self.path_to_package.values(): 666 package.generate(self.workspace_out_path) 667 668 def _generate_robolectric_resources(self): 669 if not self.jdk_path: 670 return 671 672 self._generate_jdk_resources() 673 self._generate_android_all_resources() 674 675 def _generate_jdk_resources(self): 676 # TODO(b/265596946): Create the JDK toolchain instead of using 677 # a filegroup. 678 return self._add_target( 679 JDK_PACKAGE_NAME, 680 JDK_NAME, 681 lambda: FilegroupTarget( 682 JDK_PACKAGE_NAME, 683 JDK_NAME, 684 self.resource_manager.get_src_file_path(self.jdk_path), 685 ), 686 ) 687 688 def _generate_android_all_resources(self): 689 package_name = 'android-all' 690 name = 'android-all' 691 692 return self._add_target( 693 package_name, 694 name, 695 lambda: FilegroupTarget( 696 package_name, name, self.host_out_path.joinpath(f'testcases/{name}') 697 ), 698 ) 699 700 def _symlink(self, *, src, target): 701 """Create a symbolic link in workspace pointing to source file/dir. 702 703 Args: 704 src: A string of a relative path to root of Android source tree. This is 705 the source file/dir path for which the symbolic link will be created. 706 target: A string of a relative path to workspace root. This is the 707 target file/dir path where the symbolic link will be created. 708 """ 709 symlink = self.workspace_out_path.joinpath(target) 710 symlink.parent.mkdir(parents=True, exist_ok=True) 711 symlink.symlink_to(self.resource_manager.get_src_file_path(src)) 712 713 def _create_base_files(self): 714 self._add_workspace_resource(src='WORKSPACE', dst='WORKSPACE') 715 self._add_workspace_resource(src='bazelrc', dst='.bazelrc') 716 717 self.workspace_out_path.joinpath('BUILD.bazel').touch() 718 719 def _add_bazel_bootstrap_files(self): 720 self._add_workspace_resource(src='bazel.sh', dst='bazel.sh') 721 # Restore permissions as execute permissions are not preserved by soong 722 # packaging. 723 os.chmod(self.workspace_out_path.joinpath('bazel.sh'), 0o755) 724 self._symlink( 725 src='prebuilts/jdk/jdk21/BUILD.bazel', 726 target='prebuilts/jdk/jdk21/BUILD.bazel', 727 ) 728 self._symlink( 729 src='prebuilts/jdk/jdk21/linux-x86', 730 target='prebuilts/jdk/jdk21/linux-x86', 731 ) 732 self._symlink( 733 src='prebuilts/bazel/linux-x86_64/bazel', 734 target='prebuilts/bazel/linux-x86_64/bazel', 735 ) 736 737 def _add_workspace_resource(self, src, dst): 738 """Add resource to the given destination in workspace. 739 740 Args: 741 src: A string of a relative path to root of Bazel artifacts. This is the 742 source file/dir path that will be added to workspace. 743 dst: A string of a relative path to workspace root. This is the 744 destination file/dir path where the artifacts will be added. 745 """ 746 src = self.resource_manager.get_resource_file_path(src, True) 747 dst = self.workspace_out_path.joinpath(dst) 748 dst.parent.mkdir(parents=True, exist_ok=True) 749 750 if src.is_file(): 751 shutil.copy(src, dst) 752 else: 753 shutil.copytree(src, dst, ignore=shutil.ignore_patterns('__init__.py')) 754 755 def _create_constants_file(self): 756 def variable_name(target_name): 757 return re.sub(r'[.-]', '_', target_name) + '_label' 758 759 targets = [] 760 seen = set() 761 762 for module_name in TestTarget.DEVICELESS_TEST_PREREQUISITES.union( 763 TestTarget.DEVICE_TEST_PREREQUISITES 764 ): 765 info = self.mod_info.get_module_info(module_name) 766 target = self._add_prebuilt_target(info) 767 self._resolve_dependencies(target, seen) 768 targets.append(target) 769 770 with self.workspace_out_path.joinpath('constants.bzl').open('w') as f: 771 writer = IndentWriter(f) 772 for target in targets: 773 writer.write_line( 774 '%s = "%s"' 775 % (variable_name(target.name()), target.qualified_name()) 776 ) 777 778 def _link_required_src_file_path(self, path): 779 if not self.resource_manager.get_src_file_path(path).exists(): 780 raise RuntimeError(f'Path `{path}` does not exist in source tree.') 781 782 self._symlink(src=path, target=path) 783 784 785@functools.cache 786def _get_resource_root() -> pathlib.Path: 787 tmp_resource_dir = pathlib.Path(tempfile.mkdtemp()) 788 atexit.register(lambda: shutil.rmtree(tmp_resource_dir)) 789 790 def _extract_resources( 791 resource_path: pathlib.Path, 792 dst: pathlib.Path, 793 ignore_file_names: list[str] = None, 794 ): 795 resource = importlib.resources.files(resource_path.as_posix()) 796 dst.mkdir(parents=True, exist_ok=True) 797 for child in resource.iterdir(): 798 if child.is_file(): 799 if child.name in ignore_file_names: 800 continue 801 with importlib.resources.as_file(child) as child_file: 802 shutil.copy(child_file, dst.joinpath(child.name)) 803 elif child.is_dir(): 804 _extract_resources( 805 resource_path.joinpath(child.name), 806 dst.joinpath(child.name), 807 ignore_file_names, 808 ) 809 else: 810 atest_utils.print_and_log_warning( 811 'Ignoring unknown resource: %s', child 812 ) 813 814 try: 815 _extract_resources( 816 pathlib.Path('atest/bazel/resources'), 817 tmp_resource_dir, 818 ignore_file_names=['__init__.py'], 819 ) 820 except ModuleNotFoundError as e: 821 logging.debug( 822 'Bazel resource not found from package path, possible due to running' 823 ' atest from source. Returning resource source path instead: %s', 824 e, 825 ) 826 return pathlib.Path(os.path.dirname(__file__)).joinpath('bazel/resources') 827 828 return tmp_resource_dir 829 830 831class Package: 832 """Class for generating an entire Package on disk.""" 833 834 def __init__(self, path: str): 835 self.path = path 836 self.imports = defaultdict(set) 837 self.name_to_target = OrderedDict() 838 839 def add_target(self, target): 840 target_name = target.name() 841 842 if target_name in self.name_to_target: 843 raise ValueError( 844 f'Cannot add target `{target_name}` which already' 845 f' exists in package `{self.path}`' 846 ) 847 848 self.name_to_target[target_name] = target 849 850 for i in target.required_imports(): 851 self.imports[i.bzl_package].add(i.symbol) 852 853 def generate(self, workspace_out_path: pathlib.Path): 854 package_dir = workspace_out_path.joinpath(self.path) 855 package_dir.mkdir(parents=True, exist_ok=True) 856 857 self._create_filesystem_layout(package_dir) 858 self._write_build_file(package_dir) 859 860 def _create_filesystem_layout(self, package_dir: pathlib.Path): 861 for target in self.name_to_target.values(): 862 target.create_filesystem_layout(package_dir) 863 864 def _write_build_file(self, package_dir: pathlib.Path): 865 with package_dir.joinpath('BUILD.bazel').open('w') as f: 866 f.write('package(default_visibility = ["//visibility:public"])\n') 867 f.write('\n') 868 869 for bzl_package, symbols in sorted(self.imports.items()): 870 symbols_text = ', '.join('"%s"' % s for s in sorted(symbols)) 871 f.write(f'load("{bzl_package}", {symbols_text})\n') 872 873 for target in self.name_to_target.values(): 874 f.write('\n') 875 target.write_to_build_file(f) 876 877 def get_target(self, target_name: str) -> Target: 878 return self.name_to_target.get(target_name, None) 879 880 881@dataclasses.dataclass(frozen=True) 882class Import: 883 bzl_package: str 884 symbol: str 885 886 887@dataclasses.dataclass(frozen=True) 888class Config: 889 name: str 890 out_path: pathlib.Path 891 892 893class ModuleRef: 894 895 @staticmethod 896 def for_info(info) -> ModuleRef: 897 return ModuleRef(info=info) 898 899 @staticmethod 900 def for_name(name) -> ModuleRef: 901 return ModuleRef(name=name) 902 903 def __init__(self, info=None, name=None): 904 self.info = info 905 self.name = name 906 self._target = None 907 908 def target(self) -> Target: 909 if not self._target: 910 target_name = self.info[constants.MODULE_INFO_ID] 911 raise ValueError(f'Target not set for ref `{target_name}`') 912 913 return self._target 914 915 def set(self, target): 916 self._target = target 917 918 919class Target(ABC): 920 """Abstract class for a Bazel target.""" 921 922 @abstractmethod 923 def name(self) -> str: 924 pass 925 926 def package_name(self) -> str: 927 pass 928 929 def qualified_name(self) -> str: 930 return f'//{self.package_name()}:{self.name()}' 931 932 def required_imports(self) -> Set[Import]: 933 return set() 934 935 def supported_configs(self) -> Set[Config]: 936 return set() 937 938 def dependencies(self) -> List[ModuleRef]: 939 return [] 940 941 def write_to_build_file(self, f: IO): 942 pass 943 944 def create_filesystem_layout(self, package_dir: pathlib.Path): 945 pass 946 947 948class FilegroupTarget(Target): 949 950 def __init__( 951 self, package_name: str, target_name: str, srcs_root: pathlib.Path 952 ): 953 self._package_name = package_name 954 self._target_name = target_name 955 self._srcs_root = srcs_root 956 957 def name(self) -> str: 958 return self._target_name 959 960 def package_name(self) -> str: 961 return self._package_name 962 963 def write_to_build_file(self, f: IO): 964 writer = IndentWriter(f) 965 build_file_writer = BuildFileWriter(writer) 966 967 writer.write_line('filegroup(') 968 969 with writer.indent(): 970 build_file_writer.write_string_attribute('name', self._target_name) 971 build_file_writer.write_glob_attribute( 972 'srcs', [f'{self._target_name}_files/**'] 973 ) 974 975 writer.write_line(')') 976 977 def create_filesystem_layout(self, package_dir: pathlib.Path): 978 symlink = package_dir.joinpath(f'{self._target_name}_files') 979 symlink.symlink_to(self._srcs_root) 980 981 982class TestTarget(Target): 983 """Class for generating a test target.""" 984 985 DEVICELESS_TEST_PREREQUISITES = frozenset({ 986 'adb', 987 'atest-tradefed', 988 'atest_script_help.sh', 989 'atest_tradefed.sh', 990 'tradefed', 991 'tradefed-test-framework', 992 'bazel-result-reporter', 993 }) 994 995 DEVICE_TEST_PREREQUISITES = frozenset( 996 DEVICELESS_TEST_PREREQUISITES.union( 997 frozenset({ 998 'aapt', 999 'aapt2', 1000 'compatibility-tradefed', 1001 'vts-core-tradefed-harness', 1002 }) 1003 ) 1004 ) 1005 1006 @staticmethod 1007 def create_deviceless_test_target( 1008 name: str, package_name: str, info: Dict[str, Any] 1009 ): 1010 return TestTarget( 1011 package_name, 1012 'tradefed_deviceless_test', 1013 { 1014 'name': name, 1015 'test': ModuleRef.for_info(info), 1016 'module_name': info['module_name'], 1017 'tags': info.get(constants.MODULE_TEST_OPTIONS_TAGS, []), 1018 }, 1019 TestTarget.DEVICELESS_TEST_PREREQUISITES, 1020 ) 1021 1022 @staticmethod 1023 def create_device_test_target( 1024 name: str, package_name: str, info: Dict[str, Any], is_host_driven: bool 1025 ): 1026 rule = ( 1027 'tradefed_host_driven_device_test' 1028 if is_host_driven 1029 else 'tradefed_device_driven_test' 1030 ) 1031 1032 return TestTarget( 1033 package_name, 1034 rule, 1035 { 1036 'name': name, 1037 'test': ModuleRef.for_info(info), 1038 'module_name': info['module_name'], 1039 'suites': set(info.get(constants.MODULE_COMPATIBILITY_SUITES, [])), 1040 'tradefed_deps': list( 1041 map( 1042 ModuleRef.for_name, info.get(constants.MODULE_HOST_DEPS, []) 1043 ) 1044 ), 1045 'tags': info.get(constants.MODULE_TEST_OPTIONS_TAGS, []), 1046 }, 1047 TestTarget.DEVICE_TEST_PREREQUISITES, 1048 ) 1049 1050 @staticmethod 1051 def create_tradefed_robolectric_test_target( 1052 name: str, package_name: str, info: Dict[str, Any], jdk_label: str 1053 ): 1054 return TestTarget( 1055 package_name, 1056 'tradefed_robolectric_test', 1057 { 1058 'name': name, 1059 'test': ModuleRef.for_info(info), 1060 'module_name': info['module_name'], 1061 'tags': info.get(constants.MODULE_TEST_OPTIONS_TAGS, []), 1062 'jdk': jdk_label, 1063 }, 1064 TestTarget.DEVICELESS_TEST_PREREQUISITES, 1065 ) 1066 1067 def __init__( 1068 self, 1069 package_name: str, 1070 rule_name: str, 1071 attributes: Dict[str, Any], 1072 prerequisites=frozenset(), 1073 ): 1074 self._attributes = attributes 1075 self._package_name = package_name 1076 self._rule_name = rule_name 1077 self._prerequisites = prerequisites 1078 1079 def name(self) -> str: 1080 return self._attributes['name'] 1081 1082 def package_name(self) -> str: 1083 return self._package_name 1084 1085 def required_imports(self) -> Set[Import]: 1086 return {Import('//bazel/rules:tradefed_test.bzl', self._rule_name)} 1087 1088 def dependencies(self) -> List[ModuleRef]: 1089 prerequisite_refs = map(ModuleRef.for_name, self._prerequisites) 1090 1091 declared_dep_refs = [] 1092 for value in self._attributes.values(): 1093 if isinstance(value, Iterable): 1094 declared_dep_refs.extend( 1095 [dep for dep in value if isinstance(dep, ModuleRef)] 1096 ) 1097 elif isinstance(value, ModuleRef): 1098 declared_dep_refs.append(value) 1099 1100 return declared_dep_refs + list(prerequisite_refs) 1101 1102 def write_to_build_file(self, f: IO): 1103 prebuilt_target_name = self._attributes['test'].target().qualified_name() 1104 writer = IndentWriter(f) 1105 build_file_writer = BuildFileWriter(writer) 1106 1107 writer.write_line(f'{self._rule_name}(') 1108 1109 with writer.indent(): 1110 build_file_writer.write_string_attribute('name', self._attributes['name']) 1111 1112 build_file_writer.write_string_attribute( 1113 'module_name', self._attributes['module_name'] 1114 ) 1115 1116 build_file_writer.write_string_attribute('test', prebuilt_target_name) 1117 1118 build_file_writer.write_label_list_attribute( 1119 'tradefed_deps', self._attributes.get('tradefed_deps') 1120 ) 1121 1122 build_file_writer.write_string_list_attribute( 1123 'suites', sorted(self._attributes.get('suites', [])) 1124 ) 1125 1126 build_file_writer.write_string_list_attribute( 1127 'tags', sorted(self._attributes.get('tags', [])) 1128 ) 1129 1130 build_file_writer.write_label_attribute( 1131 'jdk', self._attributes.get('jdk', None) 1132 ) 1133 1134 writer.write_line(')') 1135 1136 1137def _read_robolectric_jdk_path( 1138 test_xml_config_template: pathlib.Path, 1139) -> pathlib.Path: 1140 if not test_xml_config_template.is_file(): 1141 return None 1142 1143 xml_root = ET.parse(test_xml_config_template).getroot() 1144 option = xml_root.find(".//option[@name='java-folder']") 1145 jdk_path = pathlib.Path(option.get('value', '')) 1146 1147 if not jdk_path.is_relative_to('prebuilts/jdk'): 1148 raise ValueError( 1149 f'Failed to get "java-folder" from `{test_xml_config_template}`' 1150 ) 1151 1152 return jdk_path 1153 1154 1155class BuildFileWriter: 1156 """Class for writing BUILD files.""" 1157 1158 def __init__(self, underlying: IndentWriter): 1159 self._underlying = underlying 1160 1161 def write_string_attribute(self, attribute_name, value): 1162 if value is None: 1163 return 1164 1165 self._underlying.write_line(f'{attribute_name} = "{value}",') 1166 1167 def write_label_attribute(self, attribute_name: str, label_name: str): 1168 if label_name is None: 1169 return 1170 1171 self._underlying.write_line(f'{attribute_name} = "{label_name}",') 1172 1173 def write_string_list_attribute(self, attribute_name, values): 1174 if not values: 1175 return 1176 1177 self._underlying.write_line(f'{attribute_name} = [') 1178 1179 with self._underlying.indent(): 1180 for value in values: 1181 self._underlying.write_line(f'"{value}",') 1182 1183 self._underlying.write_line('],') 1184 1185 def write_label_list_attribute( 1186 self, attribute_name: str, modules: List[ModuleRef] 1187 ): 1188 if not modules: 1189 return 1190 1191 self._underlying.write_line(f'{attribute_name} = [') 1192 1193 with self._underlying.indent(): 1194 for label in sorted(set(m.target().qualified_name() for m in modules)): 1195 self._underlying.write_line(f'"{label}",') 1196 1197 self._underlying.write_line('],') 1198 1199 def write_glob_attribute(self, attribute_name: str, patterns: List[str]): 1200 self._underlying.write_line(f'{attribute_name} = glob([') 1201 1202 with self._underlying.indent(): 1203 for pattern in patterns: 1204 self._underlying.write_line(f'"{pattern}",') 1205 1206 self._underlying.write_line(']),') 1207 1208 1209@dataclasses.dataclass(frozen=True) 1210class Dependencies: 1211 static_dep_refs: List[ModuleRef] 1212 runtime_dep_refs: List[ModuleRef] 1213 data_dep_refs: List[ModuleRef] 1214 device_data_dep_refs: List[ModuleRef] 1215 1216 1217class SoongPrebuiltTarget(Target): 1218 """Class for generating a Soong prebuilt target on disk.""" 1219 1220 @staticmethod 1221 def create( 1222 gen: WorkspaceGenerator, info: Dict[str, Any], package_name: str = '' 1223 ): 1224 module_name = info['module_name'] 1225 1226 configs = [ 1227 Config('host', gen.host_out_path), 1228 Config('device', gen.resource_manager.get_product_out_file_path()), 1229 ] 1230 1231 installed_paths = get_module_installed_paths( 1232 info, gen.resource_manager.get_src_file_path() 1233 ) 1234 config_files = group_paths_by_config(configs, installed_paths) 1235 1236 # For test modules, we only create symbolic link to the 'testcases' 1237 # directory since the information in module-info is not accurate. 1238 if gen.mod_info.is_tradefed_testable_module(info): 1239 config_files = { 1240 c: [c.out_path.joinpath(f'testcases/{module_name}')] 1241 for c in config_files.keys() 1242 } 1243 1244 enabled_features = gen.enabled_features 1245 1246 return SoongPrebuiltTarget( 1247 info, 1248 package_name, 1249 config_files, 1250 Dependencies( 1251 static_dep_refs=find_static_dep_refs( 1252 gen.mod_info, 1253 info, 1254 configs, 1255 gen.resource_manager.get_src_file_path(), 1256 enabled_features, 1257 ), 1258 runtime_dep_refs=find_runtime_dep_refs( 1259 gen.mod_info, 1260 info, 1261 configs, 1262 gen.resource_manager.get_src_file_path(), 1263 enabled_features, 1264 ), 1265 data_dep_refs=find_data_dep_refs( 1266 gen.mod_info, 1267 info, 1268 configs, 1269 gen.resource_manager.get_src_file_path(), 1270 ), 1271 device_data_dep_refs=find_device_data_dep_refs(gen, info), 1272 ), 1273 [ 1274 c 1275 for c in configs 1276 if c.name 1277 in map(str.lower, info.get(constants.MODULE_SUPPORTED_VARIANTS, [])) 1278 ], 1279 ) 1280 1281 def __init__( 1282 self, 1283 info: Dict[str, Any], 1284 package_name: str, 1285 config_files: Dict[Config, List[pathlib.Path]], 1286 deps: Dependencies, 1287 supported_configs: List[Config], 1288 ): 1289 self._target_name = info[constants.MODULE_INFO_ID] 1290 self._module_name = info[constants.MODULE_NAME] 1291 self._package_name = package_name 1292 self.config_files = config_files 1293 self.deps = deps 1294 self.suites = info.get(constants.MODULE_COMPATIBILITY_SUITES, []) 1295 self._supported_configs = supported_configs 1296 1297 def name(self) -> str: 1298 return self._target_name 1299 1300 def package_name(self) -> str: 1301 return self._package_name 1302 1303 def required_imports(self) -> Set[Import]: 1304 return { 1305 Import('//bazel/rules:soong_prebuilt.bzl', self._rule_name()), 1306 } 1307 1308 @functools.lru_cache(maxsize=128) 1309 def supported_configs(self) -> Set[Config]: 1310 # We deduce the supported configs from the installed paths since the 1311 # build exports incorrect metadata for some module types such as 1312 # Robolectric. The information exported from the build is only used if 1313 # the module does not have any installed paths. 1314 # TODO(b/232929584): Remove this once all modules correctly export the 1315 # supported variants. 1316 supported_configs = set(self.config_files.keys()) 1317 if supported_configs: 1318 return supported_configs 1319 1320 return self._supported_configs 1321 1322 def dependencies(self) -> List[ModuleRef]: 1323 all_deps = set(self.deps.runtime_dep_refs) 1324 all_deps.update(self.deps.data_dep_refs) 1325 all_deps.update(self.deps.device_data_dep_refs) 1326 all_deps.update(self.deps.static_dep_refs) 1327 return list(all_deps) 1328 1329 def write_to_build_file(self, f: IO): 1330 writer = IndentWriter(f) 1331 build_file_writer = BuildFileWriter(writer) 1332 1333 writer.write_line(f'{self._rule_name()}(') 1334 1335 with writer.indent(): 1336 writer.write_line(f'name = "{self._target_name}",') 1337 writer.write_line(f'module_name = "{self._module_name}",') 1338 self._write_files_attribute(writer) 1339 self._write_deps_attribute( 1340 writer, 'static_deps', self.deps.static_dep_refs 1341 ) 1342 self._write_deps_attribute( 1343 writer, 'runtime_deps', self.deps.runtime_dep_refs 1344 ) 1345 self._write_deps_attribute(writer, 'data', self.deps.data_dep_refs) 1346 1347 build_file_writer.write_label_list_attribute( 1348 'device_data', self.deps.device_data_dep_refs 1349 ) 1350 build_file_writer.write_string_list_attribute( 1351 'suites', sorted(self.suites) 1352 ) 1353 1354 writer.write_line(')') 1355 1356 def create_filesystem_layout(self, package_dir: pathlib.Path): 1357 prebuilts_dir = package_dir.joinpath(self._target_name) 1358 prebuilts_dir.mkdir() 1359 1360 for config, files in self.config_files.items(): 1361 config_prebuilts_dir = prebuilts_dir.joinpath(config.name) 1362 config_prebuilts_dir.mkdir() 1363 1364 for f in files: 1365 rel_path = f.relative_to(config.out_path) 1366 symlink = config_prebuilts_dir.joinpath(rel_path) 1367 symlink.parent.mkdir(parents=True, exist_ok=True) 1368 symlink.symlink_to(f) 1369 1370 def _rule_name(self): 1371 return ( 1372 'soong_prebuilt' if self.config_files else 'soong_uninstalled_prebuilt' 1373 ) 1374 1375 def _write_files_attribute(self, writer: IndentWriter): 1376 if not self.config_files: 1377 return 1378 1379 writer.write('files = ') 1380 write_config_select( 1381 writer, 1382 self.config_files, 1383 lambda c, _: writer.write( 1384 f'glob(["{self._target_name}/{c.name}/**/*"])' 1385 ), 1386 ) 1387 writer.write_line(',') 1388 1389 def _write_deps_attribute(self, writer, attribute_name, module_refs): 1390 config_deps = filter_configs( 1391 group_targets_by_config(r.target() for r in module_refs), 1392 self.supported_configs(), 1393 ) 1394 1395 if not config_deps: 1396 return 1397 1398 for config in self.supported_configs(): 1399 config_deps.setdefault(config, []) 1400 1401 writer.write(f'{attribute_name} = ') 1402 write_config_select( 1403 writer, 1404 config_deps, 1405 lambda _, targets: write_target_list(writer, targets), 1406 ) 1407 writer.write_line(',') 1408 1409 1410def group_paths_by_config( 1411 configs: List[Config], paths: List[pathlib.Path] 1412) -> Dict[Config, List[pathlib.Path]]: 1413 1414 config_files = defaultdict(list) 1415 1416 for f in paths: 1417 matching_configs = [c for c in configs if _is_relative_to(f, c.out_path)] 1418 1419 if not matching_configs: 1420 continue 1421 1422 # The path can only appear in ANDROID_HOST_OUT for host target or 1423 # ANDROID_PRODUCT_OUT, but cannot appear in both. 1424 if len(matching_configs) > 1: 1425 raise ValueError( 1426 f'Installed path `{f}` is not in' 1427 ' ANDROID_HOST_OUT or ANDROID_PRODUCT_OUT' 1428 ) 1429 1430 config_files[matching_configs[0]].append(f) 1431 1432 return config_files 1433 1434 1435def group_targets_by_config( 1436 targets: List[Target], 1437) -> Dict[Config, List[Target]]: 1438 1439 config_to_targets = defaultdict(list) 1440 1441 for target in targets: 1442 for config in target.supported_configs(): 1443 config_to_targets[config].append(target) 1444 1445 return config_to_targets 1446 1447 1448def filter_configs( 1449 config_dict: Dict[Config, Any], 1450 configs: Set[Config], 1451) -> Dict[Config, Any]: 1452 return {k: v for (k, v) in config_dict.items() if k in configs} 1453 1454 1455def _is_relative_to(path1: pathlib.Path, path2: pathlib.Path) -> bool: 1456 """Return True if the path is relative to another path or False.""" 1457 # Note that this implementation is required because Path.is_relative_to only 1458 # exists starting with Python 3.9. 1459 try: 1460 path1.relative_to(path2) 1461 return True 1462 except ValueError: 1463 return False 1464 1465 1466def get_module_installed_paths( 1467 info: Dict[str, Any], src_root_path: pathlib.Path 1468) -> List[pathlib.Path]: 1469 1470 # Install paths in module-info are usually relative to the Android 1471 # source root ${ANDROID_BUILD_TOP}. When the output directory is 1472 # customized by the user however, the install paths are absolute. 1473 def resolve(install_path_string): 1474 install_path = pathlib.Path(install_path_string) 1475 if not install_path.expanduser().is_absolute(): 1476 return src_root_path.joinpath(install_path) 1477 return install_path 1478 1479 return map(resolve, info.get(constants.MODULE_INSTALLED, [])) 1480 1481 1482def find_runtime_dep_refs( 1483 mod_info: module_info.ModuleInfo, 1484 info: module_info.Module, 1485 configs: List[Config], 1486 src_root_path: pathlib.Path, 1487 enabled_features: List[Features], 1488) -> List[ModuleRef]: 1489 """Return module references for runtime dependencies.""" 1490 1491 # We don't use the `dependencies` module-info field for shared libraries 1492 # since it's ambiguous and could generate more targets and pull in more 1493 # dependencies than necessary. In particular, libraries that support both 1494 # static and dynamic linking could end up becoming runtime dependencies 1495 # even though the build specifies static linking. For example, if a target 1496 # 'T' is statically linked to 'U' which supports both variants, the latter 1497 # still appears as a dependency. Since we can't tell, this would result in 1498 # the shared library variant of 'U' being added on the library path. 1499 libs = set() 1500 libs.update(info.get(constants.MODULE_SHARED_LIBS, [])) 1501 libs.update(info.get(constants.MODULE_RUNTIME_DEPS, [])) 1502 1503 if Features.EXPERIMENTAL_JAVA_RUNTIME_DEPENDENCIES in enabled_features: 1504 libs.update(info.get(constants.MODULE_LIBS, [])) 1505 1506 runtime_dep_refs = _find_module_refs(mod_info, configs, src_root_path, libs) 1507 1508 runtime_library_class = {'RLIB_LIBRARIES', 'DYLIB_LIBRARIES'} 1509 # We collect rlibs even though they are technically static libraries since 1510 # they could refer to dylibs which are required at runtime. Generating 1511 # Bazel targets for these intermediate modules keeps the generator simple 1512 # and preserves the shape (isomorphic) of the Soong structure making the 1513 # workspace easier to debug. 1514 for dep_name in info.get(constants.MODULE_DEPENDENCIES, []): 1515 dep_info = mod_info.get_module_info(dep_name) 1516 if not dep_info: 1517 continue 1518 if not runtime_library_class.intersection( 1519 dep_info.get(constants.MODULE_CLASS, []) 1520 ): 1521 continue 1522 runtime_dep_refs.append(ModuleRef.for_info(dep_info)) 1523 1524 return runtime_dep_refs 1525 1526 1527def find_data_dep_refs( 1528 mod_info: module_info.ModuleInfo, 1529 info: module_info.Module, 1530 configs: List[Config], 1531 src_root_path: pathlib.Path, 1532) -> List[ModuleRef]: 1533 """Return module references for data dependencies.""" 1534 1535 return _find_module_refs( 1536 mod_info, configs, src_root_path, info.get(constants.MODULE_DATA_DEPS, []) 1537 ) 1538 1539 1540def find_device_data_dep_refs( 1541 gen: WorkspaceGenerator, 1542 info: module_info.Module, 1543) -> List[ModuleRef]: 1544 """Return module references for device data dependencies.""" 1545 1546 return _find_module_refs( 1547 gen.mod_info, 1548 [Config('device', gen.resource_manager.get_product_out_file_path())], 1549 gen.resource_manager.get_src_file_path(), 1550 info.get(constants.MODULE_TARGET_DEPS, []), 1551 ) 1552 1553 1554def find_static_dep_refs( 1555 mod_info: module_info.ModuleInfo, 1556 info: module_info.Module, 1557 configs: List[Config], 1558 src_root_path: pathlib.Path, 1559 enabled_features: List[Features], 1560) -> List[ModuleRef]: 1561 """Return module references for static libraries.""" 1562 1563 if Features.EXPERIMENTAL_JAVA_RUNTIME_DEPENDENCIES not in enabled_features: 1564 return [] 1565 1566 static_libs = set() 1567 static_libs.update(info.get(constants.MODULE_STATIC_LIBS, [])) 1568 static_libs.update(info.get(constants.MODULE_STATIC_DEPS, [])) 1569 1570 return _find_module_refs(mod_info, configs, src_root_path, static_libs) 1571 1572 1573def _find_module_refs( 1574 mod_info: module_info.ModuleInfo, 1575 configs: List[Config], 1576 src_root_path: pathlib.Path, 1577 module_names: List[str], 1578) -> List[ModuleRef]: 1579 """Return module references for modules.""" 1580 1581 module_refs = [] 1582 1583 for name in module_names: 1584 info = mod_info.get_module_info(name) 1585 if not info: 1586 continue 1587 1588 installed_paths = get_module_installed_paths(info, src_root_path) 1589 config_files = group_paths_by_config(configs, installed_paths) 1590 if not config_files: 1591 continue 1592 1593 module_refs.append(ModuleRef.for_info(info)) 1594 1595 return module_refs 1596 1597 1598class IndentWriter: 1599 1600 def __init__(self, f: IO): 1601 self._file = f 1602 self._indent_level = 0 1603 self._indent_string = 4 * ' ' 1604 self._indent_next = True 1605 1606 def write_line(self, text: str = ''): 1607 if text: 1608 self.write(text) 1609 1610 self._file.write('\n') 1611 self._indent_next = True 1612 1613 def write(self, text): 1614 if self._indent_next: 1615 self._file.write(self._indent_string * self._indent_level) 1616 self._indent_next = False 1617 1618 self._file.write(text) 1619 1620 @contextlib.contextmanager 1621 def indent(self): 1622 self._indent_level += 1 1623 yield 1624 self._indent_level -= 1 1625 1626 1627def write_config_select( 1628 writer: IndentWriter, 1629 config_dict: Dict[Config, Any], 1630 write_value_fn: Callable, 1631): 1632 writer.write_line('select({') 1633 1634 with writer.indent(): 1635 for config, value in sorted(config_dict.items(), key=lambda c: c[0].name): 1636 1637 writer.write(f'"//bazel/rules:{config.name}": ') 1638 write_value_fn(config, value) 1639 writer.write_line(',') 1640 1641 writer.write('})') 1642 1643 1644def write_target_list(writer: IndentWriter, targets: List[Target]): 1645 writer.write_line('[') 1646 1647 with writer.indent(): 1648 for label in sorted(set(t.qualified_name() for t in targets)): 1649 writer.write_line(f'"{label}",') 1650 1651 writer.write(']') 1652 1653 1654def _decorate_find_method(mod_info, finder_method_func, host, enabled_features): 1655 """A finder_method decorator to override TestInfo properties.""" 1656 1657 def use_bazel_runner(finder_obj, test_id): 1658 test_infos = finder_method_func(finder_obj, test_id) 1659 if not test_infos: 1660 return test_infos 1661 for tinfo in test_infos: 1662 m_info = mod_info.get_module_info(tinfo.test_name) 1663 1664 # TODO(b/262200630): Refactor the duplicated logic in 1665 # _decorate_find_method() and _add_test_module_targets() to 1666 # determine whether a test should run with Atest Bazel Mode. 1667 1668 # Only enable modern Robolectric tests since those are the only ones 1669 # TF currently supports. 1670 if mod_info.is_modern_robolectric_test(m_info): 1671 if Features.EXPERIMENTAL_ROBOLECTRIC_TEST in enabled_features: 1672 tinfo.test_runner = BazelTestRunner.NAME 1673 continue 1674 1675 # Only run device-driven tests in Bazel mode when '--host' is not 1676 # specified and the feature is enabled. 1677 if not host and mod_info.is_device_driven_test(m_info): 1678 if Features.EXPERIMENTAL_DEVICE_DRIVEN_TEST in enabled_features: 1679 tinfo.test_runner = BazelTestRunner.NAME 1680 continue 1681 1682 if mod_info.is_suite_in_compatibility_suites( 1683 'host-unit-tests', m_info 1684 ) or ( 1685 Features.EXPERIMENTAL_HOST_DRIVEN_TEST in enabled_features 1686 and mod_info.is_host_driven_test(m_info) 1687 ): 1688 tinfo.test_runner = BazelTestRunner.NAME 1689 return test_infos 1690 1691 return use_bazel_runner 1692 1693 1694def create_new_finder( 1695 mod_info: module_info.ModuleInfo, 1696 finder: test_finder_base.TestFinderBase, 1697 host: bool, 1698 enabled_features: List[Features] = None, 1699): 1700 """Create new test_finder_base.Finder with decorated find_method. 1701 1702 Args: 1703 mod_info: ModuleInfo object. 1704 finder: Test Finder class. 1705 host: Whether to run the host variant. 1706 enabled_features: List of enabled features. 1707 1708 Returns: 1709 List of ordered find methods. 1710 """ 1711 return test_finder_base.Finder( 1712 finder.test_finder_instance, 1713 _decorate_find_method( 1714 mod_info, finder.find_method, host, enabled_features or [] 1715 ), 1716 finder.finder_info, 1717 ) 1718 1719 1720class RunCommandError(subprocess.CalledProcessError): 1721 """CalledProcessError but including debug information when it fails.""" 1722 1723 def __str__(self): 1724 return f'{super().__str__()}\nstdout={self.stdout}\n\nstderr={self.stderr}' 1725 1726 1727def default_run_command(args: List[str], cwd: pathlib.Path) -> str: 1728 result = subprocess.run( 1729 args=args, 1730 cwd=cwd, 1731 text=True, 1732 capture_output=True, 1733 check=False, 1734 ) 1735 if result.returncode: 1736 # Provide a more detailed log message including stdout and stderr. 1737 raise RunCommandError( 1738 result.returncode, result.args, result.stdout, result.stderr 1739 ) 1740 return result.stdout 1741 1742 1743@dataclasses.dataclass 1744class BuildMetadata: 1745 build_branch: str 1746 build_target: str 1747 1748 1749class BazelTestRunner(trb.TestRunnerBase): 1750 """Bazel Test Runner class.""" 1751 1752 NAME = 'BazelTestRunner' 1753 EXECUTABLE = 'none' 1754 1755 # pylint: disable=redefined-outer-name 1756 # pylint: disable=too-many-arguments 1757 def __init__( 1758 self, 1759 results_dir, 1760 mod_info: module_info.ModuleInfo, 1761 extra_args: Dict[str, Any] = None, 1762 src_top: pathlib.Path = None, 1763 workspace_path: pathlib.Path = None, 1764 run_command: Callable = default_run_command, 1765 build_metadata: BuildMetadata = None, 1766 env: Dict[str, str] = None, 1767 generate_workspace_fn: Callable = generate_bazel_workspace, 1768 enabled_features: Set[str] = None, 1769 **kwargs, 1770 ): 1771 super().__init__(results_dir, **kwargs) 1772 self.mod_info = mod_info 1773 self.src_top = src_top or pathlib.Path( 1774 os.environ.get(constants.ANDROID_BUILD_TOP) 1775 ) 1776 self.starlark_file = _get_resource_root().joinpath( 1777 'format_as_soong_module_name.cquery' 1778 ) 1779 1780 self.bazel_workspace = workspace_path or get_bazel_workspace_dir() 1781 self.bazel_binary = self.bazel_workspace.joinpath('bazel.sh') 1782 self.run_command = run_command 1783 self._extra_args = extra_args or {} 1784 self.build_metadata = build_metadata or get_default_build_metadata() 1785 self.env = env or os.environ 1786 self._generate_workspace_fn = generate_workspace_fn 1787 self._enabled_features = ( 1788 enabled_features 1789 if enabled_features is not None 1790 else atest_configs.GLOBAL_ARGS.bazel_mode_features 1791 ) 1792 1793 # pylint: disable=unused-argument 1794 def run_tests(self, test_infos, extra_args, reporter): 1795 """Run the list of test_infos. 1796 1797 Args: 1798 test_infos: List of TestInfo. 1799 extra_args: Dict of extra args to add to test run. 1800 reporter: An instance of result_report.ResultReporter. 1801 """ 1802 ret_code = ExitCode.SUCCESS 1803 1804 try: 1805 run_cmds = self.generate_run_commands(test_infos, extra_args) 1806 except AbortRunException as e: 1807 atest_utils.colorful_print(f'Stop running test(s): {e}', constants.RED) 1808 return ExitCode.ERROR 1809 1810 for run_cmd in run_cmds: 1811 subproc = self.run(run_cmd, output_to_stdout=True) 1812 ret_code |= self.wait_for_subprocess(subproc) 1813 1814 self.organize_test_logs(test_infos) 1815 1816 return ret_code 1817 1818 def organize_test_logs(self, test_infos: List[test_info.TestInfo]): 1819 for t_info in test_infos: 1820 test_output_dir, package_name, target_suffix = ( 1821 self.retrieve_test_output_info(t_info) 1822 ) 1823 if test_output_dir.joinpath(TEST_OUTPUT_ZIP_NAME).exists(): 1824 # TEST_OUTPUT_ZIP file exist when BES uploading is enabled. 1825 # Showing the BES link to users instead of the local log. 1826 continue 1827 1828 # AtestExecutionInfo will find all log files in 'results_dir/log' 1829 # directory and generate an HTML file to display to users when 1830 # 'results_dir/log' directory exist. 1831 log_path = pathlib.Path(self.results_dir).joinpath( 1832 'log', f'{package_name}', f'{t_info.test_name}_{target_suffix}' 1833 ) 1834 log_path.parent.mkdir(parents=True, exist_ok=True) 1835 if not log_path.is_symlink(): 1836 log_path.symlink_to(test_output_dir) 1837 1838 def _get_feature_config_or_warn(self, feature, env_var_name): 1839 feature_config = self.env.get(env_var_name) 1840 if not feature_config: 1841 atest_utils.print_and_log_warning( 1842 'Ignoring `%s` because the `%s` environment variable is not set.', 1843 # pylint: disable=no-member 1844 feature, 1845 env_var_name, 1846 ) 1847 return feature_config 1848 1849 def _get_bes_publish_args(self, feature: Features) -> List[str]: 1850 bes_publish_config = self._get_feature_config_or_warn( 1851 feature, 'ATEST_BAZEL_BES_PUBLISH_CONFIG' 1852 ) 1853 1854 if not bes_publish_config: 1855 return [] 1856 1857 branch = self.build_metadata.build_branch 1858 target = self.build_metadata.build_target 1859 1860 return [ 1861 f'--config={bes_publish_config}', 1862 f'--build_metadata=ab_branch={branch}', 1863 f'--build_metadata=ab_target={target}', 1864 ] 1865 1866 def _get_remote_args(self, feature): 1867 remote_config = self._get_feature_config_or_warn( 1868 feature, 'ATEST_BAZEL_REMOTE_CONFIG' 1869 ) 1870 if not remote_config: 1871 return [] 1872 return [f'--config={remote_config}'] 1873 1874 def _get_remote_avd_args(self, feature): 1875 remote_avd_config = self._get_feature_config_or_warn( 1876 feature, 'ATEST_BAZEL_REMOTE_AVD_CONFIG' 1877 ) 1878 if not remote_avd_config: 1879 raise ValueError( 1880 'Cannot run remote device test because ' 1881 'ATEST_BAZEL_REMOTE_AVD_CONFIG ' 1882 'environment variable is not set.' 1883 ) 1884 return [f'--config={remote_avd_config}'] 1885 1886 def host_env_check(self): 1887 """Check that host env has everything we need. 1888 1889 We actually can assume the host env is fine because we have the same 1890 requirements that atest has. Update this to check for android env vars 1891 if that changes. 1892 """ 1893 1894 def get_test_runner_build_reqs(self, test_infos) -> Set[str]: 1895 if not test_infos: 1896 return set() 1897 1898 self._generate_workspace_fn( 1899 self.mod_info, 1900 self._enabled_features, 1901 ) 1902 1903 deps_expression = ' + '.join( 1904 sorted(self.test_info_target_label(i) for i in test_infos) 1905 ) 1906 1907 with tempfile.NamedTemporaryFile() as query_file: 1908 with open(query_file.name, 'w', encoding='utf-8') as _query_file: 1909 _query_file.write(f'deps(tests({deps_expression}))') 1910 1911 query_args = [ 1912 str(self.bazel_binary), 1913 'cquery', 1914 f'--query_file={query_file.name}', 1915 '--output=starlark', 1916 f'--starlark:file={self.starlark_file}', 1917 ] 1918 1919 output = self.run_command(query_args, self.bazel_workspace) 1920 1921 targets = set() 1922 robolectric_tests = set( 1923 filter( 1924 self._is_robolectric_test_suite, 1925 [test.test_name for test in test_infos], 1926 ) 1927 ) 1928 1929 modules_to_variant = _parse_cquery_output(output) 1930 1931 for module, variants in modules_to_variant.items(): 1932 1933 # Skip specifying the build variant for Robolectric test modules 1934 # since they are special. Soong builds them with the `target` 1935 # variant although are installed as 'host' modules. 1936 if module in robolectric_tests: 1937 targets.add(module) 1938 continue 1939 1940 targets.add(_soong_target_for_variants(module, variants)) 1941 1942 return targets 1943 1944 def _is_robolectric_test_suite(self, module_name: str) -> bool: 1945 return self.mod_info.is_robolectric_test_suite( 1946 self.mod_info.get_module_info(module_name) 1947 ) 1948 1949 def test_info_target_label(self, test: test_info.TestInfo) -> str: 1950 module_name = test.test_name 1951 info = self.mod_info.get_module_info(module_name) 1952 package_name = info.get(constants.MODULE_PATH)[0] 1953 target_suffix = self.get_target_suffix(info) 1954 1955 return f'//{package_name}:{module_name}_{target_suffix}' 1956 1957 def retrieve_test_output_info( 1958 self, test_info: test_info.TestInfo 1959 ) -> Tuple[pathlib.Path, str, str]: 1960 """Return test output information. 1961 1962 Args: 1963 test_info (test_info.TestInfo): Information about the test. 1964 1965 Returns: 1966 Tuple[pathlib.Path, str, str]: A tuple containing the following 1967 elements: 1968 - test_output_dir (pathlib.Path): Absolute path of the test output 1969 folder. 1970 - package_name (str): Name of the package. 1971 - target_suffix (str): Target suffix. 1972 """ 1973 module_name = test_info.test_name 1974 info = self.mod_info.get_module_info(module_name) 1975 package_name = info.get(constants.MODULE_PATH)[0] 1976 target_suffix = self.get_target_suffix(info) 1977 1978 test_output_dir = pathlib.Path( 1979 self.bazel_workspace, 1980 BAZEL_TEST_LOGS_DIR_NAME, 1981 package_name, 1982 f'{module_name}_{target_suffix}', 1983 TEST_OUTPUT_DIR_NAME, 1984 ) 1985 1986 return test_output_dir, package_name, target_suffix 1987 1988 def get_target_suffix(self, info: Dict[str, Any]) -> str: 1989 """Return 'host' or 'device' accordingly to the variant of the test.""" 1990 if not self._extra_args.get( 1991 constants.HOST, False 1992 ) and self.mod_info.is_device_driven_test(info): 1993 return 'device' 1994 return 'host' 1995 1996 @staticmethod 1997 def _get_bazel_feature_args( 1998 feature: Features, extra_args: Dict[str, Any], generator: Callable 1999 ) -> List[str]: 2000 if feature not in extra_args.get('BAZEL_MODE_FEATURES', []): 2001 return [] 2002 return generator(feature) 2003 2004 # pylint: disable=unused-argument 2005 def generate_run_commands(self, test_infos, extra_args, port=None): 2006 """Generate a list of run commands from TestInfos. 2007 2008 Args: 2009 test_infos: A set of TestInfo instances. 2010 extra_args: A Dict of extra args to append. 2011 port: Optional. An int of the port number to send events to. 2012 2013 Returns: 2014 A list of run commands to run the tests. 2015 """ 2016 startup_options = '' 2017 bazelrc = self.env.get('ATEST_BAZELRC') 2018 2019 if bazelrc: 2020 startup_options = f'--bazelrc={bazelrc}' 2021 2022 target_patterns = ' '.join( 2023 self.test_info_target_label(i) for i in test_infos 2024 ) 2025 2026 bazel_args = parse_args(test_infos, extra_args) 2027 2028 # If BES is not enabled, use the option of 2029 # '--nozip_undeclared_test_outputs' to not compress the test outputs. 2030 # And the URL of test outputs will be printed in terminal. 2031 bazel_args.extend( 2032 self._get_bazel_feature_args( 2033 Features.EXPERIMENTAL_BES_PUBLISH, 2034 extra_args, 2035 self._get_bes_publish_args, 2036 ) 2037 or ['--nozip_undeclared_test_outputs'] 2038 ) 2039 bazel_args.extend( 2040 self._get_bazel_feature_args( 2041 Features.EXPERIMENTAL_REMOTE, extra_args, self._get_remote_args 2042 ) 2043 ) 2044 bazel_args.extend( 2045 self._get_bazel_feature_args( 2046 Features.EXPERIMENTAL_REMOTE_AVD, 2047 extra_args, 2048 self._get_remote_avd_args, 2049 ) 2050 ) 2051 2052 # This is an alternative to shlex.join that doesn't exist in Python 2053 # versions < 3.8. 2054 bazel_args_str = ' '.join(shlex.quote(arg) for arg in bazel_args) 2055 2056 # Use 'cd' instead of setting the working directory in the subprocess 2057 # call for a working --dry-run command that users can run. 2058 return [ 2059 f'cd {self.bazel_workspace} && ' 2060 f'{self.bazel_binary} {startup_options} ' 2061 f'test {target_patterns} {bazel_args_str}' 2062 ] 2063 2064 2065def parse_args( 2066 test_infos: List[test_info.TestInfo], extra_args: Dict[str, Any] 2067) -> Dict[str, Any]: 2068 """Parse commandline args and passes supported args to bazel. 2069 2070 Args: 2071 test_infos: A set of TestInfo instances. 2072 extra_args: A Dict of extra args to append. 2073 2074 Returns: 2075 A list of args to append to the run command. 2076 """ 2077 2078 args_to_append = [] 2079 # Make a copy of the `extra_args` dict to avoid modifying it for other 2080 # Atest runners. 2081 extra_args_copy = extra_args.copy() 2082 2083 # Remove the `--host` flag since we already pass that in the rule's 2084 # implementation. 2085 extra_args_copy.pop(constants.HOST, None) 2086 2087 # Remove the serial arg since Bazel mode does not support device tests and 2088 # the serial / -s arg conflicts with the TF null device option specified in 2089 # the rule implementation (-n). 2090 extra_args_copy.pop(constants.SERIAL, None) 2091 2092 # Map args to their native Bazel counterparts. 2093 for arg in _SUPPORTED_BAZEL_ARGS: 2094 if arg not in extra_args_copy: 2095 continue 2096 args_to_append.extend(_map_to_bazel_args(arg, extra_args_copy[arg])) 2097 # Remove the argument since we already mapped it to a Bazel option 2098 # and no longer need it mapped to a Tradefed argument below. 2099 del extra_args_copy[arg] 2100 2101 # TODO(b/215461642): Store the extra_args in the top-level object so 2102 # that we don't have to re-parse the extra args to get BAZEL_ARG again. 2103 tf_args, _ = tfr.extra_args_to_tf_args(extra_args_copy) 2104 2105 # Add ATest include filter argument to allow testcase filtering. 2106 tf_args.extend(tfr.get_include_filter(test_infos)) 2107 2108 args_to_append.extend([f'--test_arg={i}' for i in tf_args]) 2109 2110 # Disable test result caching when wait-for-debugger flag is set. 2111 if '--wait-for-debugger' in tf_args: 2112 # Remove the --cache_test_results flag if it's already set. 2113 args_to_append = [ 2114 arg 2115 for arg in args_to_append 2116 if not arg.startswith('--cache_test_results') 2117 ] 2118 args_to_append.append('--cache_test_results=no') 2119 2120 # Default to --test_output=errors unless specified otherwise 2121 if not any(arg.startswith('--test_output=') for arg in args_to_append): 2122 args_to_append.append('--test_output=errors') 2123 2124 # Default to --test_summary=detailed unless specified otherwise, or if the 2125 # feature is disabled 2126 if not any(arg.startswith('--test_summary=') for arg in args_to_append) and ( 2127 Features.NO_BAZEL_DETAILED_SUMMARY 2128 not in extra_args.get('BAZEL_MODE_FEATURES', []) 2129 ): 2130 args_to_append.append('--test_summary=detailed') 2131 2132 return args_to_append 2133 2134 2135def _map_to_bazel_args(arg: str, arg_value: Any) -> List[str]: 2136 return ( 2137 _SUPPORTED_BAZEL_ARGS[arg](arg_value) 2138 if arg in _SUPPORTED_BAZEL_ARGS 2139 else [] 2140 ) 2141 2142 2143def _parse_cquery_output(output: str) -> Dict[str, Set[str]]: 2144 module_to_build_variants = defaultdict(set) 2145 2146 for line in filter(bool, map(str.strip, output.splitlines())): 2147 module_name, build_variant = line.split(':') 2148 module_to_build_variants[module_name].add(build_variant) 2149 2150 return module_to_build_variants 2151 2152 2153def _soong_target_for_variants( 2154 module_name: str, build_variants: Set[str] 2155) -> str: 2156 2157 if not build_variants: 2158 raise ValueError( 2159 f'Missing the build variants for module {module_name} in cquery output!' 2160 ) 2161 2162 if len(build_variants) > 1: 2163 return module_name 2164 2165 return f'{module_name}-{_CONFIG_TO_VARIANT[list(build_variants)[0]]}' 2166