blob: 471a12f505cebe705c8767a9d05d3d26831fb9f3 [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 Park85cc35a2022-07-17 11:30:47 +0900211 def __init__(self, arch: Arch, api: int, llndk: bool = False, apex: bool = False, systemapi: bool = False):
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900212 self.arch = arch
213 self.api = api
214 self.llndk = llndk
215 self.apex = apex
Jiyong Park85cc35a2022-07-17 11:30:47 +0900216 self.systemapi = systemapi
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900217
218 def _should_omit_tags(self, tags: Tags) -> bool:
219 """Returns True if the tagged object should be omitted.
220
221 This defines the rules shared between version tagging and symbol tagging.
222 """
223 # The apex and llndk tags will only exclude APIs from other modes. If in
224 # APEX or LLNDK mode and neither tag is provided, we fall back to the
225 # default behavior because all NDK symbols are implicitly available to
226 # APEX and LLNDK.
227 if tags.has_mode_tags:
Jiyong Park85cc35a2022-07-17 11:30:47 +0900228 if self.apex and tags.has_apex_tags:
229 return False
230 if self.llndk and tags.has_llndk_tags:
231 return False
232 if self.systemapi and tags.has_systemapi_tags:
233 return False
234 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900235 if not symbol_in_arch(tags, self.arch):
Dan Albertead21552021-06-04 14:30:40 -0700236 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900237 if not symbol_in_api(tags, self.arch, self.api):
Dan Albertead21552021-06-04 14:30:40 -0700238 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900239 return False
240
241 def should_omit_version(self, version: Version) -> bool:
242 """Returns True if the version section should be omitted.
243
244 We want to omit any sections that do not have any symbols we'll have in
245 the stub library. Sections that contain entirely future symbols or only
246 symbols for certain architectures.
247 """
248 if version.is_private:
Dan Albertead21552021-06-04 14:30:40 -0700249 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900250 if version.tags.has_platform_only_tags:
251 return True
252 return self._should_omit_tags(version.tags)
Dan Albert914449f2016-06-17 16:45:24 -0700253
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900254 def should_omit_symbol(self, symbol: Symbol) -> bool:
255 """Returns True if the symbol should be omitted."""
256 return self._should_omit_tags(symbol.tags)
Dan Albert08532b62016-07-28 18:09:47 -0700257
258
Dan Albertead21552021-06-04 14:30:40 -0700259def symbol_in_arch(tags: Tags, arch: Arch) -> bool:
Dan Albert914449f2016-06-17 16:45:24 -0700260 """Returns true if the symbol is present for the given architecture."""
261 has_arch_tags = False
262 for tag in tags:
263 if tag == arch:
264 return True
265 if tag in ALL_ARCHITECTURES:
266 has_arch_tags = True
267
268 # If there were no arch tags, the symbol is available for all
269 # architectures. If there were any arch tags, the symbol is only available
270 # for the tagged architectures.
271 return not has_arch_tags
272
273
Dan Albertaf7b36d2020-06-23 11:21:21 -0700274def symbol_in_api(tags: Iterable[Tag], arch: Arch, api: int) -> bool:
Dan Albertc42458e2016-07-29 13:05:39 -0700275 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700276 introduced_tag = None
277 arch_specific = False
278 for tag in tags:
279 # If there is an arch-specific tag, it should override the common one.
280 if tag.startswith('introduced=') and not arch_specific:
281 introduced_tag = tag
282 elif tag.startswith('introduced-' + arch + '='):
283 introduced_tag = tag
284 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700285 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800286 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700287
288 if introduced_tag is None:
289 # We found no "introduced" tags, so the symbol has always been
290 # available.
291 return True
292
Dan Albertc42458e2016-07-29 13:05:39 -0700293 return api >= int(get_tag_value(introduced_tag))
294
295
Dan Albertaf7b36d2020-06-23 11:21:21 -0700296def symbol_versioned_in_api(tags: Iterable[Tag], api: int) -> bool:
Dan Albertc42458e2016-07-29 13:05:39 -0700297 """Returns true if the symbol should be versioned for the given API.
298
299 This models the `versioned=API` tag. This should be a very uncommonly
300 needed tag, and is really only needed to fix versioning mistakes that are
301 already out in the wild.
302
303 For example, some of libc's __aeabi_* functions were originally placed in
304 the private version, but that was incorrect. They are now in LIBC_N, but
305 when building against any version prior to N we need the symbol to be
306 unversioned (otherwise it won't resolve on M where it is private).
307 """
308 for tag in tags:
309 if tag.startswith('versioned='):
310 return api >= int(get_tag_value(tag))
311 # If there is no "versioned" tag, the tag has been versioned for as long as
312 # it was introduced.
313 return True
314
Dan Albert914449f2016-06-17 16:45:24 -0700315
Dan Albert8bdccb92016-07-29 13:06:22 -0700316class ParseError(RuntimeError):
317 """An error that occurred while parsing a symbol file."""
Dan Albert914449f2016-06-17 16:45:24 -0700318
319
Dan Albert756f2d02018-10-09 16:36:03 -0700320class MultiplyDefinedSymbolError(RuntimeError):
321 """A symbol name was multiply defined."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700322 def __init__(self, multiply_defined_symbols: Iterable[str]) -> None:
323 super().__init__(
Dan Albert756f2d02018-10-09 16:36:03 -0700324 'Version script contains multiple definitions for: {}'.format(
325 ', '.join(multiply_defined_symbols)))
326 self.multiply_defined_symbols = multiply_defined_symbols
327
328
Dan Albert802cc822020-06-22 15:59:12 -0700329class SymbolFileParser:
Dan Albert8bdccb92016-07-29 13:06:22 -0700330 """Parses NDK symbol files."""
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900331 def __init__(self, input_file: TextIO, api_map: ApiMap, filt: Filter) -> None:
Dan Albert8bdccb92016-07-29 13:06:22 -0700332 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700333 self.api_map = api_map
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900334 self.filter = filt
Dan Albertaf7b36d2020-06-23 11:21:21 -0700335 self.current_line: Optional[str] = None
Dan Albert8bdccb92016-07-29 13:06:22 -0700336
Dan Albertaf7b36d2020-06-23 11:21:21 -0700337 def parse(self) -> List[Version]:
Dan Albert8bdccb92016-07-29 13:06:22 -0700338 """Parses the symbol file and returns a list of Version objects."""
339 versions = []
Spandan Das3f5659f2021-08-19 19:31:54 +0000340 while self.next_line():
Dan Albertaf7b36d2020-06-23 11:21:21 -0700341 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700342 if '{' in self.current_line:
343 versions.append(self.parse_version())
344 else:
345 raise ParseError(
Dan Albertaf7b36d2020-06-23 11:21:21 -0700346 f'Unexpected contents at top level: {self.current_line}')
Dan Albert756f2d02018-10-09 16:36:03 -0700347
348 self.check_no_duplicate_symbols(versions)
Dan Albert8bdccb92016-07-29 13:06:22 -0700349 return versions
350
Dan Albertaf7b36d2020-06-23 11:21:21 -0700351 def check_no_duplicate_symbols(self, versions: Iterable[Version]) -> None:
Dan Albert756f2d02018-10-09 16:36:03 -0700352 """Raises errors for multiply defined symbols.
353
354 This situation is the normal case when symbol versioning is actually
355 used, but this script doesn't currently handle that. The error message
356 will be a not necessarily obvious "error: redefition of 'foo'" from
357 stub.c, so it's better for us to catch this situation and raise a
358 better error.
359 """
360 symbol_names = set()
361 multiply_defined_symbols = set()
362 for version in versions:
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900363 if self.filter.should_omit_version(version):
Dan Albert756f2d02018-10-09 16:36:03 -0700364 continue
365
366 for symbol in version.symbols:
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900367 if self.filter.should_omit_symbol(symbol):
Dan Albert756f2d02018-10-09 16:36:03 -0700368 continue
369
370 if symbol.name in symbol_names:
371 multiply_defined_symbols.add(symbol.name)
372 symbol_names.add(symbol.name)
373 if multiply_defined_symbols:
374 raise MultiplyDefinedSymbolError(
375 sorted(list(multiply_defined_symbols)))
376
Dan Albertaf7b36d2020-06-23 11:21:21 -0700377 def parse_version(self) -> Version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700378 """Parses a single version section and returns a Version object."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700379 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700380 name = self.current_line.split('{')[0].strip()
Dan Albertead21552021-06-04 14:30:40 -0700381 tags = get_tags(self.current_line, self.api_map)
Dan Albertaf7b36d2020-06-23 11:21:21 -0700382 symbols: List[Symbol] = []
Dan Albert8bdccb92016-07-29 13:06:22 -0700383 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100384 cpp_symbols = False
Spandan Das3f5659f2021-08-19 19:31:54 +0000385 while self.next_line():
Dan Albert8bdccb92016-07-29 13:06:22 -0700386 if '}' in self.current_line:
387 # Line is something like '} BASE; # tags'. Both base and tags
388 # are optional here.
389 base = self.current_line.partition('}')[2]
390 base = base.partition('#')[0].strip()
391 if not base.endswith(';'):
392 raise ParseError(
dimitry2be7fa92017-11-21 17:47:33 +0100393 'Unterminated version/export "C++" block (expected ;).')
394 if cpp_symbols:
395 cpp_symbols = False
396 else:
397 base = base.rstrip(';').rstrip()
Dan Albertaf7b36d2020-06-23 11:21:21 -0700398 return Version(name, base or None, tags, symbols)
dimitry2be7fa92017-11-21 17:47:33 +0100399 elif 'extern "C++" {' in self.current_line:
400 cpp_symbols = True
401 elif not cpp_symbols and ':' in self.current_line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700402 visibility = self.current_line.split(':')[0].strip()
403 if visibility == 'local':
404 global_scope = False
405 elif visibility == 'global':
406 global_scope = True
407 else:
408 raise ParseError('Unknown visiblity label: ' + visibility)
dimitry2be7fa92017-11-21 17:47:33 +0100409 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700410 symbols.append(self.parse_symbol())
411 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700412 # We're in a hidden scope or in 'extern "C++"' block. Ignore
413 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700414 pass
415 raise ParseError('Unexpected EOF in version block.')
416
Dan Albertaf7b36d2020-06-23 11:21:21 -0700417 def parse_symbol(self) -> Symbol:
Dan Albert8bdccb92016-07-29 13:06:22 -0700418 """Parses a single symbol line and returns a Symbol object."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700419 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700420 if ';' not in self.current_line:
421 raise ParseError(
422 'Expected ; to terminate symbol: ' + self.current_line)
423 if '*' in self.current_line:
424 raise ParseError(
425 'Wildcard global symbols are not permitted.')
426 # Line is now in the format "<symbol-name>; # tags"
427 name, _, _ = self.current_line.strip().partition(';')
Dan Albertead21552021-06-04 14:30:40 -0700428 tags = get_tags(self.current_line, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700429 return Symbol(name, tags)
430
Dan Albertaf7b36d2020-06-23 11:21:21 -0700431 def next_line(self) -> str:
Dan Albert8bdccb92016-07-29 13:06:22 -0700432 """Returns the next non-empty non-comment line.
433
434 A return value of '' indicates EOF.
435 """
436 line = self.input_file.readline()
Spandan Das3f5659f2021-08-19 19:31:54 +0000437 while not line.strip() or line.strip().startswith('#'):
Dan Albert8bdccb92016-07-29 13:06:22 -0700438 line = self.input_file.readline()
439
440 # We want to skip empty lines, but '' indicates EOF.
Spandan Das3f5659f2021-08-19 19:31:54 +0000441 if not line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700442 break
443 self.current_line = line
444 return self.current_line