rust: Add version scripts and symbol exports

This allows Rust modules to define a version_script for shared library
variants.

This requires using a wrapper for the linker (clang++) to intercept
the flags which rustc emits.

This also adds the ability to export additional symbols in addition
to those exported by rustc by default, e.g. whole_static_library
symbols.

Bug: 314309643
Test: New Soong tests pass.
Test: m
Test: m <simple version script module>
Test: m <simple extra symbols module>

Change-Id: I93c9552e5e1181df4663d194c4df4b7053553dd4
diff --git a/rust/builder.go b/rust/builder.go
index a1e17fc..e5434ef 100644
--- a/rust/builder.go
+++ b/rust/builder.go
@@ -30,11 +30,11 @@
 	rustc = pctx.AndroidStaticRule("rustc",
 		blueprint.RuleParams{
 			Command: "$envVars $rustcCmd " +
-				"-C linker=${config.RustLinker} " +
-				"-C link-args=\"${crtBegin} ${earlyLinkFlags} ${linkFlags} ${crtEnd}\" " +
+				"-C linker=${RustcLinkerCmd} " +
+				"-C link-args=\"--android-clang-bin=${config.ClangCmd} ${crtBegin} ${earlyLinkFlags} ${linkFlags} ${crtEnd}\" " +
 				"--emit link -o $out --emit dep-info=$out.d.raw $in ${libFlags} $rustcFlags" +
 				" && grep ^$out: $out.d.raw > $out.d",
-			CommandDeps: []string{"$rustcCmd"},
+			CommandDeps: []string{"$rustcCmd", "${RustcLinkerCmd}", "${config.ClangCmd}"},
 			// Rustc deps-info writes out make compatible dep files: https://github.com/rust-lang/rust/issues/7633
 			// Rustc emits unneeded dependency lines for the .d and input .rs files.
 			// Those extra lines cause ninja warning:
@@ -102,10 +102,10 @@
 				`KYTHE_CANONICALIZE_VNAME_PATHS=prefer-relative ` +
 				`$rustExtractor $envVars ` +
 				`$rustcCmd ` +
-				`-C linker=${config.RustLinker} ` +
-				`-C link-args="${crtBegin} ${linkFlags} ${crtEnd}" ` +
+				`-C linker=${RustcLinkerCmd} ` +
+				`-C link-args="--android-clang-bin=${config.ClangCmd} ${crtBegin} ${linkFlags} ${crtEnd}" ` +
 				`$in ${libFlags} $rustcFlags`,
-			CommandDeps:    []string{"$rustExtractor", "$kytheVnames"},
+			CommandDeps:    []string{"$rustExtractor", "$kytheVnames", "${RustcLinkerCmd}", "${config.ClangCmd}"},
 			Rspfile:        "${out}.rsp",
 			RspfileContent: "$in",
 		},
@@ -119,6 +119,7 @@
 
 func init() {
 	pctx.HostBinToolVariable("SoongZipCmd", "soong_zip")
+	pctx.HostBinToolVariable("RustcLinkerCmd", "rustc_linker")
 	cc.TransformRlibstoStaticlib = TransformRlibstoStaticlib
 }
 
@@ -411,6 +412,7 @@
 	implicits = append(implicits, deps.SharedLibDeps...)
 	implicits = append(implicits, deps.srcProviderFiles...)
 	implicits = append(implicits, deps.AfdoProfiles...)
+	implicits = append(implicits, deps.LinkerDeps...)
 
 	implicits = append(implicits, deps.CrtBegin...)
 	implicits = append(implicits, deps.CrtEnd...)
diff --git a/rust/config/global.go b/rust/config/global.go
index 7b79fca..66ffc0b 100644
--- a/rust/config/global.go
+++ b/rust/config/global.go
@@ -121,7 +121,7 @@
 	pctx.StaticVariable("RustBin", "${RustPath}/bin")
 
 	pctx.ImportAs("cc_config", "android/soong/cc/config")
-	pctx.StaticVariable("RustLinker", "${cc_config.ClangBin}/clang++")
+	pctx.StaticVariable("ClangCmd", "${cc_config.ClangBin}/clang++")
 
 	pctx.StaticVariable("DeviceGlobalLinkFlags", strings.Join(deviceGlobalLinkFlags, " "))
 
diff --git a/rust/library.go b/rust/library.go
index bd3359b..9f9c402 100644
--- a/rust/library.go
+++ b/rust/library.go
@@ -69,6 +69,18 @@
 	// path to include directories to export to cc_* modules, only relevant for static/shared variants.
 	Export_include_dirs []string `android:"path,arch_variant"`
 
+	// Version script to pass to the linker. By default this will replace the
+	// implicit rustc emitted version script to mirror expected behavior in CC.
+	// This is only relevant for rust_ffi_shared modules which are exposing a
+	// versioned C API.
+	Version_script *string `android:"path,arch_variant"`
+
+	// A version_script formatted text file with additional symbols to export
+	// for rust shared or dylibs which the rustc compiler does not automatically
+	// export, e.g. additional symbols from whole_static_libs. Unlike
+	// Version_script, this is not meant to imply a stable API.
+	Extra_exported_symbols *string `android:"path,arch_variant"`
+
 	// Whether this library is part of the Rust toolchain sysroot.
 	Sysroot *bool
 
@@ -576,7 +588,31 @@
 	flags.LinkFlags = append(flags.LinkFlags, deps.depLinkFlags...)
 	flags.LinkFlags = append(flags.LinkFlags, deps.linkObjects...)
 
+	if String(library.Properties.Version_script) != "" {
+		if String(library.Properties.Extra_exported_symbols) != "" {
+			ctx.ModuleErrorf("version_script and extra_exported_symbols cannot both be set.")
+		}
+
+		if library.shared() {
+			// "-Wl,--android-version-script" signals to the rustcLinker script
+			// that the default version script should be removed.
+			flags.LinkFlags = append(flags.LinkFlags, "-Wl,--android-version-script="+android.PathForModuleSrc(ctx, String(library.Properties.Version_script)).String())
+			deps.LinkerDeps = append(deps.LinkerDeps, android.PathForModuleSrc(ctx, String(library.Properties.Version_script)))
+		} else if !library.static() && !library.rlib() {
+			// We include rlibs here because rust_ffi produces rlib variants
+			ctx.PropertyErrorf("version_script", "can only be set for rust_ffi modules")
+		}
+	}
+
+	if String(library.Properties.Extra_exported_symbols) != "" {
+		// Passing a second version script (rustc calculates and emits a
+		// default version script) will concatenate the first version script.
+		flags.LinkFlags = append(flags.LinkFlags, "-Wl,--version-script="+android.PathForModuleSrc(ctx, String(library.Properties.Extra_exported_symbols)).String())
+		deps.LinkerDeps = append(deps.LinkerDeps, android.PathForModuleSrc(ctx, String(library.Properties.Extra_exported_symbols)))
+	}
+
 	if library.dylib() {
+
 		// We need prefer-dynamic for now to avoid linking in the static stdlib. See:
 		// https://github.com/rust-lang/rust/issues/19680
 		// https://github.com/rust-lang/rust/issues/34909
diff --git a/rust/library_test.go b/rust/library_test.go
index 35a420c..adbf4fe 100644
--- a/rust/library_test.go
+++ b/rust/library_test.go
@@ -441,3 +441,60 @@
 	libfooStatic := ctx.ModuleForTests("libfoo", "linux_glibc_x86_64_static").Rule("cc")
 	android.AssertStringDoesContain(t, "cFlags for lib module", libfooStatic.Args["cFlags"], " -Irust_includes ")
 }
+
+func TestRustVersionScript(t *testing.T) {
+	ctx := testRust(t, `
+	rust_library {
+		name: "librs",
+		srcs: ["bar.rs"],
+		crate_name: "rs",
+		extra_exported_symbols: "librs.map.txt",
+	}
+	rust_ffi {
+		name: "libffi",
+		srcs: ["foo.rs"],
+		crate_name: "ffi",
+		version_script: "libffi.map.txt",
+	}
+	`)
+
+	//linkFlags
+	librs := ctx.ModuleForTests("librs", "android_arm64_armv8-a_dylib").Rule("rustc")
+	libffi := ctx.ModuleForTests("libffi", "android_arm64_armv8-a_shared").Rule("rustc")
+
+	if !strings.Contains(librs.Args["linkFlags"], "-Wl,--version-script=librs.map.txt") {
+		t.Errorf("missing expected -Wl,--version-script= linker flag for libextended shared lib, linkFlags: %#v",
+			librs.Args["linkFlags"])
+	}
+	if strings.Contains(librs.Args["linkFlags"], "-Wl,--android-version-script=librs.map.txt") {
+		t.Errorf("unexpected -Wl,--android-version-script= linker flag for libextended shared lib, linkFlags: %#v",
+			librs.Args["linkFlags"])
+	}
+
+	if !strings.Contains(libffi.Args["linkFlags"], "-Wl,--android-version-script=libffi.map.txt") {
+		t.Errorf("missing -Wl,--android-version-script= linker flag for libreplaced shared lib, linkFlags: %#v",
+			libffi.Args["linkFlags"])
+	}
+	if strings.Contains(libffi.Args["linkFlags"], "-Wl,--version-script=libffi.map.txt") {
+		t.Errorf("unexpected -Wl,--version-script= linker flag for libextended shared lib, linkFlags: %#v",
+			libffi.Args["linkFlags"])
+	}
+}
+
+func TestRustVersionScriptPropertyErrors(t *testing.T) {
+	testRustError(t, "version_script: can only be set for rust_ffi modules", `
+		rust_library {
+			name: "librs",
+			srcs: ["bar.rs"],
+			crate_name: "rs",
+			version_script: "libbar.map.txt",
+		}`)
+	testRustError(t, "version_script and extra_exported_symbols", `
+		rust_ffi {
+			name: "librs",
+			srcs: ["bar.rs"],
+			crate_name: "rs",
+			version_script: "libbar.map.txt",
+			extra_exported_symbols: "libbar.map.txt",
+		}`)
+}
diff --git a/rust/rust.go b/rust/rust.go
index 64cfa40..a7ad294 100644
--- a/rust/rust.go
+++ b/rust/rust.go
@@ -427,6 +427,7 @@
 	StaticLibs    android.Paths
 	ProcMacros    RustLibraries
 	AfdoProfiles  android.Paths
+	LinkerDeps    android.Paths
 
 	// depFlags and depLinkFlags are rustc and linker (clang) flags.
 	depFlags     []string
diff --git a/scripts/Android.bp b/scripts/Android.bp
index 00b3ca5..d39c84a 100644
--- a/scripts/Android.bp
+++ b/scripts/Android.bp
@@ -319,3 +319,11 @@
     main: "extra_install_zips_file_list.py",
     srcs: ["extra_install_zips_file_list.py"],
 }
+
+python_binary_host {
+    name: "rustc_linker",
+    main: "rustc_linker.py",
+    srcs: [
+        "rustc_linker.py",
+    ],
+}
diff --git a/scripts/rustc_linker.py b/scripts/rustc_linker.py
new file mode 100755
index 0000000..3f60708
--- /dev/null
+++ b/scripts/rustc_linker.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# 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.
+#
+
+"""
+This script is used as a replacement for the Rust linker to allow fine-grained
+control over the what gets emitted to the linker.
+"""
+
+import os
+import shutil
+import subprocess
+import sys
+import argparse
+
+replacementVersionScript = None
+
+argparser = argparse.ArgumentParser()
+argparser.add_argument('--android-clang-bin', required=True)
+args = argparser.parse_known_args()
+clang_args = [args[0].android_clang_bin] + args[1]
+
+for i, arg in enumerate(clang_args):
+   if arg.startswith('-Wl,--android-version-script='):
+      replacementVersionScript = arg.split("=")[1]
+      del clang_args[i]
+      break
+
+if replacementVersionScript:
+   versionScriptFound = False
+   for i, arg in enumerate(clang_args):
+      if arg.startswith('-Wl,--version-script='):
+         clang_args[i] ='-Wl,--version-script=' + replacementVersionScript
+         versionScriptFound = True
+         break
+
+   if not versionScriptFound:
+       # If rustc did not emit a version script, just append the arg
+       clang_args.append('-Wl,--version-script=' + replacementVersionScript)
+try:
+   subprocess.run(clang_args, encoding='utf-8', check=True)
+except subprocess.CalledProcessError as e:
+   sys.exit(-1)
+