blob: a9887f9fd216014aeb06c174aa781b1e4ec6eff6 [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
Dan Albert802cc822020-06-22 15:59:12 -0700121 no_llndk_no_apex = ('llndk' not in version.tags
122 and 'apex' not in version.tags)
Jiyong Park92d6bc12019-11-06 12:37:43 +0900123 keep = no_llndk_no_apex or \
124 ('llndk' in version.tags and llndk) or \
Jiyong Park14317652019-02-08 20:34:32 +0900125 ('apex' in version.tags and apex)
126 if not keep:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900127 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700128 if not symbol_in_arch(version.tags, arch):
Dan Albert08532b62016-07-28 18:09:47 -0700129 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700130 if not symbol_in_api(version.tags, arch, api):
131 return True
132 return False
133
134
Jiyong Park92d6bc12019-11-06 12:37:43 +0900135def should_omit_symbol(symbol, arch, api, llndk, apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700136 """Returns True if the symbol should be omitted."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900137 no_llndk_no_apex = 'llndk' not in symbol.tags and 'apex' not in symbol.tags
138 keep = no_llndk_no_apex or \
139 ('llndk' in symbol.tags and llndk) or \
Jiyong Park14317652019-02-08 20:34:32 +0900140 ('apex' in symbol.tags and apex)
141 if not keep:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900142 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700143 if not symbol_in_arch(symbol.tags, arch):
144 return True
145 if not symbol_in_api(symbol.tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -0700146 return True
147 return False
148
149
Dan Albert914449f2016-06-17 16:45:24 -0700150def symbol_in_arch(tags, arch):
151 """Returns true if the symbol is present for the given architecture."""
152 has_arch_tags = False
153 for tag in tags:
154 if tag == arch:
155 return True
156 if tag in ALL_ARCHITECTURES:
157 has_arch_tags = True
158
159 # If there were no arch tags, the symbol is available for all
160 # architectures. If there were any arch tags, the symbol is only available
161 # for the tagged architectures.
162 return not has_arch_tags
163
164
Dan Albertc42458e2016-07-29 13:05:39 -0700165def symbol_in_api(tags, arch, api):
166 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700167 introduced_tag = None
168 arch_specific = False
169 for tag in tags:
170 # If there is an arch-specific tag, it should override the common one.
171 if tag.startswith('introduced=') and not arch_specific:
172 introduced_tag = tag
173 elif tag.startswith('introduced-' + arch + '='):
174 introduced_tag = tag
175 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700176 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800177 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700178
179 if introduced_tag is None:
180 # We found no "introduced" tags, so the symbol has always been
181 # available.
182 return True
183
Dan Albertc42458e2016-07-29 13:05:39 -0700184 return api >= int(get_tag_value(introduced_tag))
185
186
187def symbol_versioned_in_api(tags, api):
188 """Returns true if the symbol should be versioned for the given API.
189
190 This models the `versioned=API` tag. This should be a very uncommonly
191 needed tag, and is really only needed to fix versioning mistakes that are
192 already out in the wild.
193
194 For example, some of libc's __aeabi_* functions were originally placed in
195 the private version, but that was incorrect. They are now in LIBC_N, but
196 when building against any version prior to N we need the symbol to be
197 unversioned (otherwise it won't resolve on M where it is private).
198 """
199 for tag in tags:
200 if tag.startswith('versioned='):
201 return api >= int(get_tag_value(tag))
202 # If there is no "versioned" tag, the tag has been versioned for as long as
203 # it was introduced.
204 return True
205
Dan Albert914449f2016-06-17 16:45:24 -0700206
Dan Albert8bdccb92016-07-29 13:06:22 -0700207class ParseError(RuntimeError):
208 """An error that occurred while parsing a symbol file."""
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 Albert802cc822020-06-22 15:59:12 -0700220class Version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700221 """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
Dan Albert802cc822020-06-22 15:59:12 -0700240class Symbol:
Dan Albert8bdccb92016-07-29 13:06:22 -0700241 """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
sophiezb858c6d2020-05-06 15:57:32 -0700249
Dan Albert802cc822020-06-22 15:59:12 -0700250class SymbolFileParser:
Dan Albert8bdccb92016-07-29 13:06:22 -0700251 """Parses NDK symbol files."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900252 def __init__(self, input_file, api_map, arch, api, llndk, apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700253 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700254 self.api_map = api_map
Dan Albert756f2d02018-10-09 16:36:03 -0700255 self.arch = arch
256 self.api = api
Jiyong Park92d6bc12019-11-06 12:37:43 +0900257 self.llndk = llndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900258 self.apex = apex
Dan Albert8bdccb92016-07-29 13:06:22 -0700259 self.current_line = None
260
261 def parse(self):
262 """Parses the symbol file and returns a list of Version objects."""
263 versions = []
264 while self.next_line() != '':
265 if '{' in self.current_line:
266 versions.append(self.parse_version())
267 else:
268 raise ParseError(
269 'Unexpected contents at top level: ' + self.current_line)
Dan Albert756f2d02018-10-09 16:36:03 -0700270
271 self.check_no_duplicate_symbols(versions)
Dan Albert8bdccb92016-07-29 13:06:22 -0700272 return versions
273
Dan Albert756f2d02018-10-09 16:36:03 -0700274 def check_no_duplicate_symbols(self, versions):
275 """Raises errors for multiply defined symbols.
276
277 This situation is the normal case when symbol versioning is actually
278 used, but this script doesn't currently handle that. The error message
279 will be a not necessarily obvious "error: redefition of 'foo'" from
280 stub.c, so it's better for us to catch this situation and raise a
281 better error.
282 """
283 symbol_names = set()
284 multiply_defined_symbols = set()
285 for version in versions:
Dan Albert802cc822020-06-22 15:59:12 -0700286 if should_omit_version(version, self.arch, self.api, self.llndk,
287 self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700288 continue
289
290 for symbol in version.symbols:
Dan Albert802cc822020-06-22 15:59:12 -0700291 if should_omit_symbol(symbol, self.arch, self.api, self.llndk,
292 self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700293 continue
294
295 if symbol.name in symbol_names:
296 multiply_defined_symbols.add(symbol.name)
297 symbol_names.add(symbol.name)
298 if multiply_defined_symbols:
299 raise MultiplyDefinedSymbolError(
300 sorted(list(multiply_defined_symbols)))
301
Dan Albert8bdccb92016-07-29 13:06:22 -0700302 def parse_version(self):
303 """Parses a single version section and returns a Version object."""
304 name = self.current_line.split('{')[0].strip()
305 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700306 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700307 symbols = []
308 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100309 cpp_symbols = False
Dan Albert8bdccb92016-07-29 13:06:22 -0700310 while self.next_line() != '':
311 if '}' in self.current_line:
312 # Line is something like '} BASE; # tags'. Both base and tags
313 # are optional here.
314 base = self.current_line.partition('}')[2]
315 base = base.partition('#')[0].strip()
316 if not base.endswith(';'):
317 raise ParseError(
dimitry2be7fa92017-11-21 17:47:33 +0100318 'Unterminated version/export "C++" block (expected ;).')
319 if cpp_symbols:
320 cpp_symbols = False
321 else:
322 base = base.rstrip(';').rstrip()
323 if base == '':
324 base = None
325 return Version(name, base, tags, symbols)
326 elif 'extern "C++" {' in self.current_line:
327 cpp_symbols = True
328 elif not cpp_symbols and ':' in self.current_line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700329 visibility = self.current_line.split(':')[0].strip()
330 if visibility == 'local':
331 global_scope = False
332 elif visibility == 'global':
333 global_scope = True
334 else:
335 raise ParseError('Unknown visiblity label: ' + visibility)
dimitry2be7fa92017-11-21 17:47:33 +0100336 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700337 symbols.append(self.parse_symbol())
338 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700339 # We're in a hidden scope or in 'extern "C++"' block. Ignore
340 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700341 pass
342 raise ParseError('Unexpected EOF in version block.')
343
344 def parse_symbol(self):
345 """Parses a single symbol line and returns a Symbol object."""
346 if ';' not in self.current_line:
347 raise ParseError(
348 'Expected ; to terminate symbol: ' + self.current_line)
349 if '*' in self.current_line:
350 raise ParseError(
351 'Wildcard global symbols are not permitted.')
352 # Line is now in the format "<symbol-name>; # tags"
353 name, _, _ = self.current_line.strip().partition(';')
354 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700355 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700356 return Symbol(name, tags)
357
358 def next_line(self):
359 """Returns the next non-empty non-comment line.
360
361 A return value of '' indicates EOF.
362 """
363 line = self.input_file.readline()
364 while line.strip() == '' or line.strip().startswith('#'):
365 line = self.input_file.readline()
366
367 # We want to skip empty lines, but '' indicates EOF.
368 if line == '':
369 break
370 self.current_line = line
371 return self.current_line
372
373
Dan Albert802cc822020-06-22 15:59:12 -0700374class Generator:
Dan Albert8bdccb92016-07-29 13:06:22 -0700375 """Output generator that writes stub source files and version scripts."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900376 def __init__(self, src_file, version_script, arch, api, llndk, apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700377 self.src_file = src_file
378 self.version_script = version_script
379 self.arch = arch
380 self.api = api
Jiyong Park92d6bc12019-11-06 12:37:43 +0900381 self.llndk = llndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900382 self.apex = apex
Dan Albert8bdccb92016-07-29 13:06:22 -0700383
384 def write(self, versions):
385 """Writes all symbol data to the output files."""
386 for version in versions:
387 self.write_version(version)
388
389 def write_version(self, version):
390 """Writes a single version block's data to the output files."""
Dan Albert802cc822020-06-22 15:59:12 -0700391 if should_omit_version(version, self.arch, self.api, self.llndk,
392 self.apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700393 return
394
Dan Albert756f2d02018-10-09 16:36:03 -0700395 section_versioned = symbol_versioned_in_api(version.tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700396 version_empty = True
397 pruned_symbols = []
398 for symbol in version.symbols:
Dan Albert802cc822020-06-22 15:59:12 -0700399 if should_omit_symbol(symbol, self.arch, self.api, self.llndk,
400 self.apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700401 continue
402
403 if symbol_versioned_in_api(symbol.tags, self.api):
404 version_empty = False
405 pruned_symbols.append(symbol)
406
407 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800408 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700409 self.version_script.write(version.name + ' {\n')
410 self.version_script.write(' global:\n')
411 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800412 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
413 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700414 self.version_script.write(' ' + symbol.name + ';\n')
415
Dan Albertf55f0782017-07-28 11:00:22 -0700416 weak = ''
417 if 'weak' in symbol.tags:
418 weak = '__attribute__((weak)) '
419
Dan Albert8bdccb92016-07-29 13:06:22 -0700420 if 'var' in symbol.tags:
Dan Albertf55f0782017-07-28 11:00:22 -0700421 self.src_file.write('{}int {} = 0;\n'.format(
422 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700423 else:
Dan Albertf55f0782017-07-28 11:00:22 -0700424 self.src_file.write('{}void {}() {{}}\n'.format(
425 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700426
Dan Albertae452cc2017-01-03 14:27:41 -0800427 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700428 base = '' if version.base is None else ' ' + version.base
429 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700430
431
Dan Albert49927d22017-03-28 15:00:46 -0700432def decode_api_level(api, api_map):
433 """Decodes the API level argument into the API level number.
434
435 For the average case, this just decodes the integer value from the string,
436 but for unreleased APIs we need to translate from the API codename (like
437 "O") to the future API level for that codename.
438 """
439 try:
440 return int(api)
441 except ValueError:
442 pass
443
444 if api == "current":
445 return FUTURE_API_LEVEL
446
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700447 return api_map[api]
Dan Albert49927d22017-03-28 15:00:46 -0700448
449
Dan Albert914449f2016-06-17 16:45:24 -0700450def parse_args():
451 """Parses and returns command line arguments."""
452 parser = argparse.ArgumentParser()
453
Dan Albert8bdccb92016-07-29 13:06:22 -0700454 parser.add_argument('-v', '--verbose', action='count', default=0)
455
Dan Albert914449f2016-06-17 16:45:24 -0700456 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700457 '--api', required=True, help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700458 parser.add_argument(
459 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700460 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700461 parser.add_argument(
Jiyong Park92d6bc12019-11-06 12:37:43 +0900462 '--llndk', action='store_true', help='Use the LLNDK variant.')
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900463 parser.add_argument(
464 '--apex', action='store_true', help='Use the APEX variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700465
466 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700467 '--api-map', type=os.path.realpath, required=True,
468 help='Path to the API level map JSON file.')
469
470 parser.add_argument(
Dan Albert914449f2016-06-17 16:45:24 -0700471 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
472 parser.add_argument(
473 'stub_src', type=os.path.realpath,
474 help='Path to output stub source file.')
475 parser.add_argument(
476 'version_script', type=os.path.realpath,
477 help='Path to output version script.')
478
479 return parser.parse_args()
480
481
482def main():
483 """Program entry point."""
484 args = parse_args()
485
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700486 with open(args.api_map) as map_file:
487 api_map = json.load(map_file)
488 api = decode_api_level(args.api, api_map)
Dan Albert49927d22017-03-28 15:00:46 -0700489
Dan Albert8bdccb92016-07-29 13:06:22 -0700490 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
491 verbosity = args.verbose
492 if verbosity > 2:
493 verbosity = 2
494 logging.basicConfig(level=verbose_map[verbosity])
495
Dan Albert914449f2016-06-17 16:45:24 -0700496 with open(args.symbol_file) as symbol_file:
Dan Albert756f2d02018-10-09 16:36:03 -0700497 try:
498 versions = SymbolFileParser(symbol_file, api_map, args.arch, api,
Jiyong Park92d6bc12019-11-06 12:37:43 +0900499 args.llndk, args.apex).parse()
Dan Albert756f2d02018-10-09 16:36:03 -0700500 except MultiplyDefinedSymbolError as ex:
501 sys.exit('{}: error: {}'.format(args.symbol_file, ex))
Dan Albert8bdccb92016-07-29 13:06:22 -0700502
503 with open(args.stub_src, 'w') as src_file:
504 with open(args.version_script, 'w') as version_file:
Dan Albert49927d22017-03-28 15:00:46 -0700505 generator = Generator(src_file, version_file, args.arch, api,
Jiyong Park92d6bc12019-11-06 12:37:43 +0900506 args.llndk, args.apex)
Dan Albert8bdccb92016-07-29 13:06:22 -0700507 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700508
509
510if __name__ == '__main__':
511 main()