blob: c5147d10554b4a1cff10dc55ef972cb6f5659244 [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
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900201class Filter:
202 """A filter encapsulates a condition that tells whether a version or a
203 symbol should be omitted or not
Dan Albertead21552021-06-04 14:30:40 -0700204 """
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900205
206 def __init__(self, arch: Arch, api: int, llndk: bool = False, apex: bool = False):
207 self.arch = arch
208 self.api = api
209 self.llndk = llndk
210 self.apex = apex
211
212 def _should_omit_tags(self, tags: Tags) -> bool:
213 """Returns True if the tagged object should be omitted.
214
215 This defines the rules shared between version tagging and symbol tagging.
216 """
217 # The apex and llndk tags will only exclude APIs from other modes. If in
218 # APEX or LLNDK mode and neither tag is provided, we fall back to the
219 # default behavior because all NDK symbols are implicitly available to
220 # APEX and LLNDK.
221 if tags.has_mode_tags:
222 if not self.apex and not self.llndk:
223 return True
224 if self.apex and not tags.has_apex_tags:
225 return True
226 if self.llndk and not tags.has_llndk_tags:
227 return True
228 if not symbol_in_arch(tags, self.arch):
Dan Albertead21552021-06-04 14:30:40 -0700229 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900230 if not symbol_in_api(tags, self.arch, self.api):
Dan Albertead21552021-06-04 14:30:40 -0700231 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900232 return False
233
234 def should_omit_version(self, version: Version) -> bool:
235 """Returns True if the version section should be omitted.
236
237 We want to omit any sections that do not have any symbols we'll have in
238 the stub library. Sections that contain entirely future symbols or only
239 symbols for certain architectures.
240 """
241 if version.is_private:
Dan Albertead21552021-06-04 14:30:40 -0700242 return True
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900243 if version.tags.has_platform_only_tags:
244 return True
245 return self._should_omit_tags(version.tags)
Dan Albert914449f2016-06-17 16:45:24 -0700246
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900247 def should_omit_symbol(self, symbol: Symbol) -> bool:
248 """Returns True if the symbol should be omitted."""
249 return self._should_omit_tags(symbol.tags)
Dan Albert08532b62016-07-28 18:09:47 -0700250
251
Dan Albertead21552021-06-04 14:30:40 -0700252def symbol_in_arch(tags: Tags, arch: Arch) -> bool:
Dan Albert914449f2016-06-17 16:45:24 -0700253 """Returns true if the symbol is present for the given architecture."""
254 has_arch_tags = False
255 for tag in tags:
256 if tag == arch:
257 return True
258 if tag in ALL_ARCHITECTURES:
259 has_arch_tags = True
260
261 # If there were no arch tags, the symbol is available for all
262 # architectures. If there were any arch tags, the symbol is only available
263 # for the tagged architectures.
264 return not has_arch_tags
265
266
Dan Albertaf7b36d2020-06-23 11:21:21 -0700267def symbol_in_api(tags: Iterable[Tag], arch: Arch, api: int) -> bool:
Dan Albertc42458e2016-07-29 13:05:39 -0700268 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700269 introduced_tag = None
270 arch_specific = False
271 for tag in tags:
272 # If there is an arch-specific tag, it should override the common one.
273 if tag.startswith('introduced=') and not arch_specific:
274 introduced_tag = tag
275 elif tag.startswith('introduced-' + arch + '='):
276 introduced_tag = tag
277 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700278 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800279 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700280
281 if introduced_tag is None:
282 # We found no "introduced" tags, so the symbol has always been
283 # available.
284 return True
285
Dan Albertc42458e2016-07-29 13:05:39 -0700286 return api >= int(get_tag_value(introduced_tag))
287
288
Dan Albertaf7b36d2020-06-23 11:21:21 -0700289def symbol_versioned_in_api(tags: Iterable[Tag], api: int) -> bool:
Dan Albertc42458e2016-07-29 13:05:39 -0700290 """Returns true if the symbol should be versioned for the given API.
291
292 This models the `versioned=API` tag. This should be a very uncommonly
293 needed tag, and is really only needed to fix versioning mistakes that are
294 already out in the wild.
295
296 For example, some of libc's __aeabi_* functions were originally placed in
297 the private version, but that was incorrect. They are now in LIBC_N, but
298 when building against any version prior to N we need the symbol to be
299 unversioned (otherwise it won't resolve on M where it is private).
300 """
301 for tag in tags:
302 if tag.startswith('versioned='):
303 return api >= int(get_tag_value(tag))
304 # If there is no "versioned" tag, the tag has been versioned for as long as
305 # it was introduced.
306 return True
307
Dan Albert914449f2016-06-17 16:45:24 -0700308
Dan Albert8bdccb92016-07-29 13:06:22 -0700309class ParseError(RuntimeError):
310 """An error that occurred while parsing a symbol file."""
Dan Albert914449f2016-06-17 16:45:24 -0700311
312
Dan Albert756f2d02018-10-09 16:36:03 -0700313class MultiplyDefinedSymbolError(RuntimeError):
314 """A symbol name was multiply defined."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700315 def __init__(self, multiply_defined_symbols: Iterable[str]) -> None:
316 super().__init__(
Dan Albert756f2d02018-10-09 16:36:03 -0700317 'Version script contains multiple definitions for: {}'.format(
318 ', '.join(multiply_defined_symbols)))
319 self.multiply_defined_symbols = multiply_defined_symbols
320
321
Dan Albert802cc822020-06-22 15:59:12 -0700322class SymbolFileParser:
Dan Albert8bdccb92016-07-29 13:06:22 -0700323 """Parses NDK symbol files."""
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900324 def __init__(self, input_file: TextIO, api_map: ApiMap, filt: Filter) -> None:
Dan Albert8bdccb92016-07-29 13:06:22 -0700325 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700326 self.api_map = api_map
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900327 self.filter = filt
Dan Albertaf7b36d2020-06-23 11:21:21 -0700328 self.current_line: Optional[str] = None
Dan Albert8bdccb92016-07-29 13:06:22 -0700329
Dan Albertaf7b36d2020-06-23 11:21:21 -0700330 def parse(self) -> List[Version]:
Dan Albert8bdccb92016-07-29 13:06:22 -0700331 """Parses the symbol file and returns a list of Version objects."""
332 versions = []
Spandan Das3f5659f2021-08-19 19:31:54 +0000333 while self.next_line():
Dan Albertaf7b36d2020-06-23 11:21:21 -0700334 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700335 if '{' in self.current_line:
336 versions.append(self.parse_version())
337 else:
338 raise ParseError(
Dan Albertaf7b36d2020-06-23 11:21:21 -0700339 f'Unexpected contents at top level: {self.current_line}')
Dan Albert756f2d02018-10-09 16:36:03 -0700340
341 self.check_no_duplicate_symbols(versions)
Dan Albert8bdccb92016-07-29 13:06:22 -0700342 return versions
343
Dan Albertaf7b36d2020-06-23 11:21:21 -0700344 def check_no_duplicate_symbols(self, versions: Iterable[Version]) -> None:
Dan Albert756f2d02018-10-09 16:36:03 -0700345 """Raises errors for multiply defined symbols.
346
347 This situation is the normal case when symbol versioning is actually
348 used, but this script doesn't currently handle that. The error message
349 will be a not necessarily obvious "error: redefition of 'foo'" from
350 stub.c, so it's better for us to catch this situation and raise a
351 better error.
352 """
353 symbol_names = set()
354 multiply_defined_symbols = set()
355 for version in versions:
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900356 if self.filter.should_omit_version(version):
Dan Albert756f2d02018-10-09 16:36:03 -0700357 continue
358
359 for symbol in version.symbols:
Jiyong Park3f9c41d2022-07-16 23:30:09 +0900360 if self.filter.should_omit_symbol(symbol):
Dan Albert756f2d02018-10-09 16:36:03 -0700361 continue
362
363 if symbol.name in symbol_names:
364 multiply_defined_symbols.add(symbol.name)
365 symbol_names.add(symbol.name)
366 if multiply_defined_symbols:
367 raise MultiplyDefinedSymbolError(
368 sorted(list(multiply_defined_symbols)))
369
Dan Albertaf7b36d2020-06-23 11:21:21 -0700370 def parse_version(self) -> Version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700371 """Parses a single version section and returns a Version object."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700372 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700373 name = self.current_line.split('{')[0].strip()
Dan Albertead21552021-06-04 14:30:40 -0700374 tags = get_tags(self.current_line, self.api_map)
Dan Albertaf7b36d2020-06-23 11:21:21 -0700375 symbols: List[Symbol] = []
Dan Albert8bdccb92016-07-29 13:06:22 -0700376 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100377 cpp_symbols = False
Spandan Das3f5659f2021-08-19 19:31:54 +0000378 while self.next_line():
Dan Albert8bdccb92016-07-29 13:06:22 -0700379 if '}' in self.current_line:
380 # Line is something like '} BASE; # tags'. Both base and tags
381 # are optional here.
382 base = self.current_line.partition('}')[2]
383 base = base.partition('#')[0].strip()
384 if not base.endswith(';'):
385 raise ParseError(
dimitry2be7fa92017-11-21 17:47:33 +0100386 'Unterminated version/export "C++" block (expected ;).')
387 if cpp_symbols:
388 cpp_symbols = False
389 else:
390 base = base.rstrip(';').rstrip()
Dan Albertaf7b36d2020-06-23 11:21:21 -0700391 return Version(name, base or None, tags, symbols)
dimitry2be7fa92017-11-21 17:47:33 +0100392 elif 'extern "C++" {' in self.current_line:
393 cpp_symbols = True
394 elif not cpp_symbols and ':' in self.current_line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700395 visibility = self.current_line.split(':')[0].strip()
396 if visibility == 'local':
397 global_scope = False
398 elif visibility == 'global':
399 global_scope = True
400 else:
401 raise ParseError('Unknown visiblity label: ' + visibility)
dimitry2be7fa92017-11-21 17:47:33 +0100402 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700403 symbols.append(self.parse_symbol())
404 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700405 # We're in a hidden scope or in 'extern "C++"' block. Ignore
406 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700407 pass
408 raise ParseError('Unexpected EOF in version block.')
409
Dan Albertaf7b36d2020-06-23 11:21:21 -0700410 def parse_symbol(self) -> Symbol:
Dan Albert8bdccb92016-07-29 13:06:22 -0700411 """Parses a single symbol line and returns a Symbol object."""
Dan Albertaf7b36d2020-06-23 11:21:21 -0700412 assert self.current_line is not None
Dan Albert8bdccb92016-07-29 13:06:22 -0700413 if ';' not in self.current_line:
414 raise ParseError(
415 'Expected ; to terminate symbol: ' + self.current_line)
416 if '*' in self.current_line:
417 raise ParseError(
418 'Wildcard global symbols are not permitted.')
419 # Line is now in the format "<symbol-name>; # tags"
420 name, _, _ = self.current_line.strip().partition(';')
Dan Albertead21552021-06-04 14:30:40 -0700421 tags = get_tags(self.current_line, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700422 return Symbol(name, tags)
423
Dan Albertaf7b36d2020-06-23 11:21:21 -0700424 def next_line(self) -> str:
Dan Albert8bdccb92016-07-29 13:06:22 -0700425 """Returns the next non-empty non-comment line.
426
427 A return value of '' indicates EOF.
428 """
429 line = self.input_file.readline()
Spandan Das3f5659f2021-08-19 19:31:54 +0000430 while not line.strip() or line.strip().startswith('#'):
Dan Albert8bdccb92016-07-29 13:06:22 -0700431 line = self.input_file.readline()
432
433 # We want to skip empty lines, but '' indicates EOF.
Spandan Das3f5659f2021-08-19 19:31:54 +0000434 if not line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700435 break
436 self.current_line = line
437 return self.current_line