Merge "Include runtime-module-host-exports in the SDK build."
diff --git a/OWNERS b/OWNERS
index adf2b4c..dbb491d 100644
--- a/OWNERS
+++ b/OWNERS
@@ -11,3 +11,4 @@
 per-file tidy.go = srhines@google.com, chh@google.com
 per-file lto.go,pgo.go = srhines@google.com, pirama@google.com, yikong@google.com
 per-file docs/map_files.md = danalbert@google.com, enh@google.com, jiyong@google.com
+per-file *ndk_api_coverage_parser.py = sophiez@google.com
\ No newline at end of file
diff --git a/cc/androidmk.go b/cc/androidmk.go
index 52480ea..b3ad610 100644
--- a/cc/androidmk.go
+++ b/cc/androidmk.go
@@ -457,6 +457,9 @@
 		entries.SetString("LOCAL_MODULE_PATH", path)
 		entries.SetString("LOCAL_MODULE_STEM", stem)
 		entries.SetBool("LOCAL_NO_NOTICE_FILE", true)
+		if c.parsedCoverageXmlPath.String() != "" {
+			entries.SetString("SOONG_NDK_API_XML", "$(SOONG_NDK_API_XML) "+c.parsedCoverageXmlPath.String())
+		}
 	})
 }
 
diff --git a/cc/ndk_library.go b/cc/ndk_library.go
index 1597b88..d79badb 100644
--- a/cc/ndk_library.go
+++ b/cc/ndk_library.go
@@ -25,8 +25,12 @@
 	"android/soong/android"
 )
 
+func init() {
+	pctx.HostBinToolVariable("ndk_api_coverage_parser", "ndk_api_coverage_parser")
+}
+
 var (
-	toolPath = pctx.SourcePathVariable("toolPath", "build/soong/cc/gen_stub_libs.py")
+	toolPath = pctx.SourcePathVariable("toolPath", "build/soong/cc/scriptlib/gen_stub_libs.py")
 
 	genStubSrc = pctx.AndroidStaticRule("genStubSrc",
 		blueprint.RuleParams{
@@ -35,6 +39,12 @@
 			CommandDeps: []string{"$toolPath"},
 		}, "arch", "apiLevel", "apiMap", "flags")
 
+	parseNdkApiRule = pctx.AndroidStaticRule("parseNdkApiRule",
+		blueprint.RuleParams{
+			Command:     "$ndk_api_coverage_parser $in $out --api-map $apiMap",
+			CommandDeps: []string{"$ndk_api_coverage_parser"},
+		}, "apiMap")
+
 	ndkLibrarySuffix = ".ndk"
 
 	ndkPrebuiltSharedLibs = []string{
@@ -111,8 +121,9 @@
 
 	properties libraryProperties
 
-	versionScriptPath android.ModuleGenPath
-	installPath       android.Path
+	versionScriptPath     android.ModuleGenPath
+	parsedCoverageXmlPath android.ModuleOutPath
+	installPath           android.Path
 }
 
 // OMG GO
@@ -308,14 +319,35 @@
 	return compileObjs(ctx, flagsToBuilderFlags(flags), subdir, srcs, nil, nil), versionScriptPath
 }
 
+func parseSymbolFileForCoverage(ctx ModuleContext, symbolFile string) android.ModuleOutPath {
+	apiLevelsJson := android.GetApiLevelsJson(ctx)
+	symbolFilePath := android.PathForModuleSrc(ctx, symbolFile)
+	outputFileName := strings.Split(symbolFilePath.Base(), ".")[0]
+	parsedApiCoveragePath := android.PathForModuleOut(ctx, outputFileName+".xml")
+	ctx.Build(pctx, android.BuildParams{
+		Rule:        parseNdkApiRule,
+		Description: "parse ndk api symbol file for api coverage: " + symbolFilePath.Rel(),
+		Outputs:     []android.WritablePath{parsedApiCoveragePath},
+		Input:       symbolFilePath,
+		Args: map[string]string{
+			"apiMap": apiLevelsJson.String(),
+		},
+	})
+	return parsedApiCoveragePath
+}
+
 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")
 	}
 
-	objs, versionScript := compileStubLibrary(ctx, flags, String(c.properties.Symbol_file),
+	symbolFile := String(c.properties.Symbol_file)
+	objs, versionScript := compileStubLibrary(ctx, flags, symbolFile,
 		c.properties.ApiLevel, "")
 	c.versionScriptPath = versionScript
+	if c.properties.ApiLevel == "current" && ctx.PrimaryArch() {
+		c.parsedCoverageXmlPath = parseSymbolFileForCoverage(ctx, symbolFile)
+	}
 	return objs
 }
 
diff --git a/cc/scriptlib/Android.bp b/cc/scriptlib/Android.bp
new file mode 100644
index 0000000..ff9a2f0
--- /dev/null
+++ b/cc/scriptlib/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2020 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.
+//
+
+python_test_host {
+    name: "test_ndk_api_coverage_parser",
+    main: "test_ndk_api_coverage_parser.py",
+    srcs: [
+        "test_ndk_api_coverage_parser.py",
+    ],
+}
+
+python_binary_host {
+    name: "ndk_api_coverage_parser",
+    main: "ndk_api_coverage_parser.py",
+    srcs: [
+        "gen_stub_libs.py",
+        "ndk_api_coverage_parser.py",
+    ],
+}
diff --git a/cc/scriptlib/__init__.py b/cc/scriptlib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cc/scriptlib/__init__.py
diff --git a/cc/gen_stub_libs.py b/cc/scriptlib/gen_stub_libs.py
similarity index 99%
rename from cc/gen_stub_libs.py
rename to cc/scriptlib/gen_stub_libs.py
index 7deb804..d61dfbb 100755
--- a/cc/gen_stub_libs.py
+++ b/cc/scriptlib/gen_stub_libs.py
@@ -246,6 +246,7 @@
     def __eq__(self, other):
         return self.name == other.name and set(self.tags) == set(other.tags)
 
+
 class SymbolFileParser(object):
     """Parses NDK symbol files."""
     def __init__(self, input_file, api_map, arch, api, llndk, apex):
diff --git a/cc/scriptlib/ndk_api_coverage_parser.py b/cc/scriptlib/ndk_api_coverage_parser.py
new file mode 100755
index 0000000..d74035b
--- /dev/null
+++ b/cc/scriptlib/ndk_api_coverage_parser.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2020 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.
+#
+"""Generates xml of NDK libraries used for API coverage analysis."""
+import argparse
+import json
+import os
+import sys
+
+from xml.etree.ElementTree import Element, SubElement, tostring
+from gen_stub_libs import ALL_ARCHITECTURES, FUTURE_API_LEVEL, MultiplyDefinedSymbolError, SymbolFileParser
+
+
+ROOT_ELEMENT_TAG = 'ndk-library'
+SYMBOL_ELEMENT_TAG = 'symbol'
+ARCHITECTURE_ATTRIBUTE_KEY = 'arch'
+DEPRECATED_ATTRIBUTE_KEY = 'is_deprecated'
+PLATFORM_ATTRIBUTE_KEY = 'is_platform'
+NAME_ATTRIBUTE_KEY = 'name'
+VARIABLE_TAG = 'var'
+EXPOSED_TARGET_TAGS = (
+    'vndk',
+    'apex',
+    'llndk',
+)
+API_LEVEL_TAG_PREFIXES = (
+    'introduced=',
+    'introduced-',
+)
+
+
+def parse_tags(tags):
+    """Parses tags and save needed tags in the created attributes.
+
+    Return attributes dictionary.
+    """
+    attributes = {}
+    arch = []
+    for tag in tags:
+        if tag.startswith(tuple(API_LEVEL_TAG_PREFIXES)):
+            key, _, value = tag.partition('=')
+            attributes.update({key: value})
+        elif tag in ALL_ARCHITECTURES:
+            arch.append(tag)
+        elif tag in EXPOSED_TARGET_TAGS:
+            attributes.update({tag: 'True'})
+    attributes.update({ARCHITECTURE_ATTRIBUTE_KEY: ','.join(arch)})
+    return attributes
+
+
+class XmlGenerator(object):
+    """Output generator that writes parsed symbol file to a xml file."""
+    def __init__(self, output_file):
+        self.output_file = output_file
+
+    def convertToXml(self, versions):
+        """Writes all symbol data to the output file."""
+        root = Element(ROOT_ELEMENT_TAG)
+        for version in versions:
+            if VARIABLE_TAG in version.tags:
+                continue
+            version_attributes = parse_tags(version.tags)
+            _, _, postfix = version.name.partition('_')
+            is_platform = postfix == 'PRIVATE' or postfix == 'PLATFORM'
+            is_deprecated = postfix == 'DEPRECATED'
+            version_attributes.update({PLATFORM_ATTRIBUTE_KEY: str(is_platform)})
+            version_attributes.update({DEPRECATED_ATTRIBUTE_KEY: str(is_deprecated)})
+            for symbol in version.symbols:
+                if VARIABLE_TAG in symbol.tags:
+                    continue
+                attributes = {NAME_ATTRIBUTE_KEY: symbol.name}
+                attributes.update(version_attributes)
+                # If same version tags already exist, it will be overwrite here.
+                attributes.update(parse_tags(symbol.tags))
+                SubElement(root, SYMBOL_ELEMENT_TAG, attributes)
+        return root
+
+    def write_xml_to_file(self, root):
+        """Write xml element root to output_file."""
+        parsed_data = tostring(root)
+        output_file = open(self.output_file, "wb")
+        output_file.write(parsed_data)
+
+    def write(self, versions):
+        root = self.convertToXml(versions)
+        self.write_xml_to_file(root)
+
+
+def parse_args():
+    """Parses and returns command line arguments."""
+    parser = argparse.ArgumentParser()
+
+    parser.add_argument('symbol_file', type=os.path.realpath, help='Path to symbol file.')
+    parser.add_argument(
+        'output_file', type=os.path.realpath,
+        help='The output parsed api coverage file.')
+    parser.add_argument(
+        '--api-map', type=os.path.realpath, required=True,
+        help='Path to the API level map JSON file.')
+    return parser.parse_args()
+
+
+def main():
+    """Program entry point."""
+    args = parse_args()
+
+    with open(args.api_map) as map_file:
+        api_map = json.load(map_file)
+
+    with open(args.symbol_file) as symbol_file:
+        try:
+            versions = SymbolFileParser(symbol_file, api_map, "", FUTURE_API_LEVEL,
+                                        True, True).parse()
+        except MultiplyDefinedSymbolError as ex:
+            sys.exit('{}: error: {}'.format(args.symbol_file, ex))
+
+    generator = XmlGenerator(args.output_file)
+    generator.write(versions)
+
+if __name__ == '__main__':
+    main()
diff --git a/cc/test_gen_stub_libs.py b/cc/scriptlib/test_gen_stub_libs.py
similarity index 100%
rename from cc/test_gen_stub_libs.py
rename to cc/scriptlib/test_gen_stub_libs.py
diff --git a/cc/scriptlib/test_ndk_api_coverage_parser.py b/cc/scriptlib/test_ndk_api_coverage_parser.py
new file mode 100644
index 0000000..a3c9b70
--- /dev/null
+++ b/cc/scriptlib/test_ndk_api_coverage_parser.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 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.
+#
+"""Tests for ndk_api_coverage_parser.py."""
+import io
+import textwrap
+import unittest
+
+from xml.etree.ElementTree import tostring
+from gen_stub_libs import FUTURE_API_LEVEL, SymbolFileParser
+import ndk_api_coverage_parser as nparser
+
+
+# pylint: disable=missing-docstring
+
+
+class ApiCoverageSymbolFileParserTest(unittest.TestCase):
+    def test_parse(self):
+        input_file = io.StringIO(textwrap.dedent(u"""\
+            LIBLOG { # introduced-arm64=24 introduced-x86=24 introduced-x86_64=24
+              global:
+                android_name_to_log_id; # apex llndk introduced=23
+                android_log_id_to_name; # llndk arm
+                __android_log_assert; # introduced-x86=23
+                __android_log_buf_print; # var
+                __android_log_buf_write;
+              local:
+                *;
+            };
+            
+            LIBLOG_PLATFORM {
+                android_fdtrack; # llndk
+                android_net; # introduced=23
+            };
+            
+            LIBLOG_FOO { # var
+                android_var;
+            };
+        """))
+        parser = SymbolFileParser(input_file, {}, "", FUTURE_API_LEVEL, True, True)
+        generator = nparser.XmlGenerator(io.StringIO())
+        result = tostring(generator.convertToXml(parser.parse())).decode()
+        expected = '<ndk-library><symbol apex="True" arch="" introduced="23" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" llndk="True" name="android_name_to_log_id" /><symbol arch="arm" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" llndk="True" name="android_log_id_to_name" /><symbol arch="" introduced-arm64="24" introduced-x86="23" introduced-x86_64="24" is_deprecated="False" is_platform="False" name="__android_log_assert" /><symbol arch="" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" name="__android_log_buf_write" /><symbol arch="" is_deprecated="False" is_platform="True" llndk="True" name="android_fdtrack" /><symbol arch="" introduced="23" is_deprecated="False" is_platform="True" name="android_net" /></ndk-library>'
+        self.assertEqual(expected, result)
+
+
+def main():
+    suite = unittest.TestLoader().loadTestsFromName(__name__)
+    unittest.TextTestRunner(verbosity=3).run(suite)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/rust/Android.bp b/rust/Android.bp
index 684db0b..b06ea8e 100644
--- a/rust/Android.bp
+++ b/rust/Android.bp
@@ -16,6 +16,7 @@
         "library.go",
         "prebuilt.go",
         "proc_macro.go",
+	"project_json.go",
         "rust.go",
         "test.go",
         "testing.go",
@@ -25,6 +26,7 @@
         "compiler_test.go",
         "coverage_test.go",
         "library_test.go",
+	"project_json_test.go",
         "rust_test.go",
         "test_test.go",
     ],
diff --git a/rust/project_json.go b/rust/project_json.go
new file mode 100644
index 0000000..909aebc
--- /dev/null
+++ b/rust/project_json.go
@@ -0,0 +1,161 @@
+// Copyright 2020 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 rust
+
+import (
+	"encoding/json"
+	"fmt"
+	"path"
+
+	"android/soong/android"
+)
+
+// This singleton collects Rust crate definitions and generates a JSON file
+// (${OUT_DIR}/soong/rust-project.json) which can be use by external tools,
+// such as rust-analyzer. It does so when either make, mm, mma, mmm or mmma is
+// called.  This singleton is enabled only if SOONG_GEN_RUST_PROJECT is set.
+// For example,
+//
+//   $ SOONG_GEN_RUST_PROJECT=1 m nothing
+
+func init() {
+	android.RegisterSingletonType("rust_project_generator", rustProjectGeneratorSingleton)
+}
+
+func rustProjectGeneratorSingleton() android.Singleton {
+	return &projectGeneratorSingleton{}
+}
+
+type projectGeneratorSingleton struct{}
+
+const (
+	// Environment variables used to control the behavior of this singleton.
+	envVariableCollectRustDeps = "SOONG_GEN_RUST_PROJECT"
+	rustProjectJsonFileName    = "rust-project.json"
+)
+
+// The format of rust-project.json is not yet finalized. A current description is available at:
+// https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/user/manual.adoc#non-cargo-based-projects
+type rustProjectDep struct {
+	Crate int    `json:"crate"`
+	Name  string `json:"name"`
+}
+
+type rustProjectCrate struct {
+	RootModule string           `json:"root_module"`
+	Edition    string           `json:"edition,omitempty"`
+	Deps       []rustProjectDep `json:"deps"`
+	Cfgs       []string         `json:"cfgs"`
+}
+
+type rustProjectJson struct {
+	Roots  []string           `json:"roots"`
+	Crates []rustProjectCrate `json:"crates"`
+}
+
+// crateInfo is used during the processing to keep track of the known crates.
+type crateInfo struct {
+	ID   int
+	Deps map[string]int
+}
+
+func mergeDependencies(ctx android.SingletonContext, project *rustProjectJson,
+	knownCrates map[string]crateInfo, module android.Module,
+	crate *rustProjectCrate, deps map[string]int) {
+
+	//TODO(tweek): The stdlib dependencies do not appear here. We need to manually add them.
+	ctx.VisitDirectDeps(module, func(child android.Module) {
+		childId, childName, ok := appendLibraryAndDeps(ctx, project, knownCrates, child)
+		if !ok {
+			return
+		}
+		if _, ok = deps[childName]; ok {
+			return
+		}
+		crate.Deps = append(crate.Deps, rustProjectDep{Crate: childId, Name: childName})
+		deps[childName] = childId
+	})
+}
+
+// appendLibraryAndDeps creates a rustProjectCrate for the module argument and
+// appends it to the rustProjectJson struct.  It visits the dependencies of the
+// module depth-first. If the current module is already in knownCrates, its
+// its dependencies are merged. Returns a tuple (id, crate_name, ok).
+func appendLibraryAndDeps(ctx android.SingletonContext, project *rustProjectJson,
+	knownCrates map[string]crateInfo, module android.Module) (int, string, bool) {
+	rModule, ok := module.(*Module)
+	if !ok {
+		return 0, "", false
+	}
+	if rModule.compiler == nil {
+		return 0, "", false
+	}
+	rustLib, ok := rModule.compiler.(*libraryDecorator)
+	if !ok {
+		return 0, "", false
+	}
+	crateName := rModule.CrateName()
+	if cInfo, ok := knownCrates[crateName]; ok {
+		// We have seen this crate already; merge any new dependencies.
+		crate := project.Crates[cInfo.ID]
+		mergeDependencies(ctx, project, knownCrates, module, &crate, cInfo.Deps)
+		return cInfo.ID, crateName, true
+	}
+	crate := rustProjectCrate{Deps: make([]rustProjectDep, 0), Cfgs: make([]string, 0)}
+	src := rustLib.Properties.Srcs[0]
+	crate.RootModule = path.Join(ctx.ModuleDir(rModule), src)
+	crate.Edition = getEdition(rustLib.baseCompiler)
+
+	deps := make(map[string]int)
+	mergeDependencies(ctx, project, knownCrates, module, &crate, deps)
+
+	id := len(project.Crates)
+	knownCrates[crateName] = crateInfo{ID: id, Deps: deps}
+	project.Crates = append(project.Crates, crate)
+	// rust-analyzer requires that all crates belong to at least one root:
+	// https://github.com/rust-analyzer/rust-analyzer/issues/4735.
+	project.Roots = append(project.Roots, path.Dir(crate.RootModule))
+	return id, crateName, true
+}
+
+func (r *projectGeneratorSingleton) GenerateBuildActions(ctx android.SingletonContext) {
+	if !ctx.Config().IsEnvTrue(envVariableCollectRustDeps) {
+		return
+	}
+
+	project := rustProjectJson{}
+	knownCrates := make(map[string]crateInfo)
+	ctx.VisitAllModules(func(module android.Module) {
+		appendLibraryAndDeps(ctx, &project, knownCrates, module)
+	})
+
+	path := android.PathForOutput(ctx, rustProjectJsonFileName)
+	err := createJsonFile(project, path)
+	if err != nil {
+		ctx.Errorf(err.Error())
+	}
+}
+
+func createJsonFile(project rustProjectJson, rustProjectPath android.WritablePath) error {
+	buf, err := json.MarshalIndent(project, "", "  ")
+	if err != nil {
+		return fmt.Errorf("JSON marshal of rustProjectJson failed: %s", err)
+	}
+	err = android.WriteFileToOutputDir(rustProjectPath, buf, 0666)
+	if err != nil {
+		return fmt.Errorf("Writing rust-project to %s failed: %s", rustProjectPath.String(), err)
+	}
+	return nil
+}
diff --git a/rust/project_json_test.go b/rust/project_json_test.go
new file mode 100644
index 0000000..6786e72
--- /dev/null
+++ b/rust/project_json_test.go
@@ -0,0 +1,55 @@
+// Copyright 2020 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 rust
+
+import (
+	"io/ioutil"
+	"path/filepath"
+	"testing"
+
+	"android/soong/android"
+	"android/soong/cc"
+)
+
+func TestProjectJson(t *testing.T) {
+	bp := `rust_library {
+		  name: "liba",
+		  srcs: ["src/lib.rs"],
+		  crate_name: "a"
+		}` + GatherRequiredDepsForTest()
+	env := map[string]string{"SOONG_GEN_RUST_PROJECT": "1"}
+	fs := map[string][]byte{
+		"foo.rs":     nil,
+		"src/lib.rs": nil,
+	}
+
+	cc.GatherRequiredFilesForTest(fs)
+
+	config := android.TestArchConfig(buildDir, env, bp, fs)
+	ctx := CreateTestContext()
+	ctx.Register(config)
+	_, errs := ctx.ParseFileList(".", []string{"Android.bp"})
+	android.FailIfErrored(t, errs)
+	_, errs = ctx.PrepareBuildActions(config)
+	android.FailIfErrored(t, errs)
+
+	// The JSON file is generated via WriteFileToOutputDir. Therefore, it
+	// won't appear in the Output of the TestingSingleton. Manually verify
+	// it exists.
+	_, err := ioutil.ReadFile(filepath.Join(buildDir, "rust-project.json"))
+	if err != nil {
+		t.Errorf("rust-project.json has not been generated")
+	}
+}
diff --git a/rust/testing.go b/rust/testing.go
index 09008a8..f94af71 100644
--- a/rust/testing.go
+++ b/rust/testing.go
@@ -100,6 +100,7 @@
 		ctx.BottomUp("rust_unit_tests", TestPerSrcMutator).Parallel()
 		ctx.BottomUp("rust_begin", BeginMutator).Parallel()
 	})
+	ctx.RegisterSingletonType("rust_project_generator", rustProjectGeneratorSingleton)
 
 	return ctx
 }
diff --git a/sh/sh_binary.go b/sh/sh_binary.go
index 7bb267d..ae653fd 100644
--- a/sh/sh_binary.go
+++ b/sh/sh_binary.go
@@ -107,6 +107,8 @@
 
 	testProperties TestProperties
 
+	installDir android.InstallPath
+
 	data       android.Paths
 	testConfig android.Path
 }
@@ -201,8 +203,8 @@
 	} else if !ctx.Host() && ctx.Config().HasMultilibConflict(ctx.Arch().ArchType) {
 		testDir = filepath.Join(testDir, ctx.Arch().ArchType.String())
 	}
-	installDir := android.PathForModuleInstall(ctx, testDir, proptools.String(s.properties.Sub_dir))
-	s.installedFile = ctx.InstallExecutable(installDir, s.outputFilePath.Base(), s.outputFilePath)
+	s.installDir = android.PathForModuleInstall(ctx, testDir, proptools.String(s.properties.Sub_dir), s.Name())
+	s.installedFile = ctx.InstallExecutable(s.installDir, s.outputFilePath.Base(), s.outputFilePath)
 
 	s.data = android.PathsForModuleSrc(ctx, s.testProperties.Data)
 
@@ -230,6 +232,7 @@
 			func(entries *android.AndroidMkEntries) {
 				s.customAndroidMkEntries(entries)
 
+				entries.SetPath("LOCAL_MODULE_PATH", s.installDir.ToMakePath())
 				entries.AddStrings("LOCAL_COMPATIBILITY_SUITE", s.testProperties.Test_suites...)
 				if s.testConfig != nil {
 					entries.SetPath("LOCAL_FULL_TEST_CONFIG", s.testConfig)
diff --git a/sh/sh_binary_test.go b/sh/sh_binary_test.go
index 6c0d96a..6ab22c5 100644
--- a/sh/sh_binary_test.go
+++ b/sh/sh_binary_test.go
@@ -55,7 +55,7 @@
 	return ctx, config
 }
 
-func TestShTestTestData(t *testing.T) {
+func TestShTest(t *testing.T) {
 	ctx, config := testShBinary(t, `
 		sh_test {
 			name: "foo",
@@ -71,10 +71,17 @@
 	mod := ctx.ModuleForTests("foo", "android_arm64_armv8-a").Module().(*ShTest)
 
 	entries := android.AndroidMkEntriesForTest(t, config, "", mod)[0]
-	expected := []string{":testdata/data1", ":testdata/sub/data2"}
-	actual := entries.EntryMap["LOCAL_TEST_DATA"]
-	if !reflect.DeepEqual(expected, actual) {
-		t.Errorf("Unexpected test data expected: %q, actual: %q", expected, actual)
+
+	expectedPath := "/tmp/target/product/test_device/data/nativetest64/foo"
+	actualPath := entries.EntryMap["LOCAL_MODULE_PATH"][0]
+	if expectedPath != actualPath {
+		t.Errorf("Unexpected LOCAL_MODULE_PATH expected: %q, actual: %q", expectedPath, actualPath)
+	}
+
+	expectedData := []string{":testdata/data1", ":testdata/sub/data2"}
+	actualData := entries.EntryMap["LOCAL_TEST_DATA"]
+	if !reflect.DeepEqual(expectedData, actualData) {
+		t.Errorf("Unexpected test data expected: %q, actual: %q", expectedData, actualData)
 	}
 }