blob: 6ff6e2cfe130f237b7dd2cd0a10f6c32c3630326 [file] [log] [blame]
#
# Copyright (C) 2023 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.
#
"""Updater for rr prebuilts."""
import contextlib
import logging
import subprocess
import textwrap
from collections.abc import AsyncIterator
from pathlib import Path
from tempfile import TemporaryDirectory
import fetchartifact
from aiohttp import ClientSession
from .buildapi import BUILD_API_URL
from .repo import Repo
THIS_DIR = Path(__file__).parent
TOP = THIS_DIR.parent.parent.parent
INSTALL_PATH = THIS_DIR / "rr/android/x86_64"
BRANCH = "aosp-rr-dev"
BUILD_TARGET = "linux"
ARTIFACT_NAME = "rr-5.6.0-Android-x86_64.tar.gz"
class Updater:
"""Tool for updating the rr prebuilts stored in this directory."""
def __init__(
self,
build_id: str,
use_current_branch: bool = False,
should_upload: bool = True,
build_api_url: str = BUILD_API_URL,
install_path: Path = INSTALL_PATH,
repo_root: Path = TOP,
) -> None:
self.build_id = build_id
self._use_current_branch = use_current_branch
self._should_upload = should_upload
self.repo = Repo(repo_root)
self.build_api_url = build_api_url
self.install_path = install_path
async def run(self) -> None:
"""Performs the update.
The updater will:
1. Download the new prebuilt
2. Create a repo branch
3. Delete the old prebuilt
4. Install the new prebuilt to the repo
5. Create a commit for the update
6. Upload the CL
"""
async with self.extracted_artifact() as extracted_path:
if not self._use_current_branch:
self.repo.start_branch(f"update-rr-prebuilt-{self.build_id}")
self.replace_current_prebuilt_with(extracted_path)
self.commit()
self.upload()
def replace_current_prebuilt_with(self, extracted_path: Path) -> None:
"""Replaces the currently installed prebuilts with those from the given path.
The removal of old prebuilts and installation of new prebuilts will be tracked
by git (i.e., `git rm` and `git add` will be called).
"""
if self.install_path.exists():
logging.info("Removing old prebuilts from %s", self.install_path)
subprocess.run(["git", "rm", "-rf", self.install_path], check=True)
self.install_path.parent.mkdir(exist_ok=True, parents=True)
logging.info("Installing prebuilts to %s", self.install_path)
extracted_path.rename(self.install_path)
logging.info("Adding %s to git index", self.install_path)
subprocess.run(["git", "add", self.install_path], check=True)
@contextlib.asynccontextmanager
async def extracted_artifact(self) -> AsyncIterator[Path]:
"""Context manager for the extracted artifact tarball.
On entry, the artifact will be downloaded and extracted to a temporary
directory. On exit, the temporary directory will be deleted.
The archive is assumed to be have common tarball layout. That is, the top level
of the tarball is expected to be a single directory which contains the contents.
The yielded path will be the path to that single top level directory to avoid
the caller needing to care about the name of the top level directory.
Yields:
The path to the contents of the archive.
"""
with TemporaryDirectory() as temp_dir:
async with self._downloaded_artifact() as path:
extract_path = Path(temp_dir)
logging.info("Extracting %s to %s", path, extract_path)
# This is using check_call rather than run as a hack to avoid having
# this mocked out in tests that just want to mock git.
subprocess.check_call(
["tar", "xf", path, "--strip-components=1", "-C", extract_path]
)
yield extract_path
@contextlib.asynccontextmanager
async def _downloaded_artifact(self) -> AsyncIterator[Path]:
"""Downloads the artifact to a temp directory and returns its path."""
async with ClientSession() as session:
with TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "artifact.tar.gz"
logging.info(
"Downloading %s from target %s of build %s to %s",
ARTIFACT_NAME,
BUILD_TARGET,
self.build_id,
path,
)
with path.open("wb") as output:
async for chunk in fetchartifact.fetch_artifact_chunked(
BUILD_TARGET,
self.build_id,
ARTIFACT_NAME,
session,
query_url_base=self.build_api_url,
):
output.write(chunk)
yield path
def commit(self) -> None:
"""Commits the changes to the git repo."""
logging.info("Committing changes")
message = textwrap.dedent(
f"""\
Update rr prebuilts to build {self.build_id}.
Bug: None
Test: treehugger
"""
)
subprocess.run(["git", "commit", "-m", message], check=True)
def upload(self) -> None:
"""Uploads the changes to gerrit.
If should_upload=False was passed to the constructor, the upload will be
skipped.
"""
if not self._should_upload:
logging.info("Skipping upload because --no-upload was passed")
return
logging.info("Uploading CL")
self.repo.upload()