blob: a6c91140d7b5c106f972c0caaa2de478b6a4ae9e [file]
# Copyright (C) 2026 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.
import json
import subprocess
from pathlib import Path
from typing import Optional
from interface.errors import ToolError
from .constants import ENVSETUP_PATH
class BuildContext:
def __init__(self, product: str, release: str, variant: str, env_overrides: Optional[dict[str, str]] = None) -> None:
"""Holds build context information."""
self.product = product
self.release = release
self.variant = variant
self.env_overrides = env_overrides or {}
# Validate that restricted keys are not in env_overrides
restricted_keys = {"TARGET_PRODUCT", "TARGET_RELEASE", "TARGET_BUILD_VARIANT"}
if self.env_overrides:
found_restricted = restricted_keys.intersection(self.env_overrides.keys())
if found_restricted:
raise ToolError(
f"The following environment variables are reserved and cannot be passed in env_vars: {', '.join(sorted(found_restricted))}. "
"Please use the top-level arguments (product, release, variant) instead."
)
self._env_snapshot = EnvSnapshot(product, release, variant)
@property
def env(self) -> dict[str, str]:
"""Lazily retrieves the environment."""
base_env = self._env_snapshot.get_env()
if self.env_overrides:
# Return a new dict with overrides applied
return {**base_env, **self.env_overrides}
return base_env
class EnvSnapshot:
"""Handles creating and loading environment caches."""
def __init__(self, product: str, release: str, variant: str) -> None:
self.product = product
self.release = release
self.variant = variant
# The script is in api/, so the sdk root is one level up
sdk_root = Path(__file__).parent.parent
self.cache_dir = sdk_root / ".cache"
self.cache_file = self.cache_dir / f"env_{product}_{release}_{variant}.json"
# The repo root is 6 levels up from the script
self.repo_root = sdk_root.parent.parent.parent.parent.parent
@property
def envsetup_path(self) -> Path:
return self.repo_root / ENVSETUP_PATH
def is_cache_fresh(self) -> bool:
"""Checks if the cache file exists and is newer than envsetup.sh."""
if not self.cache_file.exists():
return False
cache_mtime = self.cache_file.stat().st_mtime
envsetup_mtime = self.envsetup_path.stat().st_mtime
return cache_mtime > envsetup_mtime
def load(self) -> Optional[dict[str, str]]:
"""Loads the environment from the cache file."""
if not self.cache_file.exists():
return None
with open(self.cache_file, "r") as f:
return json.load(f) # type: ignore
def refresh(self) -> dict[str, str]:
"""Refreshes the environment by running the lunch command."""
lunch_target = f"{self.product}-{self.release}-{self.variant}"
# This python script will be executed in the bash subshell
python_script = "import os, json; print(json.dumps(dict(os.environ)))"
command = f"bash -c 'source {self.envsetup_path} >/dev/null && lunch {lunch_target} >/dev/null && python3 -c \"{python_script}\"'"
process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=self.repo_root,
encoding='utf-8'
)
stdout, stderr = process.communicate()
if process.returncode != 0:
raise ToolError(f"Failed to refresh environment for {lunch_target}:\n{stderr}")
env: dict[str, str] = json.loads(stdout)
self.save(env)
return env
def save(self, env: dict[str, str]) -> None:
"""Saves the environment to the cache file."""
self.cache_dir.mkdir(parents=True, exist_ok=True)
with open(self.cache_file, "w") as f:
json.dump(env, f)
def get_env(self, force_refresh: bool = False) -> dict[str, str]:
"""
Gets the build environment for a given EnvSnapshot.
Args:
force_refresh: If True, forces a refresh of the environment cache.
Returns:
A dictionary representing the environment.
"""
if not force_refresh and self.is_cache_fresh():
env = self.load()
if env:
return env
return self.refresh()