| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2024 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. |
| |
| """Configures the project layout to build DDK modules.""" |
| |
| import argparse |
| import dataclasses |
| import json |
| import logging |
| import pathlib |
| import shutil |
| import sys |
| import tempfile |
| import textwrap |
| |
| _TOOLS_BAZEL = "tools/bazel" |
| _DEVICE_BAZELRC = "device.bazelrc" |
| _FILE_MARKER_BEGIN = "### GENERATED SECTION - DO NOT MODIFY - BEGIN ###\n" |
| _FILE_MARKER_END = "### GENERATED SECTION - DO NOT MODIFY - END ###\n" |
| _MODULE_BAZEL_FILE = "MODULE.bazel" |
| |
| _KLEAF_DEPENDENCY_TEMPLATE = """\ |
| \"""Kleaf: Build Android kernels with Bazel.\""" |
| bazel_dep(name = "kleaf") |
| local_path_override( |
| module_name = "kleaf", |
| path = "{kleaf_repo}", |
| ) |
| """ |
| |
| _LOCAL_PREBUILTS_CONTENT_TEMPLATE = """\ |
| kernel_prebuilt_ext = use_extension( |
| "@kleaf//build/kernel/kleaf:kernel_prebuilt_ext.bzl", |
| "kernel_prebuilt_ext", |
| ) |
| kernel_prebuilt_ext.declare_kernel_prebuilts( |
| name = "gki_prebuilts", |
| download_configs = {download_configs}, |
| local_artifact_path = "{prebuilts_dir_relative}", |
| ) |
| use_repo(kernel_prebuilt_ext, "gki_prebuilts") |
| """ |
| |
| |
| class KleafProjectSetterError(RuntimeError): |
| pass |
| |
| |
| @dataclasses.dataclass(kw_only=True) |
| class KleafProjectSetter: |
| """Configures the project layout to build DDK modules.""" |
| |
| build_id: str | None |
| build_target: str | None |
| ddk_workspace: pathlib.Path | None |
| kleaf_repo: pathlib.Path | None |
| prebuilts_dir: pathlib.Path | None |
| url_fmt: str | None |
| |
| def _symlink_tools_bazel(self): |
| if not self.ddk_workspace or not self.kleaf_repo: |
| return |
| # Calculate the paths. |
| tools_bazel = self.ddk_workspace / _TOOLS_BAZEL |
| kleaf_tools_bazel = self.kleaf_repo / _TOOLS_BAZEL |
| # Prepare the location and clean up if necessary. |
| tools_bazel.parent.mkdir(parents=True, exist_ok=True) |
| tools_bazel.unlink(missing_ok=True) |
| tools_bazel.symlink_to(kleaf_tools_bazel) |
| |
| @staticmethod |
| def _update_file(path: pathlib.Path | str, update: str): |
| """Updates the content of a section between markers in a file.""" |
| add_content: bool = False |
| skip_line: bool = False |
| update_written: bool = False |
| if path.exists(): |
| open_mode = "r" |
| logging.info("Updating file %s.", path) |
| else: |
| open_mode = "a+" |
| logging.info("Creating file %s.", path) |
| with ( |
| open(path, open_mode, encoding="utf-8") as input_file, |
| tempfile.NamedTemporaryFile(mode="w", delete=False) as output_file, |
| ): |
| for line in input_file: |
| if add_content: |
| output_file.write(_FILE_MARKER_BEGIN) |
| output_file.write(update + "\n") |
| update_written = True |
| add_content = False |
| if _FILE_MARKER_END in line: |
| skip_line = False |
| if _FILE_MARKER_BEGIN in line: |
| skip_line = True |
| add_content = True |
| if not skip_line: |
| output_file.write(line) |
| if not update_written: |
| output_file.write(_FILE_MARKER_BEGIN) |
| output_file.write(update + "\n") |
| output_file.write(_FILE_MARKER_END) |
| shutil.move(output_file.name, path) |
| |
| def _try_rel_workspace(self, path: pathlib.Path): |
| """Tries to convert |path| to be relative to ddk_workspace.""" |
| try: |
| return path.relative_to(self.ddk_workspace) |
| except ValueError: |
| logging.warning( |
| "Path %s is not relative to DDK workspace %s, using absolute" |
| " path.", |
| path, |
| self.ddk_workspace, |
| ) |
| return path |
| |
| def _read_download_configs(self) -> str: |
| download_configs = self.prebuilts_dir / "download_configs.json" |
| with open(download_configs, "r", encoding="utf-8") as config: |
| # Compress the representation by removing empty spaces to save some space. |
| return repr(json.dumps(json.load(config), separators=(",", ":"))) |
| |
| def _generate_module_bazel(self): |
| """Configures the dependencies for the DDK workspace.""" |
| if not self.ddk_workspace: |
| return |
| module_bazel = self.ddk_workspace / _MODULE_BAZEL_FILE |
| module_bazel_content = "" |
| if self.kleaf_repo: |
| module_bazel_content += _KLEAF_DEPENDENCY_TEMPLATE.format( |
| kleaf_repo=self.kleaf_repo, |
| ) |
| if self.prebuilts_dir: |
| module_bazel_content += "\n" |
| module_bazel_content += _LOCAL_PREBUILTS_CONTENT_TEMPLATE.format( |
| # TODO: b/328770706 - Use download_configs_file when available. |
| download_configs=self._read_download_configs(), |
| # The prebuilts directory must be relative to the DDK workspace. |
| prebuilts_dir_relative=self._try_rel_workspace( |
| self.prebuilts_dir |
| ), |
| ) |
| if module_bazel_content: |
| self._update_file(module_bazel, module_bazel_content) |
| else: |
| logging.info("Nothing to update in %s", module_bazel) |
| |
| def _generate_bazelrc(self): |
| if not self.ddk_workspace or not self.kleaf_repo: |
| return |
| bazelrc = self.ddk_workspace / _DEVICE_BAZELRC |
| self._update_file( |
| bazelrc, |
| textwrap.dedent(f"""\ |
| common --config=internet |
| common --registry=file:{self.kleaf_repo}/external/bazelbuild-bazel-central-registry |
| """), |
| ) |
| |
| def _handle_ddk_workspace(self): |
| if not self.ddk_workspace: |
| return |
| self.ddk_workspace.mkdir(parents=True, exist_ok=True) |
| |
| def _handle_kleaf_repo(self): |
| if not self.kleaf_repo: |
| return |
| self.kleaf_repo.mkdir(parents=True, exist_ok=True) |
| # TODO: b/328770706 - According to the needs, syncing git repos logic should go here. |
| |
| def _handle_prebuilts(self): |
| if not self.ddk_workspace or not self.prebuilts_dir: |
| return |
| self.prebuilts_dir.mkdir(parents=True, exist_ok=True) |
| # TODO: b/328770706 - When build_id is given dowloand artifacts here. |
| |
| def _run(self): |
| self._symlink_tools_bazel() |
| self._generate_module_bazel() |
| self._generate_bazelrc() |
| |
| def run(self): |
| self._handle_ddk_workspace() |
| self._handle_kleaf_repo() |
| self._handle_prebuilts() |
| self._run() |
| |
| |
| if __name__ == "__main__": |
| |
| def abs_path(path: str) -> pathlib.Path | None: |
| path = pathlib.Path(path) |
| if not path.is_absolute(): |
| raise ValueError(f"{path} is not an absolute path.") |
| return path |
| |
| parser = argparse.ArgumentParser( |
| description=__doc__, formatter_class=argparse.RawTextHelpFormatter |
| ) |
| parser.add_argument( |
| "--build_id", |
| type=str, |
| help="the build id to download the build for, e.g. 6148204", |
| ) |
| parser.add_argument( |
| "--build_target", |
| type=str, |
| help='the build target to download, e.g. "kernel_aarch64"', |
| default="kernel_aarch64", |
| ) |
| parser.add_argument( |
| "--ddk_workspace", |
| help="Absolute path to DDK workspace root.", |
| type=abs_path, |
| default=None, |
| ) |
| parser.add_argument( |
| "--kleaf_repo", |
| help="Absolute path to Kleaf's repo dir.", |
| type=abs_path, |
| default=None, |
| ) |
| parser.add_argument( |
| "--prebuilts_dir", |
| help=( |
| "Absolute path to local GKI prebuilts. Usually, it is located" |
| " within workspace." |
| ), |
| type=abs_path, |
| default=None, |
| ) |
| parser.add_argument( |
| "--url_fmt", |
| help="URL format endpoint for CI downloads.", |
| default=None, |
| ) |
| args = parser.parse_args() |
| logging.basicConfig(level=logging.INFO, |
| format="%(levelname)s: %(message)s") |
| |
| try: |
| KleafProjectSetter(**vars(args)).run() |
| except KleafProjectSetterError as e: |
| logging.error(e, exc_info=e) |
| sys.exit(1) |