blob: abb39c2a3a9d139cb0ff33f1a77b0d65ea41e268 [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
247 while self.next_line() != '':
248 if '}' in self.current_line:
249 # Line is something like '} BASE; # tags'. Both base and tags
250 # are optional here.
251 base = self.current_line.partition('}')[2]
252 base = base.partition('#')[0].strip()
253 if not base.endswith(';'):
254 raise ParseError(
255 'Unterminated version block (expected ;).')
256 base = base.rstrip(';').rstrip()
257 if base == '':
258 base = None
259 return Version(name, base, tags, symbols)
260 elif ':' in self.current_line:
261 visibility = self.current_line.split(':')[0].strip()
262 if visibility == 'local':
263 global_scope = False
264 elif visibility == 'global':
265 global_scope = True
266 else:
267 raise ParseError('Unknown visiblity label: ' + visibility)
268 elif global_scope:
269 symbols.append(self.parse_symbol())
270 else:
271 # We're in a hidden scope. Ignore everything.
272 pass
273 raise ParseError('Unexpected EOF in version block.')
274
275 def parse_symbol(self):
276 """Parses a single symbol line and returns a Symbol object."""
277 if ';' not in self.current_line:
278 raise ParseError(
279 'Expected ; to terminate symbol: ' + self.current_line)
280 if '*' in self.current_line:
281 raise ParseError(
282 'Wildcard global symbols are not permitted.')
283 # Line is now in the format "<symbol-name>; # tags"
284 name, _, _ = self.current_line.strip().partition(';')
285 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700286 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700287 return Symbol(name, tags)
288
289 def next_line(self):
290 """Returns the next non-empty non-comment line.
291
292 A return value of '' indicates EOF.
293 """
294 line = self.input_file.readline()
295 while line.strip() == '' or line.strip().startswith('#'):
296 line = self.input_file.readline()
297
298 # We want to skip empty lines, but '' indicates EOF.
299 if line == '':
300 break
301 self.current_line = line
302 return self.current_line
303
304
305class Generator(object):
306 """Output generator that writes stub source files and version scripts."""
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700307 def __init__(self, src_file, version_script, arch, api, vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700308 self.src_file = src_file
309 self.version_script = version_script
310 self.arch = arch
311 self.api = api
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700312 self.vndk = vndk
Dan Albert8bdccb92016-07-29 13:06:22 -0700313
314 def write(self, versions):
315 """Writes all symbol data to the output files."""
316 for version in versions:
317 self.write_version(version)
318
319 def write_version(self, version):
320 """Writes a single version block's data to the output files."""
321 name = version.name
322 tags = version.tags
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700323 if should_omit_version(name, tags, self.arch, self.api, self.vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700324 return
325
Dan Albertae452cc2017-01-03 14:27:41 -0800326 section_versioned = symbol_versioned_in_api(tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700327 version_empty = True
328 pruned_symbols = []
329 for symbol in version.symbols:
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700330 if not self.vndk and 'vndk' in symbol.tags:
331 continue
Dan Albert8bdccb92016-07-29 13:06:22 -0700332 if not symbol_in_arch(symbol.tags, self.arch):
333 continue
334 if not symbol_in_api(symbol.tags, self.arch, self.api):
335 continue
336
337 if symbol_versioned_in_api(symbol.tags, self.api):
338 version_empty = False
339 pruned_symbols.append(symbol)
340
341 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800342 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700343 self.version_script.write(version.name + ' {\n')
344 self.version_script.write(' global:\n')
345 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800346 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
347 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700348 self.version_script.write(' ' + symbol.name + ';\n')
349
Dan Albertf55f0782017-07-28 11:00:22 -0700350 weak = ''
351 if 'weak' in symbol.tags:
352 weak = '__attribute__((weak)) '
353
Dan Albert8bdccb92016-07-29 13:06:22 -0700354 if 'var' in symbol.tags:
Dan Albertf55f0782017-07-28 11:00:22 -0700355 self.src_file.write('{}int {} = 0;\n'.format(
356 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700357 else:
Dan Albertf55f0782017-07-28 11:00:22 -0700358 self.src_file.write('{}void {}() {{}}\n'.format(
359 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700360
Dan Albertae452cc2017-01-03 14:27:41 -0800361 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700362 base = '' if version.base is None else ' ' + version.base
363 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700364
365
Dan Albert49927d22017-03-28 15:00:46 -0700366def decode_api_level(api, api_map):
367 """Decodes the API level argument into the API level number.
368
369 For the average case, this just decodes the integer value from the string,
370 but for unreleased APIs we need to translate from the API codename (like
371 "O") to the future API level for that codename.
372 """
373 try:
374 return int(api)
375 except ValueError:
376 pass
377
378 if api == "current":
379 return FUTURE_API_LEVEL
380
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700381 return api_map[api]
Dan Albert49927d22017-03-28 15:00:46 -0700382
383
Dan Albert914449f2016-06-17 16:45:24 -0700384def parse_args():
385 """Parses and returns command line arguments."""
386 parser = argparse.ArgumentParser()
387
Dan Albert8bdccb92016-07-29 13:06:22 -0700388 parser.add_argument('-v', '--verbose', action='count', default=0)
389
Dan Albert914449f2016-06-17 16:45:24 -0700390 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700391 '--api', required=True, help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700392 parser.add_argument(
393 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700394 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700395 parser.add_argument(
396 '--vndk', action='store_true', help='Use the VNDK variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700397
398 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700399 '--api-map', type=os.path.realpath, required=True,
400 help='Path to the API level map JSON file.')
401
402 parser.add_argument(
Dan Albert914449f2016-06-17 16:45:24 -0700403 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
404 parser.add_argument(
405 'stub_src', type=os.path.realpath,
406 help='Path to output stub source file.')
407 parser.add_argument(
408 'version_script', type=os.path.realpath,
409 help='Path to output version script.')
410
411 return parser.parse_args()
412
413
414def main():
415 """Program entry point."""
416 args = parse_args()
417
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700418 with open(args.api_map) as map_file:
419 api_map = json.load(map_file)
420 api = decode_api_level(args.api, api_map)
Dan Albert49927d22017-03-28 15:00:46 -0700421
Dan Albert8bdccb92016-07-29 13:06:22 -0700422 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
423 verbosity = args.verbose
424 if verbosity > 2:
425 verbosity = 2
426 logging.basicConfig(level=verbose_map[verbosity])
427
Dan Albert914449f2016-06-17 16:45:24 -0700428 with open(args.symbol_file) as symbol_file:
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700429 versions = SymbolFileParser(symbol_file, api_map).parse()
Dan Albert8bdccb92016-07-29 13:06:22 -0700430
431 with open(args.stub_src, 'w') as src_file:
432 with open(args.version_script, 'w') as version_file:
Dan Albert49927d22017-03-28 15:00:46 -0700433 generator = Generator(src_file, version_file, args.arch, api,
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700434 args.vndk)
Dan Albert8bdccb92016-07-29 13:06:22 -0700435 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700436
437
438if __name__ == '__main__':
439 main()