blob: 8a4f21e58ded208a1c589692a5b933a18867d69e [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 Albertfd86e9e2016-11-08 13:35:12 -080044def api_level_arg(api_str):
45 """Parses an API level, handling the "current" special case.
46
47 Args:
48 api_str: (string) Either a numeric API level or "current".
49
50 Returns:
51 (int) FUTURE_API_LEVEL if `api_str` is "current", else `api_str` parsed
52 as an integer.
53 """
54 if api_str == "current":
55 return FUTURE_API_LEVEL
56 return int(api_str)
57
58
Dan Alberta85042a2016-07-28 16:58:27 -070059def get_tags(line):
60 """Returns a list of all tags on this line."""
61 _, _, all_tags = line.strip().partition('#')
Dan Albert8bdccb92016-07-29 13:06:22 -070062 return [e for e in re.split(r'\s+', all_tags) if e.strip()]
Dan Alberta85042a2016-07-28 16:58:27 -070063
64
Dan Albertc42458e2016-07-29 13:05:39 -070065def get_tag_value(tag):
66 """Returns the value of a key/value tag.
67
68 Raises:
69 ValueError: Tag is not a key/value type tag.
70
71 Returns: Value part of tag as a string.
72 """
73 if '=' not in tag:
74 raise ValueError('Not a key/value tag: ' + tag)
75 return tag.partition('=')[2]
76
77
Dan Albert914449f2016-06-17 16:45:24 -070078def version_is_private(version):
79 """Returns True if the version name should be treated as private."""
80 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
81
82
Dan Willemsenb01e7f72017-04-03 14:28:36 -070083def should_omit_version(name, tags, arch, api, vndk):
Dan Albert08532b62016-07-28 18:09:47 -070084 """Returns True if the version section should be ommitted.
85
86 We want to omit any sections that do not have any symbols we'll have in the
87 stub library. Sections that contain entirely future symbols or only symbols
88 for certain architectures.
89 """
90 if version_is_private(name):
91 return True
Dan Albert300cb2f2016-11-04 14:52:30 -070092 if 'platform-only' in tags:
93 return True
Dan Willemsenb01e7f72017-04-03 14:28:36 -070094 if 'vndk' in tags and not vndk:
95 return True
Dan Albert08532b62016-07-28 18:09:47 -070096 if not symbol_in_arch(tags, arch):
97 return True
Dan Albertc42458e2016-07-29 13:05:39 -070098 if not symbol_in_api(tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -070099 return True
100 return False
101
102
Dan Albert914449f2016-06-17 16:45:24 -0700103def symbol_in_arch(tags, arch):
104 """Returns true if the symbol is present for the given architecture."""
105 has_arch_tags = False
106 for tag in tags:
107 if tag == arch:
108 return True
109 if tag in ALL_ARCHITECTURES:
110 has_arch_tags = True
111
112 # If there were no arch tags, the symbol is available for all
113 # architectures. If there were any arch tags, the symbol is only available
114 # for the tagged architectures.
115 return not has_arch_tags
116
117
Dan Albertc42458e2016-07-29 13:05:39 -0700118def symbol_in_api(tags, arch, api):
119 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700120 introduced_tag = None
121 arch_specific = False
122 for tag in tags:
123 # If there is an arch-specific tag, it should override the common one.
124 if tag.startswith('introduced=') and not arch_specific:
125 introduced_tag = tag
126 elif tag.startswith('introduced-' + arch + '='):
127 introduced_tag = tag
128 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700129 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800130 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700131
132 if introduced_tag is None:
133 # We found no "introduced" tags, so the symbol has always been
134 # available.
135 return True
136
Dan Albertc42458e2016-07-29 13:05:39 -0700137 return api >= int(get_tag_value(introduced_tag))
138
139
140def symbol_versioned_in_api(tags, api):
141 """Returns true if the symbol should be versioned for the given API.
142
143 This models the `versioned=API` tag. This should be a very uncommonly
144 needed tag, and is really only needed to fix versioning mistakes that are
145 already out in the wild.
146
147 For example, some of libc's __aeabi_* functions were originally placed in
148 the private version, but that was incorrect. They are now in LIBC_N, but
149 when building against any version prior to N we need the symbol to be
150 unversioned (otherwise it won't resolve on M where it is private).
151 """
152 for tag in tags:
153 if tag.startswith('versioned='):
154 return api >= int(get_tag_value(tag))
155 # If there is no "versioned" tag, the tag has been versioned for as long as
156 # it was introduced.
157 return True
158
Dan Albert914449f2016-06-17 16:45:24 -0700159
Dan Albert8bdccb92016-07-29 13:06:22 -0700160class ParseError(RuntimeError):
161 """An error that occurred while parsing a symbol file."""
162 pass
Dan Albert914449f2016-06-17 16:45:24 -0700163
164
Dan Albert8bdccb92016-07-29 13:06:22 -0700165class Version(object):
166 """A version block of a symbol file."""
167 def __init__(self, name, base, tags, symbols):
168 self.name = name
169 self.base = base
170 self.tags = tags
171 self.symbols = symbols
172
173 def __eq__(self, other):
174 if self.name != other.name:
175 return False
176 if self.base != other.base:
177 return False
178 if self.tags != other.tags:
179 return False
180 if self.symbols != other.symbols:
181 return False
182 return True
183
184
185class Symbol(object):
186 """A symbol definition from a symbol file."""
187 def __init__(self, name, tags):
188 self.name = name
189 self.tags = tags
190
191 def __eq__(self, other):
192 return self.name == other.name and set(self.tags) == set(other.tags)
193
194
195class SymbolFileParser(object):
196 """Parses NDK symbol files."""
197 def __init__(self, input_file):
198 self.input_file = input_file
199 self.current_line = None
200
201 def parse(self):
202 """Parses the symbol file and returns a list of Version objects."""
203 versions = []
204 while self.next_line() != '':
205 if '{' in self.current_line:
206 versions.append(self.parse_version())
207 else:
208 raise ParseError(
209 'Unexpected contents at top level: ' + self.current_line)
210 return versions
211
212 def parse_version(self):
213 """Parses a single version section and returns a Version object."""
214 name = self.current_line.split('{')[0].strip()
215 tags = get_tags(self.current_line)
216 symbols = []
217 global_scope = True
218 while self.next_line() != '':
219 if '}' in self.current_line:
220 # Line is something like '} BASE; # tags'. Both base and tags
221 # are optional here.
222 base = self.current_line.partition('}')[2]
223 base = base.partition('#')[0].strip()
224 if not base.endswith(';'):
225 raise ParseError(
226 'Unterminated version block (expected ;).')
227 base = base.rstrip(';').rstrip()
228 if base == '':
229 base = None
230 return Version(name, base, tags, symbols)
231 elif ':' in self.current_line:
232 visibility = self.current_line.split(':')[0].strip()
233 if visibility == 'local':
234 global_scope = False
235 elif visibility == 'global':
236 global_scope = True
237 else:
238 raise ParseError('Unknown visiblity label: ' + visibility)
239 elif global_scope:
240 symbols.append(self.parse_symbol())
241 else:
242 # We're in a hidden scope. Ignore everything.
243 pass
244 raise ParseError('Unexpected EOF in version block.')
245
246 def parse_symbol(self):
247 """Parses a single symbol line and returns a Symbol object."""
248 if ';' not in self.current_line:
249 raise ParseError(
250 'Expected ; to terminate symbol: ' + self.current_line)
251 if '*' in self.current_line:
252 raise ParseError(
253 'Wildcard global symbols are not permitted.')
254 # Line is now in the format "<symbol-name>; # tags"
255 name, _, _ = self.current_line.strip().partition(';')
256 tags = get_tags(self.current_line)
257 return Symbol(name, tags)
258
259 def next_line(self):
260 """Returns the next non-empty non-comment line.
261
262 A return value of '' indicates EOF.
263 """
264 line = self.input_file.readline()
265 while line.strip() == '' or line.strip().startswith('#'):
266 line = self.input_file.readline()
267
268 # We want to skip empty lines, but '' indicates EOF.
269 if line == '':
270 break
271 self.current_line = line
272 return self.current_line
273
274
275class Generator(object):
276 """Output generator that writes stub source files and version scripts."""
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700277 def __init__(self, src_file, version_script, arch, api, vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700278 self.src_file = src_file
279 self.version_script = version_script
280 self.arch = arch
281 self.api = api
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700282 self.vndk = vndk
Dan Albert8bdccb92016-07-29 13:06:22 -0700283
284 def write(self, versions):
285 """Writes all symbol data to the output files."""
286 for version in versions:
287 self.write_version(version)
288
289 def write_version(self, version):
290 """Writes a single version block's data to the output files."""
291 name = version.name
292 tags = version.tags
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700293 if should_omit_version(name, tags, self.arch, self.api, self.vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700294 return
295
Dan Albertae452cc2017-01-03 14:27:41 -0800296 section_versioned = symbol_versioned_in_api(tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700297 version_empty = True
298 pruned_symbols = []
299 for symbol in version.symbols:
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700300 if not self.vndk and 'vndk' in symbol.tags:
301 continue
Dan Albert8bdccb92016-07-29 13:06:22 -0700302 if not symbol_in_arch(symbol.tags, self.arch):
303 continue
304 if not symbol_in_api(symbol.tags, self.arch, self.api):
305 continue
306
307 if symbol_versioned_in_api(symbol.tags, self.api):
308 version_empty = False
309 pruned_symbols.append(symbol)
310
311 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800312 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700313 self.version_script.write(version.name + ' {\n')
314 self.version_script.write(' global:\n')
315 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800316 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
317 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700318 self.version_script.write(' ' + symbol.name + ';\n')
319
320 if 'var' in symbol.tags:
321 self.src_file.write('int {} = 0;\n'.format(symbol.name))
322 else:
323 self.src_file.write('void {}() {{}}\n'.format(symbol.name))
324
Dan Albertae452cc2017-01-03 14:27:41 -0800325 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700326 base = '' if version.base is None else ' ' + version.base
327 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700328
329
Dan Albert49927d22017-03-28 15:00:46 -0700330def decode_api_level(api, api_map):
331 """Decodes the API level argument into the API level number.
332
333 For the average case, this just decodes the integer value from the string,
334 but for unreleased APIs we need to translate from the API codename (like
335 "O") to the future API level for that codename.
336 """
337 try:
338 return int(api)
339 except ValueError:
340 pass
341
342 if api == "current":
343 return FUTURE_API_LEVEL
344
345 with open(api_map) as map_file:
346 api_levels = json.load(map_file)
347
348 return api_levels[api]
349
350
Dan Albert914449f2016-06-17 16:45:24 -0700351def parse_args():
352 """Parses and returns command line arguments."""
353 parser = argparse.ArgumentParser()
354
Dan Albert8bdccb92016-07-29 13:06:22 -0700355 parser.add_argument('-v', '--verbose', action='count', default=0)
356
Dan Albert914449f2016-06-17 16:45:24 -0700357 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700358 '--api', required=True, help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700359 parser.add_argument(
360 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700361 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700362 parser.add_argument(
363 '--vndk', action='store_true', help='Use the VNDK variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700364
365 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700366 '--api-map', type=os.path.realpath, required=True,
367 help='Path to the API level map JSON file.')
368
369 parser.add_argument(
Dan Albert914449f2016-06-17 16:45:24 -0700370 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
371 parser.add_argument(
372 'stub_src', type=os.path.realpath,
373 help='Path to output stub source file.')
374 parser.add_argument(
375 'version_script', type=os.path.realpath,
376 help='Path to output version script.')
377
378 return parser.parse_args()
379
380
381def main():
382 """Program entry point."""
383 args = parse_args()
384
Dan Albert49927d22017-03-28 15:00:46 -0700385 api = decode_api_level(args.api, args.api_map)
386
Dan Albert8bdccb92016-07-29 13:06:22 -0700387 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
388 verbosity = args.verbose
389 if verbosity > 2:
390 verbosity = 2
391 logging.basicConfig(level=verbose_map[verbosity])
392
Dan Albert914449f2016-06-17 16:45:24 -0700393 with open(args.symbol_file) as symbol_file:
Dan Albert8bdccb92016-07-29 13:06:22 -0700394 versions = SymbolFileParser(symbol_file).parse()
395
396 with open(args.stub_src, 'w') as src_file:
397 with open(args.version_script, 'w') as version_file:
Dan Albert49927d22017-03-28 15:00:46 -0700398 generator = Generator(src_file, version_file, args.arch, api,
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700399 args.vndk)
Dan Albert8bdccb92016-07-29 13:06:22 -0700400 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700401
402
403if __name__ == '__main__':
404 main()