blob: 9b40415d5ee0d7b50fd9acd3e5c258f984f1b21d [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
72 if not symbol_in_arch(tags, arch):
73 return True
Dan Albertc42458e2016-07-29 13:05:39 -070074 if not symbol_in_api(tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -070075 return True
76 return False
77
78
Dan Albert914449f2016-06-17 16:45:24 -070079def symbol_in_arch(tags, arch):
80 """Returns true if the symbol is present for the given architecture."""
81 has_arch_tags = False
82 for tag in tags:
83 if tag == arch:
84 return True
85 if tag in ALL_ARCHITECTURES:
86 has_arch_tags = True
87
88 # If there were no arch tags, the symbol is available for all
89 # architectures. If there were any arch tags, the symbol is only available
90 # for the tagged architectures.
91 return not has_arch_tags
92
93
Dan Albertc42458e2016-07-29 13:05:39 -070094def symbol_in_api(tags, arch, api):
95 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -070096 introduced_tag = None
97 arch_specific = False
98 for tag in tags:
99 # If there is an arch-specific tag, it should override the common one.
100 if tag.startswith('introduced=') and not arch_specific:
101 introduced_tag = tag
102 elif tag.startswith('introduced-' + arch + '='):
103 introduced_tag = tag
104 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700105 elif tag == 'future':
106 # This symbol is not in any released API level.
Dan Albertc42458e2016-07-29 13:05:39 -0700107 # TODO(danalbert): These need to be emitted for api == current.
Dan Alberta85042a2016-07-28 16:58:27 -0700108 # That's not a construct we have yet, so just skip it for now.
109 return False
Dan Albert914449f2016-06-17 16:45:24 -0700110
111 if introduced_tag is None:
112 # We found no "introduced" tags, so the symbol has always been
113 # available.
114 return True
115
Dan Albertc42458e2016-07-29 13:05:39 -0700116 return api >= int(get_tag_value(introduced_tag))
117
118
119def symbol_versioned_in_api(tags, api):
120 """Returns true if the symbol should be versioned for the given API.
121
122 This models the `versioned=API` tag. This should be a very uncommonly
123 needed tag, and is really only needed to fix versioning mistakes that are
124 already out in the wild.
125
126 For example, some of libc's __aeabi_* functions were originally placed in
127 the private version, but that was incorrect. They are now in LIBC_N, but
128 when building against any version prior to N we need the symbol to be
129 unversioned (otherwise it won't resolve on M where it is private).
130 """
131 for tag in tags:
132 if tag.startswith('versioned='):
133 return api >= int(get_tag_value(tag))
134 # If there is no "versioned" tag, the tag has been versioned for as long as
135 # it was introduced.
136 return True
137
Dan Albert914449f2016-06-17 16:45:24 -0700138
Dan Albert8bdccb92016-07-29 13:06:22 -0700139class ParseError(RuntimeError):
140 """An error that occurred while parsing a symbol file."""
141 pass
Dan Albert914449f2016-06-17 16:45:24 -0700142
143
Dan Albert8bdccb92016-07-29 13:06:22 -0700144class Version(object):
145 """A version block of a symbol file."""
146 def __init__(self, name, base, tags, symbols):
147 self.name = name
148 self.base = base
149 self.tags = tags
150 self.symbols = symbols
151
152 def __eq__(self, other):
153 if self.name != other.name:
154 return False
155 if self.base != other.base:
156 return False
157 if self.tags != other.tags:
158 return False
159 if self.symbols != other.symbols:
160 return False
161 return True
162
163
164class Symbol(object):
165 """A symbol definition from a symbol file."""
166 def __init__(self, name, tags):
167 self.name = name
168 self.tags = tags
169
170 def __eq__(self, other):
171 return self.name == other.name and set(self.tags) == set(other.tags)
172
173
174class SymbolFileParser(object):
175 """Parses NDK symbol files."""
176 def __init__(self, input_file):
177 self.input_file = input_file
178 self.current_line = None
179
180 def parse(self):
181 """Parses the symbol file and returns a list of Version objects."""
182 versions = []
183 while self.next_line() != '':
184 if '{' in self.current_line:
185 versions.append(self.parse_version())
186 else:
187 raise ParseError(
188 'Unexpected contents at top level: ' + self.current_line)
189 return versions
190
191 def parse_version(self):
192 """Parses a single version section and returns a Version object."""
193 name = self.current_line.split('{')[0].strip()
194 tags = get_tags(self.current_line)
195 symbols = []
196 global_scope = True
197 while self.next_line() != '':
198 if '}' in self.current_line:
199 # Line is something like '} BASE; # tags'. Both base and tags
200 # are optional here.
201 base = self.current_line.partition('}')[2]
202 base = base.partition('#')[0].strip()
203 if not base.endswith(';'):
204 raise ParseError(
205 'Unterminated version block (expected ;).')
206 base = base.rstrip(';').rstrip()
207 if base == '':
208 base = None
209 return Version(name, base, tags, symbols)
210 elif ':' in self.current_line:
211 visibility = self.current_line.split(':')[0].strip()
212 if visibility == 'local':
213 global_scope = False
214 elif visibility == 'global':
215 global_scope = True
216 else:
217 raise ParseError('Unknown visiblity label: ' + visibility)
218 elif global_scope:
219 symbols.append(self.parse_symbol())
220 else:
221 # We're in a hidden scope. Ignore everything.
222 pass
223 raise ParseError('Unexpected EOF in version block.')
224
225 def parse_symbol(self):
226 """Parses a single symbol line and returns a Symbol object."""
227 if ';' not in self.current_line:
228 raise ParseError(
229 'Expected ; to terminate symbol: ' + self.current_line)
230 if '*' in self.current_line:
231 raise ParseError(
232 'Wildcard global symbols are not permitted.')
233 # Line is now in the format "<symbol-name>; # tags"
234 name, _, _ = self.current_line.strip().partition(';')
235 tags = get_tags(self.current_line)
236 return Symbol(name, tags)
237
238 def next_line(self):
239 """Returns the next non-empty non-comment line.
240
241 A return value of '' indicates EOF.
242 """
243 line = self.input_file.readline()
244 while line.strip() == '' or line.strip().startswith('#'):
245 line = self.input_file.readline()
246
247 # We want to skip empty lines, but '' indicates EOF.
248 if line == '':
249 break
250 self.current_line = line
251 return self.current_line
252
253
254class Generator(object):
255 """Output generator that writes stub source files and version scripts."""
256 def __init__(self, src_file, version_script, arch, api):
257 self.src_file = src_file
258 self.version_script = version_script
259 self.arch = arch
260 self.api = api
261
262 def write(self, versions):
263 """Writes all symbol data to the output files."""
264 for version in versions:
265 self.write_version(version)
266
267 def write_version(self, version):
268 """Writes a single version block's data to the output files."""
269 name = version.name
270 tags = version.tags
271 if should_omit_version(name, tags, self.arch, self.api):
272 return
273
274 version_empty = True
275 pruned_symbols = []
276 for symbol in version.symbols:
277 if not symbol_in_arch(symbol.tags, self.arch):
278 continue
279 if not symbol_in_api(symbol.tags, self.arch, self.api):
280 continue
281
282 if symbol_versioned_in_api(symbol.tags, self.api):
283 version_empty = False
284 pruned_symbols.append(symbol)
285
286 if len(pruned_symbols) > 0:
287 if not version_empty:
288 self.version_script.write(version.name + ' {\n')
289 self.version_script.write(' global:\n')
290 for symbol in pruned_symbols:
291 if symbol_versioned_in_api(symbol.tags, self.api):
292 self.version_script.write(' ' + symbol.name + ';\n')
293
294 if 'var' in symbol.tags:
295 self.src_file.write('int {} = 0;\n'.format(symbol.name))
296 else:
297 self.src_file.write('void {}() {{}}\n'.format(symbol.name))
298
299 if not version_empty:
300 base = '' if version.base is None else ' ' + version.base
301 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700302
303
304def parse_args():
305 """Parses and returns command line arguments."""
306 parser = argparse.ArgumentParser()
307
Dan Albert8bdccb92016-07-29 13:06:22 -0700308 parser.add_argument('-v', '--verbose', action='count', default=0)
309
Dan Albert914449f2016-06-17 16:45:24 -0700310 parser.add_argument(
Dan Albert8bdccb92016-07-29 13:06:22 -0700311 '--api', type=int, required=True, help='API level being targeted.')
312 parser.add_argument(
313 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700314 help='Architecture being targeted.')
315
316 parser.add_argument(
317 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
318 parser.add_argument(
319 'stub_src', type=os.path.realpath,
320 help='Path to output stub source file.')
321 parser.add_argument(
322 'version_script', type=os.path.realpath,
323 help='Path to output version script.')
324
325 return parser.parse_args()
326
327
328def main():
329 """Program entry point."""
330 args = parse_args()
331
Dan Albert8bdccb92016-07-29 13:06:22 -0700332 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
333 verbosity = args.verbose
334 if verbosity > 2:
335 verbosity = 2
336 logging.basicConfig(level=verbose_map[verbosity])
337
Dan Albert914449f2016-06-17 16:45:24 -0700338 with open(args.symbol_file) as symbol_file:
Dan Albert8bdccb92016-07-29 13:06:22 -0700339 versions = SymbolFileParser(symbol_file).parse()
340
341 with open(args.stub_src, 'w') as src_file:
342 with open(args.version_script, 'w') as version_file:
343 generator = Generator(src_file, version_file, args.arch, args.api)
344 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700345
346
347if __name__ == '__main__':
348 main()