ndkstubgen: use llndk=<version> for new llndk stub

We want LLNDK symbols to be explicitly marked with llndk tag to
handle LLNDK freezing which happens before SDK freezing. If symbols
need to be frozen as LLNDK, those symbols must be marked explicitly with
correct vFRC version.

In the following example,

LIBFOO { # introduced=35
  foo;
  bar;
  bar; # llndk=202404
  baz; # llndk=202404
  qux; # llndk=202505
};

NDK libfoo will have foo and bar while LLNDK libfoo stub will have bar
and baz for 202404.

Bug: 329012338
Test: test_ndkstubgen test_symbolfile
Change-Id: I384f589b240fa047e8871964bf9550f426024dfc
This commit is contained in:
Jooyung Han 2024-03-11 15:46:48 +09:00
parent eb0f771676
commit 33eb615eb0
5 changed files with 222 additions and 17 deletions

View file

@ -2688,7 +2688,7 @@ func TestLlndkLibrary(t *testing.T) {
android.AssertArrayString(t, "variants for llndk stubs", expected, actual) android.AssertArrayString(t, "variants for llndk stubs", expected, actual)
params := result.ModuleForTests("libllndk", "android_vendor.29_arm_armv7-a-neon_shared").Description("generate stub") params := result.ModuleForTests("libllndk", "android_vendor.29_arm_armv7-a-neon_shared").Description("generate stub")
android.AssertSame(t, "use VNDK version for default stubs", "current", params.Args["apiLevel"]) android.AssertSame(t, "use Vendor API level for default stubs", "202404", params.Args["apiLevel"])
checkExportedIncludeDirs := func(module, variant string, expectedDirs ...string) { checkExportedIncludeDirs := func(module, variant string, expectedDirs ...string) {
t.Helper() t.Helper()

View file

@ -677,18 +677,16 @@ func (library *libraryDecorator) getHeaderAbiCheckerProperties(ctx android.BaseM
func (library *libraryDecorator) compile(ctx ModuleContext, flags Flags, deps PathDeps) Objects { func (library *libraryDecorator) compile(ctx ModuleContext, flags Flags, deps PathDeps) Objects {
if ctx.IsLlndk() { if ctx.IsLlndk() {
vendorApiLevel := ctx.Config().VendorApiLevel()
if vendorApiLevel == "" {
// TODO(b/321892570): Some tests relying on old fixtures which
// doesn't set vendorApiLevel. Needs to fix them.
vendorApiLevel = ctx.Config().PlatformSdkVersion().String()
}
// This is the vendor variant of an LLNDK library, build the LLNDK stubs. // This is the vendor variant of an LLNDK library, build the LLNDK stubs.
vndkVer := ctx.Module().(*Module).VndkVersion()
if !inList(vndkVer, ctx.Config().PlatformVersionActiveCodenames()) || vndkVer == "" {
// For non-enforcing devices, vndkVer is empty. Use "current" in that case, too.
vndkVer = "current"
}
if library.stubsVersion() != "" {
vndkVer = library.stubsVersion()
}
nativeAbiResult := parseNativeAbiDefinition(ctx, nativeAbiResult := parseNativeAbiDefinition(ctx,
String(library.Properties.Llndk.Symbol_file), String(library.Properties.Llndk.Symbol_file),
android.ApiLevelOrPanic(ctx, vndkVer), "--llndk") android.ApiLevelOrPanic(ctx, vendorApiLevel), "--llndk")
objs := compileStubLibrary(ctx, flags, nativeAbiResult.stubSrc) objs := compileStubLibrary(ctx, flags, nativeAbiResult.stubSrc)
if !Bool(library.Properties.Llndk.Unversioned) { if !Bool(library.Properties.Llndk.Unversioned) {
library.versionScriptPath = android.OptionalPathForPath( library.versionScriptPath = android.OptionalPathForPath(

View file

@ -463,6 +463,98 @@ class IntegrationTest(unittest.TestCase):
""") """)
self.assertEqual(expected_version, version_file.getvalue()) self.assertEqual(expected_version, version_file.getvalue())
def test_integration_with_llndk(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_34 { # introduced=34
global:
foo;
bar; # llndk
};
VERSION_35 { # introduced=35
global:
wiggle;
waggle;
waggle; # llndk=202404
bubble; # llndk=202404
duddle;
duddle; # llndk=202504
} VERSION_34;
"""))
f = copy(self.filter)
f.llndk = True
f.api = 202404
parser = symbolfile.SymbolFileParser(input_file, {}, f)
versions = parser.parse()
src_file = io.StringIO()
version_file = io.StringIO()
symbol_list_file = io.StringIO()
generator = ndkstubgen.Generator(src_file,
version_file, symbol_list_file, f)
generator.write(versions)
expected_src = textwrap.dedent("""\
void foo() {}
void bar() {}
void waggle() {}
void bubble() {}
""")
self.assertEqual(expected_src, src_file.getvalue())
expected_version = textwrap.dedent("""\
VERSION_34 {
global:
foo;
bar;
};
VERSION_35 {
global:
waggle;
bubble;
} VERSION_34;
""")
self.assertEqual(expected_version, version_file.getvalue())
def test_integration_with_llndk_with_single_version_block(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
LIBANDROID {
global:
foo; # introduced=34
bar; # introduced=35
bar; # llndk=202404
baz; # introduced=35
};
"""))
f = copy(self.filter)
f.llndk = True
f.api = 202404
parser = symbolfile.SymbolFileParser(input_file, {}, f)
versions = parser.parse()
src_file = io.StringIO()
version_file = io.StringIO()
symbol_list_file = io.StringIO()
generator = ndkstubgen.Generator(src_file,
version_file, symbol_list_file, f)
generator.write(versions)
expected_src = textwrap.dedent("""\
void foo() {}
void bar() {}
""")
self.assertEqual(expected_src, src_file.getvalue())
expected_version = textwrap.dedent("""\
LIBANDROID {
global:
foo;
bar;
};
""")
self.assertEqual(expected_version, version_file.getvalue())
def test_empty_stub(self) -> None: def test_empty_stub(self) -> None:
"""Tests that empty stubs can be generated. """Tests that empty stubs can be generated.

View file

@ -103,13 +103,24 @@ class Tags:
@property @property
def has_llndk_tags(self) -> bool: def has_llndk_tags(self) -> bool:
"""Returns True if any LL-NDK tags are set.""" """Returns True if any LL-NDK tags are set."""
return 'llndk' in self.tags for tag in self.tags:
if tag == 'llndk' or tag.startswith('llndk='):
return True
return False
@property @property
def has_platform_only_tags(self) -> bool: def has_platform_only_tags(self) -> bool:
"""Returns True if any platform-only tags are set.""" """Returns True if any platform-only tags are set."""
return 'platform-only' in self.tags return 'platform-only' in self.tags
def copy_introduced_from(self, tags: Tags) -> None:
"""Copies introduced= or introduced-*= tags."""
for tag in tags:
if tag.startswith('introduced=') or tag.startswith('introduced-'):
name, _ = split_tag(tag)
if not any(self_tag.startswith(name + '=') for self_tag in self.tags):
self.tags += (tag,)
@dataclass @dataclass
class Symbol: class Symbol:
@ -147,6 +158,8 @@ def is_api_level_tag(tag: Tag) -> bool:
"""Returns true if this tag has an API level that may need decoding.""" """Returns true if this tag has an API level that may need decoding."""
if tag.startswith('llndk-deprecated='): if tag.startswith('llndk-deprecated='):
return True return True
if tag.startswith('llndk='):
return True
if tag.startswith('introduced='): if tag.startswith('introduced='):
return True return True
if tag.startswith('introduced-'): if tag.startswith('introduced-'):
@ -237,15 +250,22 @@ class Filter:
This defines the rules shared between version tagging and symbol tagging. This defines the rules shared between version tagging and symbol tagging.
""" """
# The apex and llndk tags will only exclude APIs from other modes. If in # LLNDK mode/tags follow the similar filtering except that API level checking
# is based llndk= instead of introduced=.
if self.llndk:
if tags.has_mode_tags and not tags.has_llndk_tags:
return True
if not symbol_in_arch(tags, self.arch):
return True
if not symbol_in_llndk_api(tags, self.arch, self.api):
return True
return False
# APEX or LLNDK mode and neither tag is provided, we fall back to the # APEX or LLNDK mode and neither tag is provided, we fall back to the
# default behavior because all NDK symbols are implicitly available to # default behavior because all NDK symbols are implicitly available to
# APEX and LLNDK. # APEX and LLNDK.
if tags.has_mode_tags: if tags.has_mode_tags:
if self.apex and tags.has_apex_tags: if self.apex and tags.has_apex_tags:
return False return False
if self.llndk and tags.has_llndk_tags:
return False
if self.systemapi and tags.has_systemapi_tags: if self.systemapi and tags.has_systemapi_tags:
return False return False
return True return True
@ -266,6 +286,10 @@ class Filter:
return True return True
if version.tags.has_platform_only_tags: if version.tags.has_platform_only_tags:
return True return True
# Include all versions when targeting LLNDK because LLNDK symbols are self-versioned.
# Empty version block will be handled separately.
if self.llndk:
return False
return self._should_omit_tags(version.tags) return self._should_omit_tags(version.tags)
def should_omit_symbol(self, symbol: Symbol) -> bool: def should_omit_symbol(self, symbol: Symbol) -> bool:
@ -292,6 +316,14 @@ def symbol_in_arch(tags: Tags, arch: Arch) -> bool:
# for the tagged architectures. # for the tagged architectures.
return not has_arch_tags return not has_arch_tags
def symbol_in_llndk_api(tags: Iterable[Tag], arch: Arch, api: int) -> bool:
"""Returns true if the symbol is present for the given LLNDK API level."""
# Check llndk= first.
for tag in tags:
if tag.startswith('llndk='):
return api >= int(get_tag_value(tag))
# If not, we keep old behavior: NDK symbols in <= 34 are LLNDK symbols.
return symbol_in_api(tags, arch, 34)
def symbol_in_api(tags: Iterable[Tag], arch: Arch, api: int) -> bool: def symbol_in_api(tags: Iterable[Tag], arch: Arch, api: int) -> bool:
"""Returns true if the symbol is present for the given API level.""" """Returns true if the symbol is present for the given API level."""
@ -368,6 +400,7 @@ class SymbolFileParser:
f'Unexpected contents at top level: {self.current_line}') f'Unexpected contents at top level: {self.current_line}')
self.check_no_duplicate_symbols(versions) self.check_no_duplicate_symbols(versions)
self.check_llndk_introduced(versions)
return versions return versions
def check_no_duplicate_symbols(self, versions: Iterable[Version]) -> None: def check_no_duplicate_symbols(self, versions: Iterable[Version]) -> None:
@ -396,6 +429,31 @@ class SymbolFileParser:
raise MultiplyDefinedSymbolError( raise MultiplyDefinedSymbolError(
sorted(list(multiply_defined_symbols))) sorted(list(multiply_defined_symbols)))
def check_llndk_introduced(self, versions: Iterable[Version]) -> None:
"""Raises errors when llndk= is missing for new llndk symbols."""
if not self.filter.llndk:
return
def assert_llndk_with_version(tags: Tags, name: str) -> None:
has_llndk_introduced = False
for tag in tags:
if tag.startswith('llndk='):
has_llndk_introduced = True
break
if not has_llndk_introduced:
raise ParseError(f'{name}: missing version. `llndk=yyyymm`')
arch = self.filter.arch
for version in versions:
# llndk symbols >= introduced=35 should be tagged
# explicitly with llndk=yyyymm.
for symbol in version.symbols:
if not symbol.tags.has_llndk_tags:
continue
if symbol_in_api(symbol.tags, arch, 34):
continue
assert_llndk_with_version(symbol.tags, symbol.name)
def parse_version(self) -> Version: def parse_version(self) -> Version:
"""Parses a single version section and returns a Version object.""" """Parses a single version section and returns a Version object."""
assert self.current_line is not None assert self.current_line is not None
@ -429,7 +487,9 @@ class SymbolFileParser:
else: else:
raise ParseError('Unknown visiblity label: ' + visibility) raise ParseError('Unknown visiblity label: ' + visibility)
elif global_scope and not cpp_symbols: elif global_scope and not cpp_symbols:
symbols.append(self.parse_symbol()) symbol = self.parse_symbol()
symbol.tags.copy_introduced_from(tags)
symbols.append(symbol)
else: else:
# We're in a hidden scope or in 'extern "C++"' block. Ignore # We're in a hidden scope or in 'extern "C++"' block. Ignore
# everything. # everything.

View file

@ -344,6 +344,45 @@ class OmitSymbolTest(unittest.TestCase):
self.assertInclude(f_llndk, s_none) self.assertInclude(f_llndk, s_none)
self.assertInclude(f_llndk, s_llndk) self.assertInclude(f_llndk, s_llndk)
def test_omit_llndk_versioned(self) -> None:
f_ndk = self.filter
f_ndk.api = 35
f_llndk = copy(f_ndk)
f_llndk.llndk = True
f_llndk.api = 202404
s = Symbol('foo', Tags())
s_llndk = Symbol('foo', Tags.from_strs(['llndk']))
s_llndk_202404 = Symbol('foo', Tags.from_strs(['llndk=202404']))
s_34 = Symbol('foo', Tags.from_strs(['introduced=34']))
s_34_llndk = Symbol('foo', Tags.from_strs(['introduced=34', 'llndk']))
s_35 = Symbol('foo', Tags.from_strs(['introduced=35']))
s_35_llndk_202404 = Symbol('foo', Tags.from_strs(['introduced=35', 'llndk=202404']))
s_35_llndk_202504 = Symbol('foo', Tags.from_strs(['introduced=35', 'llndk=202504']))
# When targeting NDK, omit LLNDK tags
self.assertInclude(f_ndk, s)
self.assertOmit(f_ndk, s_llndk)
self.assertOmit(f_ndk, s_llndk_202404)
self.assertInclude(f_ndk, s_34)
self.assertOmit(f_ndk, s_34_llndk)
self.assertInclude(f_ndk, s_35)
self.assertOmit(f_ndk, s_35_llndk_202404)
self.assertOmit(f_ndk, s_35_llndk_202504)
# When targeting LLNDK, old symbols without any mode tags are included as LLNDK
self.assertInclude(f_llndk, s)
# When targeting LLNDK, old symbols with #llndk are included as LLNDK
self.assertInclude(f_llndk, s_llndk)
self.assertInclude(f_llndk, s_llndk_202404)
self.assertInclude(f_llndk, s_34)
self.assertInclude(f_llndk, s_34_llndk)
# When targeting LLNDK, new symbols(>=35) should be tagged with llndk-introduced=.
self.assertOmit(f_llndk, s_35)
self.assertInclude(f_llndk, s_35_llndk_202404)
self.assertOmit(f_llndk, s_35_llndk_202504)
def test_omit_apex(self) -> None: def test_omit_apex(self) -> None:
f_none = self.filter f_none = self.filter
f_apex = copy(f_none) f_apex = copy(f_none)
@ -451,9 +490,12 @@ class SymbolFileParseTest(unittest.TestCase):
self.assertIsNone(version.base) self.assertIsNone(version.base)
self.assertEqual(Tags.from_strs(['weak', 'introduced=35']), version.tags) self.assertEqual(Tags.from_strs(['weak', 'introduced=35']), version.tags)
# Inherit introduced= tags from version block so that
# should_omit_tags() can differently based on introduced API level when treating
# LLNDK-available symbols.
expected_symbols = [ expected_symbols = [
Symbol('baz', Tags()), Symbol('baz', Tags.from_strs(['introduced=35'])),
Symbol('qux', Tags.from_strs(['apex', 'llndk'])), Symbol('qux', Tags.from_strs(['apex', 'llndk', 'introduced=35'])),
] ]
self.assertEqual(expected_symbols, version.symbols) self.assertEqual(expected_symbols, version.symbols)
@ -601,6 +643,19 @@ class SymbolFileParseTest(unittest.TestCase):
] ]
self.assertEqual(expected_symbols, version.symbols) self.assertEqual(expected_symbols, version.symbols)
def test_parse_llndk_version_is_missing(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_1 { # introduced=35
foo;
bar; # llndk
};
"""))
f = copy(self.filter)
f.llndk = True
parser = symbolfile.SymbolFileParser(input_file, {}, f)
with self.assertRaises(symbolfile.ParseError):
parser.parse()
def main() -> None: def main() -> None:
suite = unittest.TestLoader().loadTestsFromName(__name__) suite = unittest.TestLoader().loadTestsFromName(__name__)