Use libabigail to track NDK ABIs.

The local diffing behavior is currently flagged off so we can land
this in stages.

Test: pytest cc
Test: treehugger
Test: development/tools/update_ndk_abi.sh
Test: m ndk
Bug: http://b/156513478
Change-Id: Iccb314411bc74ea3ddfea8b85b0539709295f65a
diff --git a/cc/Android.bp b/cc/Android.bp
index 1fc8d9f..46740dc 100644
--- a/cc/Android.bp
+++ b/cc/Android.bp
@@ -65,9 +65,10 @@
         "test.go",
         "toolchain_library.go",
 
-        "ndk_prebuilt.go",
+        "ndk_abi.go",
         "ndk_headers.go",
         "ndk_library.go",
+        "ndk_prebuilt.go",
         "ndk_sysroot.go",
 
         "llndk_library.go",
diff --git a/cc/cc.go b/cc/cc.go
index 91c4417..e4a52f1 100644
--- a/cc/cc.go
+++ b/cc/cc.go
@@ -2112,6 +2112,15 @@
 		}
 	}
 
+	if c.isNDKStubLibrary() {
+		// NDK stubs depend on their implementation because the ABI dumps are
+		// generated from the implementation library.
+		actx.AddFarVariationDependencies(append(ctx.Target().Variations(),
+			c.ImageVariation(),
+			blueprint.Variation{Mutator: "link", Variation: "shared"},
+		), stubImplementation, c.BaseModuleName())
+	}
+
 	// sysprop_library has to support both C++ and Java. So sysprop_library internally creates one
 	// C++ implementation library and one Java implementation library. When a module links against
 	// sysprop_library, the C++ implementation library has to be linked. syspropImplLibraries is a
diff --git a/cc/library.go b/cc/library.go
index 5e70c51..aec2e4d 100644
--- a/cc/library.go
+++ b/cc/library.go
@@ -836,16 +836,23 @@
 		if library.stubsVersion() != "" {
 			vndkVer = library.stubsVersion()
 		}
-		objs, versionScript := compileStubLibrary(ctx, flags, String(library.Properties.Llndk.Symbol_file), vndkVer, "--llndk")
+		nativeAbiResult := parseNativeAbiDefinition(ctx,
+			String(library.Properties.Llndk.Symbol_file),
+			android.ApiLevelOrPanic(ctx, vndkVer), "--llndk")
+		objs := compileStubLibrary(ctx, flags, nativeAbiResult.stubSrc)
 		if !Bool(library.Properties.Llndk.Unversioned) {
-			library.versionScriptPath = android.OptionalPathForPath(versionScript)
+			library.versionScriptPath = android.OptionalPathForPath(
+				nativeAbiResult.versionScript)
 		}
 		return objs
 	}
 	if ctx.IsVendorPublicLibrary() {
-		objs, versionScript := compileStubLibrary(ctx, flags, String(library.Properties.Vendor_public_library.Symbol_file), "current", "")
+		nativeAbiResult := parseNativeAbiDefinition(ctx,
+			String(library.Properties.Vendor_public_library.Symbol_file),
+			android.FutureApiLevel, "")
+		objs := compileStubLibrary(ctx, flags, nativeAbiResult.stubSrc)
 		if !Bool(library.Properties.Vendor_public_library.Unversioned) {
-			library.versionScriptPath = android.OptionalPathForPath(versionScript)
+			library.versionScriptPath = android.OptionalPathForPath(nativeAbiResult.versionScript)
 		}
 		return objs
 	}
@@ -855,8 +862,12 @@
 			ctx.PropertyErrorf("symbol_file", "%q doesn't have .map.txt suffix", symbolFile)
 			return Objects{}
 		}
-		objs, versionScript := compileStubLibrary(ctx, flags, String(library.Properties.Stubs.Symbol_file), library.MutatedProperties.StubsVersion, "--apex")
-		library.versionScriptPath = android.OptionalPathForPath(versionScript)
+		nativeAbiResult := parseNativeAbiDefinition(ctx, symbolFile,
+			android.ApiLevelOrPanic(ctx, library.MutatedProperties.StubsVersion),
+			"--apex")
+		objs := compileStubLibrary(ctx, flags, nativeAbiResult.stubSrc)
+		library.versionScriptPath = android.OptionalPathForPath(
+			nativeAbiResult.versionScript)
 		return objs
 	}
 
diff --git a/cc/ndk_abi.go b/cc/ndk_abi.go
new file mode 100644
index 0000000..b9b57af
--- /dev/null
+++ b/cc/ndk_abi.go
@@ -0,0 +1,102 @@
+// Copyright 2021 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cc
+
+import (
+	"android/soong/android"
+)
+
+func init() {
+	android.RegisterSingletonType("ndk_abi_dump", NdkAbiDumpSingleton)
+	android.RegisterSingletonType("ndk_abi_diff", NdkAbiDiffSingleton)
+}
+
+func getNdkAbiDumpInstallBase(ctx android.PathContext) android.OutputPath {
+	return android.PathForOutput(ctx).Join(ctx, "abi-dumps/ndk")
+}
+
+func getNdkAbiDumpTimestampFile(ctx android.PathContext) android.OutputPath {
+	return android.PathForOutput(ctx, "ndk_abi_dump.timestamp")
+}
+
+func NdkAbiDumpSingleton() android.Singleton {
+	return &ndkAbiDumpSingleton{}
+}
+
+type ndkAbiDumpSingleton struct{}
+
+func (n *ndkAbiDumpSingleton) GenerateBuildActions(ctx android.SingletonContext) {
+	var depPaths android.Paths
+	ctx.VisitAllModules(func(module android.Module) {
+		if !module.Enabled() {
+			return
+		}
+
+		if m, ok := module.(*Module); ok {
+			if installer, ok := m.installer.(*stubDecorator); ok {
+				if canDumpAbi(m) {
+					depPaths = append(depPaths, installer.abiDumpPath)
+				}
+			}
+		}
+	})
+
+	// `m dump-ndk-abi` will dump the NDK ABI.
+	// `development/tools/ndk/update_ndk_abi.sh` will dump the NDK ABI and
+	// update the golden copies in prebuilts/abi-dumps/ndk.
+	ctx.Build(pctx, android.BuildParams{
+		Rule:      android.Touch,
+		Output:    getNdkAbiDumpTimestampFile(ctx),
+		Implicits: depPaths,
+	})
+
+	ctx.Phony("dump-ndk-abi", getNdkAbiDumpTimestampFile(ctx))
+}
+
+func getNdkAbiDiffTimestampFile(ctx android.PathContext) android.WritablePath {
+	return android.PathForOutput(ctx, "ndk_abi_diff.timestamp")
+}
+
+func NdkAbiDiffSingleton() android.Singleton {
+	return &ndkAbiDiffSingleton{}
+}
+
+type ndkAbiDiffSingleton struct{}
+
+func (n *ndkAbiDiffSingleton) GenerateBuildActions(ctx android.SingletonContext) {
+	var depPaths android.Paths
+	ctx.VisitAllModules(func(module android.Module) {
+		if m, ok := module.(android.Module); ok && !m.Enabled() {
+			return
+		}
+
+		if m, ok := module.(*Module); ok {
+			if installer, ok := m.installer.(*stubDecorator); ok {
+				depPaths = append(depPaths, installer.abiDiffPaths...)
+			}
+		}
+	})
+
+	depPaths = append(depPaths, getNdkAbiDumpTimestampFile(ctx))
+
+	// `m diff-ndk-abi` will diff the NDK ABI.
+	ctx.Build(pctx, android.BuildParams{
+		Rule:      android.Touch,
+		Output:    getNdkAbiDiffTimestampFile(ctx),
+		Implicits: depPaths,
+	})
+
+	ctx.Phony("diff-ndk-abi", getNdkAbiDiffTimestampFile(ctx))
+}
diff --git a/cc/ndk_library.go b/cc/ndk_library.go
index 95d8477..f3d2ba1 100644
--- a/cc/ndk_library.go
+++ b/cc/ndk_library.go
@@ -16,6 +16,7 @@
 
 import (
 	"fmt"
+	"path/filepath"
 	"strings"
 	"sync"
 
@@ -28,6 +29,9 @@
 func init() {
 	pctx.HostBinToolVariable("ndkStubGenerator", "ndkstubgen")
 	pctx.HostBinToolVariable("ndk_api_coverage_parser", "ndk_api_coverage_parser")
+	pctx.HostBinToolVariable("abidiff", "abidiff")
+	pctx.HostBinToolVariable("abitidy", "abitidy")
+	pctx.HostBinToolVariable("abidw", "abidw")
 }
 
 var (
@@ -44,11 +48,31 @@
 			CommandDeps: []string{"$ndk_api_coverage_parser"},
 		}, "apiMap")
 
+	abidw = pctx.AndroidStaticRule("abidw",
+		blueprint.RuleParams{
+			Command: "$abidw --type-id-style hash --no-corpus-path " +
+				"--no-show-locs --no-comp-dir-path -w $symbolList $in | " +
+				"$abitidy --all -o $out",
+			CommandDeps: []string{"$abitidy", "$abidw"},
+		}, "symbolList")
+
+	abidiff = pctx.AndroidStaticRule("abidiff",
+		blueprint.RuleParams{
+			// Need to create *some* output for ninja. We don't want to use tee
+			// because we don't want to spam the build output with "nothing
+			// changed" messages, so redirect output message to $out, and if
+			// changes were detected print the output and fail.
+			Command:     "$abidiff $args $in > $out || (cat $out && false)",
+			CommandDeps: []string{"$abidiff"},
+		}, "args")
+
 	ndkLibrarySuffix = ".ndk"
 
 	ndkKnownLibsKey = android.NewOnceKey("ndkKnownLibsKey")
 	// protects ndkKnownLibs writes during parallel BeginMutator.
 	ndkKnownLibsLock sync.Mutex
+
+	stubImplementation = dependencyTag{name: "stubImplementation"}
 )
 
 // The First_version and Unversioned_until properties of this struct should not
@@ -89,6 +113,8 @@
 	versionScriptPath     android.ModuleGenPath
 	parsedCoverageXmlPath android.ModuleOutPath
 	installPath           android.Path
+	abiDumpPath           android.OutputPath
+	abiDiffPaths          android.Paths
 
 	apiLevel         android.ApiLevel
 	firstVersion     android.ApiLevel
@@ -123,6 +149,16 @@
 	if !ctx.Module().Enabled() {
 		return nil
 	}
+	if ctx.Os() != android.Android {
+		// These modules are always android.DeviceEnabled only, but
+		// those include Fuchsia devices, which we don't support.
+		ctx.Module().Disable()
+		return nil
+	}
+	if ctx.Target().NativeBridge == android.NativeBridgeEnabled {
+		ctx.Module().Disable()
+		return nil
+	}
 	firstVersion, err := nativeApiLevelFromUser(ctx,
 		String(this.properties.First_version))
 	if err != nil {
@@ -204,30 +240,45 @@
 	return addStubLibraryCompilerFlags(flags)
 }
 
-func compileStubLibrary(ctx ModuleContext, flags Flags, symbolFile, apiLevel, genstubFlags string) (Objects, android.ModuleGenPath) {
-	arch := ctx.Arch().ArchType.String()
+type ndkApiOutputs struct {
+	stubSrc       android.ModuleGenPath
+	versionScript android.ModuleGenPath
+	symbolList    android.ModuleGenPath
+}
+
+func parseNativeAbiDefinition(ctx ModuleContext, symbolFile string,
+	apiLevel android.ApiLevel, genstubFlags string) ndkApiOutputs {
 
 	stubSrcPath := android.PathForModuleGen(ctx, "stub.c")
 	versionScriptPath := android.PathForModuleGen(ctx, "stub.map")
 	symbolFilePath := android.PathForModuleSrc(ctx, symbolFile)
+	symbolListPath := android.PathForModuleGen(ctx, "abi_symbol_list.txt")
 	apiLevelsJson := android.GetApiLevelsJson(ctx)
 	ctx.Build(pctx, android.BuildParams{
 		Rule:        genStubSrc,
 		Description: "generate stubs " + symbolFilePath.Rel(),
-		Outputs:     []android.WritablePath{stubSrcPath, versionScriptPath},
-		Input:       symbolFilePath,
-		Implicits:   []android.Path{apiLevelsJson},
+		Outputs: []android.WritablePath{stubSrcPath, versionScriptPath,
+			symbolListPath},
+		Input:     symbolFilePath,
+		Implicits: []android.Path{apiLevelsJson},
 		Args: map[string]string{
-			"arch":     arch,
-			"apiLevel": apiLevel,
+			"arch":     ctx.Arch().ArchType.String(),
+			"apiLevel": apiLevel.String(),
 			"apiMap":   apiLevelsJson.String(),
 			"flags":    genstubFlags,
 		},
 	})
 
-	subdir := ""
-	srcs := []android.Path{stubSrcPath}
-	return compileObjs(ctx, flagsToBuilderFlags(flags), subdir, srcs, nil, nil), versionScriptPath
+	return ndkApiOutputs{
+		stubSrc:       stubSrcPath,
+		versionScript: versionScriptPath,
+		symbolList:    symbolListPath,
+	}
+}
+
+func compileStubLibrary(ctx ModuleContext, flags Flags, src android.Path) Objects {
+	return compileObjs(ctx, flagsToBuilderFlags(flags), "",
+		android.Paths{src}, nil, nil)
 }
 
 func parseSymbolFileForCoverage(ctx ModuleContext, symbolFile string) android.ModuleOutPath {
@@ -248,6 +299,140 @@
 	return parsedApiCoveragePath
 }
 
+func (this *stubDecorator) findImplementationLibrary(ctx ModuleContext) android.Path {
+	dep := ctx.GetDirectDepWithTag(strings.TrimSuffix(ctx.ModuleName(), ndkLibrarySuffix),
+		stubImplementation)
+	if dep == nil {
+		ctx.ModuleErrorf("Could not find implementation for stub")
+		return nil
+	}
+	impl, ok := dep.(*Module)
+	if !ok {
+		ctx.ModuleErrorf("Implementation for stub is not correct module type")
+	}
+	output := impl.UnstrippedOutputFile()
+	if output == nil {
+		ctx.ModuleErrorf("implementation module (%s) has no output", impl)
+		return nil
+	}
+
+	return output
+}
+
+func (this *stubDecorator) libraryName(ctx ModuleContext) string {
+	return strings.TrimSuffix(ctx.ModuleName(), ndkLibrarySuffix)
+}
+
+func (this *stubDecorator) findPrebuiltAbiDump(ctx ModuleContext,
+	apiLevel android.ApiLevel) android.OptionalPath {
+
+	subpath := filepath.Join("prebuilts/abi-dumps/ndk", apiLevel.String(),
+		ctx.Arch().ArchType.String(), this.libraryName(ctx), "abi.xml")
+	return android.ExistentPathForSource(ctx, subpath)
+}
+
+// Feature flag.
+func canDumpAbi(module android.Module) bool {
+	return true
+}
+
+// Feature flag to disable diffing against prebuilts.
+func canDiffAbi(module android.Module) bool {
+	return false
+}
+
+func (this *stubDecorator) dumpAbi(ctx ModuleContext, symbolList android.Path) {
+	implementationLibrary := this.findImplementationLibrary(ctx)
+	this.abiDumpPath = getNdkAbiDumpInstallBase(ctx).Join(ctx,
+		this.apiLevel.String(), ctx.Arch().ArchType.String(),
+		this.libraryName(ctx), "abi.xml")
+	ctx.Build(pctx, android.BuildParams{
+		Rule:        abidw,
+		Description: fmt.Sprintf("abidw %s", implementationLibrary),
+		Output:      this.abiDumpPath,
+		Input:       implementationLibrary,
+		Implicit:    symbolList,
+		Args: map[string]string{
+			"symbolList": symbolList.String(),
+		},
+	})
+}
+
+func findNextApiLevel(ctx ModuleContext, apiLevel android.ApiLevel) *android.ApiLevel {
+	apiLevels := append(ctx.Config().AllSupportedApiLevels(),
+		android.FutureApiLevel)
+	for _, api := range apiLevels {
+		if api.GreaterThan(apiLevel) {
+			return &api
+		}
+	}
+	return nil
+}
+
+func (this *stubDecorator) diffAbi(ctx ModuleContext) {
+	missingPrebuiltError := fmt.Sprintf(
+		"Did not find prebuilt ABI dump for %q. Generate with "+
+			"//development/tools/ndk/update_ndk_abi.sh.", this.libraryName(ctx))
+
+	// Catch any ABI changes compared to the checked-in definition of this API
+	// level.
+	abiDiffPath := android.PathForModuleOut(ctx, "abidiff.timestamp")
+	prebuiltAbiDump := this.findPrebuiltAbiDump(ctx, this.apiLevel)
+	if !prebuiltAbiDump.Valid() {
+		ctx.Build(pctx, android.BuildParams{
+			Rule:   android.ErrorRule,
+			Output: abiDiffPath,
+			Args: map[string]string{
+				"error": missingPrebuiltError,
+			},
+		})
+	} else {
+		ctx.Build(pctx, android.BuildParams{
+			Rule: abidiff,
+			Description: fmt.Sprintf("abidiff %s %s", prebuiltAbiDump,
+				this.abiDumpPath),
+			Output: abiDiffPath,
+			Inputs: android.Paths{prebuiltAbiDump.Path(), this.abiDumpPath},
+		})
+	}
+	this.abiDiffPaths = append(this.abiDiffPaths, abiDiffPath)
+
+	// Also ensure that the ABI of the next API level (if there is one) matches
+	// this API level. *New* ABI is allowed, but any changes to APIs that exist
+	// in this API level are disallowed.
+	if !this.apiLevel.IsCurrent() {
+		nextApiLevel := findNextApiLevel(ctx, this.apiLevel)
+		if nextApiLevel == nil {
+			panic(fmt.Errorf("could not determine which API level follows "+
+				"non-current API level %s", this.apiLevel))
+		}
+		nextAbiDiffPath := android.PathForModuleOut(ctx,
+			"abidiff_next.timestamp")
+		nextAbiDump := this.findPrebuiltAbiDump(ctx, *nextApiLevel)
+		if !nextAbiDump.Valid() {
+			ctx.Build(pctx, android.BuildParams{
+				Rule:   android.ErrorRule,
+				Output: nextAbiDiffPath,
+				Args: map[string]string{
+					"error": missingPrebuiltError,
+				},
+			})
+		} else {
+			ctx.Build(pctx, android.BuildParams{
+				Rule: abidiff,
+				Description: fmt.Sprintf("abidiff %s %s", this.abiDumpPath,
+					nextAbiDump),
+				Output: nextAbiDiffPath,
+				Inputs: android.Paths{this.abiDumpPath, nextAbiDump.Path()},
+				Args: map[string]string{
+					"args": "--no-added-syms",
+				},
+			})
+		}
+		this.abiDiffPaths = append(this.abiDiffPaths, nextAbiDiffPath)
+	}
+}
+
 func (c *stubDecorator) compile(ctx ModuleContext, flags Flags, deps PathDeps) Objects {
 	if !strings.HasSuffix(String(c.properties.Symbol_file), ".map.txt") {
 		ctx.PropertyErrorf("symbol_file", "must end with .map.txt")
@@ -264,9 +449,15 @@
 	}
 
 	symbolFile := String(c.properties.Symbol_file)
-	objs, versionScript := compileStubLibrary(ctx, flags, symbolFile,
-		c.apiLevel.String(), "")
-	c.versionScriptPath = versionScript
+	nativeAbiResult := parseNativeAbiDefinition(ctx, symbolFile, c.apiLevel, "")
+	objs := compileStubLibrary(ctx, flags, nativeAbiResult.stubSrc)
+	c.versionScriptPath = nativeAbiResult.versionScript
+	if canDumpAbi(ctx.Module()) {
+		c.dumpAbi(ctx, nativeAbiResult.symbolList)
+		if canDiffAbi(ctx.Module()) {
+			c.diffAbi(ctx)
+		}
+	}
 	if c.apiLevel.IsCurrent() && ctx.PrimaryArch() {
 		c.parsedCoverageXmlPath = parseSymbolFileForCoverage(ctx, symbolFile)
 	}
diff --git a/cc/ndk_sysroot.go b/cc/ndk_sysroot.go
index d8c500e..4a9601b 100644
--- a/cc/ndk_sysroot.go
+++ b/cc/ndk_sysroot.go
@@ -144,10 +144,9 @@
 		Inputs:      licensePaths,
 	})
 
-	baseDepPaths := append(installPaths, combinedLicense)
+	baseDepPaths := append(installPaths, combinedLicense,
+		getNdkAbiDiffTimestampFile(ctx))
 
-	// There's a dummy "ndk" rule defined in ndk/Android.mk that depends on
-	// this. `m ndk` will build the sysroots.
 	ctx.Build(pctx, android.BuildParams{
 		Rule:      android.Touch,
 		Output:    getNdkBaseTimestampFile(ctx),
@@ -156,6 +155,11 @@
 
 	fullDepPaths := append(staticLibInstallPaths, getNdkBaseTimestampFile(ctx))
 
+	// There's a phony "ndk" rule defined in core/main.mk that depends on this.
+	// `m ndk` will build the sysroots for the architectures in the current
+	// lunch target. `build/soong/scripts/build-ndk-prebuilts.sh` will build the
+	// sysroots for all the NDK architectures and package them so they can be
+	// imported into the NDK's build.
 	ctx.Build(pctx, android.BuildParams{
 		Rule:      android.Touch,
 		Output:    getNdkFullTimestampFile(ctx),
diff --git a/cc/ndkstubgen/__init__.py b/cc/ndkstubgen/__init__.py
index 86bf6ff..2387e69 100755
--- a/cc/ndkstubgen/__init__.py
+++ b/cc/ndkstubgen/__init__.py
@@ -18,7 +18,7 @@
 import argparse
 import json
 import logging
-import os
+from pathlib import Path
 import sys
 from typing import Iterable, TextIO
 
@@ -28,10 +28,12 @@
 
 class Generator:
     """Output generator that writes stub source files and version scripts."""
-    def __init__(self, src_file: TextIO, version_script: TextIO, arch: Arch,
-                 api: int, llndk: bool, apex: bool) -> None:
+    def __init__(self, src_file: TextIO, version_script: TextIO,
+                 symbol_list: TextIO, arch: Arch, api: int, llndk: bool,
+                 apex: bool) -> None:
         self.src_file = src_file
         self.version_script = version_script
+        self.symbol_list = symbol_list
         self.arch = arch
         self.api = api
         self.llndk = llndk
@@ -39,6 +41,7 @@
 
     def write(self, versions: Iterable[Version]) -> None:
         """Writes all symbol data to the output files."""
+        self.symbol_list.write('[abi_symbol_list]\n')
         for version in versions:
             self.write_version(version)
 
@@ -76,11 +79,11 @@
                     weak = '__attribute__((weak)) '
 
                 if 'var' in symbol.tags:
-                    self.src_file.write('{}int {} = 0;\n'.format(
-                        weak, symbol.name))
+                    self.src_file.write(f'{weak}int {symbol.name} = 0;\n')
                 else:
-                    self.src_file.write('{}void {}() {{}}\n'.format(
-                        weak, symbol.name))
+                    self.src_file.write(f'{weak}void {symbol.name}() {{}}\n')
+
+                self.symbol_list.write(f'{symbol.name}\n')
 
             if not version_empty and section_versioned:
                 base = '' if version.base is None else ' ' + version.base
@@ -91,6 +94,10 @@
     """Parses and returns command line arguments."""
     parser = argparse.ArgumentParser()
 
+    def resolved_path(raw: str) -> Path:
+        """Returns a resolved Path for the given string."""
+        return Path(raw).resolve()
+
     parser.add_argument('-v', '--verbose', action='count', default=0)
 
     parser.add_argument(
@@ -103,26 +110,23 @@
     parser.add_argument(
         '--apex', action='store_true', help='Use the APEX variant.')
 
-    # https://github.com/python/mypy/issues/1317
-    # mypy has issues with using os.path.realpath as an argument here.
-    parser.add_argument(
-        '--api-map',
-        type=os.path.realpath,  # type: ignore
-        required=True,
-        help='Path to the API level map JSON file.')
+    parser.add_argument('--api-map',
+                        type=resolved_path,
+                        required=True,
+                        help='Path to the API level map JSON file.')
 
-    parser.add_argument(
-        'symbol_file',
-        type=os.path.realpath,  # type: ignore
-        help='Path to symbol file.')
-    parser.add_argument(
-        'stub_src',
-        type=os.path.realpath,  # type: ignore
-        help='Path to output stub source file.')
-    parser.add_argument(
-        'version_script',
-        type=os.path.realpath,  # type: ignore
-        help='Path to output version script.')
+    parser.add_argument('symbol_file',
+                        type=resolved_path,
+                        help='Path to symbol file.')
+    parser.add_argument('stub_src',
+                        type=resolved_path,
+                        help='Path to output stub source file.')
+    parser.add_argument('version_script',
+                        type=resolved_path,
+                        help='Path to output version script.')
+    parser.add_argument('symbol_list',
+                        type=resolved_path,
+                        help='Path to output abigail symbol list.')
 
     return parser.parse_args()
 
@@ -131,7 +135,7 @@
     """Program entry point."""
     args = parse_args()
 
-    with open(args.api_map) as map_file:
+    with args.api_map.open() as map_file:
         api_map = json.load(map_file)
     api = symbolfile.decode_api_level(args.api, api_map)
 
@@ -141,19 +145,20 @@
         verbosity = 2
     logging.basicConfig(level=verbose_map[verbosity])
 
-    with open(args.symbol_file) as symbol_file:
+    with args.symbol_file.open() as symbol_file:
         try:
             versions = symbolfile.SymbolFileParser(symbol_file, api_map,
                                                    args.arch, api, args.llndk,
                                                    args.apex).parse()
         except symbolfile.MultiplyDefinedSymbolError as ex:
-            sys.exit('{}: error: {}'.format(args.symbol_file, ex))
+            sys.exit(f'{args.symbol_file}: error: {ex}')
 
-    with open(args.stub_src, 'w') as src_file:
-        with open(args.version_script, 'w') as version_file:
-            generator = Generator(src_file, version_file, args.arch, api,
-                                  args.llndk, args.apex)
-            generator.write(versions)
+    with args.stub_src.open('w') as src_file:
+        with args.version_script.open('w') as version_script:
+            with args.symbol_list.open('w') as symbol_list:
+                generator = Generator(src_file, version_script, symbol_list,
+                                      args.arch, api, args.llndk, args.apex)
+                generator.write(versions)
 
 
 if __name__ == '__main__':
diff --git a/cc/ndkstubgen/test_ndkstubgen.py b/cc/ndkstubgen/test_ndkstubgen.py
index 6d2c9d6..3dbab61 100755
--- a/cc/ndkstubgen/test_ndkstubgen.py
+++ b/cc/ndkstubgen/test_ndkstubgen.py
@@ -33,8 +33,10 @@
         # OmitVersionTest, PrivateVersionTest, and SymbolPresenceTest.
         src_file = io.StringIO()
         version_file = io.StringIO()
-        generator = ndkstubgen.Generator(src_file, version_file, Arch('arm'),
-                                         9, False, False)
+        symbol_list_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file,
+                                         version_file, symbol_list_file,
+                                         Arch('arm'), 9, False, False)
 
         version = symbolfile.Version('VERSION_PRIVATE', None, [], [
             symbolfile.Symbol('foo', []),
@@ -62,8 +64,10 @@
         # SymbolPresenceTest.
         src_file = io.StringIO()
         version_file = io.StringIO()
-        generator = ndkstubgen.Generator(src_file, version_file, Arch('arm'),
-                                         9, False, False)
+        symbol_list_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file,
+                                         version_file, symbol_list_file,
+                                         Arch('arm'), 9, False, False)
 
         version = symbolfile.Version('VERSION_1', None, [], [
             symbolfile.Symbol('foo', [Tag('x86')]),
@@ -96,8 +100,10 @@
     def test_write(self) -> None:
         src_file = io.StringIO()
         version_file = io.StringIO()
-        generator = ndkstubgen.Generator(src_file, version_file, Arch('arm'),
-                                         9, False, False)
+        symbol_list_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file,
+                                         version_file, symbol_list_file,
+                                         Arch('arm'), 9, False, False)
 
         versions = [
             symbolfile.Version('VERSION_1', None, [], [
@@ -141,6 +147,17 @@
         """)
         self.assertEqual(expected_version, version_file.getvalue())
 
+        expected_allowlist = textwrap.dedent("""\
+            [abi_symbol_list]
+            foo
+            bar
+            woodly
+            doodly
+            baz
+            qux
+        """)
+        self.assertEqual(expected_allowlist, symbol_list_file.getvalue())
+
 
 class IntegrationTest(unittest.TestCase):
     def test_integration(self) -> None:
@@ -186,8 +203,10 @@
 
         src_file = io.StringIO()
         version_file = io.StringIO()
-        generator = ndkstubgen.Generator(src_file, version_file, Arch('arm'),
-                                         9, False, False)
+        symbol_list_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file,
+                                         version_file, symbol_list_file,
+                                         Arch('arm'), 9, False, False)
         generator.write(versions)
 
         expected_src = textwrap.dedent("""\
@@ -215,6 +234,16 @@
         """)
         self.assertEqual(expected_version, version_file.getvalue())
 
+        expected_allowlist = textwrap.dedent("""\
+            [abi_symbol_list]
+            foo
+            baz
+            qux
+            wibble
+            wobble
+        """)
+        self.assertEqual(expected_allowlist, symbol_list_file.getvalue())
+
     def test_integration_future_api(self) -> None:
         api_map = {
             'O': 9000,
@@ -238,8 +267,10 @@
 
         src_file = io.StringIO()
         version_file = io.StringIO()
-        generator = ndkstubgen.Generator(src_file, version_file, Arch('arm'),
-                                         9001, False, False)
+        symbol_list_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file,
+                                         version_file, symbol_list_file,
+                                         Arch('arm'), 9001, False, False)
         generator.write(versions)
 
         expected_src = textwrap.dedent("""\
@@ -257,6 +288,13 @@
         """)
         self.assertEqual(expected_version, version_file.getvalue())
 
+        expected_allowlist = textwrap.dedent("""\
+            [abi_symbol_list]
+            foo
+            bar
+        """)
+        self.assertEqual(expected_allowlist, symbol_list_file.getvalue())
+
     def test_multiple_definition(self) -> None:
         input_file = io.StringIO(textwrap.dedent("""\
             VERSION_1 {
@@ -336,8 +374,10 @@
 
         src_file = io.StringIO()
         version_file = io.StringIO()
-        generator = ndkstubgen.Generator(src_file, version_file, Arch('arm'),
-                                         9, False, True)
+        symbol_list_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file,
+                                         version_file, symbol_list_file,
+                                         Arch('arm'), 9, False, True)
         generator.write(versions)
 
         expected_src = textwrap.dedent("""\