LLNDK version maps to NDK version

Instead of annotation duplicate LLNDK versions, use a corresponding
NDK version to find LLNDK symbols.
To generate LLNDK stubs, ndkstubgen sets `--llndk` argument with the
corresponding SDK version to its `--api` argument.

Bug: 361077712
Test: atest test_ndkstubgen
      atest test_symbolfile
Change-Id: I64cd6eaeae4451326bf2e74b4d2639933f004393
diff --git a/cc/api_level.go b/cc/api_level.go
index 69a0d3a..3dac571 100644
--- a/cc/api_level.go
+++ b/cc/api_level.go
@@ -41,12 +41,25 @@
 	}
 }
 
+// Native API levels cannot be less than the MinApiLevelForArch. This function
+// sets the lower bound of the API level with the MinApiLevelForArch.
+func nativeClampedApiLevel(ctx android.BaseModuleContext,
+	apiLevel android.ApiLevel) android.ApiLevel {
+
+	min := MinApiForArch(ctx, ctx.Arch().ArchType)
+
+	if apiLevel.LessThan(min) {
+		return min
+	}
+
+	return apiLevel
+}
+
 func nativeApiLevelFromUser(ctx android.BaseModuleContext,
 	raw string) (android.ApiLevel, error) {
 
-	min := MinApiForArch(ctx, ctx.Arch().ArchType)
 	if raw == "minimum" {
-		return min, nil
+		return MinApiForArch(ctx, ctx.Arch().ArchType), nil
 	}
 
 	value, err := android.ApiLevelFromUser(ctx, raw)
@@ -54,15 +67,12 @@
 		return android.NoneApiLevel, err
 	}
 
-	if value.LessThan(min) {
-		return min, nil
-	}
-
-	return value, nil
+	return nativeClampedApiLevel(ctx, value), nil
 }
 
 func nativeApiLevelOrPanic(ctx android.BaseModuleContext,
 	raw string) android.ApiLevel {
+
 	value, err := nativeApiLevelFromUser(ctx, raw)
 	if err != nil {
 		panic(err.Error())
diff --git a/cc/cc_test.go b/cc/cc_test.go
index 144b90b..98af7b6 100644
--- a/cc/cc_test.go
+++ b/cc/cc_test.go
@@ -40,9 +40,6 @@
 
 var prepareForCcTest = android.GroupFixturePreparers(
 	PrepareForIntegrationTestWithCc,
-	android.FixtureModifyProductVariables(func(variables android.FixtureProductVariables) {
-		variables.VendorApiLevel = StringPtr("202404")
-	}),
 )
 
 var apexVariationName = "apex28"
@@ -1008,7 +1005,7 @@
 	android.AssertArrayString(t, "variants for llndk stubs", expected, actual)
 
 	params := result.ModuleForTests("libllndk", "android_vendor_arm_armv7-a-neon_shared").Description("generate stub")
-	android.AssertSame(t, "use Vendor API level for default stubs", "999999", params.Args["apiLevel"])
+	android.AssertSame(t, "use Vendor API level for default stubs", "35", params.Args["apiLevel"])
 
 	checkExportedIncludeDirs := func(module, variant string, expectedSystemDirs []string, expectedDirs ...string) {
 		t.Helper()
diff --git a/cc/library.go b/cc/library.go
index 4ce506e..f4f4203 100644
--- a/cc/library.go
+++ b/cc/library.go
@@ -566,10 +566,16 @@
 
 func (library *libraryDecorator) compile(ctx ModuleContext, flags Flags, deps PathDeps) Objects {
 	if ctx.IsLlndk() {
-		futureVendorApiLevel := android.ApiLevelOrPanic(ctx, "999999")
+		// Get the matching SDK version for the vendor API level.
+		version, err := android.GetSdkVersionForVendorApiLevel(ctx.Config().VendorApiLevel())
+		if err != nil {
+			panic(err)
+		}
+
+		// This is the vendor variant of an LLNDK library, build the LLNDK stubs.
 		nativeAbiResult := parseNativeAbiDefinition(ctx,
 			String(library.Properties.Llndk.Symbol_file),
-			futureVendorApiLevel, "--llndk")
+			nativeClampedApiLevel(ctx, version), "--llndk")
 		objs := compileStubLibrary(ctx, flags, nativeAbiResult.stubSrc)
 		if !Bool(library.Properties.Llndk.Unversioned) {
 			library.versionScriptPath = android.OptionalPathForPath(
diff --git a/cc/ndkstubgen/test_ndkstubgen.py b/cc/ndkstubgen/test_ndkstubgen.py
index 22f31d9..6c24b4f 100755
--- a/cc/ndkstubgen/test_ndkstubgen.py
+++ b/cc/ndkstubgen/test_ndkstubgen.py
@@ -473,16 +473,17 @@
             VERSION_35 { # introduced=35
                 global:
                     wiggle;
-                    waggle;
-                    waggle; # llndk=202404
-                    bubble; # llndk=202404
-                    duddle;
-                    duddle; # llndk=202504
+                    waggle; # llndk
             } VERSION_34;
+            VERSION_36 { # introduced=36
+                global:
+                    abc;
+                    xyz; # llndk
+            } VERSION_35;
         """))
         f = copy(self.filter)
         f.llndk = True
-        f.api = 202404
+        f.api = 35
         parser = symbolfile.SymbolFileParser(input_file, {}, f)
         versions = parser.parse()
 
@@ -497,8 +498,8 @@
         expected_src = textwrap.dedent("""\
             void foo() {}
             void bar() {}
+            void wiggle() {}
             void waggle() {}
-            void bubble() {}
         """)
         self.assertEqual(expected_src, src_file.getvalue())
 
@@ -510,8 +511,8 @@
             };
             VERSION_35 {
                 global:
+                    wiggle;
                     waggle;
-                    bubble;
             } VERSION_34;
         """)
         self.assertEqual(expected_version, version_file.getvalue())
@@ -521,15 +522,15 @@
             LIBANDROID {
                 global:
                     foo; # introduced=34
-                    bar; # introduced=35
-                    bar; # llndk=202404
-                    baz; # introduced=35
+                    bar; # introduced=35 llndk
+                    baz; # introduced=V
+                    qux; # introduced=36
             };
         """))
         f = copy(self.filter)
         f.llndk = True
-        f.api = 202404
-        parser = symbolfile.SymbolFileParser(input_file, {}, f)
+        f.api = 35
+        parser = symbolfile.SymbolFileParser(input_file, {'V': 35}, f)
         versions = parser.parse()
 
         src_file = io.StringIO()
@@ -543,6 +544,7 @@
         expected_src = textwrap.dedent("""\
             void foo() {}
             void bar() {}
+            void baz() {}
         """)
         self.assertEqual(expected_src, src_file.getvalue())
 
@@ -551,6 +553,7 @@
                 global:
                     foo;
                     bar;
+                    baz;
             };
         """)
         self.assertEqual(expected_version, version_file.getvalue())
diff --git a/cc/symbolfile/__init__.py b/cc/symbolfile/__init__.py
index 4553616..f2bd186 100644
--- a/cc/symbolfile/__init__.py
+++ b/cc/symbolfile/__init__.py
@@ -103,24 +103,13 @@
     @property
     def has_llndk_tags(self) -> bool:
         """Returns True if any LL-NDK tags are set."""
-        for tag in self.tags:
-            if tag == 'llndk' or tag.startswith('llndk='):
-                return True
-        return False
+        return 'llndk' in self.tags
 
     @property
     def has_platform_only_tags(self) -> bool:
         """Returns True if any platform-only tags are set."""
         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
 class Symbol:
@@ -158,8 +147,6 @@
     """Returns true if this tag has an API level that may need decoding."""
     if tag.startswith('llndk-deprecated='):
         return True
-    if tag.startswith('llndk='):
-        return True
     if tag.startswith('introduced='):
         return True
     if tag.startswith('introduced-'):
@@ -245,21 +232,19 @@
         self.systemapi = systemapi
         self.ndk = ndk
 
+    def _symbol_in_arch_api(self, tags: Tags) -> bool:
+        if not symbol_in_arch(tags, self.arch):
+            return True
+        if not symbol_in_api(tags, self.arch, self.api):
+            return True
+        return False
+
     def _should_omit_tags(self, tags: Tags) -> bool:
         """Returns True if the tagged object should be omitted.
 
         This defines the rules shared between version tagging and symbol tagging.
         """
-        # 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
+        # The apex and llndk tags will only exclude APIs from other modes. If in
         # APEX or LLNDK mode and neither tag is provided, we fall back to the
         # default behavior because all NDK symbols are implicitly available to
         # APEX and LLNDK.
@@ -268,12 +253,10 @@
                 return False
             if self.systemapi and tags.has_systemapi_tags:
                 return False
+            if self.llndk and tags.has_llndk_tags:
+                return self._symbol_in_arch_api(tags)
             return True
-        if not symbol_in_arch(tags, self.arch):
-            return True
-        if not symbol_in_api(tags, self.arch, self.api):
-            return True
-        return False
+        return self._symbol_in_arch_api(tags)
 
     def should_omit_version(self, version: Version) -> bool:
         """Returns True if the version section should be omitted.
@@ -286,10 +269,6 @@
             return True
         if version.tags.has_platform_only_tags:
             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)
 
     def should_omit_symbol(self, symbol: Symbol) -> bool:
@@ -302,6 +281,7 @@
 
         return self._should_omit_tags(symbol.tags)
 
+
 def symbol_in_arch(tags: Tags, arch: Arch) -> bool:
     """Returns true if the symbol is present for the given architecture."""
     has_arch_tags = False
@@ -316,14 +296,6 @@
     # for the tagged architectures.
     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:
     """Returns true if the symbol is present for the given API level."""
@@ -400,7 +372,6 @@
                     f'Unexpected contents at top level: {self.current_line}')
 
         self.check_no_duplicate_symbols(versions)
-        self.check_llndk_introduced(versions)
         return versions
 
     def check_no_duplicate_symbols(self, versions: Iterable[Version]) -> None:
@@ -429,31 +400,6 @@
             raise MultiplyDefinedSymbolError(
                 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:
         """Parses a single version section and returns a Version object."""
         assert self.current_line is not None
@@ -487,9 +433,7 @@
                 else:
                     raise ParseError('Unknown visiblity label: ' + visibility)
             elif global_scope and not cpp_symbols:
-                symbol = self.parse_symbol()
-                symbol.tags.copy_introduced_from(tags)
-                symbols.append(symbol)
+                symbols.append(self.parse_symbol())
             else:
                 # We're in a hidden scope or in 'extern "C++"' block. Ignore
                 # everything.
diff --git a/cc/symbolfile/test_symbolfile.py b/cc/symbolfile/test_symbolfile.py
index 8b412b9..14bb737 100644
--- a/cc/symbolfile/test_symbolfile.py
+++ b/cc/symbolfile/test_symbolfile.py
@@ -344,45 +344,6 @@
         self.assertInclude(f_llndk, s_none)
         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:
         f_none = self.filter
         f_apex = copy(f_none)
@@ -494,8 +455,8 @@
         # should_omit_tags() can differently based on introduced API level when treating
         # LLNDK-available symbols.
         expected_symbols = [
-            Symbol('baz', Tags.from_strs(['introduced=35'])),
-            Symbol('qux', Tags.from_strs(['apex', 'llndk', 'introduced=35'])),
+            Symbol('baz', Tags()),
+            Symbol('qux', Tags.from_strs(['apex', 'llndk'])),
         ]
         self.assertEqual(expected_symbols, version.symbols)
 
@@ -643,19 +604,6 @@
         ]
         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:
     suite = unittest.TestLoader().loadTestsFromName(__name__)