1#!/usr/bin/env python3
2#
3# Copyright (C) 2019 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
17"""This module implements an Android.mk rewriter which fixes errors that are
18caught by prebuilt ELF checker."""
19
20from __future__ import print_function
21
22import os.path
23import re
24import sys
25
26from .android import find_android_build_top
27from .readobj import readobj
28
29
30def _report_error(line, fmt, *args):
31    """This function prints an error message."""
32    fmt = '{}: error: ' + fmt
33    print(fmt.format(line, *args), file=sys.stderr)
34
35
36class Variable(object):
37    """This class represents the value and locations of a variable."""
38
39    def __init__(self, value, locs):
40        self.value = value
41        self.locs = list(locs)
42
43
44    def __repr__(self):
45        return repr(self.value)
46
47
48    def append(self, value, locs):
49        """Append a value to this variable."""
50        self.value += value
51        self.locs.extend(locs)
52
53
54class StashedLines(object):
55    """This class stashes lines and rewrites them before flushing them."""
56
57    _KEEP = 1
58    _DELETE = 2
59
60
61    def __init__(self):
62        self._lines = []
63
64
65    def __len__(self):
66        return len(self._lines)
67
68
69    def append(self, line):
70        """This function appends a line to stashed lines."""
71        self._lines.append((self._KEEP, line))
72
73
74    def extend(self, lines):
75        """This function appends multiple lines to stashed lines."""
76        for line in lines:
77            self.append(line)
78
79
80    def replace(self, locs, line):
81        """This function replaces `locs[0]` with `line` and marks the rest of
82        line numbers in `locs` as deleted."""
83        locs = iter(locs)
84        self._lines[next(locs)] = (self._KEEP, line)
85        for loc in locs:
86            self._lines[loc] = (self._DELETE, None)
87
88
89    def flush(self, out_file):
90        """This function prints the stashed lines to `out_file` and resets the
91        stashed lines."""
92        for action, line in self._lines:
93            if action == self._KEEP:
94                print(line, file=out_file)
95        self._lines = []
96
97
98
99class Rewriter(object):  # pylint: disable=too-few-public-methods
100    """This class rewrites the input Android.mk file and adds missing
101    LOCAL_SHARED_LIBRARIES, LOCAL_MULTILIB, or LOCAL_CHECK_ELF_FILES."""
102
103
104    _INCLUDE = re.compile('\\s*include\\s+\\$\\(([A-Za-z0-9_]*)\\)')
105    _VAR = re.compile('([A-Za-z_][A-Za-z0-9_-]*)\\s*([:+]?=)\\s*(.*)$')
106
107
108    def __init__(self, mk_path, variables=None, android_build_top=None):
109        self._mk_path = mk_path
110        self._mk_dirname = os.path.dirname(mk_path)
111
112        self._variables = {}
113        if variables:
114            for key, value in variables.items():
115                self._add_var(key, value)
116
117        if android_build_top is None:
118            self._android_build_top = find_android_build_top(mk_path)
119        else:
120            self._android_build_top = android_build_top
121
122
123    def _read_prebuilt_file_path(self):
124        file_var = self._variables.get('LOCAL_SRC_FILES')
125        if file_var is not None:
126            return os.path.join(self._mk_dirname, file_var.value)
127
128        file_var = self._variables.get('LOCAL_PREBUILT_MODULE_FILE')
129        if file_var is not None:
130            return os.path.join(self._android_build_top, file_var.value)
131
132        return None
133
134
135    @staticmethod
136    def _get_module_name_for_dt_needed(dt_needed):
137        """Convert a DT_NEEDED name to the build system module name."""
138        return re.sub('\\.so$', '', dt_needed)
139
140
141    def _get_module_names_for_dt_needed_entries(self, dt_needed_entries):
142        """Convert DT_NEEDED names into build system module names."""
143        return set(self._get_module_name_for_dt_needed(dt_needed)
144                   for dt_needed in dt_needed_entries)
145
146
147    def _rewrite_build_prebuilt(self, stashed_lines, line_no):
148        check_elf_files_var = self._variables.get('LOCAL_CHECK_ELF_FILES')
149        if check_elf_files_var is not None and \
150                check_elf_files_var.value == 'false':
151            return
152
153        # Read the prebuilt ELF file
154        prebuilt_file = self._read_prebuilt_file_path()
155        if prebuilt_file is None:
156            _report_error(line_no,
157                          'LOCAL_SRC_FILES and LOCAL_PREBUILT_MODULE_FILE are '
158                          'not defined')
159            return
160
161        if not os.path.exists(prebuilt_file):
162            _report_error(line_no, 'Prebuilt file does not exist: "{}"',
163                          prebuilt_file)
164
165        is_32bit, dt_soname, dt_needed = readobj(prebuilt_file)
166
167        # Check whether LOCAL_MULTILIB is missing for 32-bit executables
168        multilib_var = self._variables.get('LOCAL_MULTILIB')
169        if not multilib_var and is_32bit:
170            stashed_lines.append('LOCAL_MULTILIB := 32')
171
172        # Check whether DT_SONAME matches with the file name
173        filename = os.path.basename(prebuilt_file)
174        if dt_soname and dt_soname != filename:
175            stashed_lines.extend([
176                '# Bypass prebuilt ELF check due to mismatched DT_SONAME',
177                'LOCAL_CHECK_ELF_FILES := false',])
178            return
179
180        # Check LOCAL_SHARED_LIBRARIES
181        shared_libs = self._get_module_names_for_dt_needed_entries(dt_needed)
182        shared_libs_var = self._variables.get('LOCAL_SHARED_LIBRARIES')
183
184        if not shared_libs_var:
185            if shared_libs:
186                stashed_lines.append('LOCAL_SHARED_LIBRARIES := ' +
187                                     ' '.join(sorted(shared_libs)))
188            return
189
190        shared_libs_specified = set(re.split('[ \t\n]', shared_libs_var.value))
191        if shared_libs != shared_libs_specified:
192            # Replace LOCAL_SHARED_LIBRARIES
193            stashed_lines.replace(shared_libs_var.locs,
194                                  'LOCAL_SHARED_LIBRARIES := ' +
195                                  ' '.join(sorted(shared_libs)))
196
197
198    def _add_var(self, key, value, locs=tuple(), is_append=False):
199        value = self._expand_vars(value)
200
201        if is_append and key in self._variables:
202            self._variables[key].append(value, locs)
203        else:
204            self._variables[key] = Variable(value, locs)
205
206
207    def _expand_vars(self, string):
208        def _lookup_variable(match):
209            key = match.group(1)
210            try:
211                return self._variables[key].value
212            except KeyError:
213                # If we cannot find the variable, leave it as-is.
214                return match.group(0)
215
216        old_string = None
217        while old_string != string:
218            old_string = string
219            string = re.sub('\\$\\(([A-Za-z][A-Za-z0-9_-]*)\\)',
220                            _lookup_variable, old_string)
221        return string
222
223
224    def _clear_vars(self):
225        self._variables = {key: value
226                           for key, value in self._variables.items()
227                           if not key.startswith('LOCAL_')}
228
229
230    def _rewrite_lines(self, lines, out_file):
231        stashed_lines = StashedLines()
232
233        line_iter = enumerate(lines)
234        for line_no, line in line_iter:
235            match = self._INCLUDE.match(line)
236            if match:
237                command = match.group(1)
238                if command == 'CLEAR_VARS':
239                    self._clear_vars()
240                elif command == 'BUILD_PREBUILT':
241                    self._rewrite_build_prebuilt(stashed_lines, line_no)
242                stashed_lines.append(line)
243                stashed_lines.flush(out_file)
244                continue
245
246            match = self._VAR.match(line)
247            if match:
248                start = len(stashed_lines)
249                stashed_lines.append(line)
250
251                key = match.group(1).strip()
252                assign_op = match.group(2).strip()
253                value = match.group(3).strip()
254
255                while value.endswith('\\'):
256                    line_no, line = next(line_iter, (-1, None))
257                    if line is None:
258                        value = value[:-1]
259                        break
260                    stashed_lines.append(line)
261                    value = value[:-1] + line.strip()
262                end = len(stashed_lines)
263                locs = range(start, end)
264
265                self._add_var(key, value, locs, assign_op == '+=')
266                continue
267
268            stashed_lines.append(line)
269
270        stashed_lines.flush(out_file)
271
272
273    def rewrite(self, out_file=sys.stdout):
274        """This function reads the content of `self._mk_path`, rewrites build
275        rules, and prints the rewritten build rules to `out_file`."""
276
277        with open(self._mk_path, 'r') as input_file:
278            lines = input_file.read().splitlines()
279
280        self._rewrite_lines(lines, out_file)
281