summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKarl Shaffer <karlshaffer@google.com>2021-05-05 00:17:32 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2021-05-05 00:17:32 +0000
commit7b211d657babd90cfc909c18dde3854c8dac314a (patch)
tree1f4fb1e246017932b2da3d2fa21066b184195803
parent24d70a0fdffcc57e5d954c7605fb55725727f090 (diff)
parent33631428e88135f243f299da13beb48ee76e8143 (diff)
downloadpesto-7b211d657babd90cfc909c18dde3854c8dac314a.tar.gz
Implement the prepare_bazel_test_env script am: 2178bb2de9 am: 8eb6bb443e am: 0fa3de4e84 am: 33631428e8
Original change: https://android-review.googlesource.com/c/platform/build/pesto/+/1677135 Change-Id: Ia199349aff514f07be49051f3e762c8da901b47a
-rw-r--r--experiments/prepare_bazel_test_env/bazelenv.py643
-rw-r--r--experiments/prepare_bazel_test_env/prepare_bazel_test_env.py91
2 files changed, 734 insertions, 0 deletions
diff --git a/experiments/prepare_bazel_test_env/bazelenv.py b/experiments/prepare_bazel_test_env/bazelenv.py
new file mode 100644
index 0000000..e42e767
--- /dev/null
+++ b/experiments/prepare_bazel_test_env/bazelenv.py
@@ -0,0 +1,643 @@
+# 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.
+"""Core logic for generating, syncing, and cleaning up a Bazel environment."""
+import abc
+import datetime
+import logging
+import os
+import pathlib
+import re
+import shutil
+import subprocess
+from typing import Set, Dict, List
+
+# Regex for BUILD files used to identify them since they can be named
+# BUILD or BUILD.bazel.
+BUILD_FILENAME_REGEX = re.compile("(BUILD|BUILD.bazel)")
+
+
+class Error(Exception):
+ """Base Error that all other errors the system throws are descendants of."""
+ pass
+
+
+class SoongExecutionError(Error):
+ """Raised when Soong fails to build provided targets."""
+ pass
+
+
+class Soong:
+ """Interface for the Soong build system.
+
+ Attributes:
+ soong_workspace: the top of the Android workspace that Soong
+ will be operating in.
+ soong_executable: the path to the executable for the soong_ui.bash
+ launcher.
+ """
+ soong_workspace: pathlib.Path
+ soong_executable: pathlib.Path
+
+ def __init__(self, soong_workspace: pathlib.Path):
+ self.soong_workspace = soong_workspace
+
+ self.soong_executable = self.soong_workspace.joinpath(
+ "build/soong/soong_ui.bash")
+ if not self.soong_executable.exists():
+ raise SoongExecutionError(
+ "Unable to find Soong executable, expected location: %s" %
+ self.soong_executable)
+
+ def build(self, build_targets: Set[str]) -> None:
+ """Builds the provided set of targets with Soong.
+
+ Of note, there is no verification for the targets that get passed in,
+ rather that responsibility passes to Soong which will fail to build if a
+ target is invalid.
+
+ Args:
+ build_targets: a set of targets to build with Soong.
+ """
+ cmd_args = [
+ str(self.soong_executable), "--build-mode", "--all-modules",
+ f"--dir={self.soong_workspace}"
+ ]
+ cmd_args.extend(build_targets)
+
+ logging.info("Building targets with Soong: %s", build_targets)
+ logging.info("Please be patient, this may take a while...")
+ logging.debug("Soong Command is: %s", " ".join(cmd_args))
+
+ try:
+ subprocess.run(cmd_args,
+ cwd=self.soong_workspace,
+ check=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as cpe:
+ raise SoongExecutionError(
+ "There was an error during the Soong build process. Please "
+ "correct the error with Soong, and try running this script "
+ "again. Soong output follows:\n\n"
+ f"{cpe.stdout.decode('utf-8')}") from cpe
+
+ logging.info("Soong command completed successfully.")
+
+
+class Resource(abc.ABC):
+ """
+ Represents a resource file that is used for scaffolding the Bazel env.
+
+ Resource is an abstract class. Inheriting classes must provide the following
+ attributes which are used by the default method implementations below.
+
+ Attributes:
+ stage_path: the path where this resource should be written when staged.
+ workspace_path: the path where this resource should be synced to in the
+ workspace.
+ """
+ stage_path: pathlib.Path
+ workspace_path: pathlib.Path
+
+ def build_targets(self) -> set:
+ return set()
+
+ @abc.abstractmethod
+ def stage(self, _):
+ """Writes a resource to its stage location."""
+ pass
+
+ @abc.abstractmethod
+ def sync(self):
+ """Syncs a resource to its workspace location."""
+
+ # Overwrite any existing file in the workspace when synced.
+ self.workspace_path.unlink(missing_ok=True)
+ self.workspace_path.symlink_to(self.stage_path)
+
+ @abc.abstractmethod
+ def clean(self):
+ """Cleans a resource from its workspace location."""
+ self.workspace_path.unlink(missing_ok=True)
+
+
+class StaticResource(Resource):
+ """Resource representing a static file to be copied.
+
+ Attributes:
+ resource_path: path to the resource on disk.
+ """
+ def __init__(self, stage_path: pathlib.Path, workspace_path: pathlib.Path,
+ resource_path: pathlib.Path):
+ self.stage_path = stage_path
+ self.workspace_path = workspace_path
+ self._resource_path = resource_path
+
+ def stage(self, _):
+ _verify_directory(self.stage_path.parent)
+ shutil.copy(self._resource_path, self.stage_path)
+ _make_executable_if_script(self.stage_path)
+
+ def sync(self):
+ super().sync()
+
+ def clean(self):
+ super().sync()
+
+ def __repr__(self):
+ return (f"StaticResource(stage_path={self.stage_path}, "
+ f"workspace_path={self.workspace_path}, "
+ f"resource_path={self._resource_path})")
+
+
+class TemplateError(Error):
+ """Raised when there is an issue while templating a template file."""
+ pass
+
+
+class TemplateResource(Resource):
+ """Resource that represents a file to be templated.
+
+ When staged, the resource is templated using the "mapping" provided
+ to the stage function.
+
+ Attributes:
+ resource_path: path to the resource on disk.
+ """
+ resource_path: pathlib.Path
+
+ # Key within templates that identifies a Soong target to build for a
+ # provided template.
+ SOONG_TARGET_KEY = "SOONG_TARGET"
+ _KEY_VALUE_SEP = ":"
+
+ # For a provided template, lines matching this regex are ignored when
+ # loading the template.
+ #
+ # This enables to contain metadata that, while visible to the script, is
+ # not visible in the generated templates. This is currently used with the
+ # SOONG_TARGET key/value pairs in templates.
+ _IGNORE_LINE_REGEX = re.compile(f"({SOONG_TARGET_KEY})")
+
+ def __init__(self, stage_path: pathlib.Path, workspace_path: pathlib.Path,
+ resource_path: pathlib.Path):
+ self.stage_path = stage_path
+ self.workspace_path = workspace_path
+ self.resource_path = resource_path
+
+ def __repr__(self):
+ return (f"TemplateResource(stage_path={self.stage_path}, "
+ f"workspace_path={self.workspace_path}, "
+ f"resource_path={self.resource_path})")
+
+ def stage(self, mapping: Dict[str, str]):
+ _verify_directory(self.stage_path.parent)
+ lines = self.resource_path.open().readlines()
+ lines = [
+ line for line in lines if not self._IGNORE_LINE_REGEX.search(line)
+ ]
+
+ try:
+ output = "".join(lines).format_map(mapping)
+ except KeyError as ke:
+ raise TemplateError(
+ f"Malformed template file: {self.resource_path}") from ke
+
+ with self.stage_path.open("w") as output_file:
+ output_file.write(output)
+ _make_executable_if_script(self.stage_path)
+
+ def sync(self):
+ super().sync()
+
+ def clean(self):
+ super().clean()
+
+ @classmethod
+ def read_value_from_template_var(cls, line: str) -> str:
+ value = line.split(cls._KEY_VALUE_SEP)[-1]
+ return value.strip()
+
+
+class BuildTemplateResource(Resource):
+ """Resource that represents a BUILD file.
+
+ This operates in the same way as a TemplateResource, however also
+ sets up the prebuilts directory needed by BUILD files.
+
+ Attributes:
+ _template_resource: underlying TemplateResource for this resource.
+ global_prebuilts_dir: the directory in the filesystem where prebuilts
+ live, this directory is symlinked to by a directory adjacent
+ to the BUILD file represented by this Resource.
+ prebuilts_stage_path: the path of the staged prebuilts directory
+ which is a symlink to the global_prebuilts_dir.
+ prebuilts_workspace_path: the path of the workspace prebuilts directory,
+ which is a symlink to the global_prebuilts_dir.
+ """
+ _template_resource: TemplateResource
+ global_prebuilts_dir: pathlib.Path
+ prebuilts_stage_path: pathlib.Path
+ prebuilts_workspace_path: pathlib.Path
+
+ def __init__(self, stage_path: pathlib.Path, workspace_path: pathlib.Path,
+ resource_path: pathlib.Path,
+ global_prebuilts_dir: pathlib.Path, prebuilts_dir_name: str):
+ self.stage_path = stage_path
+ self.workspace_path = workspace_path
+
+ self._template_resource = TemplateResource(stage_path, workspace_path,
+ resource_path)
+ self.global_prebuilts_dir = global_prebuilts_dir
+ self.prebuilts_stage_path = self.stage_path.parent.joinpath(
+ prebuilts_dir_name)
+ self.prebuilts_workspace_path = self.workspace_path.parent.joinpath(
+ prebuilts_dir_name)
+
+ def __repr__(self):
+ return ("BuildTemplateResource("
+ f"template_resource={self._template_resource}, "
+ f"global_prebuilts_dir={self.global_prebuilts_dir}, "
+ f"prebuilts_stage_path={self.prebuilts_stage_path}, "
+ f"prebuilts_workspace_path={self.prebuilts_workspace_path})")
+
+ def build_targets(self) -> set:
+ """Overrides build_targets() to read targets from the BUILD template."""
+ targets = set()
+ with self._template_resource.resource_path.open() as build_file:
+ while line := build_file.readline():
+ if self._template_resource.SOONG_TARGET_KEY in line:
+ targets.add(
+ self._template_resource.read_value_from_template_var(
+ line))
+
+ return targets
+
+ def stage(self, mapping: Dict[str, str]):
+ """Overrides stage() to stage a BUILD resource.
+
+ Delegates most actions to the _template_resource, while ensuring that
+ a generated prebuilts directory is also staged.
+ """
+ self._template_resource.stage(mapping)
+ self.prebuilts_stage_path.symlink_to(self.global_prebuilts_dir,
+ target_is_directory=True)
+
+ def sync(self):
+ """Overrides sync() to sync a BUILD resource with prebuilts.
+
+ Delegates to the _template_resource while also ensuring that the
+ generated prebuilts directory is written to the workspace.
+ """
+ self._template_resource.sync()
+
+ # Overwrite the existing prebuilts directory, if it exists.
+ self.prebuilts_workspace_path.unlink(missing_ok=True)
+ self.prebuilts_workspace_path.symlink_to(self.global_prebuilts_dir,
+ target_is_directory=True)
+
+ def clean(self):
+ """Overrides clean() to clean a BUILD resource.
+
+ Delegates most actions to the _template_resource, while also ensuring
+ that the generated prebuilts directory is removed from the workspace.
+ """
+ self._template_resource.clean()
+ self.prebuilts_workspace_path.unlink(missing_ok=True)
+
+
+def _make_executable_if_script(path: pathlib.Path) -> None:
+ """Makes the provided path executable if it is a script.
+ Args:
+ path: the path to check and conditionally make executable.
+ """
+ if path.name.endswith(".sh"):
+ # Grant full permissions (read/write/execute) for current user and
+ # read/write permissions for group.
+ path.chmod(mode=0o750)
+
+
+class ResourcesNotFoundError(Error):
+ """Raised when the required resources are not found."""
+ pass
+
+
+class Resources:
+ """Manages and loads resources from disk.
+
+ Attributes:
+ workspace_base_path: the path to the root of the workspace where
+ staged files will be synced to.
+ gendir_base_path: the path to the root of the staging directory.
+ global_prebuilts_dir_path: the path to the root of the global
+ prebuilts directory to which all generated prebuilts directories will
+ symlink to.
+ prebuilts_dir_name: the name to use for generated prebuilts
+ directories.
+ static_path: the path to the static resources directory.
+ template_path: the path to the template resources directory.
+ """
+ workspace_base_path: pathlib.Path
+ gendir_base_path: pathlib.Path
+ global_prebuilts_dir_path: pathlib.Path
+ prebuilts_dir_name: str
+ static_path: pathlib.Path
+ template_path: pathlib.Path
+
+ # Name of the directory where script runfiles are located.
+ _DATA_DIRNAME = "data"
+
+ # Name of the directory where the templates should be located.
+ _TEMPLATES_DIRNAME = "templates"
+
+ # Name of the directory where static files (to be copied to the environment)
+ # should be located.
+ _STATIC_DIRNAME = "static"
+
+ # If the script is executed from the root of the runfiles directory, this is
+ # where the data should be.
+ _RESOURCES_RUNFILES_BASEPATH = pathlib.Path(
+ "build/pesto/experiments/prepare_bazel_test_env", _DATA_DIRNAME)
+
+ # File extension for templates, determining whether or not a given file is a
+ # template.
+ _TEMPLATE_FILE_EXT = ".template"
+
+ def __init__(self,
+ workspace_base_path: pathlib.Path,
+ gendir_base_path: pathlib.Path,
+ global_prebuilts_dir_path: pathlib.Path,
+ prebuilts_dir_name: str,
+ path: pathlib.Path = _RESOURCES_RUNFILES_BASEPATH):
+ logging.debug("Resources(path=%s)", path)
+
+ self.workspace_base_path = workspace_base_path
+ self.gendir_base_path = gendir_base_path
+ self.global_prebuilts_dir_path = global_prebuilts_dir_path
+ self.prebuilts_dir_name = prebuilts_dir_name
+
+ path = path.resolve()
+
+ self.template_path = path.joinpath(Resources._TEMPLATES_DIRNAME)
+ self.static_path = path.joinpath(Resources._STATIC_DIRNAME)
+
+ if not self.template_path.exists() or not self.static_path.exists():
+ raise ResourcesNotFoundError("Unable to find resources at path "
+ f"{path}, expected the following "
+ "directories: "
+ f"{Resources._TEMPLATES_DIRNAME}, "
+ f"{Resources._STATIC_DIRNAME}")
+
+ def __repr__(self):
+ return ("Resources("
+ f"template_path={self.template_path}"
+ f"static_path={self.static_path})")
+
+ def stage(self, mapping: Dict[str, str]) -> None:
+ for resource in self.load():
+ logging.debug("Staging resource: %s", resource)
+ resource.stage(mapping)
+
+ def sync(self) -> None:
+ for resource in self.load():
+ logging.debug("Syncing resource: %s", resource)
+ resource.sync()
+
+ def clean(self) -> None:
+ for resource in self.load():
+ logging.debug("Cleaning resource: %s", resource)
+ resource.clean()
+
+ def build_targets(self) -> Set[str]:
+ return {
+ t
+ for resource in self.load() for t in resource.build_targets()
+ }
+
+ def load(self) -> List[Resource]:
+ """Loads the Resources used to scaffold the Bazel env.
+
+ Returns:
+ a list of Resource objects representing the files
+ that should be used to template the environment.
+ """
+ resources = []
+
+ # Add all templates.
+ for p in self._template_resource_paths():
+ template_relpath = Resources._strip_template_identifier_from_path(
+ p.relative_to(self.template_path))
+ stage_path = self.gendir_base_path.joinpath(template_relpath)
+ workspace_path = self.workspace_base_path.joinpath(
+ template_relpath)
+
+ if BUILD_FILENAME_REGEX.match(template_relpath.name):
+ resources.append(
+ BuildTemplateResource(
+ stage_path=stage_path,
+ workspace_path=workspace_path,
+ resource_path=p,
+ global_prebuilts_dir=self.global_prebuilts_dir_path,
+ prebuilts_dir_name=self.prebuilts_dir_name))
+ else:
+ resources.append(
+ TemplateResource(stage_path=stage_path,
+ workspace_path=workspace_path,
+ resource_path=p))
+
+ # Add all static files.
+ for p in self._static_resource_paths():
+ static_relpath = p.relative_to(self.static_path)
+ stage_path = self.gendir_base_path.joinpath(static_relpath)
+ workspace_path = self.workspace_base_path.joinpath(static_relpath)
+ resources.append(
+ StaticResource(stage_path=stage_path,
+ workspace_path=workspace_path,
+ resource_path=p))
+
+ return resources
+
+ def _static_resource_paths(self) -> List[pathlib.Path]:
+ return [p for p in self.static_path.glob("**/*") if p.is_file()]
+
+ def _template_resource_paths(self) -> List[pathlib.Path]:
+ return [p for p in self.template_path.glob("**/*") if p.is_file()]
+
+ @staticmethod
+ def _strip_template_identifier_from_path(p: pathlib.Path):
+ """Strips the template file extension from a provided path."""
+ if p.name.endswith(Resources._TEMPLATE_FILE_EXT):
+ p = p.with_name(p.name[:len(p.name) -
+ len(Resources._TEMPLATE_FILE_EXT)])
+ return p
+
+
+class AndroidBuildEnvironmentError(Error):
+ """Raised when the Android Build Environment is not properly set."""
+ pass
+
+
+class BazelTestEnvGenerator:
+ """Context for the Bazel environment generation.
+
+ This class provides access to locations within the filesystem pertinent to the
+ current execution as members that can be accessed by users of the class.
+
+ Attributes:
+ workspace_top: the top of the codebase, which also serves as the top of
+ the Bazel WORKSPACE.
+ host_out: Host artifact staging directory location.
+ host_testcases: Host testcase staging directory location.
+ product_out: Product/Target artifact staging directory location.
+ target_testcases: Product/Target testcase staging directory location.
+ staging_dir: Staging directory for generated Bazel artifacts.
+ prebuilts_dir_name: Name of the directory that should be used for the
+ directories that prebuilts are placed into when synced into the source
+ tree.
+ global_prebuilts_dir_path: The absolute path to the global prebuilts
+ directory.
+ year: the current calendar year.
+ gen_dir: Subdirectory of the staging dir where the Bazel environment
+ should be generated to.
+ """
+ workspace_top: pathlib.Path
+ host_out: pathlib.Path
+ host_testcases: pathlib.Path
+ product_out: pathlib.Path
+ target_testcases: pathlib.Path
+ staging_dir: pathlib.Path
+ prebuilts_dir_name: str = ".soong_prebuilts"
+ prebuilts_dir_path: pathlib.Path
+ gen_dir: pathlib.Path
+ year: str
+ _resources: Resources
+ _soong: Soong
+
+ # Name of the subdirectory, within the output directory, where the
+ # prebuilts directory is scaffolded.
+ #
+ # This directory then serves as the target of symlinks from across the
+ # source tree that gives any Bazel target access to the Soong staging
+ # directories.
+ GLOBAL_PREBUILTS_DIR_PATH = "prebuilts"
+
+ def __init__(self, env_dict: Dict[str, str] = os.environ):
+ try:
+ self.workspace_top = pathlib.Path(env_dict["ANDROID_BUILD_TOP"])
+ self.host_out = pathlib.Path(env_dict["ANDROID_HOST_OUT"])
+ self.host_testcases = pathlib.Path(
+ env_dict["ANDROID_HOST_OUT_TESTCASES"])
+ self.product_out = pathlib.Path(env_dict["ANDROID_PRODUCT_OUT"])
+ self.target_testcases = pathlib.Path(
+ env_dict["ANDROID_TARGET_OUT_TESTCASES"])
+ except KeyError as e:
+ raise AndroidBuildEnvironmentError(
+ "Missing expected environment variable.") from e
+
+ self.staging_dir = pathlib.Path(self.workspace_top,
+ "out/pesto-environment")
+ self.prebuilts_dir_name = BazelTestEnvGenerator.prebuilts_dir_name
+ self.global_prebuilts_dir_path = self.staging_dir.joinpath(
+ self.GLOBAL_PREBUILTS_DIR_PATH)
+ self.year = str(datetime.date.today().year)
+ self.gen_dir = pathlib.Path(self.staging_dir, "gen")
+
+ self._resources = Resources(self.workspace_top, self.gen_dir,
+ self.global_prebuilts_dir_path,
+ self.prebuilts_dir_name)
+ self._soong = Soong(self.workspace_top)
+
+ def __repr__(self):
+ return "GenerationContext(%s)" % vars(self)
+
+ def generate(self):
+ logging.info("Starting generation of Bazel environment.")
+
+ self._soong.build(self._resources.build_targets())
+
+ logging.debug("Creating fresh staging dir at: %s", self.staging_dir)
+ _verify_directory(self.staging_dir, clean=True)
+
+ logging.debug("Creating fresh gen dir at: %s", self.gen_dir)
+ _verify_directory(self.gen_dir, clean=True)
+
+ logging.debug("Creating global prebuilts directory at: %s",
+ self.global_prebuilts_dir_path)
+ _verify_directory(self.global_prebuilts_dir_path, clean=True)
+
+ # Symlink the build system provided staging directories to the
+ # global prebuilts directory.
+ self.global_prebuilts_dir_path.joinpath("host").symlink_to(
+ self.host_out)
+ self.global_prebuilts_dir_path.joinpath("host_testcases").symlink_to(
+ self.host_testcases)
+ self.global_prebuilts_dir_path.joinpath("product").symlink_to(
+ self.product_out)
+ self.global_prebuilts_dir_path.joinpath("target_testcases").symlink_to(
+ self.target_testcases)
+
+ # Load and process each resource into the gen directory.
+ self._resources.stage(mapping=vars(self))
+
+ logging.info(
+ "Generation of Bazel environment to staging directory "
+ "(%s) completed successfully.", self.staging_dir)
+
+ def sync(self):
+ logging.info(
+ "Starting synchronization of generated environment to source tree."
+ )
+
+ if not self.staging_dir.exists():
+ raise FileNotFoundError("Staging directory does not exist, "
+ "the generate function should be called "
+ " to create this directory.")
+
+ self._resources.sync()
+
+ logging.info(
+ "Successfully synchronized generated Bazel environment from "
+ "%s to %s", self.gen_dir, self.workspace_top)
+
+ def clean(self):
+ logging.info("Starting clean of generated environment.")
+
+ logging.info("Cleaning up synchronized files from the source tree.")
+ # For all of our configured templates, attempt to find the corresponding
+ # location in the source tree and remove them.
+ self._resources.clean()
+
+ logging.info("Cleaning up staging directory: %s", self.staging_dir)
+ try:
+ shutil.rmtree(self.staging_dir)
+ except FileNotFoundError:
+ logging.debug("Staging directory not found during cleanup "
+ "and may have already been removed.")
+ logging.info("Successfully cleaned up generated environment.")
+
+
+def _verify_directory(directory: pathlib.Path, clean: bool = False) -> None:
+ """Verifies that the provided directory exists, creating it if it does not.
+
+ Args:
+ directory: path to the directory to create.
+ clean: whether or not the existing directory should be removed if found or
+ reused.
+ """
+ if directory.exists() and clean:
+ logging.debug("Cleaning existing directory %s", directory)
+ shutil.rmtree(directory)
+
+ logging.debug("Verifying directory exists at %s", directory)
+ directory.mkdir(parents=True, exist_ok=True)
diff --git a/experiments/prepare_bazel_test_env/prepare_bazel_test_env.py b/experiments/prepare_bazel_test_env/prepare_bazel_test_env.py
new file mode 100644
index 0000000..b086b00
--- /dev/null
+++ b/experiments/prepare_bazel_test_env/prepare_bazel_test_env.py
@@ -0,0 +1,91 @@
+# 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.
+"""Binary that generates a simulated Bazel environment in the Android source.
+
+The script utilizes an internal repository of templates to determine the targets
+to build, builds them via Soong, then imports them into a Bazel environment
+all relying on templated BUILD files. These files can then be placed
+directly within the Android source tree to simulate what a real Bazel
+environment would look like.
+"""
+import argparse
+import logging
+
+import bazelenv
+
+_LOG_PRINT_FORMAT = ("%(asctime)s %(filename)s:%(lineno)s:%(levelname)s: "
+ "%(message)s")
+_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+
+def _configure_logging(verbose: bool) -> None:
+ """Configures logging for the application.
+
+ Args:
+ verbose: if True, all messages are logged, otherwise only INFO and above
+ are logged.
+ """
+ logging.basicConfig(format=_LOG_PRINT_FORMAT, datefmt=_LOG_DATE_FORMAT)
+ level = logging.DEBUG if verbose else logging.INFO
+ logging.root.setLevel(level)
+
+
+def _create_arg_parser():
+ parser = argparse.ArgumentParser(description=(
+ "Prepares a simulated Bazel environment that can be used to "
+ "execute tests in a Bazel environment based on Soong "
+ "produced artifacts."))
+
+ parser.add_argument("-v",
+ "--verbose",
+ help="Enables verbose logging.",
+ action="store_true")
+
+ subparsers = parser.add_subparsers(dest="action", required=True)
+
+ # For each subparser, provide a default 'func' argument that calls the
+ # corresponding method on the generator instance.
+ subparsers.add_parser(
+ "generate",
+ help="Generates the Bazel environment to the staging directory."
+ ).set_defaults(func=lambda g: g.generate())
+
+ subparsers.add_parser(
+ "sync",
+ help="Synchronizes the staged Bazel environment to the source tree."
+ ).set_defaults(func=lambda g: g.sync())
+
+ subparsers.add_parser(
+ "clean",
+ help=
+ ("Cleans up the Bazel environment by clearing anything that has been "
+ "synced to the source tree as well as the staging directory itself."
+ )).set_defaults(func=lambda g: g.clean())
+
+ return parser
+
+
+if __name__ == "__main__":
+ args = _create_arg_parser().parse_args()
+ logging.debug("prepare_bazel_test_env(%s)", args)
+
+ _configure_logging(args.verbose)
+
+ try:
+ generator = bazelenv.BazelTestEnvGenerator()
+ args.func(generator)
+ except bazelenv.Error:
+ logging.exception(
+ "A known error occurred, check the error description "
+ "or logs for more details.")