blob: 9bf07f2b2396281f60ebdb2e7c774f4e947b666b [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."""
Jiyong Park85cc35a2022-07-17 11:30:47 +090081 return self.has_apex_tags or self.has_llndk_tags or self.has_systemapi_tags
Dan Albertead21552021-06-04 14:30:40 -070082
83 @property
84 def has_apex_tags(self) -> bool:
85 """Returns True if any APEX tags are set."""
Jiyong Park85cc35a2022-07-17 11:30:47 +090086 return 'apex' in self.tags
87
88 @property
89 def has_systemapi_tags(self) -> bool:
90 """Returns True if any APEX tags are set."""
91 return 'systemapi' in self.tags
Dan Albertead21552021-06-04 14:30:40 -070092
93 @property
94 def has_llndk_tags(self) -> bool:
95 """Returns True if any LL-NDK tags are set."""
96 return 'llndk' in self.tags
97
98 @property
99 def has_platform_only_tags(self) -> bool:
100 """Returns True if any platform-only tags are set."""
101 return 'platform-only' in self.tags
102
103
104@dataclass
Dan Albertaf7b36d2020-06-23 11:21:21 -0700105class Symbol:
106 """A symbol definition from a symbol file."""
107
108 name: str
Dan Albertead21552021-06-04 14:30:40 -0700109 tags: Tags
Dan Albertaf7b36d2020-06-23 11:21:21 -0700110
111
112@dataclass
113class Version:
114 """A version block of a symbol file."""
115
116 name: str
117 base: Optional[str]
Dan Albertead21552021-06-04 14:30:40 -0700118 tags: Tags
Dan Albertaf7b36d2020-06-23 11:21:21 -0700119 symbols: List[Symbol]
120
Dan Albertead21552021-06-04 14:30:40 -0700121 @property
122 def is_private(self) -> bool:
123 """Returns True if this version block is private (platform only)."""
124 return self.name.endswith('_PRIVATE') or self.name.endswith('_PLATFORM')
Dan Albertaf7b36d2020-06-23 11:21:21 -0700125
Dan Albertead21552021-06-04 14:30:40 -0700126
127def get_tags(line: str, api_map: ApiMap) -> Tags:
Dan Alberta85042a2016-07-28 16:58:27 -0700128 """Returns a list of all tags on this line."""
129 _, _, all_tags = line.strip().partition('#')
Dan Albertead21552021-06-04 14:30:40 -0700130 return Tags(tuple(
131 decode_api_level_tag(Tag(e), api_map)
132 for e in re.split(r'\s+', all_tags) if e.strip()
133 ))
Dan Alberta85042a2016-07-28 16:58:27 -0700134
135
Dan Albertaf7b36d2020-06-23 11:21:21 -0700136def is_api_level_tag(tag: Tag) -> bool:
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700137 """Returns true if this tag has an API level that may need decoding."""
138 if tag.startswith('introduced='):
139 return True
140 if tag.startswith('introduced-'):
141 return True
142 if tag.startswith('versioned='):
143 return True
144 return False
145
146
Dan Albertaf7b36d2020-06-23 11:21:21 -0700147def decode_api_level(api: str, api_map: ApiMap) -> int:
Dan Albert06f58af2020-06-22 15:10:31 -0700148 """Decodes the API level argument into the API level number.
149
150 For the average case, this just decodes the integer value from the string,
151 but for unreleased APIs we need to translate from the API codename (like
152 "O") to the future API level for that codename.
153 """
154 try:
155 return int(api)
156 except ValueError:
157 pass
158
159 if api == "current":
160 return FUTURE_API_LEVEL
161
162 return api_map[api]
163
164
Dan Albertead21552021-06-04 14:30:40 -0700165def decode_api_level_tag(tag: Tag, api_map: ApiMap) -> Tag:
166 """Decodes API level code name in a tag.
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700167
168 Raises:
169 ParseError: An unknown version name was found in a tag.
170 """
Dan Albertead21552021-06-04 14:30:40 -0700171 if not is_api_level_tag(tag):
172 return tag
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700173
Dan Albertead21552021-06-04 14:30:40 -0700174 name, value = split_tag(tag)
175 try:
176 decoded = str(decode_api_level(value, api_map))
177 return Tag(f'{name}={decoded}')
178 except KeyError as ex:
179 raise ParseError(f'Unknown version name in tag: {tag}') from ex
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700180
181
Dan Albertaf7b36d2020-06-23 11:21:21 -0700182def split_tag(tag: Tag) -> Tuple[str, str]:
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700183 """Returns a key/value tuple of the tag.
184
185 Raises:
186 ValueError: Tag is not a key/value type tag.
187
188 Returns: Tuple of (key, value) of the tag. Both components are strings.
189 """
190 if '=' not in tag:
191 raise ValueError('Not a key/value tag: ' + tag)
192 key, _, value = tag.partition('=')
193 return key, value
194
195
Dan Albertaf7b36d2020-06-23 11:21:21 -0700196def get_tag_value(tag: Tag) -> str:
Dan Albertc42458e2016-07-29 13:05:39 -0700197 """Returns the value of a key/value tag.
198
199 Raises:
200 ValueError: Tag is not a key/value type tag.
201
202 Returns: Value part of tag as a string.
203 """
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700204 return split_tag(tag)[1]
Dan Albertc42458e2016-07-29 13:05:39 -0700205
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900206class Filter:
207 """A filter encapsulates a condition that tells whether a version or a
208 symbol should be omitted or not
Dan Albertead21552021-06-04 14:30:40 -0700209 """
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900210
Jiyong Park4ecbdb62022-09-26 20:58:27 +0900211 def __init__(self, arch: Arch, api: int, llndk: bool = False, apex: bool = False, systemapi:
212 bool = False, ndk: bool = True):
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900213 self.arch = arch
214 self.api = api
215 self.llndk = llndk
216 self.apex = apex
Jiyong Park85cc35a2022-07-17 11:30:47 +0900217 self.systemapi = systemapi
Jiyong Park4ecbdb62022-09-26 20:58:27 +0900218 self.ndk = ndk
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900219
220 def _should_omit_tags(self, tags: Tags) -> bool:
221 """Returns True if the tagged object should be omitted.
222
223 This defines the rules shared between version tagging and symbol tagging.
224 """
225 # The apex and llndk tags will only exclude APIs from other modes. If in
226 # APEX or LLNDK mode and neither tag is provided, we fall back to the
227 # default behavior because all NDK symbols are implicitly available to
228 # APEX and LLNDK.
229 if tags.has_mode_tags:
Jiyong Park85cc35a2022-07-17 11:30:47 +0900230 if self.apex and tags.has_apex_tags:
231 return False
232 if self.llndk and tags.has_llndk_tags:
233 return False
234 if self.systemapi and tags.has_systemapi_tags:
235 return False
236 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900237 if not symbol_in_arch(tags, self.arch):
Dan Albertead21552021-06-04 14:30:40 -0700238 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900239 if not symbol_in_api(tags, self.arch, self.api):
Dan Albertead21552021-06-04 14:30:40 -0700240 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900241 return False
242
243 def should_omit_version(self, version: Version) -> bool:
244 """Returns True if the version section should be omitted.
245
246 We want to omit any sections that do not have any symbols we'll have in
247 the stub library. Sections that contain entirely future symbols or only
248 symbols for certain architectures.
249 """
250 if version.is_private:
Dan Albertead21552021-06-04 14:30:40 -0700251 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900252 if version.tags.has_platform_only_tags:
253 return True
254 return self._should_omit_tags(version.tags)
Dan Albert914449f2016-06-17 16:45:24 -0700255
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900256 def should_omit_symbol(self, symbol: Symbol) -> bool:
257 """Returns True if the symbol should be omitted."""
Jiyong Park4ecbdb62022-09-26 20:58:27 +0900258 if not symbol.tags.has_mode_tags and not self.ndk:
259 # Symbols that don't have mode tags are NDK. They are usually
260 # included, but have to be omitted if NDK symbols are explicitly
261 # filtered-out
262 return True
Dan Albert08532b62016-07-28 18:09:47 -0700263
Jiyong Park4ecbdb62022-09-26 20:58:27 +0900264 return self._should_omit_tags(symbol.tags)
Dan Albert08532b62016-07-28 18:09:47 -0700265
Dan Albertead21552021-06-04 14:30:40 -0700266def symbol_in_arch(tags: Tags, arch: Arch) -> bool:
Dan Albert914449f2016-06-17 16:45:24 -0700267 """Returns true if the symbol is present for the given architecture."""
268 has_arch_tags = False
269 for tag in tags:
270 if tag == arch:
271 return True
272 if tag in ALL_ARCHITECTURES:
273 has_arch_tags = True
274
275 # If there were no arch tags, the symbol is available for all
276 # architectures. If there were any arch tags, the symbol is only available
277 # for the tagged architectures.
278 return not has_arch_tags
279
280
Dan Albertaf7b36d2020-06-23 11:21:21 -0700281def symbol_in_api(tags: Iterable[Tag], arch: Arch, api: int) -> bool:
Dan Albertc42458e2016-07-29 13:05:39 -0700282 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700283 introduced_tag = None
284 arch_specific = False
285 for tag in tags:
286 # If there is an arch-specific tag, it should override the common one.
287 if tag.startswith('introduced=') and not arch_specific:
288 introduced_tag = tag
289 elif tag.startswith('introduced-' + arch + '='):
290 introduced_tag = tag
291 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700292 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800293 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700294
295 if introduced_tag is None:
296 # We found no "introduced" tags, so the symbol has always been
297 # available.
298 return True
299
Dan Albertc42458e2016-07-29 13:05:39 -0700300 return api >= int(get_tag_value(introduced_tag))
301
302
Dan Albertaf7b36d2020-06-23 11:21:21 -0700303def symbol_versioned_in_api(tags: Iterable[Tag], api: int) -> bool:
Dan Albertc42458e2016-07-29 13:05:39 -0700304 """Returns true if the symbol should be versioned for the given API.
305
306 This models the `versioned=API` tag. This should be a very uncommonly
307 needed tag, and is really only needed to fix versioning mistakes that are
308 already out in the wild.
309
310 For example, some of libc's __aeabi_* functions were originally placed in
311 the private version, but that was incorrect. They are now in LIBC_N, but
312 when building against any version prior to N we need the symbol to be
313 unversioned (otherwise it won't resolve on M where it is private).
314 """
315 for tag in tags:
316 if tag.startswith('versioned='):
317 return api >= int(get_tag_value(tag))
318 # If there is no "versioned" tag, the tag has been versioned for as long as
319 # it was introduced.
320 return True
321
Dan Albert914449f2016-06-17 16:45:24 -0700322
Dan Albert8bdccb92016-07-29 13:06:22 -0700323class ParseError(RuntimeError):
324 """An error that occurred while parsing a symbol file."""
Dan Albert914449f2016-06-17 16:45:24 -0700325
326
Dan Albert756f2d02018-10-09 16:36:03 -0700327class MultiplyDefinedSymbolError(RuntimeError):
328 """A symbol name was multiply defined."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700329 def __init__(self, multiply_defined_symbols: Iterable[str]) -> None:
330 super().__init__(
Dan Albert756f2d02018-10-09 16:36:03 -0700331 'Version script contains multiple definitions for: {}'.format(
332 ', '.join(multiply_defined_symbols)))
333 self.multiply_defined_symbols = multiply_defined_symbols
334
335
Dan Albert802cc822020-06-22 15:59:12 -0700336class SymbolFileParser:
Dan Albert8bdccb92016-07-29 13:06:22 -0700337 """Parses NDK symbol files."""
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900338 def __init__(self, input_file: TextIO, api_map: ApiMap, filt: Filter) -> None:
Dan Albert8bdccb92016-07-29 13:06:22 -0700339 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700340 self.api_map = api_map
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900341 self.filter = filt
Dan Albertaf7b36d2020-06-23 11:21:21 -0700342 self.current_line: Optional[str] = None
Dan Albert8bdccb92016-07-29 13:06:22 -0700343
Dan Albertaf7b36d2020-06-23 11:21:21 -0700344 def parse(self) -> List[Version]:
Dan Albert8bdccb92016-07-29 13:06:22 -0700345 """Parses the symbol file and returns a list of Version objects."""
346 versions = []
Spandan Das3f5659f2021-08-19 19:31:54 +0000347 while self.next_line():
Dan Albertaf7b36d2020-06-23 11:21:21 -0700348 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700349 if '{' in self.current_line:
350 versions.append(self.parse_version())
351 else:
352 raise ParseError(
Dan Albertaf7b36d2020-06-23 11:21:21 -0700353 f'Unexpected contents at top level: {self.current_line}')
Dan Albert756f2d02018-10-09 16:36:03 -0700354
355 self.check_no_duplicate_symbols(versions)
Dan Albert8bdccb92016-07-29 13:06:22 -0700356 return versions
357
Dan Albertaf7b36d2020-06-23 11:21:21 -0700358 def check_no_duplicate_symbols(self, versions: Iterable[Version]) -> None:
Dan Albert756f2d02018-10-09 16:36:03 -0700359 """Raises errors for multiply defined symbols.
360
361 This situation is the normal case when symbol versioning is actually
362 used, but this script doesn't currently handle that. The error message
363 will be a not necessarily obvious "error: redefition of 'foo'" from
364 stub.c, so it's better for us to catch this situation and raise a
365 better error.
366 """
367 symbol_names = set()
368 multiply_defined_symbols = set()
369 for version in versions:
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900370 if self.filter.should_omit_version(version):
Dan Albert756f2d02018-10-09 16:36:03 -0700371 continue
372
373 for symbol in version.symbols:
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900374 if self.filter.should_omit_symbol(symbol):
Dan Albert756f2d02018-10-09 16:36:03 -0700375 continue
376
377 if symbol.name in symbol_names:
378 multiply_defined_symbols.add(symbol.name)
379 symbol_names.add(symbol.name)
380 if multiply_defined_symbols:
381 raise MultiplyDefinedSymbolError(
382 sorted(list(multiply_defined_symbols)))
383
Dan Albertaf7b36d2020-06-23 11:21:21 -0700384 def parse_version(self) -> Version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700385 """Parses a single version section and returns a Version object."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700386 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700387 name = self.current_line.split('{')[0].strip()
Dan Albertead21552021-06-04 14:30:40 -0700388 tags = get_tags(self.current_line, self.api_map)
Dan Albertaf7b36d2020-06-23 11:21:21 -0700389 symbols: List[Symbol] = []
Dan Albert8bdccb92016-07-29 13:06:22 -0700390 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100391 cpp_symbols = False
Spandan Das3f5659f2021-08-19 19:31:54 +0000392 while self.next_line():
Dan Albert8bdccb92016-07-29 13:06:22 -0700393 if '}' in self.current_line:
394 # Line is something like '} BASE; # tags'. Both base and tags
395 # are optional here.
396 base = self.current_line.partition('}')[2]
397 base = base.partition('#')[0].strip()
398 if not base.endswith(';'):
399 raise ParseError(
dimitry2be7fa92017-11-21 17:47:33 +0100400 'Unterminated version/export "C++" block (expected ;).')
401 if cpp_symbols:
402 cpp_symbols = False
403 else:
404 base = base.rstrip(';').rstrip()
Dan Albertaf7b36d2020-06-23 11:21:21 -0700405 return Version(name, base or None, tags, symbols)
dimitry2be7fa92017-11-21 17:47:33 +0100406 elif 'extern "C++" {' in self.current_line:
407 cpp_symbols = True
408 elif not cpp_symbols and ':' in self.current_line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700409 visibility = self.current_line.split(':')[0].strip()
410 if visibility == 'local':
411 global_scope = False
412 elif visibility == 'global':
413 global_scope = True
414 else:
415 raise ParseError('Unknown visiblity label: ' + visibility)
dimitry2be7fa92017-11-21 17:47:33 +0100416 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700417 symbols.append(self.parse_symbol())
418 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700419 # We're in a hidden scope or in 'extern "C++"' block. Ignore
420 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700421 pass
422 raise ParseError('Unexpected EOF in version block.')
423
Dan Albertaf7b36d2020-06-23 11:21:21 -0700424 def parse_symbol(self) -> Symbol:
Dan Albert8bdccb92016-07-29 13:06:22 -0700425 """Parses a single symbol line and returns a Symbol object."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700426 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700427 if ';' not in self.current_line:
428 raise ParseError(
429 'Expected ; to terminate symbol: ' + self.current_line)
430 if '*' in self.current_line:
431 raise ParseError(
432 'Wildcard global symbols are not permitted.')
433 # Line is now in the format "<symbol-name>; # tags"
434 name, _, _ = self.current_line.strip().partition(';')
Dan Albertead21552021-06-04 14:30:40 -0700435 tags = get_tags(self.current_line, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700436 return Symbol(name, tags)
437
Dan Albertaf7b36d2020-06-23 11:21:21 -0700438 def next_line(self) -> str:
Dan Albert8bdccb92016-07-29 13:06:22 -0700439 """Returns the next non-empty non-comment line.
440
441 A return value of '' indicates EOF.
442 """
443 line = self.input_file.readline()
Spandan Das3f5659f2021-08-19 19:31:54 +0000444 while not line.strip() or line.strip().startswith('#'):
Dan Albert8bdccb92016-07-29 13:06:22 -0700445 line = self.input_file.readline()
446
447 # We want to skip empty lines, but '' indicates EOF.
Spandan Das3f5659f2021-08-19 19:31:54 +0000448 if not line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700449 break
450 self.current_line = line
451 return self.current_line