1#!/usr/bin/python3 2# 3# Copyright (C) 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""A module for generating Android.bp/BUILD files for stub libraries""" 17 18from enum import Enum 19import json 20import os 21import textwrap 22from typing import List 23from lunch import walk_paths 24 25TAB = " " # 4 spaces 26 27 28class ConfigAxis(Enum): 29 """A helper class to manage the configuration axes of stub libraries.""" 30 NoConfig = "" 31 ArmConfig = "arm" 32 Arm64Config = "arm64" 33 X86Config = "x86" 34 X86_64Config = "x86_64" 35 36 def is_arch_axis(self): 37 return self != ConfigAxis.NoConfig 38 39 @classmethod 40 def get_arch_axes(cls) -> list: 41 return [a for a in cls if a.is_arch_axis()] 42 43 @classmethod 44 def get_axis(cls, value): 45 for axis in cls: 46 if axis.value == value: 47 return axis 48 return None 49 50 51class AndroidBpPropertyKey: 52 """Unique key identifying an Android.bp property.""" 53 54 def __init__(self, name: str, axis: str): 55 self.name = name 56 self.axis = axis 57 58 def __hash__(self): 59 return hash((self.name, self.axis)) 60 61 def __eq__(self, other): 62 return (isinstance(other, AndroidBpPropertyKey) 63 and self.name == other.name and self.axis == other.axis) 64 65 66class AndroidBpProperty: 67 """Properties of Android.bp modules.""" 68 69 def __init__(self, name, value, axis): 70 self.name = name 71 self.value = value 72 self.axis = axis 73 74 def extend(self, value): 75 """Extend the existing value of the property. 76 77 Parameters: 78 value: object to add to the value of 79 existing property. 80 81 Returns: 82 None 83 84 `extend` adds the value to the end of `self.value` (list). 85 If `value` is an iterator, it adds each element piecemeal. 86 87 `self.value` is a Python list, and therefore can contain 88 elements of different types. 89 90 e.g. 91 p = AndroidBpProperty("myprop", 1, ConfigAxis.NoConfig) 92 p.extend("a") 93 p.extend(["b", "c"]) 94 p.extend(("d", "2")) 95 96 print(p.value) 97 [1, 'a', 'b', 'c', 'd', '2'] 98 """ 99 100 self.value = self._as_list(self.value) 101 self.value.extend(self._as_list(value)) 102 103 def _as_list(self, value): 104 """Converts an object into a list.""" 105 if not value: 106 return [] 107 if isinstance(value, (int, bool, str)): 108 return [value] 109 if isinstance(value, list): 110 return value 111 if isinstance(value, (set, tuple)): 112 return list(value) 113 raise TypeError(f"bad type {type(value)}") 114 115 116class AndroidBpModule: 117 """Soong module of an Android.bp file.""" 118 119 def __init__(self, name: str, module_type: str): 120 if not name: 121 raise ValueError("name cannot be empty in SoongModule") 122 if not module_type: 123 raise ValueError("module_type cannot be empty in SoongModule") 124 self.name = name 125 self.module_type = module_type 126 self.properties = {} # indexed using AndroidBpPropertyKey 127 128 def add_property(self, prop: str, val: object, axis=ConfigAxis.NoConfig): 129 """Add a property to the Android.bp module. 130 131 Raises: 132 ValueError if (`prop`, `axis`) is already registered. 133 """ 134 key = AndroidBpPropertyKey(name=prop, axis=axis) 135 if key in self.properties: 136 raise ValueError(f"Property: {prop} in axis: {axis} has been" 137 "registered. Use extend_property method instead.") 138 139 p = AndroidBpProperty(prop, val, axis) 140 self.properties[key] = p 141 142 def extend_property(self, 143 prop: str, 144 val: object, 145 axis=ConfigAxis.NoConfig): 146 """Extend the value of a property.""" 147 key = AndroidBpPropertyKey(name=prop, axis=axis) 148 if key in self.properties: 149 self.properties[key].extend(val) 150 else: 151 self.add_property(prop, val, axis) 152 153 def get_properties(self, axis) -> List[AndroidBpProperty]: 154 return [prop for prop in self.properties.values() if prop.axis == axis] 155 156 def string(self, formatter=None) -> None: 157 """Return the string representation of the module using the provided 158 formatter.""" 159 if formatter is None: 160 formatter = module_formatter 161 return formatter(self) 162 163 def __str__(self): 164 return self.string() 165 166 167class AndroidBpComment: 168 """Comment in an Android.bp file.""" 169 170 def __init__(self, comment: str): 171 self.comment = comment 172 173 def _add_comment_token(self, raw: str) -> str: 174 return raw if raw.startswith("//") else "// " + raw 175 176 def split(self) -> List[str]: 177 """Split along \n tokens.""" 178 raw_comments = self.comment.split("\n") 179 return [self._add_comment_token(r) for r in raw_comments] 180 181 def string(self, formatter=None) -> str: 182 """Return the string representation of the comment using the provided 183 formatter.""" 184 if formatter is None: 185 formatter = comment_formatter 186 return formatter(self) 187 188 def __str__(self): 189 return self.string() 190 191 192class AndroidBpFile: 193 """Android.bp file.""" 194 195 def __init__(self, directory: str): 196 self._path = os.path.join(directory, "Android.bp") 197 self.components = [] 198 199 def add_module(self, soong_module: AndroidBpModule): 200 """Add a Soong module to the file.""" 201 self.components.append(soong_module) 202 203 def add_comment(self, comment: AndroidBpComment): 204 """Add a comment to the file.""" 205 self.components.append(comment) 206 207 def add_comment_string(self, comment: str): 208 """Add a comment (str) to the file.""" 209 self.components.append(AndroidBpComment(comment=comment)) 210 211 def add_license(self, lic: str) -> None: 212 raise NotImplementedError("Addition of License in generated Android.bp" 213 "files is not supported") 214 215 def string(self, formatter=None) -> None: 216 """Return the string representation of the file using the provided 217 formatter.""" 218 fragments = [ 219 component.string(formatter) for component in self.components 220 ] 221 return "\n".join(fragments) 222 223 def __str__(self): 224 return self.string() 225 226 def is_stale(self, formatter=None) -> bool: 227 """Return true if the object is newer than the file on disk.""" 228 exists = os.path.exists(self._path) 229 if not exists: 230 return True 231 with open(self._path, encoding='iso-8859-1') as f: 232 # TODO: Avoid wasteful computation using cache 233 return f.read() != self.string(formatter) 234 235 def write(self, formatter=None) -> None: 236 """Write the AndroidBpFile object to disk.""" 237 if not self.is_stale(formatter): 238 return 239 os.makedirs(os.path.dirname(self._path), exist_ok=True) 240 with open(self._path, "w+", encoding='iso-8859-1') as f: 241 f.write(self.string(formatter)) 242 243 def fullpath(self) -> str: 244 return self._path 245 246 247# Default formatters 248def comment_formatter(comment: AndroidBpComment) -> str: 249 return "\n".join(comment.split()) 250 251 252def module_properties_formatter(props: List[AndroidBpProperty], 253 indent=1) -> str: 254 formatted = "" 255 for prop in props: 256 name, val = prop.name, prop.value 257 if isinstance(val, str): 258 val_f = f'"{val}"' 259 # bool is a subclass of int, check it first 260 elif isinstance(val, bool): 261 val_f = "true" if val else "false" 262 elif isinstance(val, int): 263 val_f = val 264 elif isinstance(val, list): 265 # TODO: Align with bpfmt. 266 # This implementation splits non-singular lists into multiple lines. 267 if len(val) < 2: 268 val_f = json.dumps(val) 269 else: 270 nested_indent = indent + 1 271 val_f = json.dumps(val, indent=f"{nested_indent*TAB}") 272 # Add TAB before the closing `]` 273 val_f = val_f[: 274 -1] + indent * TAB + val_f[ 275 -1] 276 else: 277 raise NotImplementedError( 278 f"Formatter for {val} of type: {type(val)}" 279 "has not been implemented") 280 281 formatted += f"""{indent*TAB}{name}: {val_f},\n""" 282 return formatted 283 284 285def module_formatter(module: AndroidBpModule) -> str: 286 formatted = textwrap.dedent(f"""\ 287 {module.module_type} {{ 288 {TAB}name: "{module.name}", 289 """) 290 # Print the no-arch props first. 291 no_arch_props = module.get_properties(ConfigAxis.NoConfig) 292 formatted += module_properties_formatter(no_arch_props) 293 294 # Print the arch props if they exist. 295 contains_arch_props = any( 296 [prop.axis.is_arch_axis() for prop in module.properties]) 297 if contains_arch_props: 298 formatted += f"{TAB}arch: {{\n" 299 arch_axes = ConfigAxis.get_arch_axes() 300 for arch_axis in arch_axes: 301 arch_axis_props = module.get_properties(arch_axis) 302 if not arch_axis_props: 303 continue 304 formatted += f"{2*TAB}{arch_axis.value}: {{\n" 305 formatted += module_properties_formatter(arch_axis_props, indent=3) 306 formatted += f"{2*TAB}}},\n" 307 308 formatted += f"{TAB}}},\n" 309 310 formatted += "}" 311 return formatted 312 313 314class BazelTarget: 315 pass # TODO 316 317 318class BazelBuildFile: 319 pass # TODO 320 321 322class BuildFileGenerator: 323 """Class to maintain state of generated Android.bp/BUILD files.""" 324 325 def __init__(self): 326 self.android_bp_files = [] 327 self.bazel_build_files = [] 328 329 def add_android_bp_file(self, file: AndroidBpFile): 330 self.android_bp_files.append(file) 331 332 def add_bazel_build_file(self, file: BazelBuildFile): 333 raise NotImplementedError("Bazel BUILD file generation" 334 "is not supported currently") 335 336 def write(self): 337 for android_bp_file in self.android_bp_files: 338 android_bp_file.write() 339 340 def clean(self, staging_dir: str): 341 """Delete discarded Android.bp files. 342 343 This is necessary when a library is dropped from an API surface.""" 344 valid_bp_files = set([x.fullpath() for x in self.android_bp_files]) 345 for bp_file_on_disk in walk_paths(staging_dir, 346 lambda x: x.endswith("Android.bp")): 347 if bp_file_on_disk not in valid_bp_files: 348 # This library has been dropped since the last run 349 os.remove(bp_file_on_disk) 350