blob: fd49ea2580dc579dba4cabd5d26b83e12c66bce0 [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 subprocess
import dataclasses
from pathlib import Path
import sys
from typing import Any
from .constants import SOONG_UI_BASH, ACONFIG_BIN_PATH, ALL_ACONFIG_DECLARATIONS_PB_PATH, ALL_ACONFIG_DECLARATIONS_TARGET
from .env import BuildContext
from .build import build_targets
from interface.errors import ToolError
class MissingAconfigCacheError(ToolError):
"""Custom exception for when aconfig dependencies are missing."""
pass
def get_build_vars(ctx: BuildContext, *vars: str) -> dict[str, str]:
"""
Retrieves the values of one or more product variables (e.g., TARGET_ARCH, OUT_DIR).
Use this to inspect variables like OUT_DIR, TARGET_ARCH, and TRUNK STABLE build flags
(typically starting with 'RELEASE_'). For aconfig flags, use the 'aconfig' tool.
Returns a dictionary mapping variable names to values.
Args:
vars: List of build variables to retrieve (e.g. 'OUT_DIR', 'RELEASE_VERSION').
"""
if not vars:
return {}
env = ctx.env
android_build_top = env.get('ANDROID_BUILD_TOP')
if not android_build_top:
raise ToolError("ANDROID_BUILD_TOP not found in environment.")
soong_ui_path = Path(android_build_top) / SOONG_UI_BASH
# Use --dumpvars-mode with --vars="VAR1 VAR2 ..."
vars_arg = " ".join(vars)
command = [str(soong_ui_path), "--dumpvars-mode", f"--vars={vars_arg}"]
process = subprocess.run(
command,
env=env,
cwd=android_build_top,
capture_output=True,
text=True,
check=True
)
# Parse output: VAR='value'
result: dict[str, str] = {}
for line in process.stdout.splitlines():
line = line.strip()
if not line:
continue
# Naive parsing for now, assuming simple values (no embedded quotes)
# Output format is strictly: VAR='value'
if "=" in line:
key, value = line.split("=", 1)
# Remove surrounding single quotes
if value.startswith("'") and value.endswith("'"):
value = value[1:-1]
result[key] = value
return result
@dataclasses.dataclass(frozen=True)
class AconfigFlag:
package: str
name: str
namespace: str
description: str
bug: str
state: str # e.g., "ENABLED", "DISABLED"
permission: str # e.g., "READ_WRITE", "READ_ONLY"
is_fixed_read_only: bool
is_exported: bool
container: str
metadata: str
def to_dict(self) -> dict[str, Any]:
return dataclasses.asdict(self)
def get_aconfig_flag(ctx: BuildContext, package: str, flag: str) -> AconfigFlag:
"""
Retrieves the value of an aconfig flag (formatted as <package.name>.<flag_name>).
Use this ONLY for aconfig flags. For trunk stable build flags (starting with 'RELEASE_'),
use the 'build_vars' tool instead.
Note: The state of aconfig flags can change at runtime unless the flag is marked as 'fixed_read_only'.
Returns an AconfigFlag object containing details like state, permission, and description.
Args:
package: The aconfig package name (e.g. 'com.android.systemui').
flag: The aconfig flag name.
"""
env = ctx.env
top = env.get('ANDROID_BUILD_TOP')
if not top:
raise ToolError("ANDROID_BUILD_TOP not found in environment.")
android_build_top = Path(top)
android_soong_host_out = env.get('ANDROID_SOONG_HOST_OUT')
if not android_soong_host_out:
raise ToolError("ANDROID_SOONG_HOST_OUT not found in environment.")
soong_host_out_abs_path = Path(android_build_top, android_soong_host_out)
vars_dict = get_build_vars(ctx, "OUT_DIR")
out_dir_str = vars_dict["OUT_DIR"]
out_dir_abs_path = Path(android_build_top, out_dir_str)
env['OUT_DIR'] = str(out_dir_str)
aconfig_binary_abs_path = soong_host_out_abs_path / ACONFIG_BIN_PATH
cache_db_abs_path = out_dir_abs_path / ALL_ACONFIG_DECLARATIONS_PB_PATH
if not aconfig_binary_abs_path.exists() or not cache_db_abs_path.exists():
print(f"Missing aconfig dependencies, building '{ALL_ACONFIG_DECLARATIONS_TARGET}'...", file=sys.stderr)
build_targets(ctx, targets=[ALL_ACONFIG_DECLARATIONS_TARGET], clean=False)
if not aconfig_binary_abs_path.exists() or not cache_db_abs_path.exists():
raise MissingAconfigCacheError("Failed to build aconfig dependencies.")
# We use a custom multi-character delimiter to avoid collisions with
# descriptions that might contain spaces, pipes, or commas.
delimiter = "^|^"
# The format string requests every available field
fmt_string = (
f"{{package}}{delimiter}"
f"{{name}}{delimiter}"
f"{{namespace}}{delimiter}"
f"{{description}}{delimiter}"
f"{{bug}}{delimiter}"
f"{{state}}{delimiter}"
f"{{permission}}{delimiter}"
f"{{is_fixed_read_only}}{delimiter}"
f"{{is_exported}}{delimiter}"
f"{{container}}{delimiter}"
f"{{metadata}}"
)
full_flag_name = f"{package}.{flag}"
command = [
str(aconfig_binary_abs_path),
"dump-cache",
"--cache",
str(cache_db_abs_path),
f"--filter=fully_qualified_name:{full_flag_name}",
"--format",
fmt_string,
]
output = subprocess.run(
command,
env=env,
cwd=android_build_top,
capture_output=True,
check=True,
text=True,
).stdout.strip()
parts = output.split(delimiter)
if len(parts) != 11:
raise ToolError(
f"Unexpected output from aconfig: got {len(parts)} parts, expected 11. "
f"Raw output: {output}"
)
def parse_bool(val: str) -> bool:
return val.lower() == "true"
return AconfigFlag(
package=parts[0],
name=parts[1],
namespace=parts[2],
description=parts[3],
bug=parts[4],
state=parts[5],
permission=parts[6],
is_fixed_read_only=parse_bool(parts[7]),
is_exported=parse_bool(parts[8]),
container=parts[9],
metadata=parts[10],
)