blob: 2db8312e036a28aaf97d610638863b70e62b60b0 [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 Albert8bdccb92016-07-29 13:06:22 -070034def logger():
35 """Return the main logger for this module."""
36 return logging.getLogger(__name__)
Dan Albert914449f2016-06-17 16:45:24 -070037
38
Dan Alberta85042a2016-07-28 16:58:27 -070039def get_tags(line):
40 """Returns a list of all tags on this line."""
41 _, _, all_tags = line.strip().partition('#')
Dan Albert8bdccb92016-07-29 13:06:22 -070042 return [e for e in re.split(r'\s+', all_tags) if e.strip()]
Dan Alberta85042a2016-07-28 16:58:27 -070043
44
Dan Albertc42458e2016-07-29 13:05:39 -070045def get_tag_value(tag):
46 """Returns the value of a key/value tag.
47
48 Raises:
49 ValueError: Tag is not a key/value type tag.
50
51 Returns: Value part of tag as a string.
52 """
53 if '=' not in tag:
54 raise ValueError('Not a key/value tag: ' + tag)
55 return tag.partition('=')[2]
56
57
Dan Albert914449f2016-06-17 16:45:24 -070058def version_is_private(version):
59 """Returns True if the version name should be treated as private."""
60 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
61
62
Dan Albert08532b62016-07-28 18:09:47 -070063def should_omit_version(name, tags, arch, api):
64 """Returns True if the version section should be ommitted.
65
66 We want to omit any sections that do not have any symbols we'll have in the
67 stub library. Sections that contain entirely future symbols or only symbols
68 for certain architectures.
69 """
70 if version_is_private(name):
71 return True
Dan Albert300cb2f2016-11-04 14:52:30 -070072 if 'platform-only' in tags:
73 return True
Dan Albert08532b62016-07-28 18:09:47 -070074 if not symbol_in_arch(tags, arch):
75 return True
Dan Albertc42458e2016-07-29 13:05:39 -070076 if not symbol_in_api(tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -070077 return True
78 return False
79
80
Dan Albert914449f2016-06-17 16:45:24 -070081def symbol_in_arch(tags, arch):
82 """Returns true if the symbol is present for the given architecture."""
83 has_arch_tags = False
84 for tag in tags:
85 if tag == arch:
86 return True
87 if tag in ALL_ARCHITECTURES:
88 has_arch_tags = True
89
90 # If there were no arch tags, the symbol is available for all
91 # architectures. If there were any arch tags, the symbol is only available
92 # for the tagged architectures.
93 return not has_arch_tags
94
95
Dan Albertc42458e2016-07-29 13:05:39 -070096def symbol_in_api(tags, arch, api):
97 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -070098 introduced_tag = None
99 arch_specific = False
100 for tag in tags:
101 # If there is an arch-specific tag, it should override the common one.
102 if tag.startswith('introduced=') and not arch_specific:
103 introduced_tag = tag
104 elif tag.startswith('introduced-' + arch + '='):
105 introduced_tag = tag
106 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700107 elif tag == 'future':
108 # This symbol is not in any released API level.
Dan Albertc42458e2016-07-29 13:05:39 -0700109 # TODO(danalbert): These need to be emitted for api == current.
Dan Alberta85042a2016-07-28 16:58:27 -0700110 # That's not a construct we have yet, so just skip it for now.
111 return False
Dan Albert914449f2016-06-17 16:45:24 -0700112
113 if introduced_tag is None:
114 # We found no "introduced" tags, so the symbol has always been
115 # available.
116 return True
117
Dan Albertc42458e2016-07-29 13:05:39 -0700118 return api >= int(get_tag_value(introduced_tag))
119
120
121def symbol_versioned_in_api(tags, api):
122 """Returns true if the symbol should be versioned for the given API.
123
124 This models the `versioned=API` tag. This should be a very uncommonly
125 needed tag, and is really only needed to fix versioning mistakes that are
126 already out in the wild.
127
128 For example, some of libc's __aeabi_* functions were originally placed in
129 the private version, but that was incorrect. They are now in LIBC_N, but
130 when building against any version prior to N we need the symbol to be
131 unversioned (otherwise it won't resolve on M where it is private).
132 """
133 for tag in tags:
134 if tag.startswith('versioned='):
135 return api >= int(get_tag_value(tag))
136 # If there is no "versioned" tag, the tag has been versioned for as long as
137 # it was introduced.
138 return True
139
Dan Albert914449f2016-06-17 16:45:24 -0700140
Dan Albert8bdccb92016-07-29 13:06:22 -0700141class ParseError(RuntimeError):
142 """An error that occurred while parsing a symbol file."""
143 pass
Dan Albert914449f2016-06-17 16:45:24 -0700144
145
Dan Albert8bdccb92016-07-29 13:06:22 -0700146class Version(object):
147 """A version block of a symbol file."""
148 def __init__(self, name, base, tags, symbols):
149 self.name = name
150 self.base = base
151 self.tags = tags
152 self.symbols = symbols
153
154 def __eq__(self, other):
155 if self.name != other.name:
156 return False
157 if self.base != other.base:
158 return False
159 if self.tags != other.tags:
160 return False
161 if self.symbols != other.symbols:
162 return False
163 return True
164
165
166class Symbol(object):
167 """A symbol definition from a symbol file."""
168 def __init__(self, name, tags):
169 self.name = name
170 self.tags = tags
171
172 def __eq__(self, other):
173 return self.name == other.name and set(self.tags) == set(other.tags)
174
175
176class SymbolFileParser(object):
177 """Parses NDK symbol files."""
178 def __init__(self, input_file):
179 self.input_file = input_file
180 self.current_line = None
181
182 def parse(self):
183 """Parses the symbol file and returns a list of Version objects."""
184 versions = []
185 while self.next_line() != '':
186 if '{' in self.current_line:
187 versions.append(self.parse_version())
188 else:
189 raise ParseError(
190 'Unexpected contents at top level: ' + self.current_line)
191 return versions
192
193 def parse_version(self):
194 """Parses a single version section and returns a Version object."""
195 name = self.current_line.split('{')[0].strip()
196 tags = get_tags(self.current_line)
197 symbols = []
198 global_scope = True
199 while self.next_line() != '':
200 if '}' in self.current_line:
201 # Line is something like '} BASE; # tags'. Both base and tags
202 # are optional here.
203 base = self.current_line.partition('}')[2]
204 base = base.partition('#')[0].strip()
205 if not base.endswith(';'):
206 raise ParseError(
207 'Unterminated version block (expected ;).')
208 base = base.rstrip(';').rstrip()
209 if base == '':
210 base = None
211 return Version(name, base, tags, symbols)
212 elif ':' in self.current_line:
213 visibility = self.current_line.split(':')[0].strip()
214 if visibility == 'local':
215 global_scope = False
216 elif visibility == 'global':
217 global_scope = True
218 else:
219 raise ParseError('Unknown visiblity label: ' + visibility)
220 elif global_scope:
221 symbols.append(self.parse_symbol())
222 else:
223 # We're in a hidden scope. Ignore everything.
224 pass
225 raise ParseError('Unexpected EOF in version block.')
226
227 def parse_symbol(self):
228 """Parses a single symbol line and returns a Symbol object."""
229 if ';' not in self.current_line:
230 raise ParseError(
231 'Expected ; to terminate symbol: ' + self.current_line)
232 if '*' in self.current_line:
233 raise ParseError(
234 'Wildcard global symbols are not permitted.')
235 # Line is now in the format "<symbol-name>; # tags"
236 name, _, _ = self.current_line.strip().partition(';')
237 tags = get_tags(self.current_line)
238 return Symbol(name, tags)
239
240 def next_line(self):
241 """Returns the next non-empty non-comment line.
242
243 A return value of '' indicates EOF.
244 """
245 line = self.input_file.readline()
246 while line.strip() == '' or line.strip().startswith('#'):
247 line = self.input_file.readline()
248
249 # We want to skip empty lines, but '' indicates EOF.
250 if line == '':
251 break
252 self.current_line = line
253 return self.current_line
254
255
256class Generator(object):
257 """Output generator that writes stub source files and version scripts."""
258 def __init__(self, src_file, version_script, arch, api):
259 self.src_file = src_file
260 self.version_script = version_script
261 self.arch = arch
262 self.api = api
263
264 def write(self, versions):
265 """Writes all symbol data to the output files."""
266 for version in versions:
267 self.write_version(version)
268
269 def write_version(self, version):
270 """Writes a single version block's data to the output files."""
271 name = version.name
272 tags = version.tags
273 if should_omit_version(name, tags, self.arch, self.api):
274 return
275
276 version_empty = True
277 pruned_symbols = []
278 for symbol in version.symbols:
279 if not symbol_in_arch(symbol.tags, self.arch):
280 continue
281 if not symbol_in_api(symbol.tags, self.arch, self.api):
282 continue
283
284 if symbol_versioned_in_api(symbol.tags, self.api):
285 version_empty = False
286 pruned_symbols.append(symbol)
287
288 if len(pruned_symbols) > 0:
289 if not version_empty:
290 self.version_script.write(version.name + ' {\n')
291 self.version_script.write(' global:\n')
292 for symbol in pruned_symbols:
293 if symbol_versioned_in_api(symbol.tags, self.api):
294 self.version_script.write(' ' + symbol.name + ';\n')
295
296 if 'var' in symbol.tags:
297 self.src_file.write('int {} = 0;\n'.format(symbol.name))
298 else:
299 self.src_file.write('void {}() {{}}\n'.format(symbol.name))
300
301 if not version_empty:
302 base = '' if version.base is None else ' ' + version.base
303 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700304
305
306def parse_args():
307 """Parses and returns command line arguments."""
308 parser = argparse.ArgumentParser()
309
Dan Albert8bdccb92016-07-29 13:06:22 -0700310 parser.add_argument('-v', '--verbose', action='count', default=0)
311
Dan Albert914449f2016-06-17 16:45:24 -0700312 parser.add_argument(
Dan Albert8bdccb92016-07-29 13:06:22 -0700313 '--api', type=int, required=True, help='API level being targeted.')
314 parser.add_argument(
315 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700316 help='Architecture being targeted.')
317
318 parser.add_argument(
319 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
320 parser.add_argument(
321 'stub_src', type=os.path.realpath,
322 help='Path to output stub source file.')
323 parser.add_argument(
324 'version_script', type=os.path.realpath,
325 help='Path to output version script.')
326
327 return parser.parse_args()
328
329
330def main():
331 """Program entry point."""
332 args = parse_args()
333
Dan Albert8bdccb92016-07-29 13:06:22 -0700334 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
335 verbosity = args.verbose
336 if verbosity > 2:
337 verbosity = 2
338 logging.basicConfig(level=verbose_map[verbosity])
339
Dan Albert914449f2016-06-17 16:45:24 -0700340 with open(args.symbol_file) as symbol_file:
Dan Albert8bdccb92016-07-29 13:06:22 -0700341 versions = SymbolFileParser(symbol_file).parse()
342
343 with open(args.stub_src, 'w') as src_file:
344 with open(args.version_script, 'w') as version_file:
345 generator = Generator(src_file, version_file, args.arch, args.api)
346 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700347
348
349if __name__ == '__main__':
350 main()