Add SOONG_SDK_SNAPSHOT_VERSION support

SOONG_SDK_SNAPSHOT_VERSION=current will generate unversioned and
versioned prebuilts and a versioned snapshot module. This is the
default behavior. The zip file containing the generated snapshot will
be <sdk name>-current.zip.

SOONG_SDK_SNAPSHOT_VERSION=unversioned will generate unversioned
prebuilts only and the zip file containing the generated snapshot will
be <sdk name>.zip.

SOONG_SDK_SNAPSHOT_VERSION=<number> will generate versioned prebuilts
and a versioned snapshot module only. The zip file containing the
generated snapshot will be <sdk name>-<number>.zip.

Bug: 157884619
Test: m nothing
      m SOONG_SDK_SNAPSHOT_VERSION=current art-module-sdk
      - check that the generated Android.bp file has not changed
        from the default.
      m SOONG_SDK_SNAPSHOT_VERSION=none art-module-sdk
      - check that the generated Android.bp file does not contain
        versioned modules.
      m SOONG_SDK_SNAPSHOT_VERSION=2 art-module-sdk
      - check that the generated Android.bp file only contains
        version 2 of each module.
Change-Id: I087e9d7d3ad110508a3d6a39bca50cbb46b3ce82
diff --git a/sdk/sdk_test.go b/sdk/sdk_test.go
index 12545d6..a13b0d7 100644
--- a/sdk/sdk_test.go
+++ b/sdk/sdk_test.go
@@ -564,4 +564,101 @@
 			`),
 		)
 	})
+
+	t.Run("SOONG_SDK_SNAPSHOT_VERSION=unversioned", func(t *testing.T) {
+		result := android.GroupFixturePreparers(
+			preparer,
+			android.FixtureMergeEnv(map[string]string{
+				"SOONG_SDK_SNAPSHOT_VERSION": "unversioned",
+			}),
+		).RunTest(t)
+
+		checkZipFile(t, result, "out/soong/.intermediates/mysdk/common_os/mysdk.zip")
+
+		CheckSnapshot(t, result, "mysdk", "",
+			checkAndroidBpContents(`
+// This is auto-generated. DO NOT EDIT.
+
+java_import {
+    name: "myjavalib",
+    prefer: false,
+    visibility: ["//visibility:public"],
+    apex_available: ["//apex_available:platform"],
+    jars: ["java/myjavalib.jar"],
+}
+			`),
+		)
+	})
+
+	t.Run("SOONG_SDK_SNAPSHOT_VERSION=current", func(t *testing.T) {
+		result := android.GroupFixturePreparers(
+			preparer,
+			android.FixtureMergeEnv(map[string]string{
+				"SOONG_SDK_SNAPSHOT_VERSION": "current",
+			}),
+		).RunTest(t)
+
+		checkZipFile(t, result, "out/soong/.intermediates/mysdk/common_os/mysdk-current.zip")
+
+		CheckSnapshot(t, result, "mysdk", "",
+			checkAndroidBpContents(`
+// This is auto-generated. DO NOT EDIT.
+
+java_import {
+    name: "mysdk_myjavalib@current",
+    sdk_member_name: "myjavalib",
+    visibility: ["//visibility:public"],
+    apex_available: ["//apex_available:platform"],
+    jars: ["java/myjavalib.jar"],
+}
+
+java_import {
+    name: "myjavalib",
+    prefer: false,
+    visibility: ["//visibility:public"],
+    apex_available: ["//apex_available:platform"],
+    jars: ["java/myjavalib.jar"],
+}
+
+sdk_snapshot {
+    name: "mysdk@current",
+    visibility: ["//visibility:public"],
+    java_header_libs: ["mysdk_myjavalib@current"],
+}
+			`),
+		)
+	})
+
+	t.Run("SOONG_SDK_SNAPSHOT_VERSION=2", func(t *testing.T) {
+		result := android.GroupFixturePreparers(
+			preparer,
+			android.FixtureMergeEnv(map[string]string{
+				"SOONG_SDK_SNAPSHOT_VERSION": "2",
+			}),
+		).RunTest(t)
+
+		checkZipFile(t, result, "out/soong/.intermediates/mysdk/common_os/mysdk-2.zip")
+
+		CheckSnapshot(t, result, "mysdk", "",
+			checkAndroidBpContents(`
+// This is auto-generated. DO NOT EDIT.
+
+java_import {
+    name: "mysdk_myjavalib@2",
+    sdk_member_name: "myjavalib",
+    visibility: ["//visibility:public"],
+    apex_available: ["//apex_available:platform"],
+    jars: ["java/myjavalib.jar"],
+}
+
+sdk_snapshot {
+    name: "mysdk@2",
+    visibility: ["//visibility:public"],
+    java_header_libs: ["mysdk_myjavalib@2"],
+}
+			`),
+			// A versioned snapshot cannot be used on its own so add the source back in.
+			snapshotTestPreparer(checkSnapshotWithoutSource, android.FixtureWithRootAndroidBp(bp)),
+		)
+	})
 }
diff --git a/sdk/testing.go b/sdk/testing.go
index f4e85c0..3254cf9f 100644
--- a/sdk/testing.go
+++ b/sdk/testing.go
@@ -131,6 +131,7 @@
 	info := &snapshotBuildInfo{
 		t:                            t,
 		r:                            result,
+		version:                      sdk.builderForTests.version,
 		androidBpContents:            sdk.GetAndroidBpContentsForTests(),
 		androidUnversionedBpContents: sdk.GetUnversionedAndroidBpContentsForTests(),
 		androidVersionedBpContents:   sdk.GetVersionedAndroidBpContentsForTests(),
@@ -236,8 +237,13 @@
 	if dir != "" {
 		dir = filepath.Clean(dir) + "/"
 	}
-	android.AssertStringEquals(t, "Snapshot zip file in wrong place",
-		fmt.Sprintf(".intermediates/%s%s/%s/%s-current.zip", dir, name, variant, name), actual)
+	suffix := ""
+	if snapshotBuildInfo.version != soongSdkSnapshotVersionUnversioned {
+		suffix = "-" + snapshotBuildInfo.version
+	}
+
+	expectedZipPath := fmt.Sprintf(".intermediates/%s%s/%s/%s%s.zip", dir, name, variant, name, suffix)
+	android.AssertStringEquals(t, "Snapshot zip file in wrong place", expectedZipPath, actual)
 
 	// Populate a mock filesystem with the files that would have been copied by
 	// the rules.
@@ -432,6 +438,11 @@
 	// The result from RunTest()
 	r *android.TestResult
 
+	// The version of the generated snapshot.
+	//
+	// See snapshotBuilder.version for more information about this field.
+	version string
+
 	// The contents of the generated Android.bp file
 	androidBpContents string
 
diff --git a/sdk/update.go b/sdk/update.go
index 85dfc4a..36b564f 100644
--- a/sdk/update.go
+++ b/sdk/update.go
@@ -36,6 +36,20 @@
 //     By default every unversioned module in the generated snapshot has prefer: false. Building it
 //     with SOONG_SDK_SNAPSHOT_PREFER=true will force them to use prefer: true.
 //
+// SOONG_SDK_SNAPSHOT_VERSION
+//     This provides control over the version of the generated snapshot.
+//
+//     SOONG_SDK_SNAPSHOT_VERSION=current will generate unversioned and versioned prebuilts and a
+//     versioned snapshot module. This is the default behavior. The zip file containing the
+//     generated snapshot will be <sdk-name>-current.zip.
+//
+//     SOONG_SDK_SNAPSHOT_VERSION=unversioned will generate unversioned prebuilts only and the zip
+//     file containing the generated snapshot will be <sdk-name>.zip.
+//
+//     SOONG_SDK_SNAPSHOT_VERSION=<number> will generate versioned prebuilts and a versioned
+//     snapshot module only. The zip file containing the generated snapshot will be
+//     <sdk-name>-<number>.zip.
+//
 
 var pctx = android.NewPackageContext("android/soong/sdk")
 
@@ -69,6 +83,11 @@
 		})
 )
 
+const (
+	soongSdkSnapshotVersionUnversioned = "unversioned"
+	soongSdkSnapshotVersionCurrent     = "current"
+)
+
 type generatedContents struct {
 	content     strings.Builder
 	indentLevel int
@@ -257,10 +276,26 @@
 		modules: make(map[string]*bpModule),
 	}
 
+	config := ctx.Config()
+	version := config.GetenvWithDefault("SOONG_SDK_SNAPSHOT_VERSION", "current")
+
+	// Generate versioned modules in the snapshot unless an unversioned snapshot has been requested.
+	generateVersioned := version != soongSdkSnapshotVersionUnversioned
+
+	// Generate unversioned modules in the snapshot unless a numbered snapshot has been requested.
+	//
+	// Unversioned modules are not required in that case because the numbered version will be a
+	// finalized version of the snapshot that is intended to be kept separate from the
+	generateUnversioned := version == soongSdkSnapshotVersionUnversioned || version == soongSdkSnapshotVersionCurrent
+	snapshotZipFileSuffix := ""
+	if generateVersioned {
+		snapshotZipFileSuffix = "-" + version
+	}
+
 	builder := &snapshotBuilder{
 		ctx:                   ctx,
 		sdk:                   s,
-		version:               "current",
+		version:               version,
 		snapshotDir:           snapshotDir.OutputPath,
 		copies:                make(map[string]string),
 		filesToZip:            []android.Path{bp.path},
@@ -314,20 +349,26 @@
 		// Prune any empty property sets.
 		unversioned = unversioned.transform(pruneEmptySetTransformer{})
 
-		// Copy the unversioned module so it can be modified to make it versioned.
-		versioned := unversioned.deepCopy()
+		if generateVersioned {
+			// Copy the unversioned module so it can be modified to make it versioned.
+			versioned := unversioned.deepCopy()
 
-		// Transform the unversioned module into a versioned one.
-		versioned.transform(unversionedToVersionedTransformer)
-		bpFile.AddModule(versioned)
+			// Transform the unversioned module into a versioned one.
+			versioned.transform(unversionedToVersionedTransformer)
+			bpFile.AddModule(versioned)
+		}
 
-		// Transform the unversioned module to make it suitable for use in the snapshot.
-		unversioned.transform(unversionedTransformer)
-		bpFile.AddModule(unversioned)
+		if generateUnversioned {
+			// Transform the unversioned module to make it suitable for use in the snapshot.
+			unversioned.transform(unversionedTransformer)
+			bpFile.AddModule(unversioned)
+		}
 	}
 
-	// Add the sdk/module_exports_snapshot module to the bp file.
-	s.addSnapshotModule(ctx, builder, sdkVariants, memberVariantDeps)
+	if generateVersioned {
+		// Add the sdk/module_exports_snapshot module to the bp file.
+		s.addSnapshotModule(ctx, builder, sdkVariants, memberVariantDeps)
+	}
 
 	// generate Android.bp
 	bp = newGeneratedFile(ctx, "snapshot", "Android.bp")
@@ -341,7 +382,8 @@
 	filesToZip := builder.filesToZip
 
 	// zip them all
-	outputZipFile := android.PathForModuleOut(ctx, ctx.ModuleName()+"-current.zip").OutputPath
+	zipPath := fmt.Sprintf("%s%s.zip", ctx.ModuleName(), snapshotZipFileSuffix)
+	outputZipFile := android.PathForModuleOut(ctx, zipPath).OutputPath
 	outputDesc := "Building snapshot for " + ctx.ModuleName()
 
 	// If there are no zips to merge then generate the output zip directly.
@@ -353,7 +395,8 @@
 		zipFile = outputZipFile
 		desc = outputDesc
 	} else {
-		zipFile = android.PathForModuleOut(ctx, ctx.ModuleName()+"-current.unmerged.zip").OutputPath
+		intermediatePath := fmt.Sprintf("%s%s.unmerged.zip", ctx.ModuleName(), snapshotZipFileSuffix)
+		zipFile = android.PathForModuleOut(ctx, intermediatePath).OutputPath
 		desc = "Building intermediate snapshot for " + ctx.ModuleName()
 	}
 
@@ -801,9 +844,15 @@
 }
 
 type snapshotBuilder struct {
-	ctx         android.ModuleContext
-	sdk         *sdk
-	version     string
+	ctx android.ModuleContext
+	sdk *sdk
+
+	// The version of the generated snapshot.
+	//
+	// See the documentation of SOONG_SDK_SNAPSHOT_VERSION above for details of the valid values of
+	// this field.
+	version string
+
 	snapshotDir android.OutputPath
 	bpFile      *bpFile
 
diff --git a/ui/build/dumpvars.go b/ui/build/dumpvars.go
index fe0aca9..54aeda0 100644
--- a/ui/build/dumpvars.go
+++ b/ui/build/dumpvars.go
@@ -162,6 +162,8 @@
 	"OUT_DIR",
 	"AUX_OS_VARIANT_LIST",
 	"PRODUCT_SOONG_NAMESPACES",
+	"SOONG_SDK_SNAPSHOT_PREFER",
+	"SOONG_SDK_SNAPSHOT_VERSION",
 }
 
 func Banner(make_vars map[string]string) string {