blob: c49d197772f1b1f981fd656a3f3e53a808b5f2d7 [file] [log] [blame]
Dan Albert914449f2016-06-17 16:45:24 -07001#!/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
Dan Albert49927d22017-03-28 15:00:46 -070019import json
Dan Albert8bdccb92016-07-29 13:06:22 -070020import logging
Dan Albert914449f2016-06-17 16:45:24 -070021import os
22import re
Dan Albert756f2d02018-10-09 16:36:03 -070023import sys
Dan Albert914449f2016-06-17 16:45:24 -070024
25
26ALL_ARCHITECTURES = (
27 'arm',
28 'arm64',
29 'mips',
30 'mips64',
31 'x86',
32 'x86_64',
33)
34
35
Dan Albertfd86e9e2016-11-08 13:35:12 -080036# Arbitrary magic number. We use the same one in api-level.h for this purpose.
37FUTURE_API_LEVEL = 10000
38
39
Dan Albert8bdccb92016-07-29 13:06:22 -070040def logger():
41 """Return the main logger for this module."""
42 return logging.getLogger(__name__)
Dan Albert914449f2016-06-17 16:45:24 -070043
44
Dan Alberta85042a2016-07-28 16:58:27 -070045def get_tags(line):
46 """Returns a list of all tags on this line."""
47 _, _, all_tags = line.strip().partition('#')
Dan Albert8bdccb92016-07-29 13:06:22 -070048 return [e for e in re.split(r'\s+', all_tags) if e.strip()]
Dan Alberta85042a2016-07-28 16:58:27 -070049
50
Dan Albert3f6fb2d2017-03-28 16:04:25 -070051def 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
Dan Albertc42458e2016-07-29 13:05:39 -070095def 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 """
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700103 return split_tag(tag)[1]
Dan Albertc42458e2016-07-29 13:05:39 -0700104
105
Dan Albert914449f2016-06-17 16:45:24 -0700106def 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
Dan Albert756f2d02018-10-09 16:36:03 -0700111def should_omit_version(version, arch, api, vndk):
Dan Albert08532b62016-07-28 18:09:47 -0700112 """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 """
Dan Albert756f2d02018-10-09 16:36:03 -0700118 if version_is_private(version.name):
Dan Albert08532b62016-07-28 18:09:47 -0700119 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700120 if 'platform-only' in version.tags:
Dan Albert300cb2f2016-11-04 14:52:30 -0700121 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700122 if 'vndk' in version.tags and not vndk:
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700123 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700124 if not symbol_in_arch(version.tags, arch):
Dan Albert08532b62016-07-28 18:09:47 -0700125 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700126 if not symbol_in_api(version.tags, arch, api):
127 return True
128 return False
129
130
131def should_omit_symbol(symbol, arch, api, vndk):
132 """Returns True if the symbol should be omitted."""
133 if not vndk and 'vndk' in symbol.tags:
134 return True
135 if not symbol_in_arch(symbol.tags, arch):
136 return True
137 if not symbol_in_api(symbol.tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -0700138 return True
139 return False
140
141
Dan Albert914449f2016-06-17 16:45:24 -0700142def symbol_in_arch(tags, arch):
143 """Returns true if the symbol is present for the given architecture."""
144 has_arch_tags = False
145 for tag in tags:
146 if tag == arch:
147 return True
148 if tag in ALL_ARCHITECTURES:
149 has_arch_tags = True
150
151 # If there were no arch tags, the symbol is available for all
152 # architectures. If there were any arch tags, the symbol is only available
153 # for the tagged architectures.
154 return not has_arch_tags
155
156
Dan Albertc42458e2016-07-29 13:05:39 -0700157def symbol_in_api(tags, arch, api):
158 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700159 introduced_tag = None
160 arch_specific = False
161 for tag in tags:
162 # If there is an arch-specific tag, it should override the common one.
163 if tag.startswith('introduced=') and not arch_specific:
164 introduced_tag = tag
165 elif tag.startswith('introduced-' + arch + '='):
166 introduced_tag = tag
167 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700168 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800169 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700170
171 if introduced_tag is None:
172 # We found no "introduced" tags, so the symbol has always been
173 # available.
174 return True
175
Dan Albertc42458e2016-07-29 13:05:39 -0700176 return api >= int(get_tag_value(introduced_tag))
177
178
179def symbol_versioned_in_api(tags, api):
180 """Returns true if the symbol should be versioned for the given API.
181
182 This models the `versioned=API` tag. This should be a very uncommonly
183 needed tag, and is really only needed to fix versioning mistakes that are
184 already out in the wild.
185
186 For example, some of libc's __aeabi_* functions were originally placed in
187 the private version, but that was incorrect. They are now in LIBC_N, but
188 when building against any version prior to N we need the symbol to be
189 unversioned (otherwise it won't resolve on M where it is private).
190 """
191 for tag in tags:
192 if tag.startswith('versioned='):
193 return api >= int(get_tag_value(tag))
194 # If there is no "versioned" tag, the tag has been versioned for as long as
195 # it was introduced.
196 return True
197
Dan Albert914449f2016-06-17 16:45:24 -0700198
Dan Albert8bdccb92016-07-29 13:06:22 -0700199class ParseError(RuntimeError):
200 """An error that occurred while parsing a symbol file."""
201 pass
Dan Albert914449f2016-06-17 16:45:24 -0700202
203
Dan Albert756f2d02018-10-09 16:36:03 -0700204class MultiplyDefinedSymbolError(RuntimeError):
205 """A symbol name was multiply defined."""
206 def __init__(self, multiply_defined_symbols):
207 super(MultiplyDefinedSymbolError, self).__init__(
208 'Version script contains multiple definitions for: {}'.format(
209 ', '.join(multiply_defined_symbols)))
210 self.multiply_defined_symbols = multiply_defined_symbols
211
212
Dan Albert8bdccb92016-07-29 13:06:22 -0700213class Version(object):
214 """A version block of a symbol file."""
215 def __init__(self, name, base, tags, symbols):
216 self.name = name
217 self.base = base
218 self.tags = tags
219 self.symbols = symbols
220
221 def __eq__(self, other):
222 if self.name != other.name:
223 return False
224 if self.base != other.base:
225 return False
226 if self.tags != other.tags:
227 return False
228 if self.symbols != other.symbols:
229 return False
230 return True
231
232
233class Symbol(object):
234 """A symbol definition from a symbol file."""
235 def __init__(self, name, tags):
236 self.name = name
237 self.tags = tags
238
239 def __eq__(self, other):
240 return self.name == other.name and set(self.tags) == set(other.tags)
241
242
243class SymbolFileParser(object):
244 """Parses NDK symbol files."""
Dan Albert756f2d02018-10-09 16:36:03 -0700245 def __init__(self, input_file, api_map, arch, api, vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700246 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700247 self.api_map = api_map
Dan Albert756f2d02018-10-09 16:36:03 -0700248 self.arch = arch
249 self.api = api
250 self.vndk = vndk
Dan Albert8bdccb92016-07-29 13:06:22 -0700251 self.current_line = None
252
253 def parse(self):
254 """Parses the symbol file and returns a list of Version objects."""
255 versions = []
256 while self.next_line() != '':
257 if '{' in self.current_line:
258 versions.append(self.parse_version())
259 else:
260 raise ParseError(
261 'Unexpected contents at top level: ' + self.current_line)
Dan Albert756f2d02018-10-09 16:36:03 -0700262
263 self.check_no_duplicate_symbols(versions)
Dan Albert8bdccb92016-07-29 13:06:22 -0700264 return versions
265
Dan Albert756f2d02018-10-09 16:36:03 -0700266 def check_no_duplicate_symbols(self, versions):
267 """Raises errors for multiply defined symbols.
268
269 This situation is the normal case when symbol versioning is actually
270 used, but this script doesn't currently handle that. The error message
271 will be a not necessarily obvious "error: redefition of 'foo'" from
272 stub.c, so it's better for us to catch this situation and raise a
273 better error.
274 """
275 symbol_names = set()
276 multiply_defined_symbols = set()
277 for version in versions:
278 if should_omit_version(version, self.arch, self.api, self.vndk):
279 continue
280
281 for symbol in version.symbols:
282 if should_omit_symbol(symbol, self.arch, self.api, self.vndk):
283 continue
284
285 if symbol.name in symbol_names:
286 multiply_defined_symbols.add(symbol.name)
287 symbol_names.add(symbol.name)
288 if multiply_defined_symbols:
289 raise MultiplyDefinedSymbolError(
290 sorted(list(multiply_defined_symbols)))
291
Dan Albert8bdccb92016-07-29 13:06:22 -0700292 def parse_version(self):
293 """Parses a single version section and returns a Version object."""
294 name = self.current_line.split('{')[0].strip()
295 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700296 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700297 symbols = []
298 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100299 cpp_symbols = False
Dan Albert8bdccb92016-07-29 13:06:22 -0700300 while self.next_line() != '':
301 if '}' in self.current_line:
302 # Line is something like '} BASE; # tags'. Both base and tags
303 # are optional here.
304 base = self.current_line.partition('}')[2]
305 base = base.partition('#')[0].strip()
306 if not base.endswith(';'):
307 raise ParseError(
dimitry2be7fa92017-11-21 17:47:33 +0100308 'Unterminated version/export "C++" block (expected ;).')
309 if cpp_symbols:
310 cpp_symbols = False
311 else:
312 base = base.rstrip(';').rstrip()
313 if base == '':
314 base = None
315 return Version(name, base, tags, symbols)
316 elif 'extern "C++" {' in self.current_line:
317 cpp_symbols = True
318 elif not cpp_symbols and ':' in self.current_line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700319 visibility = self.current_line.split(':')[0].strip()
320 if visibility == 'local':
321 global_scope = False
322 elif visibility == 'global':
323 global_scope = True
324 else:
325 raise ParseError('Unknown visiblity label: ' + visibility)
dimitry2be7fa92017-11-21 17:47:33 +0100326 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700327 symbols.append(self.parse_symbol())
328 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700329 # We're in a hidden scope or in 'extern "C++"' block. Ignore
330 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700331 pass
332 raise ParseError('Unexpected EOF in version block.')
333
334 def parse_symbol(self):
335 """Parses a single symbol line and returns a Symbol object."""
336 if ';' not in self.current_line:
337 raise ParseError(
338 'Expected ; to terminate symbol: ' + self.current_line)
339 if '*' in self.current_line:
340 raise ParseError(
341 'Wildcard global symbols are not permitted.')
342 # Line is now in the format "<symbol-name>; # tags"
343 name, _, _ = self.current_line.strip().partition(';')
344 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700345 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700346 return Symbol(name, tags)
347
348 def next_line(self):
349 """Returns the next non-empty non-comment line.
350
351 A return value of '' indicates EOF.
352 """
353 line = self.input_file.readline()
354 while line.strip() == '' or line.strip().startswith('#'):
355 line = self.input_file.readline()
356
357 # We want to skip empty lines, but '' indicates EOF.
358 if line == '':
359 break
360 self.current_line = line
361 return self.current_line
362
363
364class Generator(object):
365 """Output generator that writes stub source files and version scripts."""
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700366 def __init__(self, src_file, version_script, arch, api, vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700367 self.src_file = src_file
368 self.version_script = version_script
369 self.arch = arch
370 self.api = api
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700371 self.vndk = vndk
Dan Albert8bdccb92016-07-29 13:06:22 -0700372
373 def write(self, versions):
374 """Writes all symbol data to the output files."""
375 for version in versions:
376 self.write_version(version)
377
378 def write_version(self, version):
379 """Writes a single version block's data to the output files."""
Dan Albert756f2d02018-10-09 16:36:03 -0700380 if should_omit_version(version, self.arch, self.api, self.vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700381 return
382
Dan Albert756f2d02018-10-09 16:36:03 -0700383 section_versioned = symbol_versioned_in_api(version.tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700384 version_empty = True
385 pruned_symbols = []
386 for symbol in version.symbols:
Dan Albert756f2d02018-10-09 16:36:03 -0700387 if should_omit_symbol(symbol, self.arch, self.api, self.vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700388 continue
389
390 if symbol_versioned_in_api(symbol.tags, self.api):
391 version_empty = False
392 pruned_symbols.append(symbol)
393
394 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800395 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700396 self.version_script.write(version.name + ' {\n')
397 self.version_script.write(' global:\n')
398 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800399 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
400 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700401 self.version_script.write(' ' + symbol.name + ';\n')
402
Dan Albertf55f0782017-07-28 11:00:22 -0700403 weak = ''
404 if 'weak' in symbol.tags:
405 weak = '__attribute__((weak)) '
406
Dan Albert8bdccb92016-07-29 13:06:22 -0700407 if 'var' in symbol.tags:
Dan Albertf55f0782017-07-28 11:00:22 -0700408 self.src_file.write('{}int {} = 0;\n'.format(
409 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700410 else:
Dan Albertf55f0782017-07-28 11:00:22 -0700411 self.src_file.write('{}void {}() {{}}\n'.format(
412 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700413
Dan Albertae452cc2017-01-03 14:27:41 -0800414 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700415 base = '' if version.base is None else ' ' + version.base
416 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700417
418
Dan Albert49927d22017-03-28 15:00:46 -0700419def decode_api_level(api, api_map):
420 """Decodes the API level argument into the API level number.
421
422 For the average case, this just decodes the integer value from the string,
423 but for unreleased APIs we need to translate from the API codename (like
424 "O") to the future API level for that codename.
425 """
426 try:
427 return int(api)
428 except ValueError:
429 pass
430
431 if api == "current":
432 return FUTURE_API_LEVEL
433
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700434 return api_map[api]
Dan Albert49927d22017-03-28 15:00:46 -0700435
436
Dan Albert914449f2016-06-17 16:45:24 -0700437def parse_args():
438 """Parses and returns command line arguments."""
439 parser = argparse.ArgumentParser()
440
Dan Albert8bdccb92016-07-29 13:06:22 -0700441 parser.add_argument('-v', '--verbose', action='count', default=0)
442
Dan Albert914449f2016-06-17 16:45:24 -0700443 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700444 '--api', required=True, help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700445 parser.add_argument(
446 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700447 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700448 parser.add_argument(
449 '--vndk', action='store_true', help='Use the VNDK variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700450
451 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700452 '--api-map', type=os.path.realpath, required=True,
453 help='Path to the API level map JSON file.')
454
455 parser.add_argument(
Dan Albert914449f2016-06-17 16:45:24 -0700456 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
457 parser.add_argument(
458 'stub_src', type=os.path.realpath,
459 help='Path to output stub source file.')
460 parser.add_argument(
461 'version_script', type=os.path.realpath,
462 help='Path to output version script.')
463
464 return parser.parse_args()
465
466
467def main():
468 """Program entry point."""
469 args = parse_args()
470
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700471 with open(args.api_map) as map_file:
472 api_map = json.load(map_file)
473 api = decode_api_level(args.api, api_map)
Dan Albert49927d22017-03-28 15:00:46 -0700474
Dan Albert8bdccb92016-07-29 13:06:22 -0700475 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
476 verbosity = args.verbose
477 if verbosity > 2:
478 verbosity = 2
479 logging.basicConfig(level=verbose_map[verbosity])
480
Dan Albert914449f2016-06-17 16:45:24 -0700481 with open(args.symbol_file) as symbol_file:
Dan Albert756f2d02018-10-09 16:36:03 -0700482 try:
483 versions = SymbolFileParser(symbol_file, api_map, args.arch, api,
484 args.vndk).parse()
485 except MultiplyDefinedSymbolError as ex:
486 sys.exit('{}: error: {}'.format(args.symbol_file, ex))
Dan Albert8bdccb92016-07-29 13:06:22 -0700487
488 with open(args.stub_src, 'w') as src_file:
489 with open(args.version_script, 'w') as version_file:
Dan Albert49927d22017-03-28 15:00:46 -0700490 generator = Generator(src_file, version_file, args.arch, api,
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700491 args.vndk)
Dan Albert8bdccb92016-07-29 13:06:22 -0700492 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700493
494
495if __name__ == '__main__':
496 main()