• 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 json
20import logging
21import os
22import re
23import sys
24
25
26ALL_ARCHITECTURES = (
27    'arm',
28    'arm64',
29    'mips',
30    'mips64',
31    'x86',
32    'x86_64',
33)
34
35
36# Arbitrary magic number. We use the same one in api-level.h for this purpose.
37FUTURE_API_LEVEL = 10000
38
39
40def logger():
41    """Return the main logger for this module."""
42    return logging.getLogger(__name__)
43
44
45def get_tags(line):
46    """Returns a list of all tags on this line."""
47    _, _, all_tags = line.strip().partition('#')
48    return [e for e in re.split(r'\s+', all_tags) if e.strip()]
49
50
51def is_api_level_tag(tag):
52    """Returns true if this tag has an API level that may need decoding."""
53    if tag.startswith('introduced='):
54        return True
55    if tag.startswith('introduced-'):
56        return True
57    if tag.startswith('versioned='):
58        return True
59    return False
60
61
62def decode_api_level_tags(tags, api_map):
63    """Decodes API level code names in a list of tags.
64
65    Raises:
66        ParseError: An unknown version name was found in a tag.
67    """
68    for idx, tag in enumerate(tags):
69        if not is_api_level_tag(tag):
70            continue
71        name, value = split_tag(tag)
72
73        try:
74            decoded = str(decode_api_level(value, api_map))
75            tags[idx] = '='.join([name, decoded])
76        except KeyError:
77            raise ParseError('Unknown version name in tag: {}'.format(tag))
78    return tags
79
80
81def split_tag(tag):
82    """Returns a key/value tuple of the tag.
83
84    Raises:
85        ValueError: Tag is not a key/value type tag.
86
87    Returns: Tuple of (key, value) of the tag. Both components are strings.
88    """
89    if '=' not in tag:
90        raise ValueError('Not a key/value tag: ' + tag)
91    key, _, value = tag.partition('=')
92    return key, value
93
94
95def get_tag_value(tag):
96    """Returns the value of a key/value tag.
97
98    Raises:
99        ValueError: Tag is not a key/value type tag.
100
101    Returns: Value part of tag as a string.
102    """
103    return split_tag(tag)[1]
104
105
106def version_is_private(version):
107    """Returns True if the version name should be treated as private."""
108    return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
109
110
111def should_omit_version(version, arch, api, vndk, apex):
112    """Returns True if the version section should be ommitted.
113
114    We want to omit any sections that do not have any symbols we'll have in the
115    stub library. Sections that contain entirely future symbols or only symbols
116    for certain architectures.
117    """
118    if version_is_private(version.name):
119        return True
120    if 'platform-only' in version.tags:
121        return True
122
123    no_vndk_no_apex = 'vndk' not in version.tags and 'apex' not in version.tags
124    keep = no_vndk_no_apex or \
125           ('vndk' in version.tags and vndk) or \
126           ('apex' in version.tags and apex)
127    if not keep:
128        return True
129    if not symbol_in_arch(version.tags, arch):
130        return True
131    if not symbol_in_api(version.tags, arch, api):
132        return True
133    return False
134
135
136def should_omit_symbol(symbol, arch, api, vndk, apex):
137    """Returns True if the symbol should be omitted."""
138    no_vndk_no_apex = 'vndk' not in symbol.tags and 'apex' not in symbol.tags
139    keep = no_vndk_no_apex or \
140           ('vndk' in symbol.tags and vndk) or \
141           ('apex' in symbol.tags and apex)
142    if not keep:
143        return True
144    if not symbol_in_arch(symbol.tags, arch):
145        return True
146    if not symbol_in_api(symbol.tags, arch, api):
147        return True
148    return False
149
150
151def symbol_in_arch(tags, arch):
152    """Returns true if the symbol is present for the given architecture."""
153    has_arch_tags = False
154    for tag in tags:
155        if tag == arch:
156            return True
157        if tag in ALL_ARCHITECTURES:
158            has_arch_tags = True
159
160    # If there were no arch tags, the symbol is available for all
161    # architectures. If there were any arch tags, the symbol is only available
162    # for the tagged architectures.
163    return not has_arch_tags
164
165
166def symbol_in_api(tags, arch, api):
167    """Returns true if the symbol is present for the given API level."""
168    introduced_tag = None
169    arch_specific = False
170    for tag in tags:
171        # If there is an arch-specific tag, it should override the common one.
172        if tag.startswith('introduced=') and not arch_specific:
173            introduced_tag = tag
174        elif tag.startswith('introduced-' + arch + '='):
175            introduced_tag = tag
176            arch_specific = True
177        elif tag == 'future':
178            return api == FUTURE_API_LEVEL
179
180    if introduced_tag is None:
181        # We found no "introduced" tags, so the symbol has always been
182        # available.
183        return True
184
185    return api >= int(get_tag_value(introduced_tag))
186
187
188def symbol_versioned_in_api(tags, api):
189    """Returns true if the symbol should be versioned for the given API.
190
191    This models the `versioned=API` tag. This should be a very uncommonly
192    needed tag, and is really only needed to fix versioning mistakes that are
193    already out in the wild.
194
195    For example, some of libc's __aeabi_* functions were originally placed in
196    the private version, but that was incorrect. They are now in LIBC_N, but
197    when building against any version prior to N we need the symbol to be
198    unversioned (otherwise it won't resolve on M where it is private).
199    """
200    for tag in tags:
201        if tag.startswith('versioned='):
202            return api >= int(get_tag_value(tag))
203    # If there is no "versioned" tag, the tag has been versioned for as long as
204    # it was introduced.
205    return True
206
207
208class ParseError(RuntimeError):
209    """An error that occurred while parsing a symbol file."""
210    pass
211
212
213class MultiplyDefinedSymbolError(RuntimeError):
214    """A symbol name was multiply defined."""
215    def __init__(self, multiply_defined_symbols):
216        super(MultiplyDefinedSymbolError, self).__init__(
217            'Version script contains multiple definitions for: {}'.format(
218                ', '.join(multiply_defined_symbols)))
219        self.multiply_defined_symbols = multiply_defined_symbols
220
221
222class Version(object):
223    """A version block of a symbol file."""
224    def __init__(self, name, base, tags, symbols):
225        self.name = name
226        self.base = base
227        self.tags = tags
228        self.symbols = symbols
229
230    def __eq__(self, other):
231        if self.name != other.name:
232            return False
233        if self.base != other.base:
234            return False
235        if self.tags != other.tags:
236            return False
237        if self.symbols != other.symbols:
238            return False
239        return True
240
241
242class Symbol(object):
243    """A symbol definition from a symbol file."""
244    def __init__(self, name, tags):
245        self.name = name
246        self.tags = tags
247
248    def __eq__(self, other):
249        return self.name == other.name and set(self.tags) == set(other.tags)
250
251class SymbolFileParser(object):
252    """Parses NDK symbol files."""
253    def __init__(self, input_file, api_map, arch, api, vndk, apex):
254        self.input_file = input_file
255        self.api_map = api_map
256        self.arch = arch
257        self.api = api
258        self.vndk = vndk
259        self.apex = apex
260        self.current_line = None
261
262    def parse(self):
263        """Parses the symbol file and returns a list of Version objects."""
264        versions = []
265        while self.next_line() != '':
266            if '{' in self.current_line:
267                versions.append(self.parse_version())
268            else:
269                raise ParseError(
270                    'Unexpected contents at top level: ' + self.current_line)
271
272        self.check_no_duplicate_symbols(versions)
273        return versions
274
275    def check_no_duplicate_symbols(self, versions):
276        """Raises errors for multiply defined symbols.
277
278        This situation is the normal case when symbol versioning is actually
279        used, but this script doesn't currently handle that. The error message
280        will be a not necessarily obvious "error: redefition of 'foo'" from
281        stub.c, so it's better for us to catch this situation and raise a
282        better error.
283        """
284        symbol_names = set()
285        multiply_defined_symbols = set()
286        for version in versions:
287            if should_omit_version(version, self.arch, self.api, self.vndk, self.apex):
288                continue
289
290            for symbol in version.symbols:
291                if should_omit_symbol(symbol, self.arch, self.api, self.vndk, self.apex):
292                    continue
293
294                if symbol.name in symbol_names:
295                    multiply_defined_symbols.add(symbol.name)
296                symbol_names.add(symbol.name)
297        if multiply_defined_symbols:
298            raise MultiplyDefinedSymbolError(
299                sorted(list(multiply_defined_symbols)))
300
301    def parse_version(self):
302        """Parses a single version section and returns a Version object."""
303        name = self.current_line.split('{')[0].strip()
304        tags = get_tags(self.current_line)
305        tags = decode_api_level_tags(tags, self.api_map)
306        symbols = []
307        global_scope = True
308        cpp_symbols = False
309        while self.next_line() != '':
310            if '}' in self.current_line:
311                # Line is something like '} BASE; # tags'. Both base and tags
312                # are optional here.
313                base = self.current_line.partition('}')[2]
314                base = base.partition('#')[0].strip()
315                if not base.endswith(';'):
316                    raise ParseError(
317                        'Unterminated version/export "C++" block (expected ;).')
318                if cpp_symbols:
319                    cpp_symbols = False
320                else:
321                    base = base.rstrip(';').rstrip()
322                    if base == '':
323                        base = None
324                    return Version(name, base, tags, symbols)
325            elif 'extern "C++" {' in self.current_line:
326                cpp_symbols = True
327            elif not cpp_symbols and ':' in self.current_line:
328                visibility = self.current_line.split(':')[0].strip()
329                if visibility == 'local':
330                    global_scope = False
331                elif visibility == 'global':
332                    global_scope = True
333                else:
334                    raise ParseError('Unknown visiblity label: ' + visibility)
335            elif global_scope and not cpp_symbols:
336                symbols.append(self.parse_symbol())
337            else:
338                # We're in a hidden scope or in 'extern "C++"' block. Ignore
339                # everything.
340                pass
341        raise ParseError('Unexpected EOF in version block.')
342
343    def parse_symbol(self):
344        """Parses a single symbol line and returns a Symbol object."""
345        if ';' not in self.current_line:
346            raise ParseError(
347                'Expected ; to terminate symbol: ' + self.current_line)
348        if '*' in self.current_line:
349            raise ParseError(
350                'Wildcard global symbols are not permitted.')
351        # Line is now in the format "<symbol-name>; # tags"
352        name, _, _ = self.current_line.strip().partition(';')
353        tags = get_tags(self.current_line)
354        tags = decode_api_level_tags(tags, self.api_map)
355        return Symbol(name, tags)
356
357    def next_line(self):
358        """Returns the next non-empty non-comment line.
359
360        A return value of '' indicates EOF.
361        """
362        line = self.input_file.readline()
363        while line.strip() == '' or line.strip().startswith('#'):
364            line = self.input_file.readline()
365
366            # We want to skip empty lines, but '' indicates EOF.
367            if line == '':
368                break
369        self.current_line = line
370        return self.current_line
371
372
373class Generator(object):
374    """Output generator that writes stub source files and version scripts."""
375    def __init__(self, src_file, version_script, arch, api, vndk, apex):
376        self.src_file = src_file
377        self.version_script = version_script
378        self.arch = arch
379        self.api = api
380        self.vndk = vndk
381        self.apex = apex
382
383    def write(self, versions):
384        """Writes all symbol data to the output files."""
385        for version in versions:
386            self.write_version(version)
387
388    def write_version(self, version):
389        """Writes a single version block's data to the output files."""
390        if should_omit_version(version, self.arch, self.api, self.vndk, self.apex):
391            return
392
393        section_versioned = symbol_versioned_in_api(version.tags, self.api)
394        version_empty = True
395        pruned_symbols = []
396        for symbol in version.symbols:
397            if should_omit_symbol(symbol, self.arch, self.api, self.vndk, self.apex):
398                continue
399
400            if symbol_versioned_in_api(symbol.tags, self.api):
401                version_empty = False
402            pruned_symbols.append(symbol)
403
404        if len(pruned_symbols) > 0:
405            if not version_empty and section_versioned:
406                self.version_script.write(version.name + ' {\n')
407                self.version_script.write('    global:\n')
408            for symbol in pruned_symbols:
409                emit_version = symbol_versioned_in_api(symbol.tags, self.api)
410                if section_versioned and emit_version:
411                    self.version_script.write('        ' + symbol.name + ';\n')
412
413                weak = ''
414                if 'weak' in symbol.tags:
415                    weak = '__attribute__((weak)) '
416
417                if 'var' in symbol.tags:
418                    self.src_file.write('{}int {} = 0;\n'.format(
419                        weak, symbol.name))
420                else:
421                    self.src_file.write('{}void {}() {{}}\n'.format(
422                        weak, symbol.name))
423
424            if not version_empty and section_versioned:
425                base = '' if version.base is None else ' ' + version.base
426                self.version_script.write('}' + base + ';\n')
427
428
429def decode_api_level(api, api_map):
430    """Decodes the API level argument into the API level number.
431
432    For the average case, this just decodes the integer value from the string,
433    but for unreleased APIs we need to translate from the API codename (like
434    "O") to the future API level for that codename.
435    """
436    try:
437        return int(api)
438    except ValueError:
439        pass
440
441    if api == "current":
442        return FUTURE_API_LEVEL
443
444    return api_map[api]
445
446
447def parse_args():
448    """Parses and returns command line arguments."""
449    parser = argparse.ArgumentParser()
450
451    parser.add_argument('-v', '--verbose', action='count', default=0)
452
453    parser.add_argument(
454        '--api', required=True, help='API level being targeted.')
455    parser.add_argument(
456        '--arch', choices=ALL_ARCHITECTURES, required=True,
457        help='Architecture being targeted.')
458    parser.add_argument(
459        '--vndk', action='store_true', help='Use the VNDK variant.')
460    parser.add_argument(
461        '--apex', action='store_true', help='Use the APEX variant.')
462
463    parser.add_argument(
464        '--api-map', type=os.path.realpath, required=True,
465        help='Path to the API level map JSON file.')
466
467    parser.add_argument(
468        'symbol_file', type=os.path.realpath, help='Path to symbol file.')
469    parser.add_argument(
470        'stub_src', type=os.path.realpath,
471        help='Path to output stub source file.')
472    parser.add_argument(
473        'version_script', type=os.path.realpath,
474        help='Path to output version script.')
475
476    return parser.parse_args()
477
478
479def main():
480    """Program entry point."""
481    args = parse_args()
482
483    with open(args.api_map) as map_file:
484        api_map = json.load(map_file)
485    api = decode_api_level(args.api, api_map)
486
487    verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
488    verbosity = args.verbose
489    if verbosity > 2:
490        verbosity = 2
491    logging.basicConfig(level=verbose_map[verbosity])
492
493    with open(args.symbol_file) as symbol_file:
494        try:
495            versions = SymbolFileParser(symbol_file, api_map, args.arch, api,
496                                        args.vndk, args.apex).parse()
497        except MultiplyDefinedSymbolError as ex:
498            sys.exit('{}: error: {}'.format(args.symbol_file, ex))
499
500    with open(args.stub_src, 'w') as src_file:
501        with open(args.version_script, 'w') as version_file:
502            generator = Generator(src_file, version_file, args.arch, api,
503                                  args.vndk, args.apex)
504            generator.write(versions)
505
506
507if __name__ == '__main__':
508    main()
509