blob: 8c99bbf3e1e669e2b5ef100f01517bc24798c851 [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:
dimitry2be7fa92017-11-21 17:47:33 +0100277 # We're in a hidden scope or in 'extern "C++"' block. Ignore everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700278 pass
279 raise ParseError('Unexpected EOF in version block.')
280
281 def parse_symbol(self):
282 """Parses a single symbol line and returns a Symbol object."""
283 if ';' not in self.current_line:
284 raise ParseError(
285 'Expected ; to terminate symbol: ' + self.current_line)
286 if '*' in self.current_line:
287 raise ParseError(
288 'Wildcard global symbols are not permitted.')
289 # Line is now in the format "<symbol-name>; # tags"
290 name, _, _ = self.current_line.strip().partition(';')
291 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700292 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700293 return Symbol(name, tags)
294
295 def next_line(self):
296 """Returns the next non-empty non-comment line.
297
298 A return value of '' indicates EOF.
299 """
300 line = self.input_file.readline()
301 while line.strip() == '' or line.strip().startswith('#'):
302 line = self.input_file.readline()
303
304 # We want to skip empty lines, but '' indicates EOF.
305 if line == '':
306 break
307 self.current_line = line
308 return self.current_line
309
310
311class Generator(object):
312 """Output generator that writes stub source files and version scripts."""
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700313 def __init__(self, src_file, version_script, arch, api, vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700314 self.src_file = src_file
315 self.version_script = version_script
316 self.arch = arch
317 self.api = api
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700318 self.vndk = vndk
Dan Albert8bdccb92016-07-29 13:06:22 -0700319
320 def write(self, versions):
321 """Writes all symbol data to the output files."""
322 for version in versions:
323 self.write_version(version)
324
325 def write_version(self, version):
326 """Writes a single version block's data to the output files."""
327 name = version.name
328 tags = version.tags
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700329 if should_omit_version(name, tags, self.arch, self.api, self.vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700330 return
331
Dan Albertae452cc2017-01-03 14:27:41 -0800332 section_versioned = symbol_versioned_in_api(tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700333 version_empty = True
334 pruned_symbols = []
335 for symbol in version.symbols:
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700336 if not self.vndk and 'vndk' in symbol.tags:
337 continue
Dan Albert8bdccb92016-07-29 13:06:22 -0700338 if not symbol_in_arch(symbol.tags, self.arch):
339 continue
340 if not symbol_in_api(symbol.tags, self.arch, self.api):
341 continue
342
343 if symbol_versioned_in_api(symbol.tags, self.api):
344 version_empty = False
345 pruned_symbols.append(symbol)
346
347 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800348 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700349 self.version_script.write(version.name + ' {\n')
350 self.version_script.write(' global:\n')
351 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800352 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
353 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700354 self.version_script.write(' ' + symbol.name + ';\n')
355
Dan Albertf55f0782017-07-28 11:00:22 -0700356 weak = ''
357 if 'weak' in symbol.tags:
358 weak = '__attribute__((weak)) '
359
Dan Albert8bdccb92016-07-29 13:06:22 -0700360 if 'var' in symbol.tags:
Dan Albertf55f0782017-07-28 11:00:22 -0700361 self.src_file.write('{}int {} = 0;\n'.format(
362 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700363 else:
Dan Albertf55f0782017-07-28 11:00:22 -0700364 self.src_file.write('{}void {}() {{}}\n'.format(
365 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700366
Dan Albertae452cc2017-01-03 14:27:41 -0800367 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700368 base = '' if version.base is None else ' ' + version.base
369 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700370
371
Dan Albert49927d22017-03-28 15:00:46 -0700372def decode_api_level(api, api_map):
373 """Decodes the API level argument into the API level number.
374
375 For the average case, this just decodes the integer value from the string,
376 but for unreleased APIs we need to translate from the API codename (like
377 "O") to the future API level for that codename.
378 """
379 try:
380 return int(api)
381 except ValueError:
382 pass
383
384 if api == "current":
385 return FUTURE_API_LEVEL
386
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700387 return api_map[api]
Dan Albert49927d22017-03-28 15:00:46 -0700388
389
Dan Albert914449f2016-06-17 16:45:24 -0700390def parse_args():
391 """Parses and returns command line arguments."""
392 parser = argparse.ArgumentParser()
393
Dan Albert8bdccb92016-07-29 13:06:22 -0700394 parser.add_argument('-v', '--verbose', action='count', default=0)
395
Dan Albert914449f2016-06-17 16:45:24 -0700396 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700397 '--api', required=True, help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700398 parser.add_argument(
399 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700400 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700401 parser.add_argument(
402 '--vndk', action='store_true', help='Use the VNDK variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700403
404 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700405 '--api-map', type=os.path.realpath, required=True,
406 help='Path to the API level map JSON file.')
407
408 parser.add_argument(
Dan Albert914449f2016-06-17 16:45:24 -0700409 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
410 parser.add_argument(
411 'stub_src', type=os.path.realpath,
412 help='Path to output stub source file.')
413 parser.add_argument(
414 'version_script', type=os.path.realpath,
415 help='Path to output version script.')
416
417 return parser.parse_args()
418
419
420def main():
421 """Program entry point."""
422 args = parse_args()
423
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700424 with open(args.api_map) as map_file:
425 api_map = json.load(map_file)
426 api = decode_api_level(args.api, api_map)
Dan Albert49927d22017-03-28 15:00:46 -0700427
Dan Albert8bdccb92016-07-29 13:06:22 -0700428 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
429 verbosity = args.verbose
430 if verbosity > 2:
431 verbosity = 2
432 logging.basicConfig(level=verbose_map[verbosity])
433
Dan Albert914449f2016-06-17 16:45:24 -0700434 with open(args.symbol_file) as symbol_file:
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700435 versions = SymbolFileParser(symbol_file, api_map).parse()
Dan Albert8bdccb92016-07-29 13:06:22 -0700436
437 with open(args.stub_src, 'w') as src_file:
438 with open(args.version_script, 'w') as version_file:
Dan Albert49927d22017-03-28 15:00:46 -0700439 generator = Generator(src_file, version_file, args.arch, api,
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700440 args.vndk)
Dan Albert8bdccb92016-07-29 13:06:22 -0700441 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700442
443
444if __name__ == '__main__':
445 main()