1#!/usr/bin/env python3 2# 3# Copyright (C) 2021 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""Builds SDK snapshots. 17 18If the environment variable TARGET_BUILD_APPS is nonempty then only the SDKs for 19the APEXes in it are built, otherwise all configured SDKs are built. 20""" 21import argparse 22import dataclasses 23import functools 24import io 25import os 26import re 27import shutil 28import subprocess 29import sys 30import tempfile 31import typing 32from collections import defaultdict 33from typing import Callable, List 34import zipfile 35 36COPYRIGHT_BOILERPLATE = """ 37// 38// Copyright (C) 2020 The Android Open Source Project 39// 40// Licensed under the Apache License, Version 2.0 (the "License"); 41// you may not use this file except in compliance with the License. 42// You may obtain a copy of the License at 43// 44// http://www.apache.org/licenses/LICENSE-2.0 45// 46// Unless required by applicable law or agreed to in writing, software 47// distributed under the License is distributed on an "AS IS" BASIS, 48// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 49// See the License for the specific language governing permissions and 50// limitations under the License. 51// 52""".lstrip() 53 54 55@dataclasses.dataclass(frozen=True) 56class ConfigVar: 57 """Represents a Soong configuration variable""" 58 # The config variable namespace, e.g. ANDROID. 59 namespace: str 60 61 # The name of the variable within the namespace. 62 name: str 63 64 65@dataclasses.dataclass(frozen=True) 66class FileTransformation: 67 """Performs a transformation on a file within an SDK snapshot zip file.""" 68 69 # The path of the file within the SDK snapshot zip file. 70 path: str 71 72 def apply(self, producer, path): 73 """Apply the transformation to the src_path to produce the dest_path.""" 74 raise NotImplementedError 75 76 77@dataclasses.dataclass(frozen=True) 78class SoongConfigBoilerplateInserter(FileTransformation): 79 """Transforms an Android.bp file to add soong config boilerplate. 80 81 The boilerplate allows the prefer setting of the modules to be controlled 82 through a Soong configuration variable. 83 """ 84 85 # The configuration variable that will control the prefer setting. 86 configVar: ConfigVar 87 88 # The bp file containing the definitions of the configuration module types 89 # to use in the sdk. 90 configBpDefFile: str 91 92 # The prefix to use for the soong config module types. 93 configModuleTypePrefix: str 94 95 def apply(self, producer, path): 96 with open(path, "r+", encoding="utf8") as file: 97 self._apply_transformation(producer, file) 98 99 def _apply_transformation(self, producer, file): 100 # TODO(b/174997203): Remove this when we have a proper way to control 101 # prefer flags in Mainline modules. 102 103 header_lines = [] 104 for line in file: 105 line = line.rstrip("\n") 106 if not line.startswith("//"): 107 break 108 header_lines.append(line) 109 110 config_module_types = set() 111 112 content_lines = [] 113 for line in file: 114 line = line.rstrip("\n") 115 116 # Check to see whether the line is the start of a new module type, 117 # e.g. <module-type> { 118 module_header = re.match("([a-z0-9_]+) +{$", line) 119 if not module_header: 120 # It is not so just add the line to the output and skip to the 121 # next line. 122 content_lines.append(line) 123 continue 124 125 module_type = module_header.group(1) 126 module_content = [] 127 128 # Iterate over the Soong module contents 129 for module_line in file: 130 module_line = module_line.rstrip("\n") 131 132 # When the end of the module has been reached then exit. 133 if module_line == "}": 134 break 135 136 # Check to see if the module is an unversioned module, i.e. 137 # without @<version>. If it is then it needs to have the soong 138 # config boilerplate added to control the setting of the prefer 139 # property. Versioned modules do not need that because they are 140 # never preferred. 141 # At the moment this differentiation between versioned and 142 # unversioned relies on the fact that the unversioned modules 143 # set "prefer: false", while the versioned modules do not. That 144 # is a little bit fragile so may require some additional checks. 145 if module_line != " prefer: false,": 146 # The line does not indicate that the module needs the 147 # soong config boilerplate so add the line and skip to the 148 # next one. 149 module_content.append(module_line) 150 continue 151 152 # Add the soong config boilerplate instead of the line: 153 # prefer: false, 154 namespace = self.configVar.namespace 155 name = self.configVar.name 156 module_content.append(f"""\ 157 // Do not prefer prebuilt if the Soong config variable "{name}" in namespace "{namespace}" is true. 158 prefer: true, 159 soong_config_variables: {{ 160 {name}: {{ 161 prefer: false, 162 }}, 163 }},""") 164 165 # Add the module type to the list of module types that need to 166 # have corresponding config module types. 167 config_module_types.add(module_type) 168 169 # Change the module type to the corresponding soong config 170 # module type by adding the prefix. 171 module_type = self.configModuleTypePrefix + module_type 172 173 # Generate the module, possibly with the new module type and 174 # containing the soong config variables entry. 175 content_lines.append(module_type + " {") 176 content_lines.extend(module_content) 177 content_lines.append("}") 178 179 if self.configBpDefFile: 180 # Add the soong_config_module_type_import module definition that 181 # imports the soong config module types into this bp file to the 182 # header lines so that they appear before any uses. 183 module_types = "\n".join([ 184 f' "{self.configModuleTypePrefix}{mt}",' 185 for mt in sorted(config_module_types) 186 ]) 187 header_lines.append(f""" 188// Soong config variable stanza added by {producer.script}. 189soong_config_module_type_import {{ 190 from: "{self.configBpDefFile}", 191 module_types: [ 192{module_types} 193 ], 194}} 195""") 196 else: 197 # Add the soong_config_module_type module definitions to the header 198 # lines so that they appear before any uses. 199 header_lines.append("") 200 for module_type in sorted(config_module_types): 201 # Create the corresponding soong config module type name by 202 # adding the prefix. 203 config_module_type = self.configModuleTypePrefix + module_type 204 header_lines.append(f""" 205// Soong config variable module type added by {producer.script}. 206soong_config_module_type {{ 207 name: "{config_module_type}", 208 module_type: "{module_type}", 209 config_namespace: "{self.configVar.namespace}", 210 bool_variables: ["{self.configVar.name}"], 211 properties: ["prefer"], 212}} 213""".lstrip()) 214 215 # Overwrite the file with the updated contents. 216 file.seek(0) 217 file.truncate() 218 file.write("\n".join(header_lines + content_lines) + "\n") 219 220 221@dataclasses.dataclass() 222class SubprocessRunner: 223 """Runs subprocesses""" 224 225 # Destination for stdout from subprocesses. 226 # 227 # This (and the following stderr) are needed to allow the tests to be run 228 # in Intellij. This ensures that the tests are run with stdout/stderr 229 # objects that work when passed to subprocess.run(stdout/stderr). Without it 230 # the tests are run with a FlushingStringIO object that has no fileno 231 # attribute - https://youtrack.jetbrains.com/issue/PY-27883. 232 stdout: io.TextIOBase = sys.stdout 233 234 # Destination for stderr from subprocesses. 235 stderr: io.TextIOBase = sys.stderr 236 237 def run(self, *args, **kwargs): 238 return subprocess.run( 239 *args, check=True, stdout=self.stdout, stderr=self.stderr, **kwargs) 240 241 242def sdk_snapshot_zip_file(snapshots_dir, sdk_name, sdk_version): 243 """Get the path to the sdk snapshot zip file.""" 244 return os.path.join(snapshots_dir, f"{sdk_name}-{sdk_version}.zip") 245 246 247@dataclasses.dataclass() 248class SnapshotBuilder: 249 """Builds sdk snapshots""" 250 251 # The path to this tool. 252 tool_path: str 253 254 # Used to run subprocesses for building snapshots. 255 subprocess_runner: SubprocessRunner 256 257 # The OUT_DIR environment variable. 258 out_dir: str 259 260 # The out/soong/mainline-sdks directory. 261 mainline_sdks_dir: str = "" 262 263 def __post_init__(self): 264 self.mainline_sdks_dir = os.path.join(self.out_dir, 265 "soong/mainline-sdks") 266 267 def get_sdk_path(self, sdk_name, sdk_version): 268 """Get the path to the sdk snapshot zip file produced by soong""" 269 return os.path.join(self.mainline_sdks_dir, 270 f"{sdk_name}-{sdk_version}.zip") 271 272 def build_snapshots(self, build_release, sdk_versions, modules): 273 # Build the SDKs once for each version. 274 for sdk_version in sdk_versions: 275 # Compute the paths to all the Soong generated sdk snapshot files 276 # required by this script. 277 paths = [ 278 sdk_snapshot_zip_file(self.mainline_sdks_dir, sdk, sdk_version) 279 for module in modules 280 for sdk in module.sdks 281 ] 282 283 # Extra environment variables to pass to the build process. 284 extraEnv = { 285 # TODO(ngeoffray): remove SOONG_ALLOW_MISSING_DEPENDENCIES, but 286 # we currently break without it. 287 "SOONG_ALLOW_MISSING_DEPENDENCIES": "true", 288 # Set SOONG_SDK_SNAPSHOT_USE_SRCJAR to generate .srcjars inside 289 # sdk zip files as expected by prebuilt drop. 290 "SOONG_SDK_SNAPSHOT_USE_SRCJAR": "true", 291 # Set SOONG_SDK_SNAPSHOT_VERSION to generate the appropriately 292 # tagged version of the sdk. 293 "SOONG_SDK_SNAPSHOT_VERSION": sdk_version, 294 } 295 extraEnv.update(build_release.soong_env) 296 297 # Unless explicitly specified in the calling environment set 298 # TARGET_BUILD_VARIANT=user. 299 # This MUST be identical to the TARGET_BUILD_VARIANT used to build 300 # the corresponding APEXes otherwise it could result in different 301 # hidden API flags, see http://b/202398851#comment29 for more info. 302 target_build_variant = os.environ.get("TARGET_BUILD_VARIANT", 303 "user") 304 cmd = [ 305 "build/soong/soong_ui.bash", 306 "--make-mode", 307 "--soong-only", 308 f"TARGET_BUILD_VARIANT={target_build_variant}", 309 "TARGET_PRODUCT=mainline_sdk", 310 "MODULE_BUILD_FROM_SOURCE=true", 311 "out/soong/apex/depsinfo/new-allowed-deps.txt.check", 312 ] + paths 313 print_command(extraEnv, cmd) 314 env = os.environ.copy() 315 env.update(extraEnv) 316 self.subprocess_runner.run(cmd, env=env) 317 return self.mainline_sdks_dir 318 319 def build_snapshots_for_build_r(self, build_release, sdk_versions, modules): 320 # Build the snapshots as standard. 321 snapshot_dir = self.build_snapshots(build_release, sdk_versions, 322 modules) 323 324 # Each module will extract needed files from the original snapshot zip 325 # file and then use that to create a replacement zip file. 326 r_snapshot_dir = os.path.join(snapshot_dir, "for-R-build") 327 shutil.rmtree(r_snapshot_dir, ignore_errors=True) 328 329 build_number_file = os.path.join(self.out_dir, "soong/build_number.txt") 330 331 for module in modules: 332 apex = module.apex 333 dest_dir = os.path.join(r_snapshot_dir, apex) 334 os.makedirs(dest_dir, exist_ok=True) 335 336 # Write the bp file in the sdk_library sub-directory rather than the 337 # root of the zip file as it will be unpacked in a directory that 338 # already contains an Android.bp file that defines the corresponding 339 # apex_set. 340 bp_file = os.path.join(dest_dir, "sdk_library/Android.bp") 341 os.makedirs(os.path.dirname(bp_file), exist_ok=True) 342 343 # The first sdk in the list is the name to use. 344 sdk_name = module.sdks[0] 345 346 with open(bp_file, "w", encoding="utf8") as bp: 347 bp.write("// DO NOT EDIT. Auto-generated by the following:\n") 348 bp.write(f"// {self.tool_path}\n") 349 bp.write(COPYRIGHT_BOILERPLATE) 350 aosp_apex = google_to_aosp_name(apex) 351 352 for library in module.for_r_build.sdk_libraries: 353 module_name = library.name 354 shared_library = str(library.shared_library).lower() 355 sdk_file = sdk_snapshot_zip_file(snapshot_dir, sdk_name, 356 "current") 357 extract_matching_files_from_zip( 358 sdk_file, dest_dir, 359 sdk_library_files_pattern( 360 scope_pattern=r"(public|system|module-lib)", 361 name_pattern=fr"({module_name}(-removed|-stubs)?)")) 362 363 bp.write(f""" 364java_sdk_library_import {{ 365 name: "{module_name}", 366 owner: "google", 367 prefer: true, 368 shared_library: {shared_library}, 369 apex_available: [ 370 "{aosp_apex}", 371 "test_{aosp_apex}", 372 ], 373 public: {{ 374 jars: ["public/{module_name}-stubs.jar"], 375 current_api: "public/{module_name}.txt", 376 removed_api: "public/{module_name}-removed.txt", 377 sdk_version: "module_current", 378 }}, 379 system: {{ 380 jars: ["system/{module_name}-stubs.jar"], 381 current_api: "system/{module_name}.txt", 382 removed_api: "system/{module_name}-removed.txt", 383 sdk_version: "module_current", 384 }}, 385 module_lib: {{ 386 jars: ["module-lib/{module_name}-stubs.jar"], 387 current_api: "module-lib/{module_name}.txt", 388 removed_api: "module-lib/{module_name}-removed.txt", 389 sdk_version: "module_current", 390 }}, 391}} 392""") 393 394 # Copy the build_number.txt file into the snapshot. 395 snapshot_build_number_file = os.path.join( 396 dest_dir, "snapshot-creation-build-number.txt") 397 shutil.copy(build_number_file, snapshot_build_number_file) 398 399 # Now zip up the files into a snapshot zip file. 400 base_file = os.path.join(r_snapshot_dir, sdk_name + "-current") 401 shutil.make_archive(base_file, "zip", dest_dir) 402 403 return r_snapshot_dir 404 405 406# A list of the sdk versions to build. Usually just current but can include a 407# numeric version too. 408SDK_VERSIONS = [ 409 # Suitable for overriding the source modules with prefer:true. 410 # Unlike "unversioned" this mode also adds "@current" suffixed modules 411 # with the same prebuilts (which are never preferred). 412 "current", 413 # Insert additional sdk versions needed for the latest build release. 414] 415 416# The initially empty list of build releases. Every BuildRelease that is created 417# automatically appends itself to this list. 418ALL_BUILD_RELEASES = [] 419 420 421@dataclasses.dataclass(frozen=True) 422@functools.total_ordering 423class BuildRelease: 424 """Represents a build release""" 425 426 # The name of the build release, e.g. Q, R, S, T, etc. 427 name: str 428 429 # The function to call to create the snapshot in the dist, that covers 430 # building and copying the snapshot into the dist. 431 creator: Callable[ 432 ["BuildRelease", "SdkDistProducer", List["MainlineModule"]], None] 433 434 # The sub-directory of dist/mainline-sdks into which the build release 435 # specific snapshots will be copied. 436 # 437 # Defaults to for-<name>-build. 438 sub_dir: str = None 439 440 # Additional environment variables to pass to Soong when building the 441 # snapshots for this build release. 442 # 443 # Defaults to { 444 # "SOONG_SDK_SNAPSHOT_TARGET_BUILD_RELEASE": <name>, 445 # } 446 soong_env: typing.Dict[str, str] = None 447 448 # The sdk versions that need to be generated for this build release. 449 sdk_versions: List[str] = \ 450 dataclasses.field(default_factory=lambda: SDK_VERSIONS) 451 452 # The position of this instance within the BUILD_RELEASES list. 453 ordinal: int = dataclasses.field(default=-1, init=False) 454 455 # Whether this build release supports the Soong config boilerplate that is 456 # used to control the prefer setting of modules via a Soong config variable. 457 supports_soong_config_boilerplate: bool = True 458 459 def __post_init__(self): 460 # The following use object.__setattr__ as this object is frozen and 461 # attempting to set the fields directly would cause an exception to be 462 # thrown. 463 object.__setattr__(self, "ordinal", len(ALL_BUILD_RELEASES)) 464 # Add this to the end of the list of all build releases. 465 ALL_BUILD_RELEASES.append(self) 466 # If no sub_dir was specified then set the default. 467 if self.sub_dir is None: 468 object.__setattr__(self, "sub_dir", f"for-{self.name}-build") 469 # If no soong_env was specified then set the default. 470 if self.soong_env is None: 471 object.__setattr__( 472 self, 473 "soong_env", 474 { 475 # Set SOONG_SDK_SNAPSHOT_TARGET_BUILD_RELEASE to generate a 476 # snapshot suitable for a specific target build release. 477 "SOONG_SDK_SNAPSHOT_TARGET_BUILD_RELEASE": self.name, 478 }) 479 480 def __eq__(self, other): 481 return self.ordinal == other.ordinal 482 483 def __le__(self, other): 484 return self.ordinal <= other.ordinal 485 486 487def create_no_dist_snapshot(_: BuildRelease, __: "SdkDistProducer", 488 modules: List["MainlineModule"]): 489 """A place holder dist snapshot creation function that does nothing.""" 490 print(f"create_no_dist_snapshot for modules {[m.apex for m in modules]}") 491 492 493def create_dist_snapshot_for_r(build_release: BuildRelease, 494 producer: "SdkDistProducer", 495 modules: List["MainlineModule"]): 496 """Generate a snapshot suitable for use in an R build.""" 497 producer.product_dist_for_build_r(build_release, modules) 498 499 500def create_sdk_snapshots_in_soong(build_release: BuildRelease, 501 producer: "SdkDistProducer", 502 modules: List["MainlineModule"]): 503 """Builds sdks and populates the dist for unbundled modules.""" 504 producer.produce_unbundled_dist_for_build_release(build_release, modules) 505 506 507def create_latest_sdk_snapshots(build_release: BuildRelease, 508 producer: "SdkDistProducer", 509 modules: List["MainlineModule"]): 510 """Builds and populates the latest release, including bundled modules.""" 511 producer.produce_unbundled_dist_for_build_release(build_release, modules) 512 producer.produce_bundled_dist_for_build_release(build_release, modules) 513 514 515def create_legacy_dist_structures(build_release: BuildRelease, 516 producer: "SdkDistProducer", 517 modules: List["MainlineModule"]): 518 """Creates legacy file structures.""" 519 520 # Only put unbundled modules in the legacy dist and stubs structures. 521 modules = [m for m in modules if not m.is_bundled()] 522 523 snapshots_dir = producer.produce_unbundled_dist_for_build_release( 524 build_release, modules) 525 526 # Create the out/dist/mainline-sdks/stubs structure. 527 # TODO(b/199759953): Remove stubs once it is no longer used by gantry. 528 # Clear and populate the stubs directory. 529 dist_dir = producer.dist_dir 530 stubs_dir = os.path.join(dist_dir, "stubs") 531 shutil.rmtree(stubs_dir, ignore_errors=True) 532 533 for module in modules: 534 apex = module.apex 535 dest_dir = os.path.join(dist_dir, "stubs", apex) 536 for sdk in module.sdks: 537 # If the sdk's name ends with -sdk then extract sdk library 538 # related files from its zip file. 539 if sdk.endswith("-sdk"): 540 sdk_file = sdk_snapshot_zip_file(snapshots_dir, sdk, "current") 541 extract_matching_files_from_zip(sdk_file, dest_dir, 542 sdk_library_files_pattern()) 543 544 545Q = BuildRelease( 546 name="Q", 547 # At the moment we do not generate a snapshot for Q. 548 creator=create_no_dist_snapshot, 549) 550R = BuildRelease( 551 name="R", 552 # Generate a simple snapshot for R. 553 creator=create_dist_snapshot_for_r, 554 # By default a BuildRelease creates an environment to pass to Soong that 555 # creates a release specific snapshot. However, Soong does not yet (and is 556 # unlikely to) support building an sdk snapshot for R so create an empty 557 # environment to pass to Soong instead. 558 soong_env={}, 559 # R does not support or need Soong config boilerplate. 560 supports_soong_config_boilerplate=False) 561S = BuildRelease( 562 name="S", 563 # Generate a snapshot for S using Soong. 564 creator=create_sdk_snapshots_in_soong, 565) 566Tiramisu = BuildRelease( 567 name="Tiramisu", 568 # Generate a snapshot for Tiramisu using Soong. 569 creator=create_sdk_snapshots_in_soong, 570) 571 572# Insert additional BuildRelease definitions for following releases here, 573# before LATEST. 574 575# The build release for the latest build supported by this build, i.e. the 576# current build. This must be the last BuildRelease defined in this script, 577# before LEGACY_BUILD_RELEASE. 578LATEST = BuildRelease( 579 name="latest", 580 creator=create_latest_sdk_snapshots, 581 # There are no build release specific environment variables to pass to 582 # Soong. 583 soong_env={}, 584) 585 586# The build release to populate the legacy dist structure that does not specify 587# a particular build release. This MUST come after LATEST so that it includes 588# all the modules for which sdk snapshot source is available. 589LEGACY_BUILD_RELEASE = BuildRelease( 590 name="legacy", 591 # There is no build release specific sub directory. 592 sub_dir="", 593 # Create snapshots needed for legacy tools. 594 creator=create_legacy_dist_structures, 595 # There are no build release specific environment variables to pass to 596 # Soong. 597 soong_env={}, 598) 599 600 601@dataclasses.dataclass(frozen=True) 602class SdkLibrary: 603 """Information about a java_sdk_library.""" 604 605 # The name of java_sdk_library module. 606 name: str 607 608 # True if the sdk_library module is a shared library. 609 shared_library: bool = False 610 611 612@dataclasses.dataclass(frozen=True) 613class ForRBuild: 614 """Data structure needed for generating a snapshot for an R build.""" 615 616 # The java_sdk_library modules to export to the r snapshot. 617 sdk_libraries: typing.List[SdkLibrary] = dataclasses.field( 618 default_factory=list) 619 620 621@dataclasses.dataclass(frozen=True) 622class MainlineModule: 623 """Represents an unbundled mainline module. 624 625 This is a module that is distributed as a prebuilt and intended to be 626 updated with Mainline trains. 627 """ 628 # The name of the apex. 629 apex: str 630 631 # The names of the sdk and module_exports. 632 sdks: list[str] 633 634 # The first build release in which the SDK snapshot for this module is 635 # needed. 636 # 637 # Note: This is not necessarily the same build release in which the SDK 638 # source was first included. So, a module that was added in build T 639 # could potentially be used in an S release and so its SDK will need 640 # to be made available for S builds. 641 first_release: BuildRelease 642 643 # The configuration variable, defaults to ANDROID:module_build_from_source 644 configVar: ConfigVar = ConfigVar( 645 namespace="ANDROID", 646 name="module_build_from_source", 647 ) 648 649 # The bp file containing the definitions of the configuration module types 650 # to use in the sdk. 651 configBpDefFile: str = "packages/modules/common/Android.bp" 652 653 # The prefix to use for the soong config module types. 654 configModuleTypePrefix: str = "module_" 655 656 for_r_build: typing.Optional[ForRBuild] = None 657 658 # The last release on which this module was optional. 659 # 660 # Some modules are optional when they are first released, usually because 661 # some vendors of Android devices have their own customizations of the 662 # module that they would like to preserve and which cannot yet be achieved 663 # through the existing APIs. Once those issues have been resolved then they 664 # will become mandatory. 665 # 666 # This field records the last build release in which they are optional. It 667 # defaults to None which indicates that the module was never optional. 668 last_optional_release: typing.Optional[BuildRelease] = None 669 670 # The short name for the module. 671 # 672 # Defaults to the last part of the apex name. 673 short_name: str = "" 674 675 def __post_init__(self): 676 # If short_name is not set then set it to the last component of the apex 677 # name. 678 if not self.short_name: 679 short_name = self.apex.rsplit(".", 1)[-1] 680 object.__setattr__(self, "short_name", short_name) 681 682 def is_bundled(self): 683 """Returns true for bundled modules. See BundledMainlineModule.""" 684 return False 685 686 def transformations(self, build_release): 687 """Returns the transformations to apply to this module's snapshot(s).""" 688 transformations = [] 689 if build_release.supports_soong_config_boilerplate: 690 691 config_var = self.configVar 692 config_module_type_prefix = self.configModuleTypePrefix 693 config_bp_def_file = self.configBpDefFile 694 695 # If the module is optional then it needs its own Soong config 696 # variable to allow it to be managed separately from other modules. 697 if (self.last_optional_release and 698 self.last_optional_release > build_release): 699 config_var = ConfigVar( 700 namespace=f"{self.short_name}_module", 701 name="source_build", 702 ) 703 config_module_type_prefix = f"{self.short_name}_prebuilt_" 704 # Optional modules don't have their own config_bp_def_file so 705 # they have to generate the soong_config_module_types inline. 706 config_bp_def_file = "" 707 708 inserter = SoongConfigBoilerplateInserter( 709 "Android.bp", 710 configVar=config_var, 711 configModuleTypePrefix=config_module_type_prefix, 712 configBpDefFile=config_bp_def_file) 713 transformations.append(inserter) 714 return transformations 715 716 def is_required_for(self, target_build_release): 717 """True if this module is required for the target build release.""" 718 return self.first_release <= target_build_release 719 720 721@dataclasses.dataclass(frozen=True) 722class BundledMainlineModule(MainlineModule): 723 """Represents a bundled Mainline module or a platform SDK for module use. 724 725 A bundled module is always preloaded into the platform images. 726 """ 727 728 # Defaults to the latest build, i.e. the build on which this script is run 729 # as bundled modules are, by definition, only needed in this build. 730 first_release: BuildRelease = LATEST 731 732 def is_bundled(self): 733 return True 734 735 def transformations(self, build_release): 736 # Bundled modules are only used on thin branches where the corresponding 737 # sources are absent, so skip transformations and keep the default 738 # `prefer: false`. 739 return [] 740 741 742# List of mainline modules. 743MAINLINE_MODULES = [ 744 MainlineModule( 745 apex="com.android.adservices", 746 sdks=["adservices-module-sdk"], 747 first_release=Tiramisu, 748 ), 749 MainlineModule( 750 apex="com.android.appsearch", 751 sdks=["appsearch-sdk"], 752 first_release=Tiramisu, 753 ), 754 MainlineModule( 755 apex="com.android.art", 756 sdks=[ 757 "art-module-sdk", 758 "art-module-test-exports", 759 "art-module-host-exports", 760 ], 761 first_release=S, 762 # Override the config... fields. 763 configVar=ConfigVar( 764 namespace="art_module", 765 name="source_build", 766 ), 767 configBpDefFile="prebuilts/module_sdk/art/SoongConfig.bp", 768 configModuleTypePrefix="art_prebuilt_", 769 ), 770 MainlineModule( 771 apex="com.android.btservices", 772 sdks=["btservices-module-sdk"], 773 first_release=Tiramisu, 774 # Bluetooth has always been and is still optional. 775 last_optional_release=LATEST, 776 ), 777 MainlineModule( 778 apex="com.android.conscrypt", 779 sdks=[ 780 "conscrypt-module-sdk", 781 "conscrypt-module-test-exports", 782 "conscrypt-module-host-exports", 783 ], 784 first_release=Q, 785 # No conscrypt java_sdk_library modules are exported to the R snapshot. 786 # Conscrypt was updatable in R but the generate_ml_bundle.sh does not 787 # appear to generate a snapshot for it. 788 for_r_build=None, 789 ), 790 MainlineModule( 791 apex="com.android.ipsec", 792 sdks=["ipsec-module-sdk"], 793 first_release=R, 794 for_r_build=ForRBuild(sdk_libraries=[ 795 SdkLibrary( 796 name="android.net.ipsec.ike", 797 shared_library=True, 798 ), 799 ]), 800 ), 801 MainlineModule( 802 apex="com.android.media", 803 sdks=["media-module-sdk"], 804 first_release=R, 805 for_r_build=ForRBuild(sdk_libraries=[ 806 SdkLibrary(name="framework-media"), 807 ]), 808 ), 809 MainlineModule( 810 apex="com.android.mediaprovider", 811 sdks=["mediaprovider-module-sdk"], 812 first_release=R, 813 for_r_build=ForRBuild(sdk_libraries=[ 814 SdkLibrary(name="framework-mediaprovider"), 815 ]), 816 ), 817 MainlineModule( 818 apex="com.android.ondevicepersonalization", 819 sdks=["ondevicepersonalization-module-sdk"], 820 first_release=Tiramisu, 821 ), 822 MainlineModule( 823 apex="com.android.permission", 824 sdks=["permission-module-sdk"], 825 first_release=R, 826 for_r_build=ForRBuild(sdk_libraries=[ 827 SdkLibrary(name="framework-permission"), 828 # framework-permission-s is not needed on R as it contains classes 829 # that are provided in R by non-updatable parts of the 830 # bootclasspath. 831 ]), 832 ), 833 MainlineModule( 834 apex="com.android.scheduling", 835 sdks=["scheduling-sdk"], 836 first_release=S, 837 ), 838 MainlineModule( 839 apex="com.android.sdkext", 840 sdks=["sdkextensions-sdk"], 841 first_release=R, 842 for_r_build=ForRBuild(sdk_libraries=[ 843 SdkLibrary(name="framework-sdkextensions"), 844 ]), 845 ), 846 MainlineModule( 847 apex="com.android.os.statsd", 848 sdks=["statsd-module-sdk"], 849 first_release=R, 850 for_r_build=ForRBuild(sdk_libraries=[ 851 SdkLibrary(name="framework-statsd"), 852 ]), 853 ), 854 MainlineModule( 855 apex="com.android.tethering", 856 sdks=["tethering-module-sdk"], 857 first_release=R, 858 for_r_build=ForRBuild(sdk_libraries=[ 859 SdkLibrary(name="framework-tethering"), 860 ]), 861 ), 862 MainlineModule( 863 apex="com.android.uwb", 864 sdks=["uwb-module-sdk"], 865 first_release=Tiramisu, 866 # Uwb has always been and is still optional. 867 last_optional_release=LATEST, 868 ), 869 MainlineModule( 870 apex="com.android.wifi", 871 sdks=["wifi-module-sdk"], 872 first_release=R, 873 for_r_build=ForRBuild(sdk_libraries=[ 874 SdkLibrary(name="framework-wifi"), 875 ]), 876 # Wifi has always been and is still optional. 877 last_optional_release=LATEST, 878 ), 879] 880 881# List of Mainline modules that currently are never built unbundled. They must 882# not specify first_release, and they don't have com.google.android 883# counterparts. 884BUNDLED_MAINLINE_MODULES = [ 885 BundledMainlineModule( 886 apex="com.android.i18n", 887 sdks=[ 888 "i18n-module-sdk", 889 "i18n-module-test-exports", 890 "i18n-module-host-exports", 891 ], 892 ), 893 BundledMainlineModule( 894 apex="com.android.runtime", 895 sdks=[ 896 "runtime-module-host-exports", 897 "runtime-module-sdk", 898 ], 899 ), 900 BundledMainlineModule( 901 apex="com.android.tzdata", 902 sdks=["tzdata-module-test-exports"], 903 ), 904] 905 906# List of platform SDKs for Mainline module use. 907PLATFORM_SDKS_FOR_MAINLINE = [ 908 BundledMainlineModule( 909 apex="platform-mainline", 910 sdks=[ 911 "platform-mainline-sdk", 912 "platform-mainline-test-exports", 913 ], 914 ), 915] 916 917 918@dataclasses.dataclass 919class SdkDistProducer: 920 """Produces the DIST_DIR/mainline-sdks and DIST_DIR/stubs directories. 921 922 Builds SDK snapshots for mainline modules and then copies them into the 923 DIST_DIR/mainline-sdks directory. Also extracts the sdk_library txt, jar and 924 srcjar files from each SDK snapshot and copies them into the DIST_DIR/stubs 925 directory. 926 """ 927 928 # Used to run subprocesses for this. 929 subprocess_runner: SubprocessRunner 930 931 # Builds sdk snapshots 932 snapshot_builder: SnapshotBuilder 933 934 # The DIST_DIR environment variable. 935 dist_dir: str = "uninitialized-dist" 936 937 # The path to this script. It may be inserted into files that are 938 # transformed to document where the changes came from. 939 script: str = sys.argv[0] 940 941 # The path to the mainline-sdks dist directory for unbundled modules. 942 # 943 # Initialized in __post_init__(). 944 mainline_sdks_dir: str = dataclasses.field(init=False) 945 946 # The path to the mainline-sdks dist directory for bundled modules and 947 # platform SDKs. 948 # 949 # Initialized in __post_init__(). 950 bundled_mainline_sdks_dir: str = dataclasses.field(init=False) 951 952 def __post_init__(self): 953 self.mainline_sdks_dir = os.path.join(self.dist_dir, "mainline-sdks") 954 self.bundled_mainline_sdks_dir = os.path.join(self.dist_dir, 955 "bundled-mainline-sdks") 956 957 def prepare(self): 958 # Clear the sdk dist directories. 959 shutil.rmtree(self.mainline_sdks_dir, ignore_errors=True) 960 shutil.rmtree(self.bundled_mainline_sdks_dir, ignore_errors=True) 961 962 def produce_dist(self, modules, build_releases): 963 # Prepare the dist directory for the sdks. 964 self.prepare() 965 966 # Group build releases so that those with the same Soong environment are 967 # run consecutively to avoid having to regenerate ninja files. 968 grouped_by_env = defaultdict(list) 969 for build_release in build_releases: 970 grouped_by_env[str(build_release.soong_env)].append(build_release) 971 ordered = [br for _, group in grouped_by_env.items() for br in group] 972 973 for build_release in ordered: 974 # Only build modules that are required for this build release. 975 filtered_modules = [ 976 m for m in modules if m.is_required_for(build_release) 977 ] 978 if filtered_modules: 979 print(f"Building SDK snapshots for {build_release.name}" 980 f" build release") 981 build_release.creator(build_release, self, filtered_modules) 982 983 def product_dist_for_build_r(self, build_release, modules): 984 # Although we only need a subset of the files that a java_sdk_library 985 # adds to an sdk snapshot generating the whole snapshot is the simplest 986 # way to ensure that all the necessary files are produced. 987 sdk_versions = build_release.sdk_versions 988 989 # Filter out any modules that do not provide sdk for R. 990 modules = [m for m in modules if m.for_r_build] 991 992 snapshot_dir = self.snapshot_builder.build_snapshots_for_build_r( 993 build_release, sdk_versions, modules) 994 self.populate_unbundled_dist(build_release, sdk_versions, modules, 995 snapshot_dir) 996 997 def produce_unbundled_dist_for_build_release(self, build_release, modules): 998 modules = [m for m in modules if not m.is_bundled()] 999 sdk_versions = build_release.sdk_versions 1000 snapshots_dir = self.snapshot_builder.build_snapshots( 1001 build_release, sdk_versions, modules) 1002 self.populate_unbundled_dist(build_release, sdk_versions, modules, 1003 snapshots_dir) 1004 return snapshots_dir 1005 1006 def produce_bundled_dist_for_build_release(self, build_release, modules): 1007 modules = [m for m in modules if m.is_bundled()] 1008 if modules: 1009 sdk_versions = build_release.sdk_versions 1010 snapshots_dir = self.snapshot_builder.build_snapshots( 1011 build_release, sdk_versions, modules) 1012 self.populate_bundled_dist(build_release, modules, snapshots_dir) 1013 1014 def populate_unbundled_dist(self, build_release, sdk_versions, modules, 1015 snapshots_dir): 1016 build_release_dist_dir = os.path.join(self.mainline_sdks_dir, 1017 build_release.sub_dir) 1018 for module in modules: 1019 for sdk_version in sdk_versions: 1020 for sdk in module.sdks: 1021 sdk_dist_dir = os.path.join(build_release_dist_dir, 1022 sdk_version) 1023 self.populate_dist_snapshot(build_release, module, sdk, 1024 sdk_dist_dir, sdk_version, 1025 snapshots_dir) 1026 1027 def populate_bundled_dist(self, build_release, modules, snapshots_dir): 1028 sdk_dist_dir = self.bundled_mainline_sdks_dir 1029 for module in modules: 1030 for sdk in module.sdks: 1031 self.populate_dist_snapshot(build_release, module, sdk, 1032 sdk_dist_dir, "current", 1033 snapshots_dir) 1034 1035 def populate_dist_snapshot(self, build_release, module, sdk, sdk_dist_dir, 1036 sdk_version, snapshots_dir): 1037 subdir = re.sub("^.+-(sdk|(host|test)-exports)$", r"\1", sdk) 1038 if subdir not in ("sdk", "host-exports", "test-exports"): 1039 raise Exception(f"{sdk} is not a valid name, expected it to end" 1040 f" with -(sdk|host-exports|test-exports)") 1041 1042 sdk_dist_subdir = os.path.join(sdk_dist_dir, module.apex, subdir) 1043 sdk_path = sdk_snapshot_zip_file(snapshots_dir, sdk, sdk_version) 1044 transformations = module.transformations(build_release) 1045 self.dist_sdk_snapshot_zip(sdk_path, sdk_dist_subdir, transformations) 1046 1047 def dist_sdk_snapshot_zip(self, src_sdk_zip, sdk_dist_dir, transformations): 1048 """Copy the sdk snapshot zip file to a dist directory. 1049 1050 If no transformations are provided then this simply copies the show sdk 1051 snapshot zip file to the dist dir. However, if transformations are 1052 provided then the files to be transformed are extracted from the 1053 snapshot zip file, they are transformed to files in a separate directory 1054 and then a new zip file is created in the dist directory with the 1055 original files replaced by the newly transformed files. 1056 """ 1057 os.makedirs(sdk_dist_dir) 1058 dest_sdk_zip = os.path.join(sdk_dist_dir, os.path.basename(src_sdk_zip)) 1059 print(f"Copying sdk snapshot {src_sdk_zip} to {dest_sdk_zip}") 1060 1061 # If no transformations are provided then just copy the zip file 1062 # directly. 1063 if len(transformations) == 0: 1064 shutil.copy(src_sdk_zip, sdk_dist_dir) 1065 return 1066 1067 with tempfile.TemporaryDirectory() as tmp_dir: 1068 # Create a single pattern that will match any of the paths provided 1069 # in the transformations. 1070 pattern = "|".join( 1071 [f"({re.escape(t.path)})" for t in transformations]) 1072 1073 # Extract the matching files from the zip into the temporary 1074 # directory. 1075 extract_matching_files_from_zip(src_sdk_zip, tmp_dir, pattern) 1076 1077 # Apply the transformations to the extracted files in situ. 1078 apply_transformations(self, tmp_dir, transformations) 1079 1080 # Replace the original entries in the zip with the transformed 1081 # files. 1082 paths = [transformation.path for transformation in transformations] 1083 copy_zip_and_replace(self, src_sdk_zip, dest_sdk_zip, tmp_dir, 1084 paths) 1085 1086 1087def print_command(env, cmd): 1088 print(" ".join([f"{name}={value}" for name, value in env.items()] + cmd)) 1089 1090 1091def sdk_library_files_pattern(*, scope_pattern=r"[^/]+", name_pattern=r"[^/]+"): 1092 """Return a pattern to match sdk_library related files in an sdk snapshot""" 1093 return rf"sdk_library/{scope_pattern}/{name_pattern}\.(txt|jar|srcjar)" 1094 1095 1096def extract_matching_files_from_zip(zip_path, dest_dir, pattern): 1097 """Extracts files from a zip file into a destination directory. 1098 1099 The extracted files are those that match the specified regular expression 1100 pattern. 1101 """ 1102 os.makedirs(dest_dir, exist_ok=True) 1103 with zipfile.ZipFile(zip_path) as zip_file: 1104 for filename in zip_file.namelist(): 1105 if re.match(pattern, filename): 1106 print(f" extracting {filename}") 1107 zip_file.extract(filename, dest_dir) 1108 1109 1110def copy_zip_and_replace(producer, src_zip_path, dest_zip_path, src_dir, paths): 1111 """Copies a zip replacing some of its contents in the process. 1112 1113 The files to replace are specified by the paths parameter and are relative 1114 to the src_dir. 1115 """ 1116 # Get the absolute paths of the source and dest zip files so that they are 1117 # not affected by a change of directory. 1118 abs_src_zip_path = os.path.abspath(src_zip_path) 1119 abs_dest_zip_path = os.path.abspath(dest_zip_path) 1120 producer.subprocess_runner.run( 1121 ["zip", "-q", abs_src_zip_path, "--out", abs_dest_zip_path] + paths, 1122 # Change into the source directory before running zip. 1123 cwd=src_dir) 1124 1125 1126def apply_transformations(producer, tmp_dir, transformations): 1127 for transformation in transformations: 1128 path = os.path.join(tmp_dir, transformation.path) 1129 1130 # Record the timestamp of the file. 1131 modified = os.path.getmtime(path) 1132 1133 # Transform the file. 1134 transformation.apply(producer, path) 1135 1136 # Reset the timestamp of the file to the original timestamp before the 1137 # transformation was applied. 1138 os.utime(path, (modified, modified)) 1139 1140 1141def create_producer(tool_path): 1142 # Variables initialized from environment variables that are set by the 1143 # calling mainline_modules_sdks.sh. 1144 out_dir = os.environ["OUT_DIR"] 1145 dist_dir = os.environ["DIST_DIR"] 1146 1147 top_dir = os.environ["ANDROID_BUILD_TOP"] 1148 tool_path = os.path.relpath(tool_path, top_dir) 1149 tool_path = tool_path.replace(".py", ".sh") 1150 1151 subprocess_runner = SubprocessRunner() 1152 snapshot_builder = SnapshotBuilder( 1153 tool_path=tool_path, 1154 subprocess_runner=subprocess_runner, 1155 out_dir=out_dir, 1156 ) 1157 return SdkDistProducer( 1158 subprocess_runner=subprocess_runner, 1159 snapshot_builder=snapshot_builder, 1160 dist_dir=dist_dir, 1161 ) 1162 1163 1164def aosp_to_google(module): 1165 """Transform an AOSP module into a Google module""" 1166 new_apex = aosp_to_google_name(module.apex) 1167 # Create a copy of the AOSP module with the internal specific APEX name. 1168 return dataclasses.replace(module, apex=new_apex) 1169 1170 1171def aosp_to_google_name(name): 1172 """Transform an AOSP module name into a Google module name""" 1173 return name.replace("com.android.", "com.google.android.") 1174 1175 1176def google_to_aosp_name(name): 1177 """Transform a Google module name into an AOSP module name""" 1178 return name.replace("com.google.android.", "com.android.") 1179 1180 1181def filter_modules(modules, target_build_apps): 1182 if target_build_apps: 1183 target_build_apps = target_build_apps.split() 1184 return [m for m in modules if m.apex in target_build_apps] 1185 return modules 1186 1187 1188def main(args): 1189 """Program entry point.""" 1190 if not os.path.exists("build/make/core/Makefile"): 1191 sys.exit("This script must be run from the top of the tree.") 1192 1193 args_parser = argparse.ArgumentParser( 1194 description="Build snapshot zips for consumption by Gantry.") 1195 args_parser.add_argument( 1196 "--tool-path", 1197 help="The path to this tool.", 1198 default="unspecified", 1199 ) 1200 args_parser.add_argument( 1201 "--build-release", 1202 action="append", 1203 choices=[br.name for br in ALL_BUILD_RELEASES], 1204 help="A target build for which snapshots are required. " 1205 "If it is \"latest\" then Mainline module SDKs from platform and " 1206 "bundled modules are included.", 1207 ) 1208 args_parser.add_argument( 1209 "--build-platform-sdks-for-mainline", 1210 action="store_true", 1211 help="Also build the platform SDKs for Mainline modules. " 1212 "Defaults to true when TARGET_BUILD_APPS is not set. " 1213 "Applicable only if the \"latest\" build release is built.", 1214 ) 1215 args = args_parser.parse_args(args) 1216 1217 build_releases = ALL_BUILD_RELEASES 1218 if args.build_release: 1219 selected_build_releases = {b.lower() for b in args.build_release} 1220 build_releases = [ 1221 b for b in build_releases 1222 if b.name.lower() in selected_build_releases 1223 ] 1224 1225 target_build_apps = os.environ.get("TARGET_BUILD_APPS") 1226 modules = filter_modules(MAINLINE_MODULES + BUNDLED_MAINLINE_MODULES, 1227 target_build_apps) 1228 1229 # Also build the platform Mainline SDKs either if no specific modules are 1230 # requested or if --build-platform-sdks-for-mainline is given. 1231 if not target_build_apps or args.build_platform_sdks_for_mainline: 1232 modules += PLATFORM_SDKS_FOR_MAINLINE 1233 1234 producer = create_producer(args.tool_path) 1235 producer.produce_dist(modules, build_releases) 1236 1237 1238if __name__ == "__main__": 1239 main(sys.argv[1:]) 1240