blob: d6cd973ef3958b84b5694766f67b2c64ec21040f [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
Dan Albertae452cc2017-01-03 14:27:41 -0800292 section_versioned = symbol_versioned_in_api(tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700293 version_empty = True
294 pruned_symbols = []
295 for symbol in version.symbols:
296 if not symbol_in_arch(symbol.tags, self.arch):
297 continue
298 if not symbol_in_api(symbol.tags, self.arch, self.api):
299 continue
300
301 if symbol_versioned_in_api(symbol.tags, self.api):
302 version_empty = False
303 pruned_symbols.append(symbol)
304
305 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800306 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700307 self.version_script.write(version.name + ' {\n')
308 self.version_script.write(' global:\n')
309 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800310 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
311 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700312 self.version_script.write(' ' + symbol.name + ';\n')
313
314 if 'var' in symbol.tags:
315 self.src_file.write('int {} = 0;\n'.format(symbol.name))
316 else:
317 self.src_file.write('void {}() {{}}\n'.format(symbol.name))
318
Dan Albertae452cc2017-01-03 14:27:41 -0800319 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700320 base = '' if version.base is None else ' ' + version.base
321 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700322
323
324def parse_args():
325 """Parses and returns command line arguments."""
326 parser = argparse.ArgumentParser()
327
Dan Albert8bdccb92016-07-29 13:06:22 -0700328 parser.add_argument('-v', '--verbose', action='count', default=0)
329
Dan Albert914449f2016-06-17 16:45:24 -0700330 parser.add_argument(
Dan Albertfd86e9e2016-11-08 13:35:12 -0800331 '--api', type=api_level_arg, required=True,
332 help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700333 parser.add_argument(
334 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700335 help='Architecture being targeted.')
336
337 parser.add_argument(
338 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
339 parser.add_argument(
340 'stub_src', type=os.path.realpath,
341 help='Path to output stub source file.')
342 parser.add_argument(
343 'version_script', type=os.path.realpath,
344 help='Path to output version script.')
345
346 return parser.parse_args()
347
348
349def main():
350 """Program entry point."""
351 args = parse_args()
352
Dan Albert8bdccb92016-07-29 13:06:22 -0700353 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
354 verbosity = args.verbose
355 if verbosity > 2:
356 verbosity = 2
357 logging.basicConfig(level=verbose_map[verbosity])
358
Dan Albert914449f2016-06-17 16:45:24 -0700359 with open(args.symbol_file) as symbol_file:
Dan Albert8bdccb92016-07-29 13:06:22 -0700360 versions = SymbolFileParser(symbol_file).parse()
361
362 with open(args.stub_src, 'w') as src_file:
363 with open(args.version_script, 'w') as version_file:
364 generator = Generator(src_file, version_file, args.arch, args.api)
365 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700366
367
368if __name__ == '__main__':
369 main()