blob: d9c97ee1d3e760cc1d42abbac005e9adc35dedb0 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (C) 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Builds SDK snapshots.
If the environment variable TARGET_BUILD_APPS is nonempty then only the SDKs for
the APEXes in it are built, otherwise all configured SDKs are built.
"""
import argparse
import dataclasses
import functools
import io
import os
import re
import shutil
import subprocess
import sys
import tempfile
import typing
from collections import defaultdict
from typing import Callable, List
import zipfile
COPYRIGHT_BOILERPLATE = """
//
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
""".lstrip()
@dataclasses.dataclass(frozen=True)
class ConfigVar:
"""Represents a Soong configuration variable"""
# The config variable namespace, e.g. ANDROID.
namespace: str
# The name of the variable within the namespace.
name: str
@dataclasses.dataclass(frozen=True)
class FileTransformation:
"""Performs a transformation on a file within an SDK snapshot zip file."""
# The path of the file within the SDK snapshot zip file.
path: str
def apply(self, producer, path):
"""Apply the transformation to the src_path to produce the dest_path."""
raise NotImplementedError
@dataclasses.dataclass(frozen=True)
class SoongConfigBoilerplateInserter(FileTransformation):
"""Transforms an Android.bp file to add soong config boilerplate.
The boilerplate allows the prefer setting of the modules to be controlled
through a Soong configuration variable.
"""
# The configuration variable that will control the prefer setting.
configVar: ConfigVar
# The bp file containing the definitions of the configuration module types
# to use in the sdk.
configBpDefFile: str
# The prefix to use for the soong config module types.
configModuleTypePrefix: str
def apply(self, producer, path):
with open(path, "r+", encoding="utf8") as file:
self._apply_transformation(producer, file)
def _apply_transformation(self, producer, file):
# TODO(b/174997203): Remove this when we have a proper way to control
# prefer flags in Mainline modules.
header_lines = []
for line in file:
line = line.rstrip("\n")
if not line.startswith("//"):
break
header_lines.append(line)
config_module_types = set()
content_lines = []
for line in file:
line = line.rstrip("\n")
# Check to see whether the line is the start of a new module type,
# e.g. <module-type> {
module_header = re.match("([a-z0-9_]+) +{$", line)
if not module_header:
# It is not so just add the line to the output and skip to the
# next line.
content_lines.append(line)
continue
module_type = module_header.group(1)
module_content = []
# Iterate over the Soong module contents
for module_line in file:
module_line = module_line.rstrip("\n")
# When the end of the module has been reached then exit.
if module_line == "}":
break
# Check to see if the module is an unversioned module, i.e.
# without @<version>. If it is then it needs to have the soong
# config boilerplate added to control the setting of the prefer
# property. Versioned modules do not need that because they are
# never preferred.
# At the moment this differentiation between versioned and
# unversioned relies on the fact that the unversioned modules
# set "prefer: false", while the versioned modules do not. That
# is a little bit fragile so may require some additional checks.
if module_line != " prefer: false,":
# The line does not indicate that the module needs the
# soong config boilerplate so add the line and skip to the
# next one.
module_content.append(module_line)
continue
# Add the soong config boilerplate instead of the line:
# prefer: false,
namespace = self.configVar.namespace
name = self.configVar.name
module_content.append(f"""\
// Do not prefer prebuilt if the Soong config variable "{name}" in namespace "{namespace}" is true.
prefer: true,
soong_config_variables: {{
{name}: {{
prefer: false,
}},
}},""")
# Add the module type to the list of module types that need to
# have corresponding config module types.
config_module_types.add(module_type)
# Change the module type to the corresponding soong config
# module type by adding the prefix.
module_type = self.configModuleTypePrefix + module_type
# Generate the module, possibly with the new module type and
# containing the soong config variables entry.
content_lines.append(module_type + " {")
content_lines.extend(module_content)
content_lines.append("}")
if self.configBpDefFile:
# Add the soong_config_module_type_import module definition that
# imports the soong config module types into this bp file to the
# header lines so that they appear before any uses.
module_types = "\n".join([
f' "{self.configModuleTypePrefix}{mt}",'
for mt in sorted(config_module_types)
])
header_lines.append(f"""
// Soong config variable stanza added by {producer.script}.
soong_config_module_type_import {{
from: "{self.configBpDefFile}",
module_types: [
{module_types}
],
}}
""")
else:
# Add the soong_config_module_type module definitions to the header
# lines so that they appear before any uses.
header_lines.append("")
for module_type in sorted(config_module_types):
# Create the corresponding soong config module type name by
# adding the prefix.
config_module_type = self.configModuleTypePrefix + module_type
header_lines.append(f"""
// Soong config variable module type added by {producer.script}.
soong_config_module_type {{
name: "{config_module_type}",
module_type: "{module_type}",
config_namespace: "{self.configVar.namespace}",
bool_variables: ["{self.configVar.name}"],
properties: ["prefer"],
}}
""".lstrip())
# Overwrite the file with the updated contents.
file.seek(0)
file.truncate()
file.write("\n".join(header_lines + content_lines) + "\n")
@dataclasses.dataclass()
class SubprocessRunner:
"""Runs subprocesses"""
# Destination for stdout from subprocesses.
#
# This (and the following stderr) are needed to allow the tests to be run
# in Intellij. This ensures that the tests are run with stdout/stderr
# objects that work when passed to subprocess.run(stdout/stderr). Without it
# the tests are run with a FlushingStringIO object that has no fileno
# attribute - https://youtrack.jetbrains.com/issue/PY-27883.
stdout: io.TextIOBase = sys.stdout
# Destination for stderr from subprocesses.
stderr: io.TextIOBase = sys.stderr
def run(self, *args, **kwargs):
return subprocess.run(
*args, check=True, stdout=self.stdout, stderr=self.stderr, **kwargs)
def sdk_snapshot_zip_file(snapshots_dir, sdk_name, sdk_version):
"""Get the path to the sdk snapshot zip file."""
return os.path.join(snapshots_dir, f"{sdk_name}-{sdk_version}.zip")
@dataclasses.dataclass()
class SnapshotBuilder:
"""Builds sdk snapshots"""
# The path to this tool.
tool_path: str
# Used to run subprocesses for building snapshots.
subprocess_runner: SubprocessRunner
# The OUT_DIR environment variable.
out_dir: str
# The out/soong/mainline-sdks directory.
mainline_sdks_dir: str = ""
def __post_init__(self):
self.mainline_sdks_dir = os.path.join(self.out_dir,
"soong/mainline-sdks")
def get_sdk_path(self, sdk_name, sdk_version):
"""Get the path to the sdk snapshot zip file produced by soong"""
return os.path.join(self.mainline_sdks_dir,
f"{sdk_name}-{sdk_version}.zip")
def build_snapshots(self, build_release, sdk_versions, modules):
# Build the SDKs once for each version.
for sdk_version in sdk_versions:
# Compute the paths to all the Soong generated sdk snapshot files
# required by this script.
paths = [
sdk_snapshot_zip_file(self.mainline_sdks_dir, sdk, sdk_version)
for module in modules
for sdk in module.sdks
]
# Extra environment variables to pass to the build process.
extraEnv = {
# TODO(ngeoffray): remove SOONG_ALLOW_MISSING_DEPENDENCIES, but
# we currently break without it.
"SOONG_ALLOW_MISSING_DEPENDENCIES": "true",
# Set SOONG_SDK_SNAPSHOT_USE_SRCJAR to generate .srcjars inside
# sdk zip files as expected by prebuilt drop.
"SOONG_SDK_SNAPSHOT_USE_SRCJAR": "true",
# Set SOONG_SDK_SNAPSHOT_VERSION to generate the appropriately
# tagged version of the sdk.
"SOONG_SDK_SNAPSHOT_VERSION": sdk_version,
}
extraEnv.update(build_release.soong_env)
# Unless explicitly specified in the calling environment set
# TARGET_BUILD_VARIANT=user.
# This MUST be identical to the TARGET_BUILD_VARIANT used to build
# the corresponding APEXes otherwise it could result in different
# hidden API flags, see http://b/202398851#comment29 for more info.
target_build_variant = os.environ.get("TARGET_BUILD_VARIANT",
"user")
cmd = [
"build/soong/soong_ui.bash",
"--make-mode",
"--soong-only",
f"TARGET_BUILD_VARIANT={target_build_variant}",
"TARGET_PRODUCT=mainline_sdk",
"MODULE_BUILD_FROM_SOURCE=true",
"out/soong/apex/depsinfo/new-allowed-deps.txt.check",
] + paths
print_command(extraEnv, cmd)
env = os.environ.copy()
env.update(extraEnv)
self.subprocess_runner.run(cmd, env=env)
return self.mainline_sdks_dir
def build_snapshots_for_build_r(self, build_release, sdk_versions, modules):
# Build the snapshots as standard.
snapshot_dir = self.build_snapshots(build_release, sdk_versions,
modules)
# Each module will extract needed files from the original snapshot zip
# file and then use that to create a replacement zip file.
r_snapshot_dir = os.path.join(snapshot_dir, "for-R-build")
shutil.rmtree(r_snapshot_dir, ignore_errors=True)
build_number_file = os.path.join(self.out_dir, "soong/build_number.txt")
for module in modules:
apex = module.apex
dest_dir = os.path.join(r_snapshot_dir, apex)
os.makedirs(dest_dir, exist_ok=True)
# Write the bp file in the sdk_library sub-directory rather than the
# root of the zip file as it will be unpacked in a directory that
# already contains an Android.bp file that defines the corresponding
# apex_set.
bp_file = os.path.join(dest_dir, "sdk_library/Android.bp")
os.makedirs(os.path.dirname(bp_file), exist_ok=True)
# The first sdk in the list is the name to use.
sdk_name = module.sdks[0]
with open(bp_file, "w", encoding="utf8") as bp:
bp.write("// DO NOT EDIT. Auto-generated by the following:\n")
bp.write(f"// {self.tool_path}\n")
bp.write(COPYRIGHT_BOILERPLATE)
aosp_apex = google_to_aosp_name(apex)
for library in module.for_r_build.sdk_libraries:
module_name = library.name
shared_library = str(library.shared_library).lower()
sdk_file = sdk_snapshot_zip_file(snapshot_dir, sdk_name,
"current")
extract_matching_files_from_zip(
sdk_file, dest_dir,
sdk_library_files_pattern(
scope_pattern=r"(public|system|module-lib)",
name_pattern=fr"({module_name}(-removed|-stubs)?)"))
bp.write(f"""
java_sdk_library_import {{
name: "{module_name}",
owner: "google",
prefer: true,
shared_library: {shared_library},
apex_available: [
"{aosp_apex}",
"test_{aosp_apex}",
],
public: {{
jars: ["public/{module_name}-stubs.jar"],
current_api: "public/{module_name}.txt",
removed_api: "public/{module_name}-removed.txt",
sdk_version: "module_current",
}},
system: {{
jars: ["system/{module_name}-stubs.jar"],
current_api: "system/{module_name}.txt",
removed_api: "system/{module_name}-removed.txt",
sdk_version: "module_current",
}},
module_lib: {{
jars: ["module-lib/{module_name}-stubs.jar"],
current_api: "module-lib/{module_name}.txt",
removed_api: "module-lib/{module_name}-removed.txt",
sdk_version: "module_current",
}},
}}
""")
# Copy the build_number.txt file into the snapshot.
snapshot_build_number_file = os.path.join(
dest_dir, "snapshot-creation-build-number.txt")
shutil.copy(build_number_file, snapshot_build_number_file)
# Now zip up the files into a snapshot zip file.
base_file = os.path.join(r_snapshot_dir, sdk_name + "-current")
shutil.make_archive(base_file, "zip", dest_dir)
return r_snapshot_dir
# A list of the sdk versions to build. Usually just current but can include a
# numeric version too.
SDK_VERSIONS = [
# Suitable for overriding the source modules with prefer:true.
# Unlike "unversioned" this mode also adds "@current" suffixed modules
# with the same prebuilts (which are never preferred).
"current",
# Insert additional sdk versions needed for the latest build release.
]
# The initially empty list of build releases. Every BuildRelease that is created
# automatically appends itself to this list.
ALL_BUILD_RELEASES = []
@dataclasses.dataclass(frozen=True)
@functools.total_ordering
class BuildRelease:
"""Represents a build release"""
# The name of the build release, e.g. Q, R, S, T, etc.
name: str
# The function to call to create the snapshot in the dist, that covers
# building and copying the snapshot into the dist.
creator: Callable[
["BuildRelease", "SdkDistProducer", List["MainlineModule"]], None]
# The sub-directory of dist/mainline-sdks into which the build release
# specific snapshots will be copied.
#
# Defaults to for-<name>-build.
sub_dir: str = None
# Additional environment variables to pass to Soong when building the
# snapshots for this build release.
#
# Defaults to {
# "SOONG_SDK_SNAPSHOT_TARGET_BUILD_RELEASE": <name>,
# }
soong_env: typing.Dict[str, str] = None
# The sdk versions that need to be generated for this build release.
sdk_versions: List[str] = \
dataclasses.field(default_factory=lambda: SDK_VERSIONS)
# The position of this instance within the BUILD_RELEASES list.
ordinal: int = dataclasses.field(default=-1, init=False)
# Whether this build release supports the Soong config boilerplate that is
# used to control the prefer setting of modules via a Soong config variable.
supports_soong_config_boilerplate: bool = True
def __post_init__(self):
# The following use object.__setattr__ as this object is frozen and
# attempting to set the fields directly would cause an exception to be
# thrown.
object.__setattr__(self, "ordinal", len(ALL_BUILD_RELEASES))
# Add this to the end of the list of all build releases.
ALL_BUILD_RELEASES.append(self)
# If no sub_dir was specified then set the default.
if self.sub_dir is None:
object.__setattr__(self, "sub_dir", f"for-{self.name}-build")
# If no soong_env was specified then set the default.
if self.soong_env is None:
object.__setattr__(
self,
"soong_env",
{
# Set SOONG_SDK_SNAPSHOT_TARGET_BUILD_RELEASE to generate a
# snapshot suitable for a specific target build release.
"SOONG_SDK_SNAPSHOT_TARGET_BUILD_RELEASE": self.name,
})
def __eq__(self, other):
return self.ordinal == other.ordinal
def __le__(self, other):
return self.ordinal <= other.ordinal
def create_no_dist_snapshot(_: BuildRelease, __: "SdkDistProducer",
modules: List["MainlineModule"]):
"""A place holder dist snapshot creation function that does nothing."""
print(f"create_no_dist_snapshot for modules {[m.apex for m in modules]}")
def create_dist_snapshot_for_r(build_release: BuildRelease,
producer: "SdkDistProducer",
modules: List["MainlineModule"]):
"""Generate a snapshot suitable for use in an R build."""
producer.product_dist_for_build_r(build_release, modules)
def create_sdk_snapshots_in_soong(build_release: BuildRelease,
producer: "SdkDistProducer",
modules: List["MainlineModule"]):
"""Builds sdks and populates the dist for unbundled modules."""
producer.produce_unbundled_dist_for_build_release(build_release, modules)
def create_latest_sdk_snapshots(build_release: BuildRelease,
producer: "SdkDistProducer",
modules: List["MainlineModule"]):
"""Builds and populates the latest release, including bundled modules."""
producer.produce_unbundled_dist_for_build_release(build_release, modules)
producer.produce_bundled_dist_for_build_release(build_release, modules)
def create_legacy_dist_structures(build_release: BuildRelease,
producer: "SdkDistProducer",
modules: List["MainlineModule"]):
"""Creates legacy file structures."""
# Only put unbundled modules in the legacy dist and stubs structures.
modules = [m for m in modules if not m.is_bundled()]
snapshots_dir = producer.produce_unbundled_dist_for_build_release(
build_release, modules)
# Create the out/dist/mainline-sdks/stubs structure.
# TODO(b/199759953): Remove stubs once it is no longer used by gantry.
# Clear and populate the stubs directory.
dist_dir = producer.dist_dir
stubs_dir = os.path.join(dist_dir, "stubs")
shutil.rmtree(stubs_dir, ignore_errors=True)
for module in modules:
apex = module.apex
dest_dir = os.path.join(dist_dir, "stubs", apex)
for sdk in module.sdks:
# If the sdk's name ends with -sdk then extract sdk library
# related files from its zip file.
if sdk.endswith("-sdk"):
sdk_file = sdk_snapshot_zip_file(snapshots_dir, sdk, "current")
extract_matching_files_from_zip(sdk_file, dest_dir,
sdk_library_files_pattern())
Q = BuildRelease(
name="Q",
# At the moment we do not generate a snapshot for Q.
creator=create_no_dist_snapshot,
)
R = BuildRelease(
name="R",
# Generate a simple snapshot for R.
creator=create_dist_snapshot_for_r,
# By default a BuildRelease creates an environment to pass to Soong that
# creates a release specific snapshot. However, Soong does not yet (and is
# unlikely to) support building an sdk snapshot for R so create an empty
# environment to pass to Soong instead.
soong_env={},
# R does not support or need Soong config boilerplate.
supports_soong_config_boilerplate=False)
S = BuildRelease(
name="S",
# Generate a snapshot for S using Soong.
creator=create_sdk_snapshots_in_soong,
)
Tiramisu = BuildRelease(
name="Tiramisu",
# Generate a snapshot for Tiramisu using Soong.
creator=create_sdk_snapshots_in_soong,
)
# Insert additional BuildRelease definitions for following releases here,
# before LATEST.
# The build release for the latest build supported by this build, i.e. the
# current build. This must be the last BuildRelease defined in this script,
# before LEGACY_BUILD_RELEASE.
LATEST = BuildRelease(
name="latest",
creator=create_latest_sdk_snapshots,
# There are no build release specific environment variables to pass to
# Soong.
soong_env={},
)
# The build release to populate the legacy dist structure that does not specify
# a particular build release. This MUST come after LATEST so that it includes
# all the modules for which sdk snapshot source is available.
LEGACY_BUILD_RELEASE = BuildRelease(
name="legacy",
# There is no build release specific sub directory.
sub_dir="",
# Create snapshots needed for legacy tools.
creator=create_legacy_dist_structures,
# There are no build release specific environment variables to pass to
# Soong.
soong_env={},
)
@dataclasses.dataclass(frozen=True)
class SdkLibrary:
"""Information about a java_sdk_library."""
# The name of java_sdk_library module.
name: str
# True if the sdk_library module is a shared library.
shared_library: bool = False
@dataclasses.dataclass(frozen=True)
class ForRBuild:
"""Data structure needed for generating a snapshot for an R build."""
# The java_sdk_library modules to export to the r snapshot.
sdk_libraries: typing.List[SdkLibrary] = dataclasses.field(
default_factory=list)
@dataclasses.dataclass(frozen=True)
class MainlineModule:
"""Represents an unbundled mainline module.
This is a module that is distributed as a prebuilt and intended to be
updated with Mainline trains.
"""
# The name of the apex.
apex: str
# The names of the sdk and module_exports.
sdks: list[str]
# The first build release in which the SDK snapshot for this module is
# needed.
#
# Note: This is not necessarily the same build release in which the SDK
# source was first included. So, a module that was added in build T
# could potentially be used in an S release and so its SDK will need
# to be made available for S builds.
first_release: BuildRelease
# The configuration variable, defaults to ANDROID:module_build_from_source
configVar: ConfigVar = ConfigVar(
namespace="ANDROID",
name="module_build_from_source",
)
# The bp file containing the definitions of the configuration module types
# to use in the sdk.
configBpDefFile: str = "packages/modules/common/Android.bp"
# The prefix to use for the soong config module types.
configModuleTypePrefix: str = "module_"
for_r_build: typing.Optional[ForRBuild] = None
# The last release on which this module was optional.
#
# Some modules are optional when they are first released, usually because
# some vendors of Android devices have their own customizations of the
# module that they would like to preserve and which cannot yet be achieved
# through the existing APIs. Once those issues have been resolved then they
# will become mandatory.
#
# This field records the last build release in which they are optional. It
# defaults to None which indicates that the module was never optional.
last_optional_release: typing.Optional[BuildRelease] = None
# The short name for the module.
#
# Defaults to the last part of the apex name.
short_name: str = ""
def __post_init__(self):
# If short_name is not set then set it to the last component of the apex
# name.
if not self.short_name:
short_name = self.apex.rsplit(".", 1)[-1]
object.__setattr__(self, "short_name", short_name)
def is_bundled(self):
"""Returns true for bundled modules. See BundledMainlineModule."""
return False
def transformations(self, build_release):
"""Returns the transformations to apply to this module's snapshot(s)."""
transformations = []
if build_release.supports_soong_config_boilerplate:
config_var = self.configVar
config_module_type_prefix = self.configModuleTypePrefix
config_bp_def_file = self.configBpDefFile
# If the module is optional then it needs its own Soong config
# variable to allow it to be managed separately from other modules.
if (self.last_optional_release and
self.last_optional_release > build_release):
config_var = ConfigVar(
namespace=f"{self.short_name}_module",
name="source_build",
)
config_module_type_prefix = f"{self.short_name}_prebuilt_"
# Optional modules don't have their own config_bp_def_file so
# they have to generate the soong_config_module_types inline.
config_bp_def_file = ""
inserter = SoongConfigBoilerplateInserter(
"Android.bp",
configVar=config_var,
configModuleTypePrefix=config_module_type_prefix,
configBpDefFile=config_bp_def_file)
transformations.append(inserter)
return transformations
def is_required_for(self, target_build_release):
"""True if this module is required for the target build release."""
return self.first_release <= target_build_release
@dataclasses.dataclass(frozen=True)
class BundledMainlineModule(MainlineModule):
"""Represents a bundled Mainline module or a platform SDK for module use.
A bundled module is always preloaded into the platform images.
"""
# Defaults to the latest build, i.e. the build on which this script is run
# as bundled modules are, by definition, only needed in this build.
first_release: BuildRelease = LATEST
def is_bundled(self):
return True
def transformations(self, build_release):
# Bundled modules are only used on thin branches where the corresponding
# sources are absent, so skip transformations and keep the default
# `prefer: false`.
return []
# List of mainline modules.
MAINLINE_MODULES = [
MainlineModule(
apex="com.android.adservices",
sdks=["adservices-module-sdk"],
first_release=Tiramisu,
),
MainlineModule(
apex="com.android.appsearch",
sdks=["appsearch-sdk"],
first_release=Tiramisu,
),
MainlineModule(
apex="com.android.art",
sdks=[
"art-module-sdk",
"art-module-test-exports",
"art-module-host-exports",
],
first_release=S,
# Override the config... fields.
configVar=ConfigVar(
namespace="art_module",
name="source_build",
),
configBpDefFile="prebuilts/module_sdk/art/SoongConfig.bp",
configModuleTypePrefix="art_prebuilt_",
),
MainlineModule(
apex="com.android.btservices",
sdks=["btservices-module-sdk"],
first_release=Tiramisu,
# Bluetooth has always been and is still optional.
last_optional_release=LATEST,
),
MainlineModule(
apex="com.android.conscrypt",
sdks=[
"conscrypt-module-sdk",
"conscrypt-module-test-exports",
"conscrypt-module-host-exports",
],
first_release=Q,
# No conscrypt java_sdk_library modules are exported to the R snapshot.
# Conscrypt was updatable in R but the generate_ml_bundle.sh does not
# appear to generate a snapshot for it.
for_r_build=None,
),
MainlineModule(
apex="com.android.ipsec",
sdks=["ipsec-module-sdk"],
first_release=R,
for_r_build=ForRBuild(sdk_libraries=[
SdkLibrary(
name="android.net.ipsec.ike",
shared_library=True,
),
]),
),
MainlineModule(
apex="com.android.media",
sdks=["media-module-sdk"],
first_release=R,
for_r_build=ForRBuild(sdk_libraries=[
SdkLibrary(name="framework-media"),
]),
),
MainlineModule(
apex="com.android.mediaprovider",
sdks=["mediaprovider-module-sdk"],
first_release=R,
for_r_build=ForRBuild(sdk_libraries=[
SdkLibrary(name="framework-mediaprovider"),
]),
),
MainlineModule(
apex="com.android.ondevicepersonalization",
sdks=["ondevicepersonalization-module-sdk"],
first_release=Tiramisu,
),
MainlineModule(
apex="com.android.permission",
sdks=["permission-module-sdk"],
first_release=R,
for_r_build=ForRBuild(sdk_libraries=[
SdkLibrary(name="framework-permission"),
# framework-permission-s is not needed on R as it contains classes
# that are provided in R by non-updatable parts of the
# bootclasspath.
]),
),
MainlineModule(
apex="com.android.scheduling",
sdks=["scheduling-sdk"],
first_release=S,
),
MainlineModule(
apex="com.android.sdkext",
sdks=["sdkextensions-sdk"],
first_release=R,
for_r_build=ForRBuild(sdk_libraries=[
SdkLibrary(name="framework-sdkextensions"),
]),
),
MainlineModule(
apex="com.android.os.statsd",
sdks=["statsd-module-sdk"],
first_release=R,
for_r_build=ForRBuild(sdk_libraries=[
SdkLibrary(name="framework-statsd"),
]),
),
MainlineModule(
apex="com.android.tethering",
sdks=["tethering-module-sdk"],
first_release=R,
for_r_build=ForRBuild(sdk_libraries=[
SdkLibrary(name="framework-tethering"),
]),
),
MainlineModule(
apex="com.android.uwb",
sdks=["uwb-module-sdk"],
first_release=Tiramisu,
# Uwb has always been and is still optional.
last_optional_release=LATEST,
),
MainlineModule(
apex="com.android.wifi",
sdks=["wifi-module-sdk"],
first_release=R,
for_r_build=ForRBuild(sdk_libraries=[
SdkLibrary(name="framework-wifi"),
]),
# Wifi has always been and is still optional.
last_optional_release=LATEST,
),
]
# List of Mainline modules that currently are never built unbundled. They must
# not specify first_release, and they don't have com.google.android
# counterparts.
BUNDLED_MAINLINE_MODULES = [
BundledMainlineModule(
apex="com.android.i18n",
sdks=[
"i18n-module-sdk",
"i18n-module-test-exports",
"i18n-module-host-exports",
],
),
BundledMainlineModule(
apex="com.android.runtime",
sdks=[
"runtime-module-host-exports",
"runtime-module-sdk",
],
),
BundledMainlineModule(
apex="com.android.tzdata",
sdks=["tzdata-module-test-exports"],
),
]
# List of platform SDKs for Mainline module use.
PLATFORM_SDKS_FOR_MAINLINE = [
BundledMainlineModule(
apex="platform-mainline",
sdks=[
"platform-mainline-sdk",
"platform-mainline-test-exports",
],
),
]
@dataclasses.dataclass
class SdkDistProducer:
"""Produces the DIST_DIR/mainline-sdks and DIST_DIR/stubs directories.
Builds SDK snapshots for mainline modules and then copies them into the
DIST_DIR/mainline-sdks directory. Also extracts the sdk_library txt, jar and
srcjar files from each SDK snapshot and copies them into the DIST_DIR/stubs
directory.
"""
# Used to run subprocesses for this.
subprocess_runner: SubprocessRunner
# Builds sdk snapshots
snapshot_builder: SnapshotBuilder
# The DIST_DIR environment variable.
dist_dir: str = "uninitialized-dist"
# The path to this script. It may be inserted into files that are
# transformed to document where the changes came from.
script: str = sys.argv[0]
# The path to the mainline-sdks dist directory for unbundled modules.
#
# Initialized in __post_init__().
mainline_sdks_dir: str = dataclasses.field(init=False)
# The path to the mainline-sdks dist directory for bundled modules and
# platform SDKs.
#
# Initialized in __post_init__().
bundled_mainline_sdks_dir: str = dataclasses.field(init=False)
def __post_init__(self):
self.mainline_sdks_dir = os.path.join(self.dist_dir, "mainline-sdks")
self.bundled_mainline_sdks_dir = os.path.join(self.dist_dir,
"bundled-mainline-sdks")
def prepare(self):
# Clear the sdk dist directories.
shutil.rmtree(self.mainline_sdks_dir, ignore_errors=True)
shutil.rmtree(self.bundled_mainline_sdks_dir, ignore_errors=True)
def produce_dist(self, modules, build_releases):
# Prepare the dist directory for the sdks.
self.prepare()
# Group build releases so that those with the same Soong environment are
# run consecutively to avoid having to regenerate ninja files.
grouped_by_env = defaultdict(list)
for build_release in build_releases:
grouped_by_env[str(build_release.soong_env)].append(build_release)
ordered = [br for _, group in grouped_by_env.items() for br in group]
for build_release in ordered:
# Only build modules that are required for this build release.
filtered_modules = [
m for m in modules if m.is_required_for(build_release)
]
if filtered_modules:
print(f"Building SDK snapshots for {build_release.name}"
f" build release")
build_release.creator(build_release, self, filtered_modules)
def product_dist_for_build_r(self, build_release, modules):
# Although we only need a subset of the files that a java_sdk_library
# adds to an sdk snapshot generating the whole snapshot is the simplest
# way to ensure that all the necessary files are produced.
sdk_versions = build_release.sdk_versions
# Filter out any modules that do not provide sdk for R.
modules = [m for m in modules if m.for_r_build]
snapshot_dir = self.snapshot_builder.build_snapshots_for_build_r(
build_release, sdk_versions, modules)
self.populate_unbundled_dist(build_release, sdk_versions, modules,
snapshot_dir)
def produce_unbundled_dist_for_build_release(self, build_release, modules):
modules = [m for m in modules if not m.is_bundled()]
sdk_versions = build_release.sdk_versions
snapshots_dir = self.snapshot_builder.build_snapshots(
build_release, sdk_versions, modules)
self.populate_unbundled_dist(build_release, sdk_versions, modules,
snapshots_dir)
return snapshots_dir
def produce_bundled_dist_for_build_release(self, build_release, modules):
modules = [m for m in modules if m.is_bundled()]
if modules:
sdk_versions = build_release.sdk_versions
snapshots_dir = self.snapshot_builder.build_snapshots(
build_release, sdk_versions, modules)
self.populate_bundled_dist(build_release, modules, snapshots_dir)
def populate_unbundled_dist(self, build_release, sdk_versions, modules,
snapshots_dir):
build_release_dist_dir = os.path.join(self.mainline_sdks_dir,
build_release.sub_dir)
for module in modules:
for sdk_version in sdk_versions:
for sdk in module.sdks:
sdk_dist_dir = os.path.join(build_release_dist_dir,
sdk_version)
self.populate_dist_snapshot(build_release, module, sdk,
sdk_dist_dir, sdk_version,
snapshots_dir)
def populate_bundled_dist(self, build_release, modules, snapshots_dir):
sdk_dist_dir = self.bundled_mainline_sdks_dir
for module in modules:
for sdk in module.sdks:
self.populate_dist_snapshot(build_release, module, sdk,
sdk_dist_dir, "current",
snapshots_dir)
def populate_dist_snapshot(self, build_release, module, sdk, sdk_dist_dir,
sdk_version, snapshots_dir):
subdir = re.sub("^.+-(sdk|(host|test)-exports)$", r"\1", sdk)
if subdir not in ("sdk", "host-exports", "test-exports"):
raise Exception(f"{sdk} is not a valid name, expected it to end"
f" with -(sdk|host-exports|test-exports)")
sdk_dist_subdir = os.path.join(sdk_dist_dir, module.apex, subdir)
sdk_path = sdk_snapshot_zip_file(snapshots_dir, sdk, sdk_version)
transformations = module.transformations(build_release)
self.dist_sdk_snapshot_zip(sdk_path, sdk_dist_subdir, transformations)
def dist_sdk_snapshot_zip(self, src_sdk_zip, sdk_dist_dir, transformations):
"""Copy the sdk snapshot zip file to a dist directory.
If no transformations are provided then this simply copies the show sdk
snapshot zip file to the dist dir. However, if transformations are
provided then the files to be transformed are extracted from the
snapshot zip file, they are transformed to files in a separate directory
and then a new zip file is created in the dist directory with the
original files replaced by the newly transformed files.
"""
os.makedirs(sdk_dist_dir)
dest_sdk_zip = os.path.join(sdk_dist_dir, os.path.basename(src_sdk_zip))
print(f"Copying sdk snapshot {src_sdk_zip} to {dest_sdk_zip}")
# If no transformations are provided then just copy the zip file
# directly.
if len(transformations) == 0:
shutil.copy(src_sdk_zip, sdk_dist_dir)
return
with tempfile.TemporaryDirectory() as tmp_dir:
# Create a single pattern that will match any of the paths provided
# in the transformations.
pattern = "|".join(
[f"({re.escape(t.path)})" for t in transformations])
# Extract the matching files from the zip into the temporary
# directory.
extract_matching_files_from_zip(src_sdk_zip, tmp_dir, pattern)
# Apply the transformations to the extracted files in situ.
apply_transformations(self, tmp_dir, transformations)
# Replace the original entries in the zip with the transformed
# files.
paths = [transformation.path for transformation in transformations]
copy_zip_and_replace(self, src_sdk_zip, dest_sdk_zip, tmp_dir,
paths)
def print_command(env, cmd):
print(" ".join([f"{name}={value}" for name, value in env.items()] + cmd))
def sdk_library_files_pattern(*, scope_pattern=r"[^/]+", name_pattern=r"[^/]+"):
"""Return a pattern to match sdk_library related files in an sdk snapshot"""
return rf"sdk_library/{scope_pattern}/{name_pattern}\.(txt|jar|srcjar)"
def extract_matching_files_from_zip(zip_path, dest_dir, pattern):
"""Extracts files from a zip file into a destination directory.
The extracted files are those that match the specified regular expression
pattern.
"""
os.makedirs(dest_dir, exist_ok=True)
with zipfile.ZipFile(zip_path) as zip_file:
for filename in zip_file.namelist():
if re.match(pattern, filename):
print(f" extracting {filename}")
zip_file.extract(filename, dest_dir)
def copy_zip_and_replace(producer, src_zip_path, dest_zip_path, src_dir, paths):
"""Copies a zip replacing some of its contents in the process.
The files to replace are specified by the paths parameter and are relative
to the src_dir.
"""
# Get the absolute paths of the source and dest zip files so that they are
# not affected by a change of directory.
abs_src_zip_path = os.path.abspath(src_zip_path)
abs_dest_zip_path = os.path.abspath(dest_zip_path)
producer.subprocess_runner.run(
["zip", "-q", abs_src_zip_path, "--out", abs_dest_zip_path] + paths,
# Change into the source directory before running zip.
cwd=src_dir)
def apply_transformations(producer, tmp_dir, transformations):
for transformation in transformations:
path = os.path.join(tmp_dir, transformation.path)
# Record the timestamp of the file.
modified = os.path.getmtime(path)
# Transform the file.
transformation.apply(producer, path)
# Reset the timestamp of the file to the original timestamp before the
# transformation was applied.
os.utime(path, (modified, modified))
def create_producer(tool_path):
# Variables initialized from environment variables that are set by the
# calling mainline_modules_sdks.sh.
out_dir = os.environ["OUT_DIR"]
dist_dir = os.environ["DIST_DIR"]
top_dir = os.environ["ANDROID_BUILD_TOP"]
tool_path = os.path.relpath(tool_path, top_dir)
tool_path = tool_path.replace(".py", ".sh")
subprocess_runner = SubprocessRunner()
snapshot_builder = SnapshotBuilder(
tool_path=tool_path,
subprocess_runner=subprocess_runner,
out_dir=out_dir,
)
return SdkDistProducer(
subprocess_runner=subprocess_runner,
snapshot_builder=snapshot_builder,
dist_dir=dist_dir,
)
def aosp_to_google(module):
"""Transform an AOSP module into a Google module"""
new_apex = aosp_to_google_name(module.apex)
# Create a copy of the AOSP module with the internal specific APEX name.
return dataclasses.replace(module, apex=new_apex)
def aosp_to_google_name(name):
"""Transform an AOSP module name into a Google module name"""
return name.replace("com.android.", "com.google.android.")
def google_to_aosp_name(name):
"""Transform a Google module name into an AOSP module name"""
return name.replace("com.google.android.", "com.android.")
def filter_modules(modules, target_build_apps):
if target_build_apps:
target_build_apps = target_build_apps.split()
return [m for m in modules if m.apex in target_build_apps]
return modules
def main(args):
"""Program entry point."""
if not os.path.exists("build/make/core/Makefile"):
sys.exit("This script must be run from the top of the tree.")
args_parser = argparse.ArgumentParser(
description="Build snapshot zips for consumption by Gantry.")
args_parser.add_argument(
"--tool-path",
help="The path to this tool.",
default="unspecified",
)
args_parser.add_argument(
"--build-release",
action="append",
choices=[br.name for br in ALL_BUILD_RELEASES],
help="A target build for which snapshots are required. "
"If it is \"latest\" then Mainline module SDKs from platform and "
"bundled modules are included.",
)
args_parser.add_argument(
"--build-platform-sdks-for-mainline",
action="store_true",
help="Also build the platform SDKs for Mainline modules. "
"Defaults to true when TARGET_BUILD_APPS is not set. "
"Applicable only if the \"latest\" build release is built.",
)
args = args_parser.parse_args(args)
build_releases = ALL_BUILD_RELEASES
if args.build_release:
selected_build_releases = {b.lower() for b in args.build_release}
build_releases = [
b for b in build_releases
if b.name.lower() in selected_build_releases
]
target_build_apps = os.environ.get("TARGET_BUILD_APPS")
modules = filter_modules(MAINLINE_MODULES + BUNDLED_MAINLINE_MODULES,
target_build_apps)
# Also build the platform Mainline SDKs either if no specific modules are
# requested or if --build-platform-sdks-for-mainline is given.
if not target_build_apps or args.build_platform_sdks_for_mainline:
modules += PLATFORM_SDKS_FOR_MAINLINE
producer = create_producer(args.tool_path)
producer.produce_dist(modules, build_releases)
if __name__ == "__main__":
main(sys.argv[1:])