blob: eb233f8fe1c355c7ce2101188fd806d6822952b7 [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 Albert08532b62016-07-28 18:09:47 -070082def should_omit_version(name, tags, arch, api):
83 """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 Albert08532b62016-07-28 18:09:47 -070093 if not symbol_in_arch(tags, arch):
94 return True
Dan Albertc42458e2016-07-29 13:05:39 -070095 if not symbol_in_api(tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -070096 return True
97 return False
98
99
Dan Albert914449f2016-06-17 16:45:24 -0700100def symbol_in_arch(tags, arch):
101 """Returns true if the symbol is present for the given architecture."""
102 has_arch_tags = False
103 for tag in tags:
104 if tag == arch:
105 return True
106 if tag in ALL_ARCHITECTURES:
107 has_arch_tags = True
108
109 # If there were no arch tags, the symbol is available for all
110 # architectures. If there were any arch tags, the symbol is only available
111 # for the tagged architectures.
112 return not has_arch_tags
113
114
Dan Albertc42458e2016-07-29 13:05:39 -0700115def symbol_in_api(tags, arch, api):
116 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700117 introduced_tag = None
118 arch_specific = False
119 for tag in tags:
120 # If there is an arch-specific tag, it should override the common one.
121 if tag.startswith('introduced=') and not arch_specific:
122 introduced_tag = tag
123 elif tag.startswith('introduced-' + arch + '='):
124 introduced_tag = tag
125 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700126 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800127 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700128
129 if introduced_tag is None:
130 # We found no "introduced" tags, so the symbol has always been
131 # available.
132 return True
133
Dan Albertc42458e2016-07-29 13:05:39 -0700134 return api >= int(get_tag_value(introduced_tag))
135
136
137def symbol_versioned_in_api(tags, api):
138 """Returns true if the symbol should be versioned for the given API.
139
140 This models the `versioned=API` tag. This should be a very uncommonly
141 needed tag, and is really only needed to fix versioning mistakes that are
142 already out in the wild.
143
144 For example, some of libc's __aeabi_* functions were originally placed in
145 the private version, but that was incorrect. They are now in LIBC_N, but
146 when building against any version prior to N we need the symbol to be
147 unversioned (otherwise it won't resolve on M where it is private).
148 """
149 for tag in tags:
150 if tag.startswith('versioned='):
151 return api >= int(get_tag_value(tag))
152 # If there is no "versioned" tag, the tag has been versioned for as long as
153 # it was introduced.
154 return True
155
Dan Albert914449f2016-06-17 16:45:24 -0700156
Dan Albert8bdccb92016-07-29 13:06:22 -0700157class ParseError(RuntimeError):
158 """An error that occurred while parsing a symbol file."""
159 pass
Dan Albert914449f2016-06-17 16:45:24 -0700160
161
Dan Albert8bdccb92016-07-29 13:06:22 -0700162class Version(object):
163 """A version block of a symbol file."""
164 def __init__(self, name, base, tags, symbols):
165 self.name = name
166 self.base = base
167 self.tags = tags
168 self.symbols = symbols
169
170 def __eq__(self, other):
171 if self.name != other.name:
172 return False
173 if self.base != other.base:
174 return False
175 if self.tags != other.tags:
176 return False
177 if self.symbols != other.symbols:
178 return False
179 return True
180
181
182class Symbol(object):
183 """A symbol definition from a symbol file."""
184 def __init__(self, name, tags):
185 self.name = name
186 self.tags = tags
187
188 def __eq__(self, other):
189 return self.name == other.name and set(self.tags) == set(other.tags)
190
191
192class SymbolFileParser(object):
193 """Parses NDK symbol files."""
194 def __init__(self, input_file):
195 self.input_file = input_file
196 self.current_line = None
197
198 def parse(self):
199 """Parses the symbol file and returns a list of Version objects."""
200 versions = []
201 while self.next_line() != '':
202 if '{' in self.current_line:
203 versions.append(self.parse_version())
204 else:
205 raise ParseError(
206 'Unexpected contents at top level: ' + self.current_line)
207 return versions
208
209 def parse_version(self):
210 """Parses a single version section and returns a Version object."""
211 name = self.current_line.split('{')[0].strip()
212 tags = get_tags(self.current_line)
213 symbols = []
214 global_scope = True
215 while self.next_line() != '':
216 if '}' in self.current_line:
217 # Line is something like '} BASE; # tags'. Both base and tags
218 # are optional here.
219 base = self.current_line.partition('}')[2]
220 base = base.partition('#')[0].strip()
221 if not base.endswith(';'):
222 raise ParseError(
223 'Unterminated version block (expected ;).')
224 base = base.rstrip(';').rstrip()
225 if base == '':
226 base = None
227 return Version(name, base, tags, symbols)
228 elif ':' in self.current_line:
229 visibility = self.current_line.split(':')[0].strip()
230 if visibility == 'local':
231 global_scope = False
232 elif visibility == 'global':
233 global_scope = True
234 else:
235 raise ParseError('Unknown visiblity label: ' + visibility)
236 elif global_scope:
237 symbols.append(self.parse_symbol())
238 else:
239 # We're in a hidden scope. Ignore everything.
240 pass
241 raise ParseError('Unexpected EOF in version block.')
242
243 def parse_symbol(self):
244 """Parses a single symbol line and returns a Symbol object."""
245 if ';' not in self.current_line:
246 raise ParseError(
247 'Expected ; to terminate symbol: ' + self.current_line)
248 if '*' in self.current_line:
249 raise ParseError(
250 'Wildcard global symbols are not permitted.')
251 # Line is now in the format "<symbol-name>; # tags"
252 name, _, _ = self.current_line.strip().partition(';')
253 tags = get_tags(self.current_line)
254 return Symbol(name, tags)
255
256 def next_line(self):
257 """Returns the next non-empty non-comment line.
258
259 A return value of '' indicates EOF.
260 """
261 line = self.input_file.readline()
262 while line.strip() == '' or line.strip().startswith('#'):
263 line = self.input_file.readline()
264
265 # We want to skip empty lines, but '' indicates EOF.
266 if line == '':
267 break
268 self.current_line = line
269 return self.current_line
270
271
272class Generator(object):
273 """Output generator that writes stub source files and version scripts."""
274 def __init__(self, src_file, version_script, arch, api):
275 self.src_file = src_file
276 self.version_script = version_script
277 self.arch = arch
278 self.api = api
279
280 def write(self, versions):
281 """Writes all symbol data to the output files."""
282 for version in versions:
283 self.write_version(version)
284
285 def write_version(self, version):
286 """Writes a single version block's data to the output files."""
287 name = version.name
288 tags = version.tags
289 if should_omit_version(name, tags, self.arch, self.api):
290 return
291
292 version_empty = True
293 pruned_symbols = []
294 for symbol in version.symbols:
295 if not symbol_in_arch(symbol.tags, self.arch):
296 continue
297 if not symbol_in_api(symbol.tags, self.arch, self.api):
298 continue
299
300 if symbol_versioned_in_api(symbol.tags, self.api):
301 version_empty = False
302 pruned_symbols.append(symbol)
303
304 if len(pruned_symbols) > 0:
305 if not version_empty:
306 self.version_script.write(version.name + ' {\n')
307 self.version_script.write(' global:\n')
308 for symbol in pruned_symbols:
309 if symbol_versioned_in_api(symbol.tags, self.api):
310 self.version_script.write(' ' + symbol.name + ';\n')
311
312 if 'var' in symbol.tags:
313 self.src_file.write('int {} = 0;\n'.format(symbol.name))
314 else:
315 self.src_file.write('void {}() {{}}\n'.format(symbol.name))
316
317 if not version_empty:
318 base = '' if version.base is None else ' ' + version.base
319 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700320
321
322def parse_args():
323 """Parses and returns command line arguments."""
324 parser = argparse.ArgumentParser()
325
Dan Albert8bdccb92016-07-29 13:06:22 -0700326 parser.add_argument('-v', '--verbose', action='count', default=0)
327
Dan Albert914449f2016-06-17 16:45:24 -0700328 parser.add_argument(
Dan Albertfd86e9e2016-11-08 13:35:12 -0800329 '--api', type=api_level_arg, required=True,
330 help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700331 parser.add_argument(
332 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700333 help='Architecture being targeted.')
334
335 parser.add_argument(
336 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
337 parser.add_argument(
338 'stub_src', type=os.path.realpath,
339 help='Path to output stub source file.')
340 parser.add_argument(
341 'version_script', type=os.path.realpath,
342 help='Path to output version script.')
343
344 return parser.parse_args()
345
346
347def main():
348 """Program entry point."""
349 args = parse_args()
350
Dan Albert8bdccb92016-07-29 13:06:22 -0700351 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
352 verbosity = args.verbose
353 if verbosity > 2:
354 verbosity = 2
355 logging.basicConfig(level=verbose_map[verbosity])
356
Dan Albert914449f2016-06-17 16:45:24 -0700357 with open(args.symbol_file) as symbol_file:
Dan Albert8bdccb92016-07-29 13:06:22 -0700358 versions = SymbolFileParser(symbol_file).parse()
359
360 with open(args.stub_src, 'w') as src_file:
361 with open(args.version_script, 'w') as version_file:
362 generator = Generator(src_file, version_file, args.arch, args.api)
363 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700364
365
366if __name__ == '__main__':
367 main()