Add new min_sdk_version attribute on android_binary and pipe to dex/desugar actions.

Also introduce an allowlist to restrict packages able to set this new attribute. Eventually we may roll this out more broadly, but there are outstanding concerns around useability and scalability.

RELNOTES: None
PiperOrigin-RevId: 451524093
Change-Id: I9383cac05a20c5115b2202828ca0cea951244d53
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
index f6bcde6..7a3ddc9 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
@@ -184,6 +184,13 @@
       ruleContext.attributeError("multidex", "Multidex must be enabled");
     }
 
+    if (ruleContext.attributes().isAttributeValueExplicitlySpecified("min_sdk_version")
+        && Allowlist.hasAllowlist(ruleContext, "allow_min_sdk_version")
+        && !Allowlist.isAvailable(ruleContext, "allow_min_sdk_version")) {
+      ruleContext.attributeError(
+          "min_sdk_version", "Target is not permitted to set min_sdk_version");
+    }
+
     if (AndroidCommon.getAndroidConfig(ruleContext).desugarJava8Libs()
         && getMultidexMode(ruleContext) == MultidexMode.OFF) {
       // Multidex is required so we can include legacy libs as a separate .dex file.
@@ -263,30 +270,6 @@
                       : null);
     }
 
-    Artifact manifestValidation = null;
-    if (Allowlist.hasAllowlist(ruleContext, "android_multidex_native_min_sdk_allowlist")
-        && !Allowlist.isAvailable(ruleContext, "android_multidex_native_min_sdk_allowlist")
-        && getMultidexMode(ruleContext) == MultidexMode.NATIVE
-        && ruleContext.isAttrDefined("$validate_manifest", LABEL)) {
-      manifestValidation =
-          ruleContext.getPackageRelativeArtifact(
-              ruleContext.getLabel().getName() + "_manifest_validation_output",
-              ruleContext.getBinOrGenfilesDirectory());
-      ruleContext.registerAction(
-          createSpawnActionBuilder(ruleContext)
-              .setExecutable(ruleContext.getExecutablePrerequisite("$validate_manifest"))
-              .setProgressMessage("Validating %{input}")
-              .setMnemonic("ValidateManifest")
-              .addInput(manifest.getManifest())
-              .addOutput(manifestValidation)
-              .addCommandLine(
-                  CustomCommandLine.builder()
-                      .addExecPath("--manifest", manifest.getManifest())
-                      .addExecPath("--output", manifestValidation)
-                      .build())
-              .build(ruleContext));
-    }
-
     boolean shrinkResourceCycles =
         shouldShrinkResourceCycles(
             dataContext.getAndroidConfig(), ruleContext, dataContext.isResourceShrinkingEnabled());
@@ -415,6 +398,38 @@
         AndroidBinaryMobileInstall.createMobileInstallResourceApks(
             ruleContext, dataContext, manifest);
 
+    Artifact manifestValidation = null;
+    boolean shouldValidateMultidex =
+        (Allowlist.hasAllowlist(ruleContext, "android_multidex_native_min_sdk_allowlist")
+            && !Allowlist.isAvailable(ruleContext, "android_multidex_native_min_sdk_allowlist")
+            && getMultidexMode(ruleContext) == MultidexMode.NATIVE);
+    boolean shouldValidateMinSdk = getMinSdkVersion(ruleContext) > 0;
+    if (ruleContext.isAttrDefined("$validate_manifest", LABEL)
+        && (shouldValidateMultidex || shouldValidateMinSdk)) {
+      manifestValidation =
+          ruleContext.getPackageRelativeArtifact(
+              ruleContext.getLabel().getName() + "_manifest_validation_output",
+              ruleContext.getBinOrGenfilesDirectory());
+      ruleContext.registerAction(
+          createSpawnActionBuilder(ruleContext)
+              .setExecutable(ruleContext.getExecutablePrerequisite("$validate_manifest"))
+              .setProgressMessage("Validating %{input}")
+              .setMnemonic("ValidateManifest")
+              .addInput(manifest.getManifest())
+              .addOutput(manifestValidation)
+              .addCommandLine(
+                  CustomCommandLine.builder()
+                      .addExecPath("--manifest", manifest.getManifest())
+                      .addExecPath("--output", manifestValidation)
+                      .addFormatted(
+                          "--validate_multidex=%s", Boolean.toString(shouldValidateMultidex))
+                      .add(
+                          "--expected_min_sdk_version",
+                          Integer.toString(getMinSdkVersion(ruleContext)))
+                      .build())
+              .build(ruleContext));
+    }
+
     return createAndroidBinary(
         ruleContext,
         dataContext,
@@ -1267,6 +1282,8 @@
               + "\" not supported by this version of the Android SDK");
     }
 
+    int minSdkVersion = getMinSdkVersion(ruleContext);
+
     int dexShards = ruleContext.attributes().get("dex_shards", Type.INTEGER).toIntUnchecked();
     if (dexShards > 1) {
       if (multidexMode == MultidexMode.OFF) {
@@ -1306,6 +1323,7 @@
             common,
             inclusionFilterJar,
             dexopts,
+            minSdkVersion,
             androidSemantics,
             attributes,
             derivedJarFunction,
@@ -1320,6 +1338,7 @@
             proguardedJar,
             classesDex,
             dexopts,
+            minSdkVersion,
             /*multidex=*/ false,
             /*mainDexList=*/ null);
       }
@@ -1356,6 +1375,7 @@
                 common,
                 inclusionFilterJar,
                 dexopts,
+                minSdkVersion,
                 androidSemantics,
                 attributes,
                 derivedJarFunction,
@@ -1381,7 +1401,13 @@
                 dexopts);
           } else {
             AndroidCommon.createDexAction(
-                ruleContext, shard, shardDex, dexopts, /*multidex=*/ true, /*mainDexList=*/ null);
+                ruleContext,
+                shard,
+                shardDex,
+                dexopts,
+                minSdkVersion,
+                /*multidex=*/ true,
+                /*mainDexList=*/ null);
           }
         }
         ImmutableList<Artifact> shardDexes = shardDexesBuilder.build();
@@ -1417,6 +1443,7 @@
               common,
               inclusionFilterJar,
               dexopts,
+              minSdkVersion,
               androidSemantics,
               attributes,
               derivedJarFunction,
@@ -1436,6 +1463,7 @@
               proguardedJar,
               classesDexIntermediate,
               dexopts,
+              minSdkVersion,
               /*multidex=*/ true,
               mainDexList);
           createCleanDexZipAction(ruleContext, classesDexIntermediate, classesDex);
@@ -1455,6 +1483,7 @@
       AndroidCommon common,
       @Nullable Artifact inclusionFilterJar,
       List<String> dexopts,
+      int minSdkVersion,
       AndroidSemantics androidSemantics,
       JavaTargetAttributes attributes,
       Function<Artifact, Artifact> derivedJarFunction,
@@ -1471,7 +1500,12 @@
               ruleContext,
               collectRuntimeJars(common, attributes),
               collectDexArchives(
-                  ruleContext, common, dexopts, androidSemantics, derivedJarFunction));
+                  ruleContext,
+                  common,
+                  dexopts,
+                  minSdkVersion,
+                  androidSemantics,
+                  derivedJarFunction));
     } else {
       if (proguardedJar != null
           && AndroidCommon.getAndroidConfig(ruleContext).incrementalDexingShardsAfterProguard()
@@ -1493,6 +1527,7 @@
             "$dexbuilder_after_proguard",
             proguardedJar,
             DexArchiveAspect.topLevelDexbuilderDexopts(dexopts),
+            minSdkVersion,
             dexArchives.get(0));
       } else {
         createShuffleJarActions(
@@ -1503,6 +1538,7 @@
             common,
             inclusionFilterJar,
             dexopts,
+            minSdkVersion,
             androidSemantics,
             attributes,
             derivedJarFunction,
@@ -1771,6 +1807,7 @@
               jar,
               attributes.getBootClassPath().bootclasspath(),
               attributes.getCompileTimeClassPath(),
+              getMinSdkVersion(ruleContext),
               ruleContext.getDerivedArtifact(
                   jarPath.replaceName(jarPath.getBaseName() + "_desugared.jar"), jar.getRoot()));
       result.addDesugaredJar(jar, desugared);
@@ -1797,6 +1834,7 @@
       RuleContext ruleContext,
       AndroidCommon common,
       List<String> dexopts,
+      int minSdkVersion,
       AndroidSemantics semantics,
       Function<Artifact, Artifact> derivedJarFunction) {
     DexArchiveProvider.Builder result = new DexArchiveProvider.Builder();
@@ -1820,6 +1858,7 @@
               "$dexbuilder",
               derivedJarFunction.apply(jar),
               incrementalDexopts,
+              minSdkVersion,
               ruleContext.getDerivedArtifact(
                   jarPath.replaceName(jarPath.getBaseName() + ".dex.zip"), jar.getRoot()));
       result.addDexArchive(incrementalDexopts, dexArchive, jar);
@@ -1835,6 +1874,7 @@
       AndroidCommon common,
       @Nullable Artifact inclusionFilterJar,
       List<String> dexopts,
+      int minSdkVersion,
       AndroidSemantics semantics,
       JavaTargetAttributes attributes,
       Function<Artifact, Artifact> derivedJarFunction,
@@ -1889,7 +1929,8 @@
         // there should be very few or no Jar files that still end up in shards.  The dexing
         // step below will have to deal with those in addition to merging .dex files together.
         Map<Artifact, Artifact> dexArchives =
-            collectDexArchives(ruleContext, common, dexopts, semantics, derivedJarFunction);
+            collectDexArchives(
+                ruleContext, common, dexopts, minSdkVersion, semantics, derivedJarFunction);
         classpath = toDexedClasspath(ruleContext, classpath, dexArchives);
         shardCommandLine.add("--split_dexed_classes");
       } else {
@@ -1916,6 +1957,7 @@
             "$dexbuilder_after_proguard",
             shuffleOutputs.get(i),
             DexArchiveAspect.topLevelDexbuilderDexopts(dexopts),
+            minSdkVersion,
             shards.get(i));
       }
     }
@@ -2178,6 +2220,13 @@
     }
   }
 
+  private static int getMinSdkVersion(RuleContext ruleContext) {
+    if (ruleContext.getRule().isAttrDefined("min_sdk_version", Type.INTEGER)) {
+      return ruleContext.attributes().get("min_sdk_version", Type.INTEGER).toIntUnchecked();
+    }
+    return 0;
+  }
+
   /**
    * List of Android SDKs that contain runtimes that do not support the native multidexing
    * introduced in Android L. If someone tries to build an android_binary that has multidex=native
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryMobileInstall.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryMobileInstall.java
index 17467d3..42cb097 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryMobileInstall.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryMobileInstall.java
@@ -369,7 +369,7 @@
             ruleContext,
             split ? "split_stub_application/classes.dex" : "stub_application/classes.dex");
     AndroidCommon.createDexAction(
-        ruleContext, stubDeployJar, stubDex, ImmutableList.<String>of(), false, null);
+        ruleContext, stubDeployJar, stubDex, ImmutableList.<String>of(), 0, false, null);
 
     return stubDex;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java
index 38a39b1..28c705c 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java
@@ -187,6 +187,7 @@
       Artifact jarToDex,
       Artifact classesDex,
       List<String> dexOptions,
+      int minSdkVersion,
       boolean multidex,
       Artifact mainDexList) {
     CustomCommandLine.Builder commandLine = CustomCommandLine.builder();
@@ -201,6 +202,9 @@
     }
 
     commandLine.addAll(dexOptions);
+    if (minSdkVersion > 0) {
+      commandLine.add("--min_sdk_version", Integer.toString(minSdkVersion));
+    }
     if (multidex) {
       commandLine.add("--multi-dex");
       if (mainDexList != null) {
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java
index 6d42fc9..880d6a1 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java
@@ -571,6 +571,15 @@
           <code>en_XA</code> and/or <code>ar_XB</code> pseudo-locales.
           <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
           .add(attr(ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME, STRING_LIST))
+          /* <!-- #BLAZE_RULE($android_binary_base).ATTRIBUTE(min_sdk_version) -->
+          The minSdkVersion. This must match the minSdkVersion specified in the AndroidManifest.xml.
+          If set, will be used for dex/desugaring.
+          <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+          .add(
+              attr("min_sdk_version", INTEGER)
+                  .undocumented("experimental")
+                  .value(StarlarkInt.of(0))
+                  .nonconfigurable("AspectParameters don't support configurations."))
           /* <!-- #BLAZE_RULE($android_binary_base).ATTRIBUTE(shrink_resources) -->
           Whether to perform resource shrinking. Resources that are not used by the binary will be
           removed from the APK. This is only supported for rules using local resources (i.e. the
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/DexArchiveAspect.java b/src/main/java/com/google/devtools/build/lib/rules/android/DexArchiveAspect.java
index 9d5a333..9779543 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/DexArchiveAspect.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/DexArchiveAspect.java
@@ -19,6 +19,7 @@
 import static com.google.devtools.build.lib.packages.BuildType.LABEL;
 import static com.google.devtools.build.lib.packages.BuildType.TRISTATE;
 import static com.google.devtools.build.lib.packages.StarlarkProviderIdentifier.forKey;
+import static com.google.devtools.build.lib.packages.Type.INTEGER;
 import static com.google.devtools.build.lib.rules.android.AndroidCommon.getAndroidConfig;
 
 import com.google.common.base.Function;
@@ -97,6 +98,8 @@
         AspectParameters.Builder result = new AspectParameters.Builder();
         TriState incrementalAttr = attributes.get("incremental_dexing", TRISTATE);
         result.addAttribute("incremental_dexing", incrementalAttr.name());
+        result.addAttribute(
+            "min_sdk_version", attributes.get("min_sdk_version", INTEGER).toString());
         return result.build();
       };
   /**
@@ -239,9 +242,14 @@
 
     Iterable<Artifact> extraToolchainJars = getPlatformBasedToolchainJars(ruleContext);
 
+    int minSdkVersion = 0;
+    if (!params.getAttribute("min_sdk_version").isEmpty()) {
+      minSdkVersion = Integer.valueOf(params.getOnlyValueOfAttribute("min_sdk_version"));
+    }
+
     Function<Artifact, Artifact> desugaredJars =
         desugarJarsIfRequested(
-            ctadBase.getConfiguredTarget(), ruleContext, result, extraToolchainJars);
+            ctadBase.getConfiguredTarget(), ruleContext, minSdkVersion, result, extraToolchainJars);
 
     TriState incrementalAttr =
         TriState.valueOf(params.getOnlyValueOfAttribute("incremental_dexing"));
@@ -280,6 +288,7 @@
                   ASPECT_DEXBUILDER_PREREQ,
                   desugaredJars.apply(jar),
                   incrementalDexopts,
+                  minSdkVersion,
                   AndroidBinary.getDxArtifact(ruleContext, uniqueFilename));
           dexArchives.addDexArchive(incrementalDexopts, dexArchive, jar);
         }
@@ -296,6 +305,7 @@
   private Function<Artifact, Artifact> desugarJarsIfRequested(
       ConfiguredTarget base,
       RuleContext ruleContext,
+      int minSdkVersion,
       ConfiguredAspect.Builder result,
       Iterable<Artifact> extraToolchainJars) {
     if (!getAndroidConfig(ruleContext).desugarJava8()) {
@@ -344,7 +354,12 @@
       for (Artifact jar : jarsToProcess) {
         Artifact desugared =
             createDesugarAction(
-                ruleContext, basenameClash, jar, bootclasspath, compileTimeClasspath);
+                ruleContext,
+                basenameClash,
+                jar,
+                bootclasspath,
+                compileTimeClasspath,
+                minSdkVersion);
         newlyDesugared.put(jar, desugared);
         desugaredJars.addDesugaredJar(jar, desugared);
       }
@@ -466,13 +481,15 @@
       boolean disambiguateBasenames,
       Artifact jar,
       NestedSet<Artifact> bootclasspath,
-      NestedSet<Artifact> compileTimeClasspath) {
+      NestedSet<Artifact> compileTimeClasspath,
+      int minSdkVersion) {
     return createDesugarAction(
         ruleContext,
         ASPECT_DESUGAR_PREREQ,
         jar,
         bootclasspath,
         compileTimeClasspath,
+        minSdkVersion,
         AndroidBinary.getDxArtifact(
             ruleContext,
             (disambiguateBasenames ? jar.getRootRelativePathString() : jar.getFilename())
@@ -485,6 +502,7 @@
       Artifact jar,
       NestedSet<Artifact> bootclasspath,
       NestedSet<Artifact> classpath,
+      int minSdkVersion,
       Artifact result) {
     SpawnAction.Builder action =
         new SpawnAction.Builder()
@@ -519,6 +537,9 @@
     if (stripOutputPaths) {
       args.stripOutputPaths(result.getExecPath().subFragment(0, 1));
     }
+    if (minSdkVersion > 0) {
+      args.add("--min_sdk_version", Integer.toString(minSdkVersion));
+    }
 
     action
         .addCommandLine(
@@ -545,8 +566,10 @@
       Artifact jar,
       NestedSet<Artifact> bootclasspath,
       NestedSet<Artifact> classpath,
+      int minSdkVersion,
       Artifact result) {
-    return createDesugarAction(ruleContext, "$desugar", jar, bootclasspath, classpath, result);
+    return createDesugarAction(
+        ruleContext, "$desugar", jar, bootclasspath, classpath, minSdkVersion, result);
   }
 
   /**
@@ -576,6 +599,7 @@
       String dexbuilderPrereq,
       Artifact jar,
       Set<String> incrementalDexopts,
+      int minSdkVersion,
       Artifact dexArchive) {
     SpawnAction.Builder dexbuilder =
         new SpawnAction.Builder()
@@ -602,6 +626,9 @@
             .addExecPath("--input_jar", jar)
             .addExecPath("--output_zip", dexArchive)
             .addAll(ImmutableList.copyOf(incrementalDexopts));
+    if (minSdkVersion > 0) {
+      args.add("--min_sdk_version", Integer.toString(minSdkVersion));
+    }
     if (stripOutputPaths) {
       args.stripOutputPaths(dexArchive.getExecPath().subFragment(0, 1));
     }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java b/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java
index 3ca0159..0983bfb 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java
@@ -696,6 +696,94 @@
     assertThat(found).isEqualTo(2 /* signed and unsigned apks */);
   }
 
+  @Test
+  public void testSimpleBinary_dexNoMinSdkVersion() throws Exception {
+    scratch.overwriteFile(
+        "java/android/BUILD",
+        "android_binary(name = 'app',",
+        "               srcs = ['A.java'],",
+        "               manifest = 'AndroidManifest.xml',",
+        "               resource_files = glob(['res/**']),",
+        "               multidex = 'legacy',",
+        "              )");
+    useConfiguration("--experimental_desugar_java8_libs");
+    ConfiguredTarget binary = getConfiguredTarget("//java/android:app");
+
+    List<String> args =
+        getGeneratingSpawnActionArgs(
+            ActionsTestUtil.getFirstArtifactEndingWith(
+                actionsTestUtil().artifactClosureOf(getFilesToBuild(binary)),
+                "/libapp.jar.dex.zip"));
+    assertThat(args).doesNotContain("--min_sdk_version");
+  }
+
+  @Test
+  public void testSimpleBinary_dexMinSdkVersion() throws Exception {
+    scratch.overwriteFile(
+        "java/android/BUILD",
+        "android_binary(name = 'app',",
+        "               srcs = ['A.java'],",
+        "               manifest = 'AndroidManifest.xml',",
+        "               resource_files = glob(['res/**']),",
+        "               min_sdk_version = 28,",
+        "               multidex = 'legacy',",
+        "              )");
+    useConfiguration("--experimental_desugar_java8_libs");
+    ConfiguredTarget binary = getConfiguredTarget("//java/android:app");
+
+    List<String> args =
+        getGeneratingSpawnActionArgs(
+            ActionsTestUtil.getFirstArtifactEndingWith(
+                actionsTestUtil().artifactClosureOf(getFilesToBuild(binary)),
+                "/libapp.jar.dex.zip"));
+    assertThat(args).contains("--min_sdk_version");
+    assertThat(args).contains("28");
+  }
+
+  @Test
+  public void testSimpleBinary_desugarNoMinSdkVersion() throws Exception {
+    scratch.overwriteFile(
+        "java/android/BUILD",
+        "android_binary(name = 'app',",
+        "               srcs = ['A.java'],",
+        "               manifest = 'AndroidManifest.xml',",
+        "               resource_files = glob(['res/**']),",
+        "               multidex = 'legacy',",
+        "              )");
+    useConfiguration("--experimental_desugar_java8_libs");
+    ConfiguredTarget binary = getConfiguredTarget("//java/android:app");
+
+    List<String> args =
+        getGeneratingSpawnActionArgs(
+            ActionsTestUtil.getFirstArtifactEndingWith(
+                actionsTestUtil().artifactClosureOf(getFilesToBuild(binary)),
+                "/libapp.jar_desugared.jar"));
+    assertThat(args).doesNotContain("--min_sdk_version");
+  }
+
+  @Test
+  public void testSimpleBinary_desugarMinSdkVersion() throws Exception {
+    scratch.overwriteFile(
+        "java/android/BUILD",
+        "android_binary(name = 'app',",
+        "               srcs = ['A.java'],",
+        "               manifest = 'AndroidManifest.xml',",
+        "               resource_files = glob(['res/**']),",
+        "               min_sdk_version = 28,",
+        "               multidex = 'legacy',",
+        "              )");
+    useConfiguration("--experimental_desugar_java8_libs");
+    ConfiguredTarget binary = getConfiguredTarget("//java/android:app");
+
+    List<String> args =
+        getGeneratingSpawnActionArgs(
+            ActionsTestUtil.getFirstArtifactEndingWith(
+                actionsTestUtil().artifactClosureOf(getFilesToBuild(binary)),
+                "/libapp.jar_desugared.jar"));
+    assertThat(args).contains("--min_sdk_version");
+    assertThat(args).contains("28");
+  }
+
   // regression test for #3169099
   @Test
   public void testBinarySrcs() throws Exception {