Add java_boot_libs to sdk

The build has some implicit dependencies (via the boot jars
configuration) on a number of modules, e.g. core-oj, apache-xml, that
are part of the java boot class path and which are provided by mainline
modules (e.g. art, conscrypt, runtime-i18n) but which are not otherwise
used outside those mainline modules.

As they are not needed outside the mainline modules adding them to
the sdk/module-exports as either java_libs, or java_header_libs would
end up exporting more information than was strictly necessary. This
change adds the java_boot_libs property to allow those modules to be
exported as part of the sdk/module_exports without exposing any
unnecessary information.

Some points to note:
* The java_import has to have a valid file for the src property
  otherwise it will be disabled.
* The src property is supposed to reference a jar file but the
  java_boot_libs property will make it reference an empty file (not
  an empty jar) so that any attempt to use that file as a jar, e.g.
  compiling against it, will cause a build failure.
* The name of the file passed to the src property should make it
  clear that the file is not intended to be used.
* The test makes sure that only the jar file is copied to the
  snapshot.

Test: m nothing
Bug: 171061220
Change-Id: I175331e4c8e3874ab70a67cdc2f76ed1576e41eb
diff --git a/android/sdk.go b/android/sdk.go
index f2cdc88..bab0ed8 100644
--- a/android/sdk.go
+++ b/android/sdk.go
@@ -177,6 +177,12 @@
 	// to the zip
 	CopyToSnapshot(src Path, dest string)
 
+	// Return the path to an empty file.
+	//
+	// This can be used by sdk member types that need to create an empty file in the snapshot, simply
+	// pass the value returned from this to the CopyToSnapshot() method.
+	EmptyFile() Path
+
 	// Unzip the supplied zip into the snapshot relative directory destDir.
 	UnzipToSnapshot(zipPath Path, destDir string)
 
diff --git a/android/testing.go b/android/testing.go
index a66b1e1..92ce014 100644
--- a/android/testing.go
+++ b/android/testing.go
@@ -470,6 +470,10 @@
 // The build and source paths should be distinguishable based on their contents.
 func NormalizePathForTesting(path Path) string {
 	p := path.String()
+	// Allow absolute paths to /dev/
+	if strings.HasPrefix(p, "/dev/") {
+		return p
+	}
 	if w, ok := path.(WritablePath); ok {
 		rel, err := filepath.Rel(w.buildDir(), p)
 		if err != nil {
diff --git a/java/java.go b/java/java.go
index f684a00..99bc55d 100644
--- a/java/java.go
+++ b/java/java.go
@@ -40,20 +40,55 @@
 	// Register sdk member types.
 	android.RegisterSdkMemberType(javaHeaderLibsSdkMemberType)
 
+	// Register java implementation libraries for use only in module_exports (not sdk).
 	android.RegisterSdkMemberType(&librarySdkMemberType{
 		android.SdkMemberTypeBase{
 			PropertyName: "java_libs",
 		},
-		func(j *Library) android.Path {
+		func(_ android.SdkMemberContext, j *Library) android.Path {
 			implementationJars := j.ImplementationAndResourcesJars()
 			if len(implementationJars) != 1 {
 				panic(fmt.Errorf("there must be only one implementation jar from %q", j.Name()))
 			}
-
 			return implementationJars[0]
 		},
+		sdkSnapshotFilePathForJar,
+		copyEverythingToSnapshot,
 	})
 
+	// Register java boot libraries for use in sdk.
+	//
+	// The build has some implicit dependencies (via the boot jars configuration) on a number of
+	// modules, e.g. core-oj, apache-xml, that are part of the java boot class path and which are
+	// provided by mainline modules (e.g. art, conscrypt, runtime-i18n) but which are not otherwise
+	// used outside those mainline modules.
+	//
+	// As they are not needed outside the mainline modules adding them to the sdk/module-exports as
+	// either java_libs, or java_header_libs would end up exporting more information than was strictly
+	// necessary. The java_boot_libs property to allow those modules to be exported as part of the
+	// sdk/module_exports without exposing any unnecessary information.
+	android.RegisterSdkMemberType(&librarySdkMemberType{
+		android.SdkMemberTypeBase{
+			PropertyName: "java_boot_libs",
+			SupportsSdk:  true,
+		},
+		func(ctx android.SdkMemberContext, j *Library) android.Path {
+			// Java boot libs are only provided in the SDK to provide access to their dex implementation
+			// jar for use by dexpreopting and boot jars package check. They do not need to provide an
+			// actual implementation jar but the java_import will need a file that exists so just copy an
+			// empty file. Any attempt to use that file as a jar will cause a build error.
+			return ctx.SnapshotBuilder().EmptyFile()
+		},
+		func(osPrefix, name string) string {
+			// Create a special name for the implementation jar to try and provide some useful information
+			// to a developer that attempts to compile against this.
+			// TODO(b/175714559): Provide a proper error message in Soong not ninja.
+			return filepath.Join(osPrefix, "java_boot_libs", "snapshot", "jars", "are", "invalid", name+jarFileSuffix)
+		},
+		onlyCopyJarToSnapshot,
+	})
+
+	// Register java test libraries for use only in module_exports (not sdk).
 	android.RegisterSdkMemberType(&testSdkMemberType{
 		SdkMemberTypeBase: android.SdkMemberTypeBase{
 			PropertyName: "java_tests",
@@ -2165,9 +2200,22 @@
 
 	// Function to retrieve the appropriate output jar (implementation or header) from
 	// the library.
-	jarToExportGetter func(j *Library) android.Path
+	jarToExportGetter func(ctx android.SdkMemberContext, j *Library) android.Path
+
+	// Function to compute the snapshot relative path to which the named library's
+	// jar should be copied.
+	snapshotPathGetter func(osPrefix, name string) string
+
+	// True if only the jar should be copied to the snapshot, false if the jar plus any additional
+	// files like aidl files should also be copied.
+	onlyCopyJarToSnapshot bool
 }
 
+const (
+	onlyCopyJarToSnapshot    = true
+	copyEverythingToSnapshot = false
+)
+
 func (mt *librarySdkMemberType) AddDependencies(mctx android.BottomUpMutatorContext, dependencyTag blueprint.DependencyTag, names []string) {
 	mctx.AddVariationDependencies(nil, dependencyTag, names...)
 }
@@ -2195,21 +2243,32 @@
 func (p *librarySdkMemberProperties) PopulateFromVariant(ctx android.SdkMemberContext, variant android.Module) {
 	j := variant.(*Library)
 
-	p.JarToExport = ctx.MemberType().(*librarySdkMemberType).jarToExportGetter(j)
+	p.JarToExport = ctx.MemberType().(*librarySdkMemberType).jarToExportGetter(ctx, j)
+
 	p.AidlIncludeDirs = j.AidlIncludeDirs()
 }
 
 func (p *librarySdkMemberProperties) AddToPropertySet(ctx android.SdkMemberContext, propertySet android.BpPropertySet) {
 	builder := ctx.SnapshotBuilder()
 
+	memberType := ctx.MemberType().(*librarySdkMemberType)
+
 	exportedJar := p.JarToExport
 	if exportedJar != nil {
-		snapshotRelativeJavaLibPath := sdkSnapshotFilePathForJar(p.OsPrefix(), ctx.Name())
+		// Delegate the creation of the snapshot relative path to the member type.
+		snapshotRelativeJavaLibPath := memberType.snapshotPathGetter(p.OsPrefix(), ctx.Name())
+
+		// Copy the exported jar to the snapshot.
 		builder.CopyToSnapshot(exportedJar, snapshotRelativeJavaLibPath)
 
 		propertySet.AddProperty("jars", []string{snapshotRelativeJavaLibPath})
 	}
 
+	// Do not copy anything else to the snapshot.
+	if memberType.onlyCopyJarToSnapshot {
+		return
+	}
+
 	aidlIncludeDirs := p.AidlIncludeDirs
 	if len(aidlIncludeDirs) != 0 {
 		sdkModuleContext := ctx.SdkModuleContext()
@@ -2230,7 +2289,7 @@
 		PropertyName: "java_header_libs",
 		SupportsSdk:  true,
 	},
-	func(j *Library) android.Path {
+	func(_ android.SdkMemberContext, j *Library) android.Path {
 		headerJars := j.HeaderJars()
 		if len(headerJars) != 1 {
 			panic(fmt.Errorf("there must be only one header jar from %q", j.Name()))
@@ -2238,6 +2297,8 @@
 
 		return headerJars[0]
 	},
+	sdkSnapshotFilePathForJar,
+	copyEverythingToSnapshot,
 }
 
 // java_library builds and links sources into a `.jar` file for the device, and possibly for the host as well.
diff --git a/sdk/java_sdk_test.go b/sdk/java_sdk_test.go
index d989c5b..488afd8 100644
--- a/sdk/java_sdk_test.go
+++ b/sdk/java_sdk_test.go
@@ -472,6 +472,61 @@
 	)
 }
 
+func TestSnapshotWithJavaBootLibrary(t *testing.T) {
+	result := testSdkWithJava(t, `
+		module_exports {
+			name: "myexports",
+			java_boot_libs: ["myjavalib"],
+		}
+
+		java_library {
+			name: "myjavalib",
+			srcs: ["Test.java"],
+			java_resources: ["resource.txt"],
+			// The aidl files should not be copied to the snapshot because a java_boot_libs member is not
+			// intended to be used for compiling Java, only for accessing the dex implementation jar.
+			aidl: {
+				export_include_dirs: ["aidl"],
+			},
+			system_modules: "none",
+			sdk_version: "none",
+			compile_dex: true,
+		}
+	`)
+
+	result.CheckSnapshot("myexports", "",
+		checkAndroidBpContents(`
+// This is auto-generated. DO NOT EDIT.
+
+java_import {
+    name: "myexports_myjavalib@current",
+    sdk_member_name: "myjavalib",
+    visibility: ["//visibility:public"],
+    apex_available: ["//apex_available:platform"],
+    jars: ["java_boot_libs/snapshot/jars/are/invalid/myjavalib.jar"],
+}
+
+java_import {
+    name: "myjavalib",
+    prefer: false,
+    visibility: ["//visibility:public"],
+    apex_available: ["//apex_available:platform"],
+    jars: ["java_boot_libs/snapshot/jars/are/invalid/myjavalib.jar"],
+}
+
+module_exports_snapshot {
+    name: "myexports@current",
+    visibility: ["//visibility:public"],
+    java_boot_libs: ["myexports_myjavalib@current"],
+}
+
+`),
+		checkAllCopyRules(`
+.intermediates/myexports/common_os/empty -> java_boot_libs/snapshot/jars/are/invalid/myjavalib.jar
+`),
+	)
+}
+
 func TestHostSnapshotWithJavaImplLibrary(t *testing.T) {
 	result := testSdkWithJava(t, `
 		module_exports {
diff --git a/sdk/update.go b/sdk/update.go
index 377aaae..b5bc9f4 100644
--- a/sdk/update.go
+++ b/sdk/update.go
@@ -653,6 +653,9 @@
 	filesToZip  android.Paths
 	zipsToMerge android.Paths
 
+	// The path to an empty file.
+	emptyFile android.WritablePath
+
 	prebuiltModules map[string]*bpModule
 	prebuiltOrder   []*bpModule
 
@@ -703,6 +706,19 @@
 	s.zipsToMerge = append(s.zipsToMerge, tmpZipPath)
 }
 
+func (s *snapshotBuilder) EmptyFile() android.Path {
+	if s.emptyFile == nil {
+		ctx := s.ctx
+		s.emptyFile = android.PathForModuleOut(ctx, "empty")
+		s.ctx.Build(pctx, android.BuildParams{
+			Rule:   android.Touch,
+			Output: s.emptyFile,
+		})
+	}
+
+	return s.emptyFile
+}
+
 func (s *snapshotBuilder) AddPrebuiltModule(member android.SdkMember, moduleType string) android.BpModule {
 	name := member.Name()
 	if s.prebuiltModules[name] != nil {