blob: 7c704068ed72fca67258bf885929f19507401c39 [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 Albert8bdccb92016-07-29 13:06:22 -070019import logging
Dan Albert914449f2016-06-17 16:45:24 -070020import os
21import re
22
23
24ALL_ARCHITECTURES = (
25 'arm',
26 'arm64',
27 'mips',
28 'mips64',
29 '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 Albertfd86e9e2016-11-08 13:35:12 -080043def api_level_arg(api_str):
44 """Parses an API level, handling the "current" special case.
45
46 Args:
47 api_str: (string) Either a numeric API level or "current".
48
49 Returns:
50 (int) FUTURE_API_LEVEL if `api_str` is "current", else `api_str` parsed
51 as an integer.
52 """
53 if api_str == "current":
54 return FUTURE_API_LEVEL
55 return int(api_str)
56
57
Dan Alberta85042a2016-07-28 16:58:27 -070058def get_tags(line):
59 """Returns a list of all tags on this line."""
60 _, _, all_tags = line.strip().partition('#')
Dan Albert8bdccb92016-07-29 13:06:22 -070061 return [e for e in re.split(r'\s+', all_tags) if e.strip()]
Dan Alberta85042a2016-07-28 16:58:27 -070062
63
Dan Albertc42458e2016-07-29 13:05:39 -070064def get_tag_value(tag):
65 """Returns the value of a key/value tag.
66
67 Raises:
68 ValueError: Tag is not a key/value type tag.
69
70 Returns: Value part of tag as a string.
71 """
72 if '=' not in tag:
73 raise ValueError('Not a key/value tag: ' + tag)
74 return tag.partition('=')[2]
75
76
Dan Albert914449f2016-06-17 16:45:24 -070077def version_is_private(version):
78 """Returns True if the version name should be treated as private."""
79 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
80
81
Dan Willemsenb01e7f72017-04-03 14:28:36 -070082def should_omit_version(name, tags, arch, api, vndk):
Dan Albert08532b62016-07-28 18:09:47 -070083 """Returns True if the version section should be ommitted.
84
85 We want to omit any sections that do not have any symbols we'll have in the
86 stub library. Sections that contain entirely future symbols or only symbols
87 for certain architectures.
88 """
89 if version_is_private(name):
90 return True
Dan Albert300cb2f2016-11-04 14:52:30 -070091 if 'platform-only' in tags:
92 return True
Dan Willemsenb01e7f72017-04-03 14:28:36 -070093 if 'vndk' in tags and not vndk:
94 return True
Dan Albert08532b62016-07-28 18:09:47 -070095 if not symbol_in_arch(tags, arch):
96 return True
Dan Albertc42458e2016-07-29 13:05:39 -070097 if not symbol_in_api(tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -070098 return True
99 return False
100
101
Dan Albert914449f2016-06-17 16:45:24 -0700102def symbol_in_arch(tags, arch):
103 """Returns true if the symbol is present for the given architecture."""
104 has_arch_tags = False
105 for tag in tags:
106 if tag == arch:
107 return True
108 if tag in ALL_ARCHITECTURES:
109 has_arch_tags = True
110
111 # If there were no arch tags, the symbol is available for all
112 # architectures. If there were any arch tags, the symbol is only available
113 # for the tagged architectures.
114 return not has_arch_tags
115
116
Dan Albertc42458e2016-07-29 13:05:39 -0700117def symbol_in_api(tags, arch, api):
118 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700119 introduced_tag = None
120 arch_specific = False
121 for tag in tags:
122 # If there is an arch-specific tag, it should override the common one.
123 if tag.startswith('introduced=') and not arch_specific:
124 introduced_tag = tag
125 elif tag.startswith('introduced-' + arch + '='):
126 introduced_tag = tag
127 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700128 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800129 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700130
131 if introduced_tag is None:
132 # We found no "introduced" tags, so the symbol has always been
133 # available.
134 return True
135
Dan Albertc42458e2016-07-29 13:05:39 -0700136 return api >= int(get_tag_value(introduced_tag))
137
138
139def symbol_versioned_in_api(tags, api):
140 """Returns true if the symbol should be versioned for the given API.
141
142 This models the `versioned=API` tag. This should be a very uncommonly
143 needed tag, and is really only needed to fix versioning mistakes that are
144 already out in the wild.
145
146 For example, some of libc's __aeabi_* functions were originally placed in
147 the private version, but that was incorrect. They are now in LIBC_N, but
148 when building against any version prior to N we need the symbol to be
149 unversioned (otherwise it won't resolve on M where it is private).
150 """
151 for tag in tags:
152 if tag.startswith('versioned='):
153 return api >= int(get_tag_value(tag))
154 # If there is no "versioned" tag, the tag has been versioned for as long as
155 # it was introduced.
156 return True
157
Dan Albert914449f2016-06-17 16:45:24 -0700158
Dan Albert8bdccb92016-07-29 13:06:22 -0700159class ParseError(RuntimeError):
160 """An error that occurred while parsing a symbol file."""
161 pass
Dan Albert914449f2016-06-17 16:45:24 -0700162
163
Dan Albert8bdccb92016-07-29 13:06:22 -0700164class Version(object):
165 """A version block of a symbol file."""
166 def __init__(self, name, base, tags, symbols):
167 self.name = name
168 self.base = base
169 self.tags = tags
170 self.symbols = symbols
171
172 def __eq__(self, other):
173 if self.name != other.name:
174 return False
175 if self.base != other.base:
176 return False
177 if self.tags != other.tags:
178 return False
179 if self.symbols != other.symbols:
180 return False
181 return True
182
183
184class Symbol(object):
185 """A symbol definition from a symbol file."""
186 def __init__(self, name, tags):
187 self.name = name
188 self.tags = tags
189
190 def __eq__(self, other):
191 return self.name == other.name and set(self.tags) == set(other.tags)
192
193
194class SymbolFileParser(object):
195 """Parses NDK symbol files."""
196 def __init__(self, input_file):
197 self.input_file = input_file
198 self.current_line = None
199
200 def parse(self):
201 """Parses the symbol file and returns a list of Version objects."""
202 versions = []
203 while self.next_line() != '':
204 if '{' in self.current_line:
205 versions.append(self.parse_version())
206 else:
207 raise ParseError(
208 'Unexpected contents at top level: ' + self.current_line)
209 return versions
210
211 def parse_version(self):
212 """Parses a single version section and returns a Version object."""
213 name = self.current_line.split('{')[0].strip()
214 tags = get_tags(self.current_line)
215 symbols = []
216 global_scope = True
217 while self.next_line() != '':
218 if '}' in self.current_line:
219 # Line is something like '} BASE; # tags'. Both base and tags
220 # are optional here.
221 base = self.current_line.partition('}')[2]
222 base = base.partition('#')[0].strip()
223 if not base.endswith(';'):
224 raise ParseError(
225 'Unterminated version block (expected ;).')
226 base = base.rstrip(';').rstrip()
227 if base == '':
228 base = None
229 return Version(name, base, tags, symbols)
230 elif ':' in self.current_line:
231 visibility = self.current_line.split(':')[0].strip()
232 if visibility == 'local':
233 global_scope = False
234 elif visibility == 'global':
235 global_scope = True
236 else:
237 raise ParseError('Unknown visiblity label: ' + visibility)
238 elif global_scope:
239 symbols.append(self.parse_symbol())
240 else:
241 # We're in a hidden scope. Ignore everything.
242 pass
243 raise ParseError('Unexpected EOF in version block.')
244
245 def parse_symbol(self):
246 """Parses a single symbol line and returns a Symbol object."""
247 if ';' not in self.current_line:
248 raise ParseError(
249 'Expected ; to terminate symbol: ' + self.current_line)
250 if '*' in self.current_line:
251 raise ParseError(
252 'Wildcard global symbols are not permitted.')
253 # Line is now in the format "<symbol-name>; # tags"
254 name, _, _ = self.current_line.strip().partition(';')
255 tags = get_tags(self.current_line)
256 return Symbol(name, tags)
257
258 def next_line(self):
259 """Returns the next non-empty non-comment line.
260
261 A return value of '' indicates EOF.
262 """
263 line = self.input_file.readline()
264 while line.strip() == '' or line.strip().startswith('#'):
265 line = self.input_file.readline()
266
267 # We want to skip empty lines, but '' indicates EOF.
268 if line == '':
269 break
270 self.current_line = line
271 return self.current_line
272
273
274class Generator(object):
275 """Output generator that writes stub source files and version scripts."""
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700276 def __init__(self, src_file, version_script, arch, api, vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700277 self.src_file = src_file
278 self.version_script = version_script
279 self.arch = arch
280 self.api = api
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700281 self.vndk = vndk
Dan Albert8bdccb92016-07-29 13:06:22 -0700282
283 def write(self, versions):
284 """Writes all symbol data to the output files."""
285 for version in versions:
286 self.write_version(version)
287
288 def write_version(self, version):
289 """Writes a single version block's data to the output files."""
290 name = version.name
291 tags = version.tags
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700292 if should_omit_version(name, tags, self.arch, self.api, self.vndk):
Dan Albert8bdccb92016-07-29 13:06:22 -0700293 return
294
Dan Albertae452cc2017-01-03 14:27:41 -0800295 section_versioned = symbol_versioned_in_api(tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700296 version_empty = True
297 pruned_symbols = []
298 for symbol in version.symbols:
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700299 if not self.vndk and 'vndk' in symbol.tags:
300 continue
Dan Albert8bdccb92016-07-29 13:06:22 -0700301 if not symbol_in_arch(symbol.tags, self.arch):
302 continue
303 if not symbol_in_api(symbol.tags, self.arch, self.api):
304 continue
305
306 if symbol_versioned_in_api(symbol.tags, self.api):
307 version_empty = False
308 pruned_symbols.append(symbol)
309
310 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800311 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700312 self.version_script.write(version.name + ' {\n')
313 self.version_script.write(' global:\n')
314 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800315 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
316 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700317 self.version_script.write(' ' + symbol.name + ';\n')
318
319 if 'var' in symbol.tags:
320 self.src_file.write('int {} = 0;\n'.format(symbol.name))
321 else:
322 self.src_file.write('void {}() {{}}\n'.format(symbol.name))
323
Dan Albertae452cc2017-01-03 14:27:41 -0800324 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700325 base = '' if version.base is None else ' ' + version.base
326 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700327
328
329def parse_args():
330 """Parses and returns command line arguments."""
331 parser = argparse.ArgumentParser()
332
Dan Albert8bdccb92016-07-29 13:06:22 -0700333 parser.add_argument('-v', '--verbose', action='count', default=0)
334
Dan Albert914449f2016-06-17 16:45:24 -0700335 parser.add_argument(
Dan Albertfd86e9e2016-11-08 13:35:12 -0800336 '--api', type=api_level_arg, required=True,
337 help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700338 parser.add_argument(
339 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700340 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700341 parser.add_argument(
342 '--vndk', action='store_true', help='Use the VNDK variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700343
344 parser.add_argument(
345 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
346 parser.add_argument(
347 'stub_src', type=os.path.realpath,
348 help='Path to output stub source file.')
349 parser.add_argument(
350 'version_script', type=os.path.realpath,
351 help='Path to output version script.')
352
353 return parser.parse_args()
354
355
356def main():
357 """Program entry point."""
358 args = parse_args()
359
Dan Albert8bdccb92016-07-29 13:06:22 -0700360 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
361 verbosity = args.verbose
362 if verbosity > 2:
363 verbosity = 2
364 logging.basicConfig(level=verbose_map[verbosity])
365
Dan Albert914449f2016-06-17 16:45:24 -0700366 with open(args.symbol_file) as symbol_file:
Dan Albert8bdccb92016-07-29 13:06:22 -0700367 versions = SymbolFileParser(symbol_file).parse()
368
369 with open(args.stub_src, 'w') as src_file:
370 with open(args.version_script, 'w') as version_file:
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700371 generator = Generator(src_file, version_file, args.arch, args.api,
372 args.vndk)
Dan Albert8bdccb92016-07-29 13:06:22 -0700373 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700374
375
376if __name__ == '__main__':
377 main()