blob: 0de703ce140575cf3f25549950673194073fdb3a [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
Jiyong Park92d6bc12019-11-06 12:37:43 +0900111def should_omit_version(version, arch, api, llndk, apex):
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
Jiyong Park14317652019-02-08 20:34:32 +0900122
Jiyong Park92d6bc12019-11-06 12:37:43 +0900123 no_llndk_no_apex = 'llndk' not in version.tags and 'apex' not in version.tags
124 keep = no_llndk_no_apex or \
125 ('llndk' in version.tags and llndk) or \
Jiyong Park14317652019-02-08 20:34:32 +0900126 ('apex' in version.tags and apex)
127 if not keep:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900128 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700129 if not symbol_in_arch(version.tags, arch):
Dan Albert08532b62016-07-28 18:09:47 -0700130 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700131 if not symbol_in_api(version.tags, arch, api):
132 return True
133 return False
134
135
Jiyong Park92d6bc12019-11-06 12:37:43 +0900136def should_omit_symbol(symbol, arch, api, llndk, apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700137 """Returns True if the symbol should be omitted."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900138 no_llndk_no_apex = 'llndk' not in symbol.tags and 'apex' not in symbol.tags
139 keep = no_llndk_no_apex or \
140 ('llndk' in symbol.tags and llndk) or \
Jiyong Park14317652019-02-08 20:34:32 +0900141 ('apex' in symbol.tags and apex)
142 if not keep:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900143 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700144 if not symbol_in_arch(symbol.tags, arch):
145 return True
146 if not symbol_in_api(symbol.tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -0700147 return True
148 return False
149
150
Dan Albert914449f2016-06-17 16:45:24 -0700151def 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
Dan Albertc42458e2016-07-29 13:05:39 -0700166def symbol_in_api(tags, arch, api):
167 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700168 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
Dan Alberta85042a2016-07-28 16:58:27 -0700177 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800178 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700179
180 if introduced_tag is None:
181 # We found no "introduced" tags, so the symbol has always been
182 # available.
183 return True
184
Dan Albertc42458e2016-07-29 13:05:39 -0700185 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
Dan Albert914449f2016-06-17 16:45:24 -0700207
Dan Albert8bdccb92016-07-29 13:06:22 -0700208class ParseError(RuntimeError):
209 """An error that occurred while parsing a symbol file."""
210 pass
Dan Albert914449f2016-06-17 16:45:24 -0700211
212
Dan Albert756f2d02018-10-09 16:36:03 -0700213class 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
Dan Albert8bdccb92016-07-29 13:06:22 -0700222class 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
Dan Albert8bdccb92016-07-29 13:06:22 -0700251class SymbolFileParser(object):
252 """Parses NDK symbol files."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900253 def __init__(self, input_file, api_map, arch, api, llndk, apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700254 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700255 self.api_map = api_map
Dan Albert756f2d02018-10-09 16:36:03 -0700256 self.arch = arch
257 self.api = api
Jiyong Park92d6bc12019-11-06 12:37:43 +0900258 self.llndk = llndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900259 self.apex = apex
Dan Albert8bdccb92016-07-29 13:06:22 -0700260 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)
Dan Albert756f2d02018-10-09 16:36:03 -0700271
272 self.check_no_duplicate_symbols(versions)
Dan Albert8bdccb92016-07-29 13:06:22 -0700273 return versions
274
Dan Albert756f2d02018-10-09 16:36:03 -0700275 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:
Jiyong Park92d6bc12019-11-06 12:37:43 +0900287 if should_omit_version(version, self.arch, self.api, self.llndk, self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700288 continue
289
290 for symbol in version.symbols:
Jiyong Park92d6bc12019-11-06 12:37:43 +0900291 if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700292 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
Dan Albert8bdccb92016-07-29 13:06:22 -0700301 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)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700305 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700306 symbols = []
307 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100308 cpp_symbols = False
Dan Albert8bdccb92016-07-29 13:06:22 -0700309 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(
dimitry2be7fa92017-11-21 17:47:33 +0100317 '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:
Dan Albert8bdccb92016-07-29 13:06:22 -0700328 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)
dimitry2be7fa92017-11-21 17:47:33 +0100335 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700336 symbols.append(self.parse_symbol())
337 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700338 # We're in a hidden scope or in 'extern "C++"' block. Ignore
339 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700340 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)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700354 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700355 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."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900375 def __init__(self, src_file, version_script, arch, api, llndk, apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700376 self.src_file = src_file
377 self.version_script = version_script
378 self.arch = arch
379 self.api = api
Jiyong Park92d6bc12019-11-06 12:37:43 +0900380 self.llndk = llndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900381 self.apex = apex
Dan Albert8bdccb92016-07-29 13:06:22 -0700382
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."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900390 if should_omit_version(version, self.arch, self.api, self.llndk, self.apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700391 return
392
Dan Albert756f2d02018-10-09 16:36:03 -0700393 section_versioned = symbol_versioned_in_api(version.tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700394 version_empty = True
395 pruned_symbols = []
396 for symbol in version.symbols:
Jiyong Park92d6bc12019-11-06 12:37:43 +0900397 if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700398 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:
Dan Albertae452cc2017-01-03 14:27:41 -0800405 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700406 self.version_script.write(version.name + ' {\n')
407 self.version_script.write(' global:\n')
408 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800409 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
410 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700411 self.version_script.write(' ' + symbol.name + ';\n')
412
Dan Albertf55f0782017-07-28 11:00:22 -0700413 weak = ''
414 if 'weak' in symbol.tags:
415 weak = '__attribute__((weak)) '
416
Dan Albert8bdccb92016-07-29 13:06:22 -0700417 if 'var' in symbol.tags:
Dan Albertf55f0782017-07-28 11:00:22 -0700418 self.src_file.write('{}int {} = 0;\n'.format(
419 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700420 else:
Dan Albertf55f0782017-07-28 11:00:22 -0700421 self.src_file.write('{}void {}() {{}}\n'.format(
422 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700423
Dan Albertae452cc2017-01-03 14:27:41 -0800424 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700425 base = '' if version.base is None else ' ' + version.base
426 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700427
428
Dan Albert49927d22017-03-28 15:00:46 -0700429def 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
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700444 return api_map[api]
Dan Albert49927d22017-03-28 15:00:46 -0700445
446
Dan Albert914449f2016-06-17 16:45:24 -0700447def parse_args():
448 """Parses and returns command line arguments."""
449 parser = argparse.ArgumentParser()
450
Dan Albert8bdccb92016-07-29 13:06:22 -0700451 parser.add_argument('-v', '--verbose', action='count', default=0)
452
Dan Albert914449f2016-06-17 16:45:24 -0700453 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700454 '--api', required=True, help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700455 parser.add_argument(
456 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700457 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700458 parser.add_argument(
Jiyong Park92d6bc12019-11-06 12:37:43 +0900459 '--llndk', action='store_true', help='Use the LLNDK variant.')
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900460 parser.add_argument(
461 '--apex', action='store_true', help='Use the APEX variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700462
463 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700464 '--api-map', type=os.path.realpath, required=True,
465 help='Path to the API level map JSON file.')
466
467 parser.add_argument(
Dan Albert914449f2016-06-17 16:45:24 -0700468 '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
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700483 with open(args.api_map) as map_file:
484 api_map = json.load(map_file)
485 api = decode_api_level(args.api, api_map)
Dan Albert49927d22017-03-28 15:00:46 -0700486
Dan Albert8bdccb92016-07-29 13:06:22 -0700487 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
Dan Albert914449f2016-06-17 16:45:24 -0700493 with open(args.symbol_file) as symbol_file:
Dan Albert756f2d02018-10-09 16:36:03 -0700494 try:
495 versions = SymbolFileParser(symbol_file, api_map, args.arch, api,
Jiyong Park92d6bc12019-11-06 12:37:43 +0900496 args.llndk, args.apex).parse()
Dan Albert756f2d02018-10-09 16:36:03 -0700497 except MultiplyDefinedSymbolError as ex:
498 sys.exit('{}: error: {}'.format(args.symbol_file, ex))
Dan Albert8bdccb92016-07-29 13:06:22 -0700499
500 with open(args.stub_src, 'w') as src_file:
501 with open(args.version_script, 'w') as version_file:
Dan Albert49927d22017-03-28 15:00:46 -0700502 generator = Generator(src_file, version_file, args.arch, api,
Jiyong Park92d6bc12019-11-06 12:37:43 +0900503 args.llndk, args.apex)
Dan Albert8bdccb92016-07-29 13:06:22 -0700504 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700505
506
507if __name__ == '__main__':
508 main()