• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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