Add preview API for R8 partial shrinking

Bug: b/309743298
Change-Id: Iec44c1d485365e93544153a9a745167091c72b74
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 24d8545..93926dd 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -247,7 +247,7 @@
 
   static void runInternal(AndroidApp app, InternalOptions options, ExecutorService executor)
       throws IOException {
-    if (options.r8PartialCompilationOptions.enabled) {
+    if (options.partialCompilationConfiguration.isEnabled()) {
       try {
         new R8Partial(options).runInternal(app, executor);
       } catch (ResourceException e) {
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 1077122..6a7475f 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -51,6 +51,7 @@
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.InternalOptions.MappingComposeOptions;
 import com.android.tools.r8.utils.ProgramClassCollection;
+import com.android.tools.r8.utils.R8PartialCompilationConfiguration;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.SemanticVersion;
 import com.android.tools.r8.utils.SetUtils;
@@ -149,6 +150,7 @@
     private AndroidResourceConsumer androidResourceConsumer = null;
     private ResourceShrinkerConfiguration resourceShrinkerConfiguration =
         ResourceShrinkerConfiguration.DEFAULT_CONFIGURATION;
+    private R8PartialCompilationConfiguration partialCompilationConfiguration = null;
 
     private final ProguardConfigurationParserOptions.Builder parserOptionsBuilder =
         ProguardConfigurationParserOptions.builder().readEnvironment();
@@ -554,6 +556,43 @@
       this.readEmbeddedRulesFromClasspathAndLibrary = enable;
     }
 
+    /**
+     * Configure partial shrinking in R8, where R8 is only applied to a part of the input.
+     *
+     * <p>The patterns in {@code includePatterns} and {@code excludePatterns} are comma separated
+     * lists of string patterns of fully qualified names of packages/classes. The patterns support
+     * the wildcards {@code *} and {@code **}. The wildcards are only supported at the end of the
+     * pattern, so only prefix matching. If the character just before the wildcard is a {@code .}
+     * (package separator) then the difference between {@code *} and {@code **} is that {@code *}
+     * only includes classes in the same package, whereas {@code **} includes classes in subpackages
+     * as well. If the character before the wildcard is not a {@code .} (package separator) then
+     * {@code *} and {@code **} will both match all classes with that prefix.
+     *
+     * <p>If {@code includePatterns} is not specified ({@code null} or an empty string), the default
+     * is {@code "androidx.**,kotlin.**,kotlinx.**"}.
+     *
+     * <p>The include patterns are processed first collecting all possible include classes. Then the
+     * exclude patterns are applied removing all matching classes from the collected include
+     * classes.
+     *
+     * @param includePatterns patterns for classes to include in R8 shrinking (see above for
+     *     semantics)
+     * @param excludePatterns patterns for classes to exclude from R8 shrinking (see above for
+     *     semantics)
+     * @return
+     */
+    @Deprecated
+    public Builder enableExperimentalPartialShrinking(
+        String includePatterns, String excludePatterns) {
+      if (includePatterns == null || includePatterns.isEmpty()) {
+        includePatterns = "androidx.**,kotlin.**,kotlinx.**";
+      }
+      partialCompilationConfiguration =
+          R8PartialCompilationConfiguration.fromIncludeExcludePatterns(
+              includePatterns, excludePatterns);
+      return self();
+    }
+
     @Override
     protected InternalProgramOutputPathConsumer createProgramOutputConsumer(
         Path path,
@@ -799,7 +838,8 @@
               androidResourceConsumer,
               resourceShrinkerConfiguration,
               keepSpecifications,
-              buildMetadataConsumer);
+              buildMetadataConsumer,
+              partialCompilationConfiguration);
 
       if (inputDependencyGraphConsumer != null) {
         inputDependencyGraphConsumer.finished();
@@ -1016,6 +1056,7 @@
   private final AndroidResourceConsumer androidResourceConsumer;
   private final ResourceShrinkerConfiguration resourceShrinkerConfiguration;
   private final Consumer<? super R8BuildMetadata> buildMetadataConsumer;
+  private final R8PartialCompilationConfiguration partialCompilationConfiguration;
 
   /** Get a new {@link R8Command.Builder}. */
   public static Builder builder() {
@@ -1115,7 +1156,8 @@
       AndroidResourceConsumer androidResourceConsumer,
       ResourceShrinkerConfiguration resourceShrinkerConfiguration,
       List<KeepSpecificationSource> keepSpecifications,
-      Consumer<? super R8BuildMetadata> buildMetadataConsumer) {
+      Consumer<? super R8BuildMetadata> buildMetadataConsumer,
+      R8PartialCompilationConfiguration partialCompilationConfiguration) {
     super(
         inputApp,
         mode,
@@ -1165,6 +1207,7 @@
     this.androidResourceConsumer = androidResourceConsumer;
     this.resourceShrinkerConfiguration = resourceShrinkerConfiguration;
     this.buildMetadataConsumer = buildMetadataConsumer;
+    this.partialCompilationConfiguration = partialCompilationConfiguration;
   }
 
   private R8Command(boolean printHelp, boolean printVersion) {
@@ -1194,6 +1237,7 @@
     androidResourceConsumer = null;
     resourceShrinkerConfiguration = null;
     buildMetadataConsumer = null;
+    partialCompilationConfiguration = null;
   }
 
   public DexItemFactory getDexItemFactory() {
@@ -1343,6 +1387,10 @@
     }
 
     // EXPERIMENTAL flags.
+    if (partialCompilationConfiguration != null) {
+      internal.partialCompilationConfiguration = partialCompilationConfiguration;
+    }
+
     assert !internal.forceProguardCompatibility;
     internal.forceProguardCompatibility = forceProguardCompatibility;
 
diff --git a/src/main/java/com/android/tools/r8/R8Partial.java b/src/main/java/com/android/tools/r8/R8Partial.java
index ff1d8a6..42b5b5b 100644
--- a/src/main/java/com/android/tools/r8/R8Partial.java
+++ b/src/main/java/com/android/tools/r8/R8Partial.java
@@ -38,23 +38,22 @@
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Consumer;
-import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 class R8Partial {
 
   private final InternalOptions options;
-  private final Consumer<AndroidApp> r8InputApp;
-  private final Consumer<AndroidApp> d8InputApp;
-  private final Consumer<AndroidApp> r8OutputApp;
-  private final Consumer<AndroidApp> d8OutputApp;
+  private final Consumer<AndroidApp> r8InputAppConsumer;
+  private final Consumer<AndroidApp> d8InputAppConsumer;
+  private final Consumer<AndroidApp> r8OutputAppConsumer;
+  private final Consumer<AndroidApp> d8OutputAppConsumer;
 
   R8Partial(InternalOptions options) {
     this.options = options;
-    this.r8InputApp = options.r8PartialCompilationOptions.r8InputApp;
-    this.d8InputApp = options.r8PartialCompilationOptions.d8InputApp;
-    this.r8OutputApp = options.r8PartialCompilationOptions.r8OutputApp;
-    this.d8OutputApp = options.r8PartialCompilationOptions.d8OutputApp;
+    this.r8InputAppConsumer = options.partialCompilationConfiguration.r8InputAppConsumer;
+    this.d8InputAppConsumer = options.partialCompilationConfiguration.d8InputAppConsumer;
+    this.r8OutputAppConsumer = options.partialCompilationConfiguration.r8OutputAppConsumer;
+    this.d8OutputAppConsumer = options.partialCompilationConfiguration.d8OutputAppConsumer;
   }
 
   static void runForTesting(AndroidApp app, InternalOptions options)
@@ -77,8 +76,8 @@
     ProgramConsumer originalProgramConsumer = options.programConsumer;
     MapConsumer originalMapConsumer = options.mapConsumer;
 
-    Path tmp = options.r8PartialCompilationOptions.getTemp();
-    Path dumpFile = options.r8PartialCompilationOptions.getDumpFile();
+    Path tmp = options.partialCompilationConfiguration.getTempDir();
+    Path dumpFile = options.partialCompilationConfiguration.getDumpFile();
 
     // Create a dump of the compiler input.
     // TODO(b/309743298): Do not use compiler dump to handle splitting the compilation. This should
@@ -120,15 +119,15 @@
         AppInfoWithClassHierarchy.createForDesugaring(
             AppInfo.createInitialAppInfo(dapp, GlobalSyntheticsStrategy.forNonSynthesizing()));
 
-    Predicate<String> isR8 = options.r8PartialCompilationOptions.isR8;
-    Set<String> d8classes = new HashSet<>();
+    Set<DexProgramClass> d8classes = new HashSet<>();
     appInfo
         .classes()
         .forEach(
             clazz -> {
-              String key = clazz.toSourceString();
-              if (!d8classes.contains(key) && !isR8.test(key)) {
-                d8classes.add(key);
+              if (!d8classes.contains(clazz)
+                  && !options.partialCompilationConfiguration.test(
+                      clazz.getType().getDescriptor())) {
+                d8classes.add(clazz);
                 // TODO(b/309743298): Improve this to only visit each class once and stop at
                 //  library boundary.
                 appInfo.forEachSuperType(
@@ -137,7 +136,7 @@
                       DexProgramClass superClass =
                           asProgramClassOrNull(appInfo.definitionFor(superType));
                       if (superClass != null) {
-                        d8classes.add(superClass.toSourceString());
+                        d8classes.add(superClass);
                       }
                     });
               }
@@ -146,7 +145,7 @@
     // Filter the program input into the D8 and R8 parts.
     Set<String> d8ZipEntries =
         d8classes.stream()
-            .map(name -> name.replace('.', '/') + ".class")
+            .map(clazz -> ZipUtils.zipEntryNameForClass(clazz.getClassReference()))
             .collect(Collectors.toSet());
     ZipBuilder d8ProgramBuilder = ZipBuilder.builder(tmp.resolve("d8-program.jar"));
     ZipBuilder r8ProgramBuilder = ZipBuilder.builder(tmp.resolve("r8-program.jar"));
@@ -178,21 +177,21 @@
           Files.readString(dump.getDesugaredLibraryFile(), UTF_8));
     }
     AndroidAppConsumers d8OutputAppSink = null;
-    if (d8OutputApp != null) {
+    if (d8OutputAppConsumer != null) {
       d8OutputAppSink = new AndroidAppConsumers(d8Builder);
     }
     d8Builder.validate();
     D8Command d8command = d8Builder.makeCommand();
     AndroidApp d8App = d8command.getInputApp();
-    if (d8InputApp != null) {
-      d8InputApp.accept(d8App);
+    if (d8InputAppConsumer != null) {
+      d8InputAppConsumer.accept(d8App);
     }
     InternalOptions d8Options = d8command.getInternalOptions();
     assert d8Options.getMinApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N)
         : "Default interface methods not yet supported";
     D8.runInternal(d8App, d8Options, executor);
-    if (d8OutputApp != null) {
-      d8OutputApp.accept(d8OutputAppSink.build());
+    if (d8OutputAppConsumer != null) {
+      d8OutputAppConsumer.accept(d8OutputAppSink.build());
     }
 
     // Run trace references to produce keep rules for the D8 compiled part.
@@ -235,15 +234,15 @@
     r8Builder.validate();
     R8Command r8Command = r8Builder.makeCommand();
     AndroidApp r8App = r8Command.getInputApp();
-    if (r8InputApp != null) {
-      r8InputApp.accept(r8App);
+    if (r8InputAppConsumer != null) {
+      r8InputAppConsumer.accept(r8App);
     }
     InternalOptions r8Options = r8Command.getInternalOptions();
     r8Options.mapConsumer = originalMapConsumer;
     r8Options.quiet = true; // Don't write the R8 version.
     R8.runInternal(r8App, r8Options, executor);
-    if (r8OutputApp != null) {
-      r8OutputApp.accept(r8OutputAppSink.build());
+    if (r8OutputAppConsumer != null) {
+      r8OutputAppConsumer.accept(r8OutputAppSink.build());
     }
 
     // Emit resources and merged DEX to the output consumer.
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 7b907e9..763198e 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -114,14 +114,11 @@
 import com.android.tools.r8.verticalclassmerging.VerticallyMergedClasses;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Predicates;
-import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -1022,9 +1019,10 @@
   private final ArtProfileOptions artProfileOptions = new ArtProfileOptions(this);
   private final StartupOptions startupOptions = new StartupOptions();
   private final InstrumentationOptions instrumentationOptions;
-  public final R8PartialCompilationOptions r8PartialCompilationOptions =
-      new R8PartialCompilationOptions(
-          System.getProperty("com.android.tools.r8.r8PartialCompilation"));
+  public R8PartialCompilationConfiguration partialCompilationConfiguration =
+      R8PartialCompilationConfiguration.fromIncludeExcludePatterns(
+          System.getProperty("com.android.tools.r8.experimentalPartialShrinkingIncludePatterns"),
+          System.getProperty("com.android.tools.r8.experimentalPartialShrinkingExcludePatterns"));
   public final TestingOptions testing = new TestingOptions();
 
   public List<ProguardConfigurationRule> mainDexKeepRules = ImmutableList.of();
@@ -2282,43 +2280,6 @@
     }
   }
 
-  public static class R8PartialCompilationOptions {
-    public boolean enabled;
-    public Path tempDir = null;
-    public Predicate<String> isR8 = null;
-    public Consumer<AndroidApp> r8InputApp;
-    public Consumer<AndroidApp> d8InputApp;
-    public Consumer<AndroidApp> r8OutputApp;
-    public Consumer<AndroidApp> d8OutputApp;
-
-    R8PartialCompilationOptions(String partialR8) {
-      this.enabled = partialR8 != null;
-      if (this.enabled) {
-        final List<String> prefixes = Splitter.on(",").splitToList(partialR8);
-        this.isR8 =
-            name -> {
-              for (int i = 0; i < prefixes.size(); i++) {
-                if (name.startsWith(prefixes.get(i))) {
-                  return true;
-                }
-              }
-              return false;
-            };
-      }
-    }
-
-    public synchronized Path getTemp() throws IOException {
-      if (tempDir == null) {
-        tempDir = Files.createTempDirectory("r8PartialCompilation");
-      }
-      return tempDir;
-    }
-
-    public Path getDumpFile() throws IOException {
-      return getTemp().resolve("dump.zip");
-    }
-  }
-
   public static class TestingOptions {
 
     public boolean enableEmbeddedKeepAnnotations =
diff --git a/src/main/java/com/android/tools/r8/utils/R8PartialCompilationConfiguration.java b/src/main/java/com/android/tools/r8/utils/R8PartialCompilationConfiguration.java
new file mode 100644
index 0000000..a1c3b2d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/R8PartialCompilationConfiguration.java
@@ -0,0 +1,283 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.graph.DexString;
+import com.google.common.base.Predicates;
+import com.google.common.base.Splitter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+public class R8PartialCompilationConfiguration implements Predicate<DexString> {
+
+  private final boolean enabled;
+  private Path tempDir = null;
+  private final List<Predicate<DexString>> includePredicates;
+  private final List<Predicate<DexString>> excludePredicates;
+
+  public Consumer<AndroidApp> r8InputAppConsumer;
+  public Consumer<AndroidApp> d8InputAppConsumer;
+  public Consumer<AndroidApp> r8OutputAppConsumer;
+  public Consumer<AndroidApp> d8OutputAppConsumer;
+
+  private static final R8PartialCompilationConfiguration disabledConfiguration =
+      new R8PartialCompilationConfiguration(false, null, null);
+
+  private R8PartialCompilationConfiguration(
+      boolean enabled,
+      List<Predicate<DexString>> includePredicates,
+      List<Predicate<DexString>> excludePredicates) {
+    assert !enabled || !includePredicates.isEmpty();
+    assert !enabled || excludePredicates != null;
+    this.enabled = enabled;
+    this.includePredicates = includePredicates;
+    this.excludePredicates = excludePredicates;
+  }
+
+  @Override
+  public boolean test(DexString name) {
+    for (Predicate<DexString> isR8ClassPredicate : includePredicates) {
+      if (isR8ClassPredicate.test(name)) {
+        for (Predicate<DexString> isD8ClassPredicate : excludePredicates) {
+          if (isD8ClassPredicate.test(name)) {
+            return false;
+          }
+        }
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static R8PartialCompilationConfiguration disabledConfiguration() {
+    return disabledConfiguration;
+  }
+
+  public static R8PartialCompilationConfiguration fromIncludeExcludePatterns(
+      String includePatterns, String excludePatterns) {
+    boolean enabled = includePatterns != null || excludePatterns != null;
+    if (!enabled) {
+      return disabledConfiguration();
+    }
+    Builder builder = builder();
+    if (includePatterns != null) {
+      Splitter.on(",").splitToList(includePatterns).forEach(builder::addJavaTypeIncludePattern);
+    }
+    if (excludePatterns != null) {
+      Splitter.on(",").splitToList(excludePatterns).forEach(builder::addJavaTypeExcludePattern);
+    }
+    return builder.build();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public synchronized Path getTempDir() throws IOException {
+    if (tempDir == null) {
+      setTempDir(Files.createTempDirectory("r8PartialCompilation"));
+    }
+    return tempDir;
+  }
+
+  public void setTempDir(Path tempDir) {
+    this.tempDir = tempDir;
+  }
+
+  public Path getDumpFile() throws IOException {
+    return getTempDir().resolve("dump.zip");
+  }
+
+  public static class Builder {
+    private final List<Predicate<DexString>> includePredicates = new ArrayList<>();
+    private final List<Predicate<DexString>> excludePredicates = new ArrayList<>();
+
+    private Builder() {}
+
+    public R8PartialCompilationConfiguration build() {
+      return new R8PartialCompilationConfiguration(
+          !includePredicates.isEmpty(), includePredicates, excludePredicates);
+    }
+
+    public Builder includeAll() {
+      includePredicates.add(Predicates.alwaysTrue());
+      return this;
+    }
+
+    public Builder addJavaTypeIncludePattern(String pattern) {
+      includePredicates.add(
+          createMatcher("L" + DescriptorUtils.getBinaryNameFromJavaType(pattern)));
+      return this;
+    }
+
+    public Builder addJavaTypeExcludePattern(String pattern) {
+      excludePredicates.add(
+          createMatcher("L" + DescriptorUtils.getBinaryNameFromJavaType(pattern)));
+      return this;
+    }
+
+    public Builder addDescriptorIncludePattern(String pattern) {
+      includePredicates.add(createMatcher(pattern));
+      return this;
+    }
+
+    public Builder addDescriptorExcludePattern(String pattern) {
+      excludePredicates.add(createMatcher(pattern));
+      return this;
+    }
+
+    private Predicate<DexString> createMatcher(String descriptorPrefix) {
+      assert descriptorPrefix.startsWith("L");
+      assert descriptorPrefix.indexOf('.') == -1;
+
+      if (descriptorPrefix.equals("L**")) {
+        return new AllClassesMatcher();
+      } else if (descriptorPrefix.equals("L*")) {
+        return new UnnamedPackageMatcher();
+      } else if (descriptorPrefix.endsWith("/**")) {
+        return new PackageAndSubpackagePrefixMatcher(
+            descriptorPrefix.substring(0, descriptorPrefix.length() - 2));
+      } else if (descriptorPrefix.endsWith("/*")) {
+        return new PackagePrefixMatcher(
+            descriptorPrefix.substring(0, descriptorPrefix.length() - 1));
+      }
+      if (descriptorPrefix.endsWith("*")) {
+        return new ClassPrefixMatcher(descriptorPrefix.substring(0, descriptorPrefix.length() - 1));
+      } else {
+        return new ClassNameMatcher(descriptorPrefix + ';');
+      }
+    }
+
+    public Builder includeClasses(Class<?>... classes) {
+      return includeClasses(Arrays.asList(classes));
+    }
+
+    public Builder includeClasses(Collection<Class<?>> classes) {
+      classes.forEach(
+          clazz ->
+              includePredicates.add(
+                  descriptor ->
+                      descriptor.toString().equals(DescriptorUtils.javaClassToDescriptor(clazz))));
+      return this;
+    }
+
+    public Builder includeJavaType(Predicate<String> include) {
+      includePredicates.add(
+          descriptor -> include.test(DescriptorUtils.descriptorToJavaType(descriptor.toString())));
+      return this;
+    }
+
+    public Builder excludeClasses(Class<?>... classes) {
+      return excludeClasses(Arrays.asList(classes));
+    }
+
+    public Builder excludeClasses(Collection<Class<?>> classes) {
+      classes.forEach(
+          clazz ->
+              excludePredicates.add(
+                  descriptor ->
+                      descriptor.toString().equals(DescriptorUtils.javaClassToDescriptor(clazz))));
+      return this;
+    }
+
+    public Builder excludeJavaType(Predicate<String> exclude) {
+      excludePredicates.add(
+          descriptor -> exclude.test(DescriptorUtils.descriptorToJavaType(descriptor.toString())));
+      return this;
+    }
+  }
+
+  private static class AllClassesMatcher implements Predicate<DexString> {
+
+    AllClassesMatcher() {}
+
+    @Override
+    public boolean test(DexString descriptor) {
+      return true;
+    }
+  }
+
+  private static class UnnamedPackageMatcher implements Predicate<DexString> {
+
+    UnnamedPackageMatcher() {}
+
+    @Override
+    public boolean test(DexString descriptor) {
+      return descriptor.indexOf('/') == -1;
+    }
+  }
+
+  private static class PackageAndSubpackagePrefixMatcher implements Predicate<DexString> {
+
+    private final byte[] descriptorPrefix;
+
+    PackageAndSubpackagePrefixMatcher(String descriptorPrefix) {
+      this.descriptorPrefix = DexString.encodeToMutf8(descriptorPrefix);
+    }
+
+    @Override
+    public boolean test(DexString descriptor) {
+      return descriptor.startsWith(descriptorPrefix);
+    }
+  }
+
+  private static class PackagePrefixMatcher implements Predicate<DexString> {
+
+    private final byte[] descriptorPrefix;
+    private final int descriptorPrefixLength;
+
+    PackagePrefixMatcher(String descriptorPrefix) {
+      this.descriptorPrefix = DexString.encodeToMutf8(descriptorPrefix);
+      this.descriptorPrefixLength = descriptorPrefix.length();
+    }
+
+    @Override
+    public boolean test(DexString descriptor) {
+      return descriptor.startsWith(descriptorPrefix)
+          && descriptor.lastIndexOf('/') == descriptorPrefixLength - 1;
+    }
+  }
+
+  private static class ClassPrefixMatcher implements Predicate<DexString> {
+
+    private final byte[] descriptorPrefix;
+    private final int descriptorPrefixLength;
+
+    ClassPrefixMatcher(String descriptorPrefix) {
+      this.descriptorPrefix = DexString.encodeToMutf8(descriptorPrefix);
+      this.descriptorPrefixLength = descriptorPrefix.length();
+    }
+
+    @Override
+    public boolean test(DexString descriptor) {
+      return descriptor.startsWith(descriptorPrefix)
+          && descriptor.lastIndexOf('/') < descriptorPrefixLength - 1;
+    }
+  }
+
+  private static class ClassNameMatcher implements Predicate<DexString> {
+
+    private final String descriptor;
+
+    ClassNameMatcher(String descriptor) {
+      this.descriptor = descriptor;
+    }
+
+    @Override
+    public boolean test(DexString descriptor) {
+      return descriptor.toString().equals(this.descriptor);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
index 5d349fc..bd1aace 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
@@ -36,6 +36,7 @@
 import com.android.tools.r8.compilerapi.testsetup.ApiTestingSetUpTest;
 import com.android.tools.r8.compilerapi.wrappers.CommandLineParserTest;
 import com.android.tools.r8.compilerapi.wrappers.EnableMissingLibraryApiModelingTest;
+import com.android.tools.r8.partial.PartialShrinkingPreviewApiTest;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -79,7 +80,7 @@
           MainDexRulesTest.ApiTest.class);
 
   private static final List<Class<? extends CompilerApiTest>> CLASSES_PENDING_BINARY_COMPATIBILITY =
-      ImmutableList.of();
+      ImmutableList.of(PartialShrinkingPreviewApiTest.ApiTest.class);
 
   private final TemporaryFolder temp;
 
diff --git a/src/test/java/com/android/tools/r8/partial/ClassHierarchyInterleavedD8AndR8Test.java b/src/test/java/com/android/tools/r8/partial/ClassHierarchyInterleavedD8AndR8Test.java
index a44f2bb..32bfa65 100644
--- a/src/test/java/com/android/tools/r8/partial/ClassHierarchyInterleavedD8AndR8Test.java
+++ b/src/test/java/com/android/tools/r8/partial/ClassHierarchyInterleavedD8AndR8Test.java
@@ -14,9 +14,10 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.R8PartialCompilationConfiguration;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import java.util.function.Predicate;
+import java.util.function.Consumer;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -41,7 +42,7 @@
   }
 
   private void runTest(
-      Predicate<String> isR8,
+      Consumer<R8PartialCompilationConfiguration.Builder> partialConfigurationBuilderConsumer,
       ThrowingConsumer<CodeInspector, RuntimeException> d8Inspector,
       ThrowingConsumer<CodeInspector, RuntimeException> inspector)
       throws Exception {
@@ -50,7 +51,7 @@
         .setMinApi(parameters)
         .addProgramClasses(A.class, B.class, C.class, Main.class)
         .addKeepMainRule(Main.class)
-        .setR8PartialConfigurationPredicate(isR8)
+        .setR8PartialConfiguration(partialConfigurationBuilderConsumer)
         .compile()
         .inspectD8Input(d8Inspector)
         .inspect(inspector)
@@ -61,7 +62,8 @@
   @Test
   public void testD8Top() throws Exception {
     runTest(
-        name -> !name.equals(A.class.getTypeName()),
+        partialConfigurationBuilder ->
+            partialConfigurationBuilder.includeAll().excludeClasses(A.class),
         inspector -> {
           assertThat(inspector.programClass(A.class), isPresent());
           assertThat(inspector.programClass(B.class), isAbsent());
@@ -77,7 +79,8 @@
   @Test
   public void testD8Middle() throws Exception {
     runTest(
-        name -> !name.equals(B.class.getTypeName()),
+        partialConfigurationBuilder ->
+            partialConfigurationBuilder.includeAll().excludeClasses(B.class),
         inspector -> {
           assertThat(inspector.programClass(A.class), isPresent());
           assertThat(inspector.programClass(B.class), isPresent());
@@ -93,7 +96,8 @@
   @Test
   public void testD8Bottom() throws Exception {
     runTest(
-        name -> !name.equals(C.class.getTypeName()),
+        partialConfigurationBuilder ->
+            partialConfigurationBuilder.includeAll().excludeClasses(C.class),
         inspector -> {
           assertThat(inspector.programClass(A.class), isPresent());
           assertThat(inspector.programClass(B.class), isPresent());
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicPreviewPatternsTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicPreviewPatternsTest.java
new file mode 100644
index 0000000..b6c55e9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicPreviewPatternsTest.java
@@ -0,0 +1,286 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.partial;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.partial.pkg1.A1;
+import com.android.tools.r8.partial.pkg1.A2;
+import com.android.tools.r8.partial.pkg1.subpkg.B;
+import com.android.tools.r8.partial.pkg2.C1;
+import com.android.tools.r8.partial.pkg2.C2;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PartialCompilationBasicPreviewPatternsTest extends TestBase {
+
+  private static String PKG1 = getPackageName(A1.class);
+  private static String SUBPKG = getPackageName(B.class);
+  private static String PKG2 = getPackageName(C2.class);
+
+  private static String getPackageName(Class<?> clazz) {
+    return clazz.getTypeName().substring(0, clazz.getTypeName().lastIndexOf('.'));
+  }
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  // Test with min API level 24.
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(DexVm.Version.V7_0_0)
+        .withApiLevel(AndroidApiLevel.N)
+        .build();
+  }
+
+  private static final List<Class<?>> ALL_CLASSES =
+      ImmutableList.of(A1.class, A2.class, B.class, C1.class, C2.class, Main.class);
+  private static final String[] ALL_TYPE_NAMES =
+      new String[] {
+        A1.class.getTypeName(),
+        A2.class.getTypeName(),
+        B.class.getTypeName(),
+        C1.class.getTypeName(),
+        C2.class.getTypeName()
+      };
+
+  @Test
+  public void pkg1AndSubpackagesCompiledWithR8() throws Exception {
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(ALL_CLASSES)
+        .setR8PartialConfiguration(builder -> builder.addJavaTypeIncludePattern(PKG1 + ".**"))
+        .compile()
+        .inspectD8Input(
+            inspector ->
+                assertTrue(inspector.hasExactlyProgramClasses(C1.class, C2.class, Main.class)))
+        .inspectR8Input(
+            inspector ->
+                assertTrue(inspector.hasExactlyProgramClasses(A1.class, A2.class, B.class)))
+        .inspect(
+            inspector ->
+                assertTrue(inspector.hasExactlyProgramClasses(C1.class, C2.class, Main.class)))
+        .run(parameters.getRuntime(), Main.class, ALL_TYPE_NAMES)
+        .assertSuccessWithOutputLines(
+            "Not instantiated",
+            "Not instantiated",
+            "Not instantiated",
+            "Instantiated",
+            "Instantiated");
+  }
+
+  @Test
+  public void pkg1CompiledWithR8() throws Exception {
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(ALL_CLASSES)
+        .setR8PartialConfiguration(builder -> builder.addJavaTypeIncludePattern(PKG1 + ".*"))
+        .compile()
+        .inspectD8Input(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(B.class, C1.class, C2.class, Main.class)))
+        .inspectR8Input(
+            inspector -> assertTrue(inspector.hasExactlyProgramClasses(A1.class, A2.class)))
+        .inspect(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(B.class, C1.class, C2.class, Main.class)))
+        .run(parameters.getRuntime(), Main.class, ALL_TYPE_NAMES)
+        .assertSuccessWithOutputLines(
+            "Not instantiated", "Not instantiated", "Instantiated", "Instantiated", "Instantiated");
+  }
+
+  @Test
+  public void pkg1AndSubpackagesExcludeSubPkgCompiledWithR8() throws Exception {
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(ALL_CLASSES)
+        .setR8PartialConfiguration(
+            builder ->
+                builder
+                    .addJavaTypeIncludePattern(PKG1 + ".**")
+                    .addJavaTypeExcludePattern(SUBPKG + ".*"))
+        .compile()
+        .inspectD8Input(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(C1.class, C2.class, B.class, Main.class)))
+        .inspectR8Input(
+            inspector -> assertTrue(inspector.hasExactlyProgramClasses(A1.class, A2.class)))
+        .inspect(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(B.class, C1.class, C2.class, Main.class)))
+        .run(parameters.getRuntime(), Main.class, ALL_TYPE_NAMES)
+        .assertSuccessWithOutputLines(
+            "Not instantiated", "Not instantiated", "Instantiated", "Instantiated", "Instantiated");
+  }
+
+  @Test
+  public void pkg1AndSubpackagesExcludeAPrefixCompiledWithR8() throws Exception {
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(ALL_CLASSES)
+        .setR8PartialConfiguration(
+            builder ->
+                builder
+                    .addJavaTypeIncludePattern(PKG1 + ".**")
+                    .addJavaTypeExcludePattern(PKG1 + ".A*"))
+        .compile()
+        .inspectD8Input(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(
+                        A1.class, A2.class, C1.class, C2.class, Main.class)))
+        .inspectR8Input(inspector -> assertTrue(inspector.hasExactlyProgramClasses(B.class)))
+        .inspect(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(
+                        A1.class, A2.class, C1.class, C2.class, Main.class)))
+        .run(parameters.getRuntime(), Main.class, ALL_TYPE_NAMES)
+        .assertSuccessWithOutputLines(
+            "Instantiated", "Instantiated", "Not instantiated", "Instantiated", "Instantiated");
+  }
+
+  @Test
+  public void pkg1AndSubpackagesExcludeA1AndA2CompiledWithR8() throws Exception {
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(ALL_CLASSES)
+        .setR8PartialConfiguration(
+            builder ->
+                builder
+                    .addJavaTypeIncludePattern(PKG1 + ".**")
+                    .addJavaTypeExcludePattern(PKG1 + ".A1")
+                    .addJavaTypeExcludePattern(PKG1 + ".A2"))
+        .compile()
+        .inspectD8Input(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(
+                        A1.class, A2.class, C1.class, C2.class, Main.class)))
+        .inspectR8Input(inspector -> assertTrue(inspector.hasExactlyProgramClasses(B.class)))
+        .inspect(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(
+                        A1.class, A2.class, C1.class, C2.class, Main.class)))
+        .run(parameters.getRuntime(), Main.class, ALL_TYPE_NAMES)
+        .assertSuccessWithOutputLines(
+            "Instantiated", "Instantiated", "Not instantiated", "Instantiated", "Instantiated");
+  }
+
+  @Test
+  public void allExeptC1CompiledWithR8() throws Exception {
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(ALL_CLASSES)
+        .setR8PartialConfiguration(
+            builder ->
+                builder
+                    .addJavaTypeIncludePattern(PKG1 + ".**")
+                    .addJavaTypeIncludePattern(PKG2 + ".**")
+                    .addJavaTypeExcludePattern(PKG2 + ".C1"))
+        .compile()
+        .inspectD8Input(
+            inspector -> assertTrue(inspector.hasExactlyProgramClasses(C1.class, Main.class)))
+        .inspectR8Input(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(A1.class, A2.class, B.class, C2.class)))
+        .inspect(inspector -> assertTrue(inspector.hasExactlyProgramClasses(C1.class, Main.class)))
+        .run(parameters.getRuntime(), Main.class, ALL_TYPE_NAMES)
+        .assertSuccessWithOutputLines(
+            "Not instantiated",
+            "Not instantiated",
+            "Not instantiated",
+            "Instantiated",
+            "Not instantiated");
+  }
+
+  @Test
+  public void allExeptA1CompiledWithR8() throws Exception {
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(ALL_CLASSES)
+        .setR8PartialConfiguration(
+            builder ->
+                builder
+                    .addJavaTypeIncludePattern(PKG1 + ".*")
+                    .addJavaTypeIncludePattern(SUBPKG + ".*")
+                    .addJavaTypeIncludePattern(PKG2 + ".*")
+                    .addJavaTypeExcludePattern(PKG1 + ".A1"))
+        .compile()
+        .inspectD8Input(
+            inspector -> assertTrue(inspector.hasExactlyProgramClasses(A1.class, Main.class)))
+        .inspectR8Input(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(A2.class, B.class, C1.class, C2.class)))
+        .inspect(inspector -> assertTrue(inspector.hasExactlyProgramClasses(A1.class, Main.class)))
+        .run(parameters.getRuntime(), Main.class, ALL_TYPE_NAMES)
+        .assertSuccessWithOutputLines(
+            "Instantiated",
+            "Not instantiated",
+            "Not instantiated",
+            "Not instantiated",
+            "Not instantiated");
+  }
+
+  @Test
+  public void allExeptBCompiledWithR8() throws Exception {
+    testForR8Partial(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(ALL_CLASSES)
+        .setR8PartialConfiguration(
+            builder ->
+                builder
+                    .addJavaTypeIncludePattern(PKG1 + ".**")
+                    .addJavaTypeIncludePattern(PKG2 + ".**")
+                    .addJavaTypeExcludePattern(SUBPKG + ".*"))
+        .compile()
+        .inspectD8Input(
+            inspector -> assertTrue(inspector.hasExactlyProgramClasses(B.class, Main.class)))
+        .inspectR8Input(
+            inspector ->
+                assertTrue(
+                    inspector.hasExactlyProgramClasses(A1.class, A2.class, C1.class, C2.class)))
+        .inspect(inspector -> assertTrue(inspector.hasExactlyProgramClasses(B.class, Main.class)))
+        .run(parameters.getRuntime(), Main.class, ALL_TYPE_NAMES)
+        .assertSuccessWithOutputLines(
+            "Not instantiated",
+            "Not instantiated",
+            "Instantiated",
+            "Not instantiated",
+            "Not instantiated");
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) throws Exception {
+      for (String arg : args) {
+        try {
+          Class.forName(arg);
+          System.out.println("Instantiated");
+        } catch (ClassNotFoundException e) {
+          System.out.println("Not instantiated");
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicTest.java
index 6206e13..e55a8d5 100644
--- a/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicTest.java
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicTest.java
@@ -39,7 +39,7 @@
         .setMinApi(parameters)
         .addProgramClasses(A.class, B.class, Main.class)
         .addKeepMainRule(Main.class)
-        .setR8PartialConfiguration(builder -> builder.includeAll().excludeClasses(A.class).build())
+        .setR8PartialConfiguration(builder -> builder.includeAll().excludeClasses(A.class))
         .compile()
         .inspectR8Input(
             inspector -> {
@@ -81,7 +81,7 @@
         .setMinApi(parameters)
         .addProgramClasses(A.class, B.class, Main.class)
         .addKeepMainRule(Main.class)
-        .setR8PartialConfiguration(builder -> builder.includeAll().excludeClasses(B.class).build())
+        .setR8PartialConfiguration(builder -> builder.includeAll().excludeClasses(B.class))
         .compile()
         .inspectR8Input(
             inspector -> {
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationDemoTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationDemoTest.java
index 0ccd8ce..43ad334 100644
--- a/src/test/java/com/android/tools/r8/partial/PartialCompilationDemoTest.java
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationDemoTest.java
@@ -236,10 +236,10 @@
   private void runR8Partial(Path tempDir, CompilerDump dump, Path output, Predicate<String> isR8)
       throws IOException, CompilationFailedException {
     testForR8Partial(parameters.getBackend())
-        .setR8PartialConfigurationPredicate(isR8)
+        .setR8PartialConfigurationJavaTypePredicate(isR8)
         .addOptionsModification(
             options -> {
-              options.r8PartialCompilationOptions.tempDir = tempDir;
+              options.partialCompilationConfiguration.setTempDir(tempDir);
 
               // For compiling nowonandroid.
               options.testing.allowUnnecessaryDontWarnWildcards = true;
diff --git a/src/test/java/com/android/tools/r8/partial/PartialShrinkingPreviewApiTest.java b/src/test/java/com/android/tools/r8/partial/PartialShrinkingPreviewApiTest.java
new file mode 100644
index 0000000..f5784c5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialShrinkingPreviewApiTest.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.partial;
+
+import static com.android.tools.r8.MarkerMatcher.markerMinApi;
+import static com.android.tools.r8.MarkerMatcher.markerR8Mode;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.R8;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.compilerapi.CompilerApiTest;
+import com.android.tools.r8.compilerapi.CompilerApiTestRunner;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+
+public class PartialShrinkingPreviewApiTest extends CompilerApiTestRunner {
+
+  public static final int MIN_API_LEVEL = 31;
+
+  public PartialShrinkingPreviewApiTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Override
+  public Class<? extends CompilerApiTest> binaryTestClass() {
+    return ApiTest.class;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    ApiTest test = new ApiTest(ApiTest.PARAMETERS);
+    runTest(test::runR8);
+  }
+
+  private void runTest(ThrowingConsumer<ProgramConsumer, Exception> test) throws Exception {
+    Path output = temp.newFolder().toPath().resolve("out.jar");
+    test.accept(new DexIndexedConsumer.ArchiveConsumer(output));
+    assertThat(
+        new CodeInspector(output).getMarkers(),
+        CoreMatchers.everyItem(
+            CoreMatchers.allOf(
+                markerMinApi(AndroidApiLevel.getAndroidApiLevel(MIN_API_LEVEL)),
+                markerR8Mode("full"))));
+  }
+
+  public static class ApiTest extends CompilerApiTest {
+
+    public ApiTest(Object parameters) {
+      super(parameters);
+    }
+
+    public void runR8(ProgramConsumer programConsumer) throws Exception {
+      R8.run(
+          R8Command.builder()
+              .addClassProgramData(getBytesForClass(getMockClass()), Origin.unknown())
+              .addProguardConfiguration(getKeepMainRules(getMockClass()), Origin.unknown())
+              .addLibraryFiles(getJava8RuntimeJar())
+              .setProgramConsumer(programConsumer)
+              .enableExperimentalPartialShrinking("**", null)
+              .setMinApiLevel(MIN_API_LEVEL)
+              .build());
+    }
+
+    @Test
+    public void testR8() throws Exception {
+      runR8(DexIndexedConsumer.emptyConsumer());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/partial/pkg1/A1.java b/src/test/java/com/android/tools/r8/partial/pkg1/A1.java
new file mode 100644
index 0000000..e435721
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/pkg1/A1.java
@@ -0,0 +1,6 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.partial.pkg1;
+
+public class A1 {}
diff --git a/src/test/java/com/android/tools/r8/partial/pkg1/A2.java b/src/test/java/com/android/tools/r8/partial/pkg1/A2.java
new file mode 100644
index 0000000..7cac5c2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/pkg1/A2.java
@@ -0,0 +1,6 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.partial.pkg1;
+
+public class A2 {}
diff --git a/src/test/java/com/android/tools/r8/partial/pkg1/subpkg/B.java b/src/test/java/com/android/tools/r8/partial/pkg1/subpkg/B.java
new file mode 100644
index 0000000..ee4cb46
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/pkg1/subpkg/B.java
@@ -0,0 +1,6 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.partial.pkg1.subpkg;
+
+public class B {}
diff --git a/src/test/java/com/android/tools/r8/partial/pkg2/C1.java b/src/test/java/com/android/tools/r8/partial/pkg2/C1.java
new file mode 100644
index 0000000..a6225c1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/pkg2/C1.java
@@ -0,0 +1,6 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.partial.pkg2;
+
+public class C1 {}
diff --git a/src/test/java/com/android/tools/r8/partial/pkg2/C2.java b/src/test/java/com/android/tools/r8/partial/pkg2/C2.java
new file mode 100644
index 0000000..139666a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/pkg2/C2.java
@@ -0,0 +1,6 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.partial.pkg2;
+
+public class C2 {}
diff --git a/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
index 9ea93eb..3dacb92 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
@@ -10,23 +10,17 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableList;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
+import com.android.tools.r8.utils.R8PartialCompilationConfiguration;
 import java.util.List;
 import java.util.function.Consumer;
-import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
 
 public class R8PartialTestBuilder
     extends R8TestBuilder<R8PartialTestCompileResult, R8TestRunResult, R8PartialTestBuilder> {
 
-  private R8PartialConfiguration r8PartialConfiguration =
-      R8PartialConfiguration.defaultConfiguration();
+  private R8PartialCompilationConfiguration r8PartialConfiguration =
+      R8PartialCompilationConfiguration.disabledConfiguration();
 
   private R8PartialTestBuilder(TestState state, Builder builder, Backend backend) {
     super(state, builder, backend);
@@ -52,106 +46,30 @@
     return this;
   }
 
-  public static class R8PartialConfiguration implements Predicate<String> {
-    private static final R8PartialConfiguration defaultConfiguration =
-        new R8PartialConfiguration(ImmutableList.of(), ImmutableList.of());
-    private final List<Predicate<String>> includePredicates;
-    private final List<Predicate<String>> excludePredicates;
-
-    public R8PartialConfiguration(
-        List<Predicate<String>> includePredicates, List<Predicate<String>> excludePredicates) {
-      this.includePredicates = includePredicates;
-      this.excludePredicates = excludePredicates;
-    }
-
-    private static R8PartialConfiguration defaultConfiguration() {
-      return defaultConfiguration;
-    }
-
-    public static Builder builder() {
-      return new Builder();
-    }
-
-    public boolean test(String name) {
-      for (Predicate<String> isR8ClassPredicate : includePredicates) {
-        if (isR8ClassPredicate.test(name)) {
-          for (Predicate<String> isD8ClassPredicate : excludePredicates) {
-            if (isD8ClassPredicate.test(name)) {
-              return false;
-            }
-          }
-          return true;
-        }
-      }
-      return false;
-    }
-
-    public static class Builder {
-      private final List<Predicate<String>> includePredicates = new ArrayList<>();
-      private final List<Predicate<String>> excludePredicates = new ArrayList<>();
-
-      public R8PartialConfiguration build() {
-        return new R8PartialConfiguration(includePredicates, excludePredicates);
-      }
-
-      public Builder includeAll() {
-        includePredicates.add(Predicates.alwaysTrue());
-        return this;
-      }
-
-      public Builder includeClasses(Class<?>... classes) {
-        return includeClasses(Arrays.asList(classes));
-      }
-
-      public Builder includeClasses(Collection<Class<?>> classes) {
-        Collection<String> typeNames =
-            classes.stream().map(Class::getTypeName).collect(Collectors.toList());
-        includePredicates.add(typeNames::contains);
-        return this;
-      }
-
-      public Builder include(Predicate<String> include) {
-        includePredicates.add(include);
-        return this;
-      }
-
-      public Builder excludeClasses(Class<?>... classes) {
-        return excludeClasses(Arrays.asList(classes));
-      }
-
-      public Builder excludeClasses(Collection<Class<?>> classes) {
-        Collection<String> typeNames =
-            classes.stream().map(Class::getTypeName).collect(Collectors.toList());
-        excludePredicates.add(typeNames::contains);
-        return this;
-      }
-
-      public Builder exclude(Predicate<String> exclude) {
-        excludePredicates.add(exclude);
-        return this;
-      }
-    }
-  }
-
-  public R8PartialTestBuilder setR8PartialConfigurationPredicate(Predicate<String> include) {
-    assert r8PartialConfiguration == R8PartialConfiguration.defaultConfiguration()
+  public R8PartialTestBuilder setR8PartialConfigurationJavaTypePredicate(
+      Predicate<String> include) {
+    assert r8PartialConfiguration.equals(R8PartialCompilationConfiguration.disabledConfiguration())
         : "Overwriting configuration...?";
-    r8PartialConfiguration = R8PartialConfiguration.builder().include(include).build();
+    r8PartialConfiguration =
+        R8PartialCompilationConfiguration.builder().includeJavaType(include).build();
     return self();
   }
 
-  public R8PartialTestBuilder setR8PartialConfiguration(R8PartialConfiguration configuration) {
-    assert r8PartialConfiguration == R8PartialConfiguration.defaultConfiguration()
+  public R8PartialTestBuilder setR8PartialConfiguration(
+      R8PartialCompilationConfiguration configuration) {
+    assert r8PartialConfiguration.equals(R8PartialCompilationConfiguration.disabledConfiguration())
         : "Overwriting configuration...?";
     r8PartialConfiguration = configuration;
     return self();
   }
 
   public R8PartialTestBuilder setR8PartialConfiguration(
-      Function<R8PartialConfiguration.Builder, R8PartialConfiguration> fn) {
-    assert r8PartialConfiguration == R8PartialConfiguration.defaultConfiguration()
+      Consumer<R8PartialCompilationConfiguration.Builder> consumer) {
+    assert r8PartialConfiguration.equals(R8PartialCompilationConfiguration.disabledConfiguration())
         : "Overwriting configuration...?";
-    r8PartialConfiguration = fn.apply(R8PartialConfiguration.builder());
+    R8PartialCompilationConfiguration.Builder builder = R8PartialCompilationConfiguration.builder();
+    consumer.accept(builder);
+    r8PartialConfiguration = builder.build();
     return self();
   }
 
@@ -165,18 +83,17 @@
       Box<List<ProguardConfigurationRule>> syntheticProguardRulesConsumer,
       StringBuilder proguardMapBuilder)
       throws CompilationFailedException {
-    Box<AndroidApp> r8InputApp = new Box<>();
-    Box<AndroidApp> d8InputApp = new Box<>();
-    Box<AndroidApp> r8OutputApp = new Box<>();
-    Box<AndroidApp> d8OutputApp = new Box<>();
+    Box<AndroidApp> r8InputAppBox = new Box<>();
+    Box<AndroidApp> d8InputAppBox = new Box<>();
+    Box<AndroidApp> r8OutputAppBox = new Box<>();
+    Box<AndroidApp> d8OutputAppBox = new Box<>();
     Consumer<InternalOptions> configureR8PartialCompilation =
         options -> {
-          options.r8PartialCompilationOptions.enabled = true;
-          options.r8PartialCompilationOptions.isR8 = r8PartialConfiguration;
-          options.r8PartialCompilationOptions.r8InputApp = r8InputApp::set;
-          options.r8PartialCompilationOptions.d8InputApp = d8InputApp::set;
-          options.r8PartialCompilationOptions.r8OutputApp = r8OutputApp::set;
-          options.r8PartialCompilationOptions.d8OutputApp = d8OutputApp::set;
+          options.partialCompilationConfiguration = r8PartialConfiguration;
+          options.partialCompilationConfiguration.r8InputAppConsumer = r8InputAppBox::set;
+          options.partialCompilationConfiguration.d8InputAppConsumer = d8InputAppBox::set;
+          options.partialCompilationConfiguration.r8OutputAppConsumer = r8OutputAppBox::set;
+          options.partialCompilationConfiguration.d8OutputAppConsumer = d8OutputAppBox::set;
         };
     ToolHelper.runAndBenchmarkR8PartialWithoutResult(
         builder, configureR8PartialCompilation.andThen(optionsConsumer), benchmarkResults);
@@ -195,9 +112,9 @@
         resourceShrinkerOutput,
         resourceShrinkerOutputForFeatures,
         buildMetadata != null ? buildMetadata.get() : null,
-        r8InputApp.get(),
-        d8InputApp.get(),
-        r8OutputApp.get(),
-        d8OutputApp.get());
+        r8InputAppBox.get(),
+        d8InputAppBox.get(),
+        r8OutputAppBox.get(),
+        d8OutputAppBox.get());
   }
 }
diff --git a/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java b/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
index d055bdc..d9af1e0 100644
--- a/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
+++ b/src/test/testbase/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
@@ -47,6 +47,7 @@
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.BiMapContainer;
+import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Timing;
@@ -63,6 +64,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
@@ -424,6 +426,18 @@
     return builder.build();
   }
 
+  public boolean hasExactlyProgramClasses(Class<?>... classes) {
+    return hasExactlyProgramClasses(Arrays.asList(classes));
+  }
+
+  public boolean hasExactlyProgramClasses(Collection<Class<?>> classes) {
+    Set<ClassReference> descriptors =
+        classes.stream().map(Reference::classFromClass).collect(Collectors.toSet());
+    BooleanBox allFound = new BooleanBox(true);
+    forAllClasses(clazz -> allFound.and(descriptors.remove(clazz.reference)));
+    return descriptors.isEmpty() && allFound.get();
+  }
+
   public Stream<InstructionSubject> streamInstructions() {
     return allClasses().stream()
         .flatMap(cls -> cls.allMethods(MethodSubject::hasCode).stream())