• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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