blob: 32765a7c12534e327aa115fffb17e8ea3fe8da33 [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
23
24
25ALL_ARCHITECTURES = (
26 'arm',
27 'arm64',
28 'mips',
29 'mips64',
30 'x86',
31 'x86_64',
32)
33
34
Dan Albertfd86e9e2016-11-08 13:35:12 -080035# Arbitrary magic number. We use the same one in api-level.h for this purpose.
36FUTURE_API_LEVEL = 10000
37
38
Dan Albert8bdccb92016-07-29 13:06:22 -070039def logger():
40 """Return the main logger for this module."""
41 return logging.getLogger(__name__)
Dan Albert914449f2016-06-17 16:45:24 -070042
43
Dan Alberta85042a2016-07-28 16:58:27 -070044def get_tags(line):
45 """Returns a list of all tags on this line."""
46 _, _, all_tags = line.strip().partition('#')
Dan Albert8bdccb92016-07-29 13:06:22 -070047 return [e for e in re.split(r'\s+', all_tags) if e.strip()]
Dan Alberta85042a2016-07-28 16:58:27 -070048
49
Dan Albert3f6fb2d2017-03-28 16:04:25 -070050def is_api_level_tag(tag):
51 """Returns true if this tag has an API level that may need decoding."""
52 if tag.startswith('introduced='):
53 return True
54 if tag.startswith('introduced-'):
55 return True
56 if tag.startswith('versioned='):
57 return True
58 return False
59
60
61def decode_api_level_tags(tags, api_map):
62 """Decodes API level code names in a list of tags.
63
64 Raises:
65 ParseError: An unknown version name was found in a tag.
66 """
67 for idx, tag in enumerate(tags):
68 if not is_api_level_tag(tag):
69 continue
70 name, value = split_tag(tag)
71
72 try:
73 decoded = str(decode_api_level(value, api_map))
74 tags[idx] = '='.join([name, decoded])
75 except KeyError:
76 raise ParseError('Unknown version name in tag: {}'.format(tag))
77 return tags
78
79
80def split_tag(tag):
81 """Returns a key/value tuple of the tag.
82
83 Raises:
84 ValueError: Tag is not a key/value type tag.
85
86 Returns: Tuple of (key, value) of the tag. Both components are strings.
87 """
88 if '=' not in tag:
89 raise ValueError('Not a key/value tag: ' + tag)
90 key, _, value = tag.partition('=')
91 return key, value
92
93
Dan Albertc42458e2016-07-29 13:05:39 -070094def get_tag_value(tag):
95 """Returns the value of a key/value tag.
96
97 Raises:
98 ValueError: Tag is not a key/value type tag.
99
100 Returns: Value part of tag as a string.
101 """
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700102 return split_tag(tag)[1]
Dan Albertc42458e2016-07-29 13:05:39 -0700103
104
Dan Albert914449f2016-06-17 16:45:24 -0700105def version_is_private(version):
106 """Returns True if the version name should be treated as private."""
107 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
108
109
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700110def should_omit_version(name, tags, arch, api, vndk):
Dan Albert08532b62016-07-28 18:09:47 -0700111 """Returns True if the version section should be ommitted.
112
113 We want to omit any sections that do not have any symbols we'll have in the
114 stub library. Sections that contain entirely future symbols or only symbols
115 for certain architectures.
116 """
117 if version_is_private(name):
118 return True
Dan Albert300cb2f2016-11-04 14:52:30 -0700119 if 'platform-only' in tags:
120 return True
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700121 if 'vndk' in tags and not vndk:
122 return True
Dan Albert08532b62016-07-28 18:09:47 -0700123 if not symbol_in_arch(tags, arch):
124 return True
Dan Albertc42458e2016-07-29 13:05:39 -0700125 if not symbol_in_api(tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -0700126 return True
127 return False
128
129
Dan Albert914449f2016-06-17 16:45:24 -0700130def symbol_in_arch(tags, arch):
131 """Returns true if the symbol is present for the given architecture."""
132 has_arch_tags = False
133 for tag in tags:
134 if tag == arch:
135 return True
136 if tag in ALL_ARCHITECTURES:
137 has_arch_tags = True
138
139 # If there were no arch tags, the symbol is available for all
140 # architectures. If there were any arch tags, the symbol is only available
141 # for the tagged architectures.
142 return not has_arch_tags
143
144
Dan Albertc42458e2016-07-29 13:05:39 -0700145def symbol_in_api(tags, arch, api):
146 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700147 introduced_tag = None
148 arch_specific = False
149 for tag in tags:
150 # If there is an arch-specific tag, it should override the common one.
151 if tag.startswith('introduced=') and not arch_specific:
152 introduced_tag = tag
153 elif tag.startswith('introduced-' + arch + '='):
154 introduced_tag = tag
155 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700156 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800157 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700158
159 if introduced_tag is None:
160 # We found no "introduced" tags, so the symbol has always been
161 # available.
162 return True
163
Dan Albertc42458e2016-07-29 13:05:39 -0700164 return api >= int(get_tag_value(introduced_tag))
165
166
167def symbol_versioned_in_api(tags, api):
168 """Returns true if the symbol should be versioned for the given API.
169
170 This models the `versioned=API` tag. This should be a very uncommonly
171 needed tag, and is really only needed to fix versioning mistakes that are
172 already out in the wild.
173
174 For example, some of libc's __aeabi_* functions were originally placed in
175 the private version, but that was incorrect. They are now in LIBC_N, but
176 when building against any version prior to N we need the symbol to be
177 unversioned (otherwise it won't resolve on M where it is private).
178 """
179 for tag in tags:
180 if tag.startswith('versioned='):
181 return api >= int(get_tag_value(tag))
182 # If there is no "versioned" tag, the tag has been versioned for as long as
183 # it was introduced.
184 return True
185
Dan Albert914449f2016-06-17 16:45:24 -0700186
Dan Albert8bdccb92016-07-29 13:06:22 -0700187class ParseError(RuntimeError):
188 """An error that occurred while parsing a symbol file."""
189 pass
Dan Albert914449f2016-06-17 16:45:24 -0700190
191
Dan Albert8bdccb92016-07-29 13:06:22 -0700192class Version(object):
193 """A version block of a symbol file."""
194 def __init__(self, name, base, tags, symbols):
195 self.name = name
196 self.base = base
197 self.tags = tags
198 self.symbols = symbols
199
200 def __eq__(self, other):
201 if self.name != other.name:
202 return False
203 if self.base != other.base:
204 return False
205 if self.tags != other.tags:
206 return False
207 if self.symbols != other.symbols:
208 return False
209 return True
210
211
212class Symbol(object):
213 """A symbol definition from a symbol file."""
214 def __init__(self, name, tags):
215 self.name = name
216 self.tags = tags
217
218 def __eq__(self, other):
219 return self.name == other.name and set(self.tags) == set(other.tags)
220
221
222class SymbolFileParser(object):
223 """Parses NDK symbol files."""
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700224 def __init__(self, input_file, api_map):
Dan Albert8bdccb92016-07-29 13:06:22 -0700225 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700226 self.api_map = api_map
Dan Albert8bdccb92016-07-29 13:06:22 -0700227 self.current_line = None
228
229 def parse(self):
230 """Parses the symbol file and returns a list of Version objects."""
231 versions = []
232 while self.next_line() != '':
233 if '{' in self.current_line:
234 versions.append(self.parse_version())
235 else:
236 raise ParseError(
237 'Unexpected contents at top level: ' + self.current_line)
238 return versions
239
240 def parse_version(self):
241 """Parses a single version section and returns a Version object."""
242 name = self.current_line.split('{')[0].strip()
243 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700244 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700245 symbols = []
246 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100247 cpp_symbols = False
Dan Albert8bdccb92016-07-29 13:06:22 -0700248 while self.next_line() != '':
249 if '}' in self.current_line:
250 # Line is something like '} BASE; # tags'. Both base and tags
251 # are optional here.
252 base = self.current_line.partition('}')[2]
253 base = base.partition('#')[0].strip()
254 if not base.endswith(';'):
255 raise ParseError(
dimitry2be7fa92017-11-21 17:47:33 +0100256 'Unterminated version/export "C++" block (expected ;).')
257 if cpp_symbols:
258 cpp_symbols = False
259 else:
260 base = base.rstrip(';').rstrip()
261 if base == '':
262 base = None
263 return Version(name, base, tags, symbols)
264 elif 'extern "C++" {' in self.current_line:
265 cpp_symbols = True
266 elif not cpp_symbols and ':' in self.current_line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700267 visibility = self.current_line.split(':')[0].strip()
268 if visibility == 'local':
269 global_scope = False
270 elif visibility == 'global':
271 global_scope = True
272 else:
273 raise ParseError('Unknown visiblity label: ' + visibility)
dimitry2be7fa92017-11-21 17:47:33 +0100274 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700275 symbols.append(self.parse_symbol())
276 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700277 # We're in a hidden scope or in 'extern "C++"' block. Ignore
278 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700279 pass
280 raise ParseError('Unexpected EOF in version block.')
281
282 def parse_symbol(self):
283 """Parses a single symbol line and returns a Symbol object."""
284 if ';' not in self.current_line:
285 raise ParseError(
286 'Expected ; to terminate symbol: ' + self.current_line)
287 if '*' in self.current_line:
288 raise ParseError(
289 'Wildcard global symbols are not permitted.')
290 # Line is now in the format "<symbol-name>; # tags"
291 name, _, _ = self.current_line.strip().partition(';')
292 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700293 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700294 return Symbol(name, tags)
295
296 def next_line(self):
297 """Returns the next non-empty non-comment line.
298
299 A return value of '' indicates EOF.
300 """
301 line = self.input_file.readline()
302 while line.strip() == '' or line.strip().startswith('#'):
303 line = self.input_file.readline()
304
305 # We want to skip empty lines, but '' indicates EOF.
306 if line == '':
307 break
308 self.current_line = line
309 return self.current_line
310
311
312class Generator(object):
313 """Output generator that writes stub source files and version scripts."""
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700314 def __init__(self, src_file, version_script, arch, api, vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700315 self.src_file = src_file
316 self.version_script = version_script
317 self.arch = arch
318 self.api = api
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700319 self.vndk = vndk
Dan Albert8bdccb92016-07-29 13:06:22 -0700320
321 def write(self, versions):
322 """Writes all symbol data to the output files."""
323 for version in versions:
324 self.write_version(version)
325
326 def write_version(self, version):
327 """Writes a single version block's data to the output files."""
328 name = version.name
329 tags = version.tags
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700330 if should_omit_version(name, tags, self.arch, self.api, self.vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700331 return
332
Dan Albertae452cc2017-01-03 14:27:41 -0800333 section_versioned = symbol_versioned_in_api(tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700334 version_empty = True
335 pruned_symbols = []
336 for symbol in version.symbols:
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700337 if not self.vndk and 'vndk' in symbol.tags:
338 continue
Dan Albert8bdccb92016-07-29 13:06:22 -0700339 if not symbol_in_arch(symbol.tags, self.arch):
340 continue
341 if not symbol_in_api(symbol.tags, self.arch, self.api):
342 continue
343
344 if symbol_versioned_in_api(symbol.tags, self.api):
345 version_empty = False
346 pruned_symbols.append(symbol)
347
348 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800349 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700350 self.version_script.write(version.name + ' {\n')
351 self.version_script.write(' global:\n')
352 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800353 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
354 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700355 self.version_script.write(' ' + symbol.name + ';\n')
356
Dan Albertf55f0782017-07-28 11:00:22 -0700357 weak = ''
358 if 'weak' in symbol.tags:
359 weak = '__attribute__((weak)) '
360
Dan Albert8bdccb92016-07-29 13:06:22 -0700361 if 'var' in symbol.tags:
Dan Albertf55f0782017-07-28 11:00:22 -0700362 self.src_file.write('{}int {} = 0;\n'.format(
363 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700364 else:
Dan Albertf55f0782017-07-28 11:00:22 -0700365 self.src_file.write('{}void {}() {{}}\n'.format(
366 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700367
Dan Albertae452cc2017-01-03 14:27:41 -0800368 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700369 base = '' if version.base is None else ' ' + version.base
370 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700371
372
Dan Albert49927d22017-03-28 15:00:46 -0700373def decode_api_level(api, api_map):
374 """Decodes the API level argument into the API level number.
375
376 For the average case, this just decodes the integer value from the string,
377 but for unreleased APIs we need to translate from the API codename (like
378 "O") to the future API level for that codename.
379 """
380 try:
381 return int(api)
382 except ValueError:
383 pass
384
385 if api == "current":
386 return FUTURE_API_LEVEL
387
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700388 return api_map[api]
Dan Albert49927d22017-03-28 15:00:46 -0700389
390
Dan Albert914449f2016-06-17 16:45:24 -0700391def parse_args():
392 """Parses and returns command line arguments."""
393 parser = argparse.ArgumentParser()
394
Dan Albert8bdccb92016-07-29 13:06:22 -0700395 parser.add_argument('-v', '--verbose', action='count', default=0)
396
Dan Albert914449f2016-06-17 16:45:24 -0700397 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700398 '--api', required=True, help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700399 parser.add_argument(
400 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700401 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700402 parser.add_argument(
403 '--vndk', action='store_true', help='Use the VNDK variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700404
405 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700406 '--api-map', type=os.path.realpath, required=True,
407 help='Path to the API level map JSON file.')
408
409 parser.add_argument(
Dan Albert914449f2016-06-17 16:45:24 -0700410 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
411 parser.add_argument(
412 'stub_src', type=os.path.realpath,
413 help='Path to output stub source file.')
414 parser.add_argument(
415 'version_script', type=os.path.realpath,
416 help='Path to output version script.')
417
418 return parser.parse_args()
419
420
421def main():
422 """Program entry point."""
423 args = parse_args()
424
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700425 with open(args.api_map) as map_file:
426 api_map = json.load(map_file)
427 api = decode_api_level(args.api, api_map)
Dan Albert49927d22017-03-28 15:00:46 -0700428
Dan Albert8bdccb92016-07-29 13:06:22 -0700429 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
430 verbosity = args.verbose
431 if verbosity > 2:
432 verbosity = 2
433 logging.basicConfig(level=verbose_map[verbosity])
434
Dan Albert914449f2016-06-17 16:45:24 -0700435 with open(args.symbol_file) as symbol_file:
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700436 versions = SymbolFileParser(symbol_file, api_map).parse()
Dan Albert8bdccb92016-07-29 13:06:22 -0700437
438 with open(args.stub_src, 'w') as src_file:
439 with open(args.version_script, 'w') as version_file:
Dan Albert49927d22017-03-28 15:00:46 -0700440 generator = Generator(src_file, version_file, args.arch, api,
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700441 args.vndk)
Dan Albert8bdccb92016-07-29 13:06:22 -0700442 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700443
444
445if __name__ == '__main__':
446 main()