• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright (C) 2016 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"""Generates source for stub shared libraries for the NDK."""
18import argparse
19import logging
20import os
21import re
22
23
24ALL_ARCHITECTURES = (
25    'arm',
26    'arm64',
27    'mips',
28    'mips64',
29    'x86',
30    'x86_64',
31)
32
33
34# Arbitrary magic number. We use the same one in api-level.h for this purpose.
35FUTURE_API_LEVEL = 10000
36
37
38def logger():
39    """Return the main logger for this module."""
40    return logging.getLogger(__name__)
41
42
43def api_level_arg(api_str):
44    """Parses an API level, handling the "current" special case.
45
46    Args:
47        api_str: (string) Either a numeric API level or "current".
48
49    Returns:
50        (int) FUTURE_API_LEVEL if `api_str` is "current", else `api_str` parsed
51        as an integer.
52    """
53    if api_str == "current":
54        return FUTURE_API_LEVEL
55    return int(api_str)
56
57
58def get_tags(line):
59    """Returns a list of all tags on this line."""
60    _, _, all_tags = line.strip().partition('#')
61    return [e for e in re.split(r'\s+', all_tags) if e.strip()]
62
63
64def get_tag_value(tag):
65    """Returns the value of a key/value tag.
66
67    Raises:
68        ValueError: Tag is not a key/value type tag.
69
70    Returns: Value part of tag as a string.
71    """
72    if '=' not in tag:
73        raise ValueError('Not a key/value tag: ' + tag)
74    return tag.partition('=')[2]
75
76
77def version_is_private(version):
78    """Returns True if the version name should be treated as private."""
79    return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
80
81
82def should_omit_version(name, tags, arch, api, vndk):
83    """Returns True if the version section should be ommitted.
84
85    We want to omit any sections that do not have any symbols we'll have in the
86    stub library. Sections that contain entirely future symbols or only symbols
87    for certain architectures.
88    """
89    if version_is_private(name):
90        return True
91    if 'platform-only' in tags:
92        return True
93    if 'vndk' in tags and not vndk:
94        return True
95    if not symbol_in_arch(tags, arch):
96        return True
97    if not symbol_in_api(tags, arch, api):
98        return True
99    return False
100
101
102def symbol_in_arch(tags, arch):
103    """Returns true if the symbol is present for the given architecture."""
104    has_arch_tags = False
105    for tag in tags:
106        if tag == arch:
107            return True
108        if tag in ALL_ARCHITECTURES:
109            has_arch_tags = True
110
111    # If there were no arch tags, the symbol is available for all
112    # architectures. If there were any arch tags, the symbol is only available
113    # for the tagged architectures.
114    return not has_arch_tags
115
116
117def symbol_in_api(tags, arch, api):
118    """Returns true if the symbol is present for the given API level."""
119    introduced_tag = None
120    arch_specific = False
121    for tag in tags:
122        # If there is an arch-specific tag, it should override the common one.
123        if tag.startswith('introduced=') and not arch_specific:
124            introduced_tag = tag
125        elif tag.startswith('introduced-' + arch + '='):
126            introduced_tag = tag
127            arch_specific = True
128        elif tag == 'future':
129            return api == FUTURE_API_LEVEL
130
131    if introduced_tag is None:
132        # We found no "introduced" tags, so the symbol has always been
133        # available.
134        return True
135
136    return api >= int(get_tag_value(introduced_tag))
137
138
139def symbol_versioned_in_api(tags, api):
140    """Returns true if the symbol should be versioned for the given API.
141
142    This models the `versioned=API` tag. This should be a very uncommonly
143    needed tag, and is really only needed to fix versioning mistakes that are
144    already out in the wild.
145
146    For example, some of libc's __aeabi_* functions were originally placed in
147    the private version, but that was incorrect. They are now in LIBC_N, but
148    when building against any version prior to N we need the symbol to be
149    unversioned (otherwise it won't resolve on M where it is private).
150    """
151    for tag in tags:
152        if tag.startswith('versioned='):
153            return api >= int(get_tag_value(tag))
154    # If there is no "versioned" tag, the tag has been versioned for as long as
155    # it was introduced.
156    return True
157
158
159class ParseError(RuntimeError):
160    """An error that occurred while parsing a symbol file."""
161    pass
162
163
164class Version(object):
165    """A version block of a symbol file."""
166    def __init__(self, name, base, tags, symbols):
167        self.name = name
168        self.base = base
169        self.tags = tags
170        self.symbols = symbols
171
172    def __eq__(self, other):
173        if self.name != other.name:
174            return False
175        if self.base != other.base:
176            return False
177        if self.tags != other.tags:
178            return False
179        if self.symbols != other.symbols:
180            return False
181        return True
182
183
184class Symbol(object):
185    """A symbol definition from a symbol file."""
186    def __init__(self, name, tags):
187        self.name = name
188        self.tags = tags
189
190    def __eq__(self, other):
191        return self.name == other.name and set(self.tags) == set(other.tags)
192
193
194class SymbolFileParser(object):
195    """Parses NDK symbol files."""
196    def __init__(self, input_file):
197        self.input_file = input_file
198        self.current_line = None
199
200    def parse(self):
201        """Parses the symbol file and returns a list of Version objects."""
202        versions = []
203        while self.next_line() != '':
204            if '{' in self.current_line:
205                versions.append(self.parse_version())
206            else:
207                raise ParseError(
208                    'Unexpected contents at top level: ' + self.current_line)
209        return versions
210
211    def parse_version(self):
212        """Parses a single version section and returns a Version object."""
213        name = self.current_line.split('{')[0].strip()
214        tags = get_tags(self.current_line)
215        symbols = []
216        global_scope = True
217        while self.next_line() != '':
218            if '}' in self.current_line:
219                # Line is something like '} BASE; # tags'. Both base and tags
220                # are optional here.
221                base = self.current_line.partition('}')[2]
222                base = base.partition('#')[0].strip()
223                if not base.endswith(';'):
224                    raise ParseError(
225                        'Unterminated version block (expected ;).')
226                base = base.rstrip(';').rstrip()
227                if base == '':
228                    base = None
229                return Version(name, base, tags, symbols)
230            elif ':' in self.current_line:
231                visibility = self.current_line.split(':')[0].strip()
232                if visibility == 'local':
233                    global_scope = False
234                elif visibility == 'global':
235                    global_scope = True
236                else:
237                    raise ParseError('Unknown visiblity label: ' + visibility)
238            elif global_scope:
239                symbols.append(self.parse_symbol())
240            else:
241                # We're in a hidden scope. Ignore everything.
242                pass
243        raise ParseError('Unexpected EOF in version block.')
244
245    def parse_symbol(self):
246        """Parses a single symbol line and returns a Symbol object."""
247        if ';' not in self.current_line:
248            raise ParseError(
249                'Expected ; to terminate symbol: ' + self.current_line)
250        if '*' in self.current_line:
251            raise ParseError(
252                'Wildcard global symbols are not permitted.')
253        # Line is now in the format "<symbol-name>; # tags"
254        name, _, _ = self.current_line.strip().partition(';')
255        tags = get_tags(self.current_line)
256        return Symbol(name, tags)
257
258    def next_line(self):
259        """Returns the next non-empty non-comment line.
260
261        A return value of '' indicates EOF.
262        """
263        line = self.input_file.readline()
264        while line.strip() == '' or line.strip().startswith('#'):
265            line = self.input_file.readline()
266
267            # We want to skip empty lines, but '' indicates EOF.
268            if line == '':
269                break
270        self.current_line = line
271        return self.current_line
272
273
274class Generator(object):
275    """Output generator that writes stub source files and version scripts."""
276    def __init__(self, src_file, version_script, arch, api, vndk):
277        self.src_file = src_file
278        self.version_script = version_script
279        self.arch = arch
280        self.api = api
281        self.vndk = vndk
282
283    def write(self, versions):
284        """Writes all symbol data to the output files."""
285        for version in versions:
286            self.write_version(version)
287
288    def write_version(self, version):
289        """Writes a single version block's data to the output files."""
290        name = version.name
291        tags = version.tags
292        if should_omit_version(name, tags, self.arch, self.api, self.vndk):
293            return
294
295        section_versioned = symbol_versioned_in_api(tags, self.api)
296        version_empty = True
297        pruned_symbols = []
298        for symbol in version.symbols:
299            if not self.vndk and 'vndk' in symbol.tags:
300                continue
301            if not symbol_in_arch(symbol.tags, self.arch):
302                continue
303            if not symbol_in_api(symbol.tags, self.arch, self.api):
304                continue
305
306            if symbol_versioned_in_api(symbol.tags, self.api):
307                version_empty = False
308            pruned_symbols.append(symbol)
309
310        if len(pruned_symbols) > 0:
311            if not version_empty and section_versioned:
312                self.version_script.write(version.name + ' {\n')
313                self.version_script.write('    global:\n')
314            for symbol in pruned_symbols:
315                emit_version = symbol_versioned_in_api(symbol.tags, self.api)
316                if section_versioned and emit_version:
317                    self.version_script.write('        ' + symbol.name + ';\n')
318
319                if 'var' in symbol.tags:
320                    self.src_file.write('int {} = 0;\n'.format(symbol.name))
321                else:
322                    self.src_file.write('void {}() {{}}\n'.format(symbol.name))
323
324            if not version_empty and section_versioned:
325                base = '' if version.base is None else ' ' + version.base
326                self.version_script.write('}' + base + ';\n')
327
328
329def parse_args():
330    """Parses and returns command line arguments."""
331    parser = argparse.ArgumentParser()
332
333    parser.add_argument('-v', '--verbose', action='count', default=0)
334
335    parser.add_argument(
336        '--api', type=api_level_arg, required=True,
337        help='API level being targeted.')
338    parser.add_argument(
339        '--arch', choices=ALL_ARCHITECTURES, required=True,
340        help='Architecture being targeted.')
341    parser.add_argument(
342        '--vndk', action='store_true', help='Use the VNDK variant.')
343
344    parser.add_argument(
345        'symbol_file', type=os.path.realpath, help='Path to symbol file.')
346    parser.add_argument(
347        'stub_src', type=os.path.realpath,
348        help='Path to output stub source file.')
349    parser.add_argument(
350        'version_script', type=os.path.realpath,
351        help='Path to output version script.')
352
353    return parser.parse_args()
354
355
356def main():
357    """Program entry point."""
358    args = parse_args()
359
360    verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
361    verbosity = args.verbose
362    if verbosity > 2:
363        verbosity = 2
364    logging.basicConfig(level=verbose_map[verbosity])
365
366    with open(args.symbol_file) as symbol_file:
367        versions = SymbolFileParser(symbol_file).parse()
368
369    with open(args.stub_src, 'w') as src_file:
370        with open(args.version_script, 'w') as version_file:
371            generator = Generator(src_file, version_file, args.arch, args.api,
372                                  args.vndk)
373            generator.write(versions)
374
375
376if __name__ == '__main__':
377    main()
378