blob: f8d18414c9aed25b5e7d6dcabe47a4746157df21 [file] [log] [blame]
Dan Albert914449f2016-06-17 16:45:24 -07001#
2# Copyright (C) 2016 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
Dan Albert06f58af2020-06-22 15:10:31 -070016"""Parser for Android's version script information."""
Dan Albertead21552021-06-04 14:30:40 -070017from __future__ import annotations
18
19from dataclasses import dataclass, field
Dan Albert8bdccb92016-07-29 13:06:22 -070020import logging
Dan Albert914449f2016-06-17 16:45:24 -070021import re
Dan Albertaf7b36d2020-06-23 11:21:21 -070022from typing import (
23 Dict,
24 Iterable,
Dan Albertead21552021-06-04 14:30:40 -070025 Iterator,
Dan Albertaf7b36d2020-06-23 11:21:21 -070026 List,
27 Mapping,
28 NewType,
29 Optional,
30 TextIO,
31 Tuple,
Dan Albertead21552021-06-04 14:30:40 -070032 Union,
Dan Albertaf7b36d2020-06-23 11:21:21 -070033)
34
35
36ApiMap = Mapping[str, int]
37Arch = NewType('Arch', str)
38Tag = NewType('Tag', str)
Dan Albert914449f2016-06-17 16:45:24 -070039
40
41ALL_ARCHITECTURES = (
Dan Albertaf7b36d2020-06-23 11:21:21 -070042 Arch('arm'),
43 Arch('arm64'),
44 Arch('x86'),
45 Arch('x86_64'),
Dan Albert914449f2016-06-17 16:45:24 -070046)
47
48
Dan Albertfd86e9e2016-11-08 13:35:12 -080049# Arbitrary magic number. We use the same one in api-level.h for this purpose.
50FUTURE_API_LEVEL = 10000
51
52
Dan Albertaf7b36d2020-06-23 11:21:21 -070053def logger() -> logging.Logger:
Dan Albert8bdccb92016-07-29 13:06:22 -070054 """Return the main logger for this module."""
55 return logging.getLogger(__name__)
Dan Albert914449f2016-06-17 16:45:24 -070056
57
Dan Albertaf7b36d2020-06-23 11:21:21 -070058@dataclass
Dan Albertead21552021-06-04 14:30:40 -070059class Tags:
60 """Container class for the tags attached to a symbol or version."""
61
62 tags: tuple[Tag, ...] = field(default_factory=tuple)
63
64 @classmethod
65 def from_strs(cls, strs: Iterable[str]) -> Tags:
66 """Constructs tags from a collection of strings.
67
68 Does not decode API levels.
69 """
70 return Tags(tuple(Tag(s) for s in strs))
71
72 def __contains__(self, tag: Union[Tag, str]) -> bool:
73 return tag in self.tags
74
75 def __iter__(self) -> Iterator[Tag]:
76 yield from self.tags
77
78 @property
79 def has_mode_tags(self) -> bool:
80 """Returns True if any mode tags (apex, llndk, etc) are set."""
81 return self.has_apex_tags or self.has_llndk_tags
82
83 @property
84 def has_apex_tags(self) -> bool:
85 """Returns True if any APEX tags are set."""
Dan Albert56f52de2021-06-04 14:31:58 -070086 return 'apex' in self.tags or 'systemapi' in self.tags
Dan Albertead21552021-06-04 14:30:40 -070087
88 @property
89 def has_llndk_tags(self) -> bool:
90 """Returns True if any LL-NDK tags are set."""
91 return 'llndk' in self.tags
92
93 @property
94 def has_platform_only_tags(self) -> bool:
95 """Returns True if any platform-only tags are set."""
96 return 'platform-only' in self.tags
97
98
99@dataclass
Dan Albertaf7b36d2020-06-23 11:21:21 -0700100class Symbol:
101 """A symbol definition from a symbol file."""
102
103 name: str
Dan Albertead21552021-06-04 14:30:40 -0700104 tags: Tags
Dan Albertaf7b36d2020-06-23 11:21:21 -0700105
106
107@dataclass
108class Version:
109 """A version block of a symbol file."""
110
111 name: str
112 base: Optional[str]
Dan Albertead21552021-06-04 14:30:40 -0700113 tags: Tags
Dan Albertaf7b36d2020-06-23 11:21:21 -0700114 symbols: List[Symbol]
115
Dan Albertead21552021-06-04 14:30:40 -0700116 @property
117 def is_private(self) -> bool:
118 """Returns True if this version block is private (platform only)."""
119 return self.name.endswith('_PRIVATE') or self.name.endswith('_PLATFORM')
Dan Albertaf7b36d2020-06-23 11:21:21 -0700120
Dan Albertead21552021-06-04 14:30:40 -0700121
122def get_tags(line: str, api_map: ApiMap) -> Tags:
Dan Alberta85042a2016-07-28 16:58:27 -0700123 """Returns a list of all tags on this line."""
124 _, _, all_tags = line.strip().partition('#')
Dan Albertead21552021-06-04 14:30:40 -0700125 return Tags(tuple(
126 decode_api_level_tag(Tag(e), api_map)
127 for e in re.split(r'\s+', all_tags) if e.strip()
128 ))
Dan Alberta85042a2016-07-28 16:58:27 -0700129
130
Dan Albertaf7b36d2020-06-23 11:21:21 -0700131def is_api_level_tag(tag: Tag) -> bool:
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700132 """Returns true if this tag has an API level that may need decoding."""
133 if tag.startswith('introduced='):
134 return True
135 if tag.startswith('introduced-'):
136 return True
137 if tag.startswith('versioned='):
138 return True
139 return False
140
141
Dan Albertaf7b36d2020-06-23 11:21:21 -0700142def decode_api_level(api: str, api_map: ApiMap) -> int:
Dan Albert06f58af2020-06-22 15:10:31 -0700143 """Decodes the API level argument into the API level number.
144
145 For the average case, this just decodes the integer value from the string,
146 but for unreleased APIs we need to translate from the API codename (like
147 "O") to the future API level for that codename.
148 """
149 try:
150 return int(api)
151 except ValueError:
152 pass
153
154 if api == "current":
155 return FUTURE_API_LEVEL
156
157 return api_map[api]
158
159
Dan Albertead21552021-06-04 14:30:40 -0700160def decode_api_level_tag(tag: Tag, api_map: ApiMap) -> Tag:
161 """Decodes API level code name in a tag.
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700162
163 Raises:
164 ParseError: An unknown version name was found in a tag.
165 """
Dan Albertead21552021-06-04 14:30:40 -0700166 if not is_api_level_tag(tag):
167 return tag
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700168
Dan Albertead21552021-06-04 14:30:40 -0700169 name, value = split_tag(tag)
170 try:
171 decoded = str(decode_api_level(value, api_map))
172 return Tag(f'{name}={decoded}')
173 except KeyError as ex:
174 raise ParseError(f'Unknown version name in tag: {tag}') from ex
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700175
176
Dan Albertaf7b36d2020-06-23 11:21:21 -0700177def split_tag(tag: Tag) -> Tuple[str, str]:
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700178 """Returns a key/value tuple of the tag.
179
180 Raises:
181 ValueError: Tag is not a key/value type tag.
182
183 Returns: Tuple of (key, value) of the tag. Both components are strings.
184 """
185 if '=' not in tag:
186 raise ValueError('Not a key/value tag: ' + tag)
187 key, _, value = tag.partition('=')
188 return key, value
189
190
Dan Albertaf7b36d2020-06-23 11:21:21 -0700191def get_tag_value(tag: Tag) -> str:
Dan Albertc42458e2016-07-29 13:05:39 -0700192 """Returns the value of a key/value tag.
193
194 Raises:
195 ValueError: Tag is not a key/value type tag.
196
197 Returns: Value part of tag as a string.
198 """
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700199 return split_tag(tag)[1]
Dan Albertc42458e2016-07-29 13:05:39 -0700200
201
Dan Albertead21552021-06-04 14:30:40 -0700202def _should_omit_tags(tags: Tags, arch: Arch, api: int, llndk: bool,
203 apex: bool) -> bool:
204 """Returns True if the tagged object should be omitted.
205
206 This defines the rules shared between version tagging and symbol tagging.
207 """
208 # The apex and llndk tags will only exclude APIs from other modes. If in
209 # APEX or LLNDK mode and neither tag is provided, we fall back to the
210 # default behavior because all NDK symbols are implicitly available to APEX
211 # and LLNDK.
212 if tags.has_mode_tags:
213 if not apex and not llndk:
214 return True
215 if apex and not tags.has_apex_tags:
216 return True
217 if llndk and not tags.has_llndk_tags:
218 return True
219 if not symbol_in_arch(tags, arch):
220 return True
221 if not symbol_in_api(tags, arch, api):
222 return True
223 return False
Dan Albert914449f2016-06-17 16:45:24 -0700224
225
Dan Albertaf7b36d2020-06-23 11:21:21 -0700226def should_omit_version(version: Version, arch: Arch, api: int, llndk: bool,
227 apex: bool) -> bool:
Dan Albertead21552021-06-04 14:30:40 -0700228 """Returns True if the version section should be omitted.
Dan Albert08532b62016-07-28 18:09:47 -0700229
230 We want to omit any sections that do not have any symbols we'll have in the
231 stub library. Sections that contain entirely future symbols or only symbols
232 for certain architectures.
233 """
Dan Albertead21552021-06-04 14:30:40 -0700234 if version.is_private:
Dan Albert08532b62016-07-28 18:09:47 -0700235 return True
Dan Albertead21552021-06-04 14:30:40 -0700236 if version.tags.has_platform_only_tags:
Dan Albert300cb2f2016-11-04 14:52:30 -0700237 return True
Dan Albertead21552021-06-04 14:30:40 -0700238 return _should_omit_tags(version.tags, arch, api, llndk, apex)
Dan Albert756f2d02018-10-09 16:36:03 -0700239
240
Dan Albertaf7b36d2020-06-23 11:21:21 -0700241def should_omit_symbol(symbol: Symbol, arch: Arch, api: int, llndk: bool,
242 apex: bool) -> bool:
Dan Albert756f2d02018-10-09 16:36:03 -0700243 """Returns True if the symbol should be omitted."""
Dan Albertead21552021-06-04 14:30:40 -0700244 return _should_omit_tags(symbol.tags, arch, api, llndk, apex)
Dan Albert08532b62016-07-28 18:09:47 -0700245
246
Dan Albertead21552021-06-04 14:30:40 -0700247def symbol_in_arch(tags: Tags, arch: Arch) -> bool:
Dan Albert914449f2016-06-17 16:45:24 -0700248 """Returns true if the symbol is present for the given architecture."""
249 has_arch_tags = False
250 for tag in tags:
251 if tag == arch:
252 return True
253 if tag in ALL_ARCHITECTURES:
254 has_arch_tags = True
255
256 # If there were no arch tags, the symbol is available for all
257 # architectures. If there were any arch tags, the symbol is only available
258 # for the tagged architectures.
259 return not has_arch_tags
260
261
Dan Albertaf7b36d2020-06-23 11:21:21 -0700262def symbol_in_api(tags: Iterable[Tag], arch: Arch, api: int) -> bool:
Dan Albertc42458e2016-07-29 13:05:39 -0700263 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700264 introduced_tag = None
265 arch_specific = False
266 for tag in tags:
267 # If there is an arch-specific tag, it should override the common one.
268 if tag.startswith('introduced=') and not arch_specific:
269 introduced_tag = tag
270 elif tag.startswith('introduced-' + arch + '='):
271 introduced_tag = tag
272 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700273 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800274 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700275
276 if introduced_tag is None:
277 # We found no "introduced" tags, so the symbol has always been
278 # available.
279 return True
280
Dan Albertc42458e2016-07-29 13:05:39 -0700281 return api >= int(get_tag_value(introduced_tag))
282
283
Dan Albertaf7b36d2020-06-23 11:21:21 -0700284def symbol_versioned_in_api(tags: Iterable[Tag], api: int) -> bool:
Dan Albertc42458e2016-07-29 13:05:39 -0700285 """Returns true if the symbol should be versioned for the given API.
286
287 This models the `versioned=API` tag. This should be a very uncommonly
288 needed tag, and is really only needed to fix versioning mistakes that are
289 already out in the wild.
290
291 For example, some of libc's __aeabi_* functions were originally placed in
292 the private version, but that was incorrect. They are now in LIBC_N, but
293 when building against any version prior to N we need the symbol to be
294 unversioned (otherwise it won't resolve on M where it is private).
295 """
296 for tag in tags:
297 if tag.startswith('versioned='):
298 return api >= int(get_tag_value(tag))
299 # If there is no "versioned" tag, the tag has been versioned for as long as
300 # it was introduced.
301 return True
302
Dan Albert914449f2016-06-17 16:45:24 -0700303
Dan Albert8bdccb92016-07-29 13:06:22 -0700304class ParseError(RuntimeError):
305 """An error that occurred while parsing a symbol file."""
Dan Albert914449f2016-06-17 16:45:24 -0700306
307
Dan Albert756f2d02018-10-09 16:36:03 -0700308class MultiplyDefinedSymbolError(RuntimeError):
309 """A symbol name was multiply defined."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700310 def __init__(self, multiply_defined_symbols: Iterable[str]) -> None:
311 super().__init__(
Dan Albert756f2d02018-10-09 16:36:03 -0700312 'Version script contains multiple definitions for: {}'.format(
313 ', '.join(multiply_defined_symbols)))
314 self.multiply_defined_symbols = multiply_defined_symbols
315
316
Dan Albert802cc822020-06-22 15:59:12 -0700317class SymbolFileParser:
Dan Albert8bdccb92016-07-29 13:06:22 -0700318 """Parses NDK symbol files."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700319 def __init__(self, input_file: TextIO, api_map: ApiMap, arch: Arch,
320 api: int, llndk: bool, apex: bool) -> None:
Dan Albert8bdccb92016-07-29 13:06:22 -0700321 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700322 self.api_map = api_map
Dan Albert756f2d02018-10-09 16:36:03 -0700323 self.arch = arch
324 self.api = api
Jiyong Park92d6bc12019-11-06 12:37:43 +0900325 self.llndk = llndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900326 self.apex = apex
Dan Albertaf7b36d2020-06-23 11:21:21 -0700327 self.current_line: Optional[str] = None
Dan Albert8bdccb92016-07-29 13:06:22 -0700328
Dan Albertaf7b36d2020-06-23 11:21:21 -0700329 def parse(self) -> List[Version]:
Dan Albert8bdccb92016-07-29 13:06:22 -0700330 """Parses the symbol file and returns a list of Version objects."""
331 versions = []
Spandan Das3f5659f2021-08-19 19:31:54 +0000332 while self.next_line():
Dan Albertaf7b36d2020-06-23 11:21:21 -0700333 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700334 if '{' in self.current_line:
335 versions.append(self.parse_version())
336 else:
337 raise ParseError(
Dan Albertaf7b36d2020-06-23 11:21:21 -0700338 f'Unexpected contents at top level: {self.current_line}')
Dan Albert756f2d02018-10-09 16:36:03 -0700339
340 self.check_no_duplicate_symbols(versions)
Dan Albert8bdccb92016-07-29 13:06:22 -0700341 return versions
342
Dan Albertaf7b36d2020-06-23 11:21:21 -0700343 def check_no_duplicate_symbols(self, versions: Iterable[Version]) -> None:
Dan Albert756f2d02018-10-09 16:36:03 -0700344 """Raises errors for multiply defined symbols.
345
346 This situation is the normal case when symbol versioning is actually
347 used, but this script doesn't currently handle that. The error message
348 will be a not necessarily obvious "error: redefition of 'foo'" from
349 stub.c, so it's better for us to catch this situation and raise a
350 better error.
351 """
352 symbol_names = set()
353 multiply_defined_symbols = set()
354 for version in versions:
Dan Albert802cc822020-06-22 15:59:12 -0700355 if should_omit_version(version, self.arch, self.api, self.llndk,
356 self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700357 continue
358
359 for symbol in version.symbols:
Dan Albert802cc822020-06-22 15:59:12 -0700360 if should_omit_symbol(symbol, self.arch, self.api, self.llndk,
361 self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700362 continue
363
364 if symbol.name in symbol_names:
365 multiply_defined_symbols.add(symbol.name)
366 symbol_names.add(symbol.name)
367 if multiply_defined_symbols:
368 raise MultiplyDefinedSymbolError(
369 sorted(list(multiply_defined_symbols)))
370
Dan Albertaf7b36d2020-06-23 11:21:21 -0700371 def parse_version(self) -> Version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700372 """Parses a single version section and returns a Version object."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700373 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700374 name = self.current_line.split('{')[0].strip()
Dan Albertead21552021-06-04 14:30:40 -0700375 tags = get_tags(self.current_line, self.api_map)
Dan Albertaf7b36d2020-06-23 11:21:21 -0700376 symbols: List[Symbol] = []
Dan Albert8bdccb92016-07-29 13:06:22 -0700377 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100378 cpp_symbols = False
Spandan Das3f5659f2021-08-19 19:31:54 +0000379 while self.next_line():
Dan Albert8bdccb92016-07-29 13:06:22 -0700380 if '}' in self.current_line:
381 # Line is something like '} BASE; # tags'. Both base and tags
382 # are optional here.
383 base = self.current_line.partition('}')[2]
384 base = base.partition('#')[0].strip()
385 if not base.endswith(';'):
386 raise ParseError(
dimitry2be7fa92017-11-21 17:47:33 +0100387 'Unterminated version/export "C++" block (expected ;).')
388 if cpp_symbols:
389 cpp_symbols = False
390 else:
391 base = base.rstrip(';').rstrip()
Dan Albertaf7b36d2020-06-23 11:21:21 -0700392 return Version(name, base or None, tags, symbols)
dimitry2be7fa92017-11-21 17:47:33 +0100393 elif 'extern "C++" {' in self.current_line:
394 cpp_symbols = True
395 elif not cpp_symbols and ':' in self.current_line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700396 visibility = self.current_line.split(':')[0].strip()
397 if visibility == 'local':
398 global_scope = False
399 elif visibility == 'global':
400 global_scope = True
401 else:
402 raise ParseError('Unknown visiblity label: ' + visibility)
dimitry2be7fa92017-11-21 17:47:33 +0100403 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700404 symbols.append(self.parse_symbol())
405 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700406 # We're in a hidden scope or in 'extern "C++"' block. Ignore
407 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700408 pass
409 raise ParseError('Unexpected EOF in version block.')
410
Dan Albertaf7b36d2020-06-23 11:21:21 -0700411 def parse_symbol(self) -> Symbol:
Dan Albert8bdccb92016-07-29 13:06:22 -0700412 """Parses a single symbol line and returns a Symbol object."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700413 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700414 if ';' not in self.current_line:
415 raise ParseError(
416 'Expected ; to terminate symbol: ' + self.current_line)
417 if '*' in self.current_line:
418 raise ParseError(
419 'Wildcard global symbols are not permitted.')
420 # Line is now in the format "<symbol-name>; # tags"
421 name, _, _ = self.current_line.strip().partition(';')
Dan Albertead21552021-06-04 14:30:40 -0700422 tags = get_tags(self.current_line, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700423 return Symbol(name, tags)
424
Dan Albertaf7b36d2020-06-23 11:21:21 -0700425 def next_line(self) -> str:
Dan Albert8bdccb92016-07-29 13:06:22 -0700426 """Returns the next non-empty non-comment line.
427
428 A return value of '' indicates EOF.
429 """
430 line = self.input_file.readline()
Spandan Das3f5659f2021-08-19 19:31:54 +0000431 while not line.strip() or line.strip().startswith('#'):
Dan Albert8bdccb92016-07-29 13:06:22 -0700432 line = self.input_file.readline()
433
434 # We want to skip empty lines, but '' indicates EOF.
Spandan Das3f5659f2021-08-19 19:31:54 +0000435 if not line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700436 break
437 self.current_line = line
438 return self.current_line