blob: 7deb804c304bf59d0718c8972fce844624f76fab [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',
Dan Albert914449f2016-06-17 16:45:24 -070029 'x86',
30 'x86_64',
31)
32
33
Dan Albertfd86e9e2016-11-08 13:35:12 -080034# Arbitrary magic number. We use the same one in api-level.h for this purpose.
35FUTURE_API_LEVEL = 10000
36
37
Dan Albert8bdccb92016-07-29 13:06:22 -070038def logger():
39 """Return the main logger for this module."""
40 return logging.getLogger(__name__)
Dan Albert914449f2016-06-17 16:45:24 -070041
42
Dan Alberta85042a2016-07-28 16:58:27 -070043def get_tags(line):
44 """Returns a list of all tags on this line."""
45 _, _, all_tags = line.strip().partition('#')
Dan Albert8bdccb92016-07-29 13:06:22 -070046 return [e for e in re.split(r'\s+', all_tags) if e.strip()]
Dan Alberta85042a2016-07-28 16:58:27 -070047
48
Dan Albert3f6fb2d2017-03-28 16:04:25 -070049def is_api_level_tag(tag):
50 """Returns true if this tag has an API level that may need decoding."""
51 if tag.startswith('introduced='):
52 return True
53 if tag.startswith('introduced-'):
54 return True
55 if tag.startswith('versioned='):
56 return True
57 return False
58
59
60def decode_api_level_tags(tags, api_map):
61 """Decodes API level code names in a list of tags.
62
63 Raises:
64 ParseError: An unknown version name was found in a tag.
65 """
66 for idx, tag in enumerate(tags):
67 if not is_api_level_tag(tag):
68 continue
69 name, value = split_tag(tag)
70
71 try:
72 decoded = str(decode_api_level(value, api_map))
73 tags[idx] = '='.join([name, decoded])
74 except KeyError:
75 raise ParseError('Unknown version name in tag: {}'.format(tag))
76 return tags
77
78
79def split_tag(tag):
80 """Returns a key/value tuple of the tag.
81
82 Raises:
83 ValueError: Tag is not a key/value type tag.
84
85 Returns: Tuple of (key, value) of the tag. Both components are strings.
86 """
87 if '=' not in tag:
88 raise ValueError('Not a key/value tag: ' + tag)
89 key, _, value = tag.partition('=')
90 return key, value
91
92
Dan Albertc42458e2016-07-29 13:05:39 -070093def get_tag_value(tag):
94 """Returns the value of a key/value tag.
95
96 Raises:
97 ValueError: Tag is not a key/value type tag.
98
99 Returns: Value part of tag as a string.
100 """
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700101 return split_tag(tag)[1]
Dan Albertc42458e2016-07-29 13:05:39 -0700102
103
Dan Albert914449f2016-06-17 16:45:24 -0700104def version_is_private(version):
105 """Returns True if the version name should be treated as private."""
106 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
107
108
Jiyong Park92d6bc12019-11-06 12:37:43 +0900109def should_omit_version(version, arch, api, llndk, apex):
Dan Albert08532b62016-07-28 18:09:47 -0700110 """Returns True if the version section should be ommitted.
111
112 We want to omit any sections that do not have any symbols we'll have in the
113 stub library. Sections that contain entirely future symbols or only symbols
114 for certain architectures.
115 """
Dan Albert756f2d02018-10-09 16:36:03 -0700116 if version_is_private(version.name):
Dan Albert08532b62016-07-28 18:09:47 -0700117 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700118 if 'platform-only' in version.tags:
Dan Albert300cb2f2016-11-04 14:52:30 -0700119 return True
Jiyong Park14317652019-02-08 20:34:32 +0900120
Jiyong Park92d6bc12019-11-06 12:37:43 +0900121 no_llndk_no_apex = 'llndk' not in version.tags and 'apex' not in version.tags
122 keep = no_llndk_no_apex or \
123 ('llndk' in version.tags and llndk) or \
Jiyong Park14317652019-02-08 20:34:32 +0900124 ('apex' in version.tags and apex)
125 if not keep:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900126 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700127 if not symbol_in_arch(version.tags, arch):
Dan Albert08532b62016-07-28 18:09:47 -0700128 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700129 if not symbol_in_api(version.tags, arch, api):
130 return True
131 return False
132
133
Jiyong Park92d6bc12019-11-06 12:37:43 +0900134def should_omit_symbol(symbol, arch, api, llndk, apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700135 """Returns True if the symbol should be omitted."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900136 no_llndk_no_apex = 'llndk' not in symbol.tags and 'apex' not in symbol.tags
137 keep = no_llndk_no_apex or \
138 ('llndk' in symbol.tags and llndk) or \
Jiyong Park14317652019-02-08 20:34:32 +0900139 ('apex' in symbol.tags and apex)
140 if not keep:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900141 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700142 if not symbol_in_arch(symbol.tags, arch):
143 return True
144 if not symbol_in_api(symbol.tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -0700145 return True
146 return False
147
148
Dan Albert914449f2016-06-17 16:45:24 -0700149def symbol_in_arch(tags, arch):
150 """Returns true if the symbol is present for the given architecture."""
151 has_arch_tags = False
152 for tag in tags:
153 if tag == arch:
154 return True
155 if tag in ALL_ARCHITECTURES:
156 has_arch_tags = True
157
158 # If there were no arch tags, the symbol is available for all
159 # architectures. If there were any arch tags, the symbol is only available
160 # for the tagged architectures.
161 return not has_arch_tags
162
163
Dan Albertc42458e2016-07-29 13:05:39 -0700164def symbol_in_api(tags, arch, api):
165 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700166 introduced_tag = None
167 arch_specific = False
168 for tag in tags:
169 # If there is an arch-specific tag, it should override the common one.
170 if tag.startswith('introduced=') and not arch_specific:
171 introduced_tag = tag
172 elif tag.startswith('introduced-' + arch + '='):
173 introduced_tag = tag
174 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700175 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800176 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700177
178 if introduced_tag is None:
179 # We found no "introduced" tags, so the symbol has always been
180 # available.
181 return True
182
Dan Albertc42458e2016-07-29 13:05:39 -0700183 return api >= int(get_tag_value(introduced_tag))
184
185
186def symbol_versioned_in_api(tags, api):
187 """Returns true if the symbol should be versioned for the given API.
188
189 This models the `versioned=API` tag. This should be a very uncommonly
190 needed tag, and is really only needed to fix versioning mistakes that are
191 already out in the wild.
192
193 For example, some of libc's __aeabi_* functions were originally placed in
194 the private version, but that was incorrect. They are now in LIBC_N, but
195 when building against any version prior to N we need the symbol to be
196 unversioned (otherwise it won't resolve on M where it is private).
197 """
198 for tag in tags:
199 if tag.startswith('versioned='):
200 return api >= int(get_tag_value(tag))
201 # If there is no "versioned" tag, the tag has been versioned for as long as
202 # it was introduced.
203 return True
204
Dan Albert914449f2016-06-17 16:45:24 -0700205
Dan Albert8bdccb92016-07-29 13:06:22 -0700206class ParseError(RuntimeError):
207 """An error that occurred while parsing a symbol file."""
208 pass
Dan Albert914449f2016-06-17 16:45:24 -0700209
210
Dan Albert756f2d02018-10-09 16:36:03 -0700211class MultiplyDefinedSymbolError(RuntimeError):
212 """A symbol name was multiply defined."""
213 def __init__(self, multiply_defined_symbols):
214 super(MultiplyDefinedSymbolError, self).__init__(
215 'Version script contains multiple definitions for: {}'.format(
216 ', '.join(multiply_defined_symbols)))
217 self.multiply_defined_symbols = multiply_defined_symbols
218
219
Dan Albert8bdccb92016-07-29 13:06:22 -0700220class Version(object):
221 """A version block of a symbol file."""
222 def __init__(self, name, base, tags, symbols):
223 self.name = name
224 self.base = base
225 self.tags = tags
226 self.symbols = symbols
227
228 def __eq__(self, other):
229 if self.name != other.name:
230 return False
231 if self.base != other.base:
232 return False
233 if self.tags != other.tags:
234 return False
235 if self.symbols != other.symbols:
236 return False
237 return True
238
239
240class Symbol(object):
241 """A symbol definition from a symbol file."""
242 def __init__(self, name, tags):
243 self.name = name
244 self.tags = tags
245
246 def __eq__(self, other):
247 return self.name == other.name and set(self.tags) == set(other.tags)
248
Dan Albert8bdccb92016-07-29 13:06:22 -0700249class SymbolFileParser(object):
250 """Parses NDK symbol files."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900251 def __init__(self, input_file, api_map, arch, api, llndk, apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700252 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700253 self.api_map = api_map
Dan Albert756f2d02018-10-09 16:36:03 -0700254 self.arch = arch
255 self.api = api
Jiyong Park92d6bc12019-11-06 12:37:43 +0900256 self.llndk = llndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900257 self.apex = apex
Dan Albert8bdccb92016-07-29 13:06:22 -0700258 self.current_line = None
259
260 def parse(self):
261 """Parses the symbol file and returns a list of Version objects."""
262 versions = []
263 while self.next_line() != '':
264 if '{' in self.current_line:
265 versions.append(self.parse_version())
266 else:
267 raise ParseError(
268 'Unexpected contents at top level: ' + self.current_line)
Dan Albert756f2d02018-10-09 16:36:03 -0700269
270 self.check_no_duplicate_symbols(versions)
Dan Albert8bdccb92016-07-29 13:06:22 -0700271 return versions
272
Dan Albert756f2d02018-10-09 16:36:03 -0700273 def check_no_duplicate_symbols(self, versions):
274 """Raises errors for multiply defined symbols.
275
276 This situation is the normal case when symbol versioning is actually
277 used, but this script doesn't currently handle that. The error message
278 will be a not necessarily obvious "error: redefition of 'foo'" from
279 stub.c, so it's better for us to catch this situation and raise a
280 better error.
281 """
282 symbol_names = set()
283 multiply_defined_symbols = set()
284 for version in versions:
Jiyong Park92d6bc12019-11-06 12:37:43 +0900285 if should_omit_version(version, self.arch, self.api, self.llndk, self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700286 continue
287
288 for symbol in version.symbols:
Jiyong Park92d6bc12019-11-06 12:37:43 +0900289 if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700290 continue
291
292 if symbol.name in symbol_names:
293 multiply_defined_symbols.add(symbol.name)
294 symbol_names.add(symbol.name)
295 if multiply_defined_symbols:
296 raise MultiplyDefinedSymbolError(
297 sorted(list(multiply_defined_symbols)))
298
Dan Albert8bdccb92016-07-29 13:06:22 -0700299 def parse_version(self):
300 """Parses a single version section and returns a Version object."""
301 name = self.current_line.split('{')[0].strip()
302 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700303 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700304 symbols = []
305 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100306 cpp_symbols = False
Dan Albert8bdccb92016-07-29 13:06:22 -0700307 while self.next_line() != '':
308 if '}' in self.current_line:
309 # Line is something like '} BASE; # tags'. Both base and tags
310 # are optional here.
311 base = self.current_line.partition('}')[2]
312 base = base.partition('#')[0].strip()
313 if not base.endswith(';'):
314 raise ParseError(
dimitry2be7fa92017-11-21 17:47:33 +0100315 'Unterminated version/export "C++" block (expected ;).')
316 if cpp_symbols:
317 cpp_symbols = False
318 else:
319 base = base.rstrip(';').rstrip()
320 if base == '':
321 base = None
322 return Version(name, base, tags, symbols)
323 elif 'extern "C++" {' in self.current_line:
324 cpp_symbols = True
325 elif not cpp_symbols and ':' in self.current_line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700326 visibility = self.current_line.split(':')[0].strip()
327 if visibility == 'local':
328 global_scope = False
329 elif visibility == 'global':
330 global_scope = True
331 else:
332 raise ParseError('Unknown visiblity label: ' + visibility)
dimitry2be7fa92017-11-21 17:47:33 +0100333 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700334 symbols.append(self.parse_symbol())
335 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700336 # We're in a hidden scope or in 'extern "C++"' block. Ignore
337 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700338 pass
339 raise ParseError('Unexpected EOF in version block.')
340
341 def parse_symbol(self):
342 """Parses a single symbol line and returns a Symbol object."""
343 if ';' not in self.current_line:
344 raise ParseError(
345 'Expected ; to terminate symbol: ' + self.current_line)
346 if '*' in self.current_line:
347 raise ParseError(
348 'Wildcard global symbols are not permitted.')
349 # Line is now in the format "<symbol-name>; # tags"
350 name, _, _ = self.current_line.strip().partition(';')
351 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700352 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700353 return Symbol(name, tags)
354
355 def next_line(self):
356 """Returns the next non-empty non-comment line.
357
358 A return value of '' indicates EOF.
359 """
360 line = self.input_file.readline()
361 while line.strip() == '' or line.strip().startswith('#'):
362 line = self.input_file.readline()
363
364 # We want to skip empty lines, but '' indicates EOF.
365 if line == '':
366 break
367 self.current_line = line
368 return self.current_line
369
370
371class Generator(object):
372 """Output generator that writes stub source files and version scripts."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900373 def __init__(self, src_file, version_script, arch, api, llndk, apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700374 self.src_file = src_file
375 self.version_script = version_script
376 self.arch = arch
377 self.api = api
Jiyong Park92d6bc12019-11-06 12:37:43 +0900378 self.llndk = llndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900379 self.apex = apex
Dan Albert8bdccb92016-07-29 13:06:22 -0700380
381 def write(self, versions):
382 """Writes all symbol data to the output files."""
383 for version in versions:
384 self.write_version(version)
385
386 def write_version(self, version):
387 """Writes a single version block's data to the output files."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900388 if should_omit_version(version, self.arch, self.api, self.llndk, self.apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700389 return
390
Dan Albert756f2d02018-10-09 16:36:03 -0700391 section_versioned = symbol_versioned_in_api(version.tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700392 version_empty = True
393 pruned_symbols = []
394 for symbol in version.symbols:
Jiyong Park92d6bc12019-11-06 12:37:43 +0900395 if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700396 continue
397
398 if symbol_versioned_in_api(symbol.tags, self.api):
399 version_empty = False
400 pruned_symbols.append(symbol)
401
402 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800403 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700404 self.version_script.write(version.name + ' {\n')
405 self.version_script.write(' global:\n')
406 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800407 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
408 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700409 self.version_script.write(' ' + symbol.name + ';\n')
410
Dan Albertf55f0782017-07-28 11:00:22 -0700411 weak = ''
412 if 'weak' in symbol.tags:
413 weak = '__attribute__((weak)) '
414
Dan Albert8bdccb92016-07-29 13:06:22 -0700415 if 'var' in symbol.tags:
Dan Albertf55f0782017-07-28 11:00:22 -0700416 self.src_file.write('{}int {} = 0;\n'.format(
417 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700418 else:
Dan Albertf55f0782017-07-28 11:00:22 -0700419 self.src_file.write('{}void {}() {{}}\n'.format(
420 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700421
Dan Albertae452cc2017-01-03 14:27:41 -0800422 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700423 base = '' if version.base is None else ' ' + version.base
424 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700425
426
Dan Albert49927d22017-03-28 15:00:46 -0700427def decode_api_level(api, api_map):
428 """Decodes the API level argument into the API level number.
429
430 For the average case, this just decodes the integer value from the string,
431 but for unreleased APIs we need to translate from the API codename (like
432 "O") to the future API level for that codename.
433 """
434 try:
435 return int(api)
436 except ValueError:
437 pass
438
439 if api == "current":
440 return FUTURE_API_LEVEL
441
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700442 return api_map[api]
Dan Albert49927d22017-03-28 15:00:46 -0700443
444
Dan Albert914449f2016-06-17 16:45:24 -0700445def parse_args():
446 """Parses and returns command line arguments."""
447 parser = argparse.ArgumentParser()
448
Dan Albert8bdccb92016-07-29 13:06:22 -0700449 parser.add_argument('-v', '--verbose', action='count', default=0)
450
Dan Albert914449f2016-06-17 16:45:24 -0700451 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700452 '--api', required=True, help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700453 parser.add_argument(
454 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700455 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700456 parser.add_argument(
Jiyong Park92d6bc12019-11-06 12:37:43 +0900457 '--llndk', action='store_true', help='Use the LLNDK variant.')
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900458 parser.add_argument(
459 '--apex', action='store_true', help='Use the APEX variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700460
461 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700462 '--api-map', type=os.path.realpath, required=True,
463 help='Path to the API level map JSON file.')
464
465 parser.add_argument(
Dan Albert914449f2016-06-17 16:45:24 -0700466 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
467 parser.add_argument(
468 'stub_src', type=os.path.realpath,
469 help='Path to output stub source file.')
470 parser.add_argument(
471 'version_script', type=os.path.realpath,
472 help='Path to output version script.')
473
474 return parser.parse_args()
475
476
477def main():
478 """Program entry point."""
479 args = parse_args()
480
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700481 with open(args.api_map) as map_file:
482 api_map = json.load(map_file)
483 api = decode_api_level(args.api, api_map)
Dan Albert49927d22017-03-28 15:00:46 -0700484
Dan Albert8bdccb92016-07-29 13:06:22 -0700485 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
486 verbosity = args.verbose
487 if verbosity > 2:
488 verbosity = 2
489 logging.basicConfig(level=verbose_map[verbosity])
490
Dan Albert914449f2016-06-17 16:45:24 -0700491 with open(args.symbol_file) as symbol_file:
Dan Albert756f2d02018-10-09 16:36:03 -0700492 try:
493 versions = SymbolFileParser(symbol_file, api_map, args.arch, api,
Jiyong Park92d6bc12019-11-06 12:37:43 +0900494 args.llndk, args.apex).parse()
Dan Albert756f2d02018-10-09 16:36:03 -0700495 except MultiplyDefinedSymbolError as ex:
496 sys.exit('{}: error: {}'.format(args.symbol_file, ex))
Dan Albert8bdccb92016-07-29 13:06:22 -0700497
498 with open(args.stub_src, 'w') as src_file:
499 with open(args.version_script, 'w') as version_file:
Dan Albert49927d22017-03-28 15:00:46 -0700500 generator = Generator(src_file, version_file, args.arch, api,
Jiyong Park92d6bc12019-11-06 12:37:43 +0900501 args.llndk, args.apex)
Dan Albert8bdccb92016-07-29 13:06:22 -0700502 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700503
504
505if __name__ == '__main__':
506 main()