Version 2.1.44

Cherry-pick: "Report unsupported based on desugared library version."
CL: https://r8-review.googlesource.com/c/r8/+/52289

Cherry-pick: "Backwards compatible desugared library configuration."
CL: https://r8-review.googlesource.com/c/r8/+/52241

Cherry-pick: "Update test expectation after error message change."
CL: https://r8-review.googlesource.com/c/r8/+/52220

Cherry-pick: "Specify wrapper types in the desugared library configuration."
CL: https://r8-review.googlesource.com/c/r8/+/52101

Cherry-pick: "Generate conversion wrappers in the desugared library."
CL: https://r8-review.googlesource.com/c/r8/+/52042

Cherry-pick: "Test wrapper merging on all VMs and API levels."
CL: https://r8-review.googlesource.com/c/r8/+/51859

Bug: 157681341
Bug: 158645207
Change-Id: If2c471396179dfd99e18b2f36b8a513cbae12ac4
diff --git a/.gitignore b/.gitignore
index 0629bac..40cf737 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,6 +135,8 @@
 third_party/protobuf-lite/
 third_party/r8
 third_party/r8.tar.gz
+third_party/r8-releases/2.0.74
+third_party/r8-releases/2.0.74.tar.gz
 third_party/r8mappings
 third_party/r8mappings.tar.gz
 third_party/remapper
diff --git a/build.gradle b/build.gradle
index 338df88..ffc0819 100644
--- a/build.gradle
+++ b/build.gradle
@@ -334,6 +334,7 @@
                 "proguard/proguard5.2.1",
                 "proguard/proguard6.0.1",
                 "r8",
+                "r8-releases/2.0.74",
                 "r8mappings",
                 "tachiyomi"
         ],
diff --git a/src/main/java/com/android/tools/r8/GenerateLintFiles.java b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
index 7d5b31e..dcf33f1 100644
--- a/src/main/java/com/android/tools/r8/GenerateLintFiles.java
+++ b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
@@ -69,7 +69,7 @@
 
   private final Set<DexMethod> parallelMethods = Sets.newIdentityHashSet();
 
-  private GenerateLintFiles(String desugarConfigurationPath, String outputDirectory) {
+  public GenerateLintFiles(String desugarConfigurationPath, String outputDirectory) {
     this.desugaredLibraryConfiguration =
         readDesugaredLibraryConfiguration(desugarConfigurationPath);
     this.outputDirectory =
@@ -356,20 +356,24 @@
         apiLevel >= desugaredLibraryConfiguration.getRequiredCompilationApiLevel().getLevel();
         apiLevel--) {
       System.out.println("Generating lint files for compile API " + apiLevel);
-      generateLintFiles(
-          AndroidApiLevel.getAndroidApiLevel(apiLevel),
-          minApiLevel -> minApiLevel == AndroidApiLevel.L || minApiLevel == AndroidApiLevel.B,
-          (minApiLevel, method) -> {
-            assert minApiLevel == AndroidApiLevel.L || minApiLevel == AndroidApiLevel.B;
-            if (minApiLevel == AndroidApiLevel.L) {
-              return true;
-            }
-            assert minApiLevel == AndroidApiLevel.B;
-            return !parallelMethods.contains(method.method);
-          });
+      run(apiLevel);
     }
   }
 
+  public void run(int apiLevel) throws Exception {
+    generateLintFiles(
+        AndroidApiLevel.getAndroidApiLevel(apiLevel),
+        minApiLevel -> minApiLevel == AndroidApiLevel.L || minApiLevel == AndroidApiLevel.B,
+        (minApiLevel, method) -> {
+          assert minApiLevel == AndroidApiLevel.L || minApiLevel == AndroidApiLevel.B;
+          if (minApiLevel == AndroidApiLevel.L) {
+            return true;
+          }
+          assert minApiLevel == AndroidApiLevel.B;
+          return !parallelMethods.contains(method.method);
+        });
+  }
+
   public static void main(String[] args) throws Exception {
     if (args.length != 2) {
       System.out.println("Usage: GenerateLineFiles <desuage configuration> <output directory>");
diff --git a/src/main/java/com/android/tools/r8/L8.java b/src/main/java/com/android/tools/r8/L8.java
index 4bef8cec..4b35d17 100644
--- a/src/main/java/com/android/tools/r8/L8.java
+++ b/src/main/java/com/android/tools/r8/L8.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.graph.LazyLoadedDexApplication;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.desugar.PrefixRewritingMapper;
 import com.android.tools.r8.jar.CfApplicationWriter;
@@ -118,11 +119,13 @@
       // on it.
       options.enableLoadStoreOptimization = false;
 
-      DexApplication app = new ApplicationReader(inputApp, options, timing).read(executor);
+      LazyLoadedDexApplication lazyApp =
+          new ApplicationReader(inputApp, options, timing).read(executor);
+
       PrefixRewritingMapper rewritePrefix =
           options.desugaredLibraryConfiguration.createPrefixRewritingMapper(options);
 
-      app = new L8TreePruner(options).prune(app, rewritePrefix);
+      DexApplication app = new L8TreePruner(options).prune(lazyApp, rewritePrefix);
       AppInfo appInfo = new AppInfo(app);
 
       AppView<?> appView = AppView.createForL8(appInfo, options, rewritePrefix);
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 7093d91..84eb5f4 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "2.1.43";
+  public static final String LABEL = "2.1.44";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index ede2193..ca70ab0 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -74,11 +74,11 @@
     this.inputApp = inputApp;
   }
 
-  public DexApplication read() throws IOException {
+  public LazyLoadedDexApplication read() throws IOException {
     return read((StringResource) null);
   }
 
-  public DexApplication read(StringResource proguardMap) throws IOException {
+  public LazyLoadedDexApplication read(StringResource proguardMap) throws IOException {
     ExecutorService executor = Executors.newSingleThreadExecutor();
     try {
       return read(proguardMap, executor);
@@ -87,20 +87,20 @@
     }
   }
 
-  public final DexApplication read(ExecutorService executorService) throws IOException {
+  public final LazyLoadedDexApplication read(ExecutorService executorService) throws IOException {
     return read(
         null, executorService, ProgramClassCollection.defaultConflictResolver(options.reporter));
   }
 
-  public final DexApplication read(StringResource proguardMap, ExecutorService executorService)
-      throws IOException {
+  public final LazyLoadedDexApplication read(
+      StringResource proguardMap, ExecutorService executorService) throws IOException {
     return read(
         proguardMap,
         executorService,
         ProgramClassCollection.defaultConflictResolver(options.reporter));
   }
 
-  public final DexApplication read(
+  public final LazyLoadedDexApplication read(
       StringResource proguardMap,
       ExecutorService executorService,
       ProgramClassConflictResolver resolver)
diff --git a/src/main/java/com/android/tools/r8/dex/CodeToKeep.java b/src/main/java/com/android/tools/r8/dex/CodeToKeep.java
index f327f21..9eb7caa 100644
--- a/src/main/java/com/android/tools/r8/dex/CodeToKeep.java
+++ b/src/main/java/com/android/tools/r8/dex/CodeToKeep.java
@@ -64,7 +64,14 @@
     }
 
     private boolean shouldKeep(DexType type) {
-      return namingLens.prefixRewrittenType(type) != null || potentialTypesToKeep.contains(type);
+      return namingLens.prefixRewrittenType(type) != null
+          || potentialTypesToKeep.contains(type)
+          // TODO(b/158632510): This should prefix match on DexString.
+          || type.toDescriptorString()
+              .startsWith(
+                  "L"
+                      + options.desugaredLibraryConfiguration
+                          .getSynthesizedLibraryClassesPackagePrefix());
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
index 9e96ab4..7127794 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
@@ -88,6 +88,11 @@
     return programClasses.get(type);
   }
 
+  public DexLibraryClass libraryDefintionFor(DexType type) {
+    assert type.isClassType() : "Cannot lookup library definition for type: " + type;
+    return libraryClasses.get(type);
+  }
+
   static class AllClasses {
 
     // Mapping of all types to their definitions.
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/DesugaredLibraryConversionWrapperAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/DesugaredLibraryConversionWrapperAnalysis.java
index 96b5de8..1fa68dd 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/DesugaredLibraryConversionWrapperAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/DesugaredLibraryConversionWrapperAnalysis.java
@@ -4,28 +4,23 @@
 package com.android.tools.r8.graph.analysis;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClasspathClass;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter.Mode;
-import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import java.util.ArrayList;
 import java.util.IdentityHashMap;
-import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 
 public class DesugaredLibraryConversionWrapperAnalysis extends EnqueuerAnalysis
     implements EnqueuerInvokeAnalysis {
 
   private final AppView<?> appView;
   private final DesugaredLibraryAPIConverter converter;
-  private Map<DexType, DexProgramClass> synthesizedWrappers = new IdentityHashMap<>();
+  private Map<DexType, DexClasspathClass> synthesizedWrappers = new IdentityHashMap<>();
 
   public DesugaredLibraryConversionWrapperAnalysis(AppView<?> appView) {
     this.appView = appView;
@@ -71,34 +66,7 @@
     return converter.generateCallbackMethods();
   }
 
-  public List<DexProgramClass> generateWrappers() {
-    return converter.synthesizeWrappers(synthesizedWrappers);
-  }
-
-  // Generate a mock classpath class for all vivified types.
-  // Types will be available at runtime in the desugared library dex file.
-  public List<DexClasspathClass> generateWrappersSuperTypeMock(List<DexProgramClass> wrappers) {
-    List<DexClasspathClass> classpathClasses = new ArrayList<>();
-    for (DexProgramClass wrapper : wrappers) {
-      boolean mockIsInterface = wrapper.interfaces.size() == 1;
-      DexType mockType = mockIsInterface ? wrapper.interfaces.values[0] : wrapper.superType;
-      if (appView.definitionFor(mockType) == null) {
-        assert DesugaredLibraryAPIConverter.isVivifiedType(mockType);
-        assert wrapper.instanceFields().size() == 1;
-        DexType typeToMock = wrapper.instanceFields().get(0).field.type;
-        DexClass classToMock = appView.definitionFor(typeToMock);
-        assert classToMock != null;
-        DexClasspathClass mockedSuperClass =
-            converter.synthesizeClasspathMock(classToMock, mockType, mockIsInterface);
-        classpathClasses.add(mockedSuperClass);
-        for (DexEncodedMethod virtualMethod : wrapper.virtualMethods()) {
-          // The mock is generated at the end of the enqueuing phase, so we need to manually set the
-          // library override.
-          assert mockedSuperClass.lookupVirtualMethod(virtualMethod.method) != null;
-          virtualMethod.setLibraryMethodOverride(OptionalBool.TRUE);
-        }
-      }
-    }
-    return classpathClasses;
+  public void generateWrappers(Consumer<DexClasspathClass> synthesizedCallback) {
+    converter.synthesizeWrappers(synthesizedWrappers, synthesizedCallback);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
index 88108f9..c0aea76 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
@@ -44,8 +44,8 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
 
-// TODO(b/134732760): In progress.
 // I convert library calls with desugared parameters/return values so they can work normally.
 // In the JSON of the desugared library, one can specify conversions between desugared and
 // non-desugared types. If no conversion is specified, D8/R8 simply generate wrapper classes around
@@ -286,7 +286,9 @@
     }
     SortedProgramMethodSet callbacks = generateCallbackMethods();
     irConverter.processMethodsConcurrently(callbacks, executorService);
-    wrapperSynthesizor.finalizeWrappersForD8(builder, irConverter, executorService);
+    if (appView.options().isDesugaredLibraryCompilation()) {
+      wrapperSynthesizor.finalizeWrappersForL8(builder, irConverter, executorService);
+    }
   }
 
   public SortedProgramMethodSet generateCallbackMethods() {
@@ -312,14 +314,10 @@
     return allCallbackMethods;
   }
 
-  public List<DexProgramClass> synthesizeWrappers(
-      Map<DexType, DexProgramClass> synthesizedWrappers) {
-    return wrapperSynthesizor.synthesizeWrappers(synthesizedWrappers);
-  }
-
-  public DexClasspathClass synthesizeClasspathMock(
-      DexClass classToMock, DexType mockType, boolean mockIsInterface) {
-    return wrapperSynthesizor.synthesizeClasspathMock(classToMock, mockType, mockIsInterface);
+  public void synthesizeWrappers(
+      Map<DexType, DexClasspathClass> synthesizedWrappers,
+      Consumer<DexClasspathClass> synthesizedCallback) {
+    wrapperSynthesizor.synthesizeWrappersForClasspath(synthesizedWrappers, synthesizedCallback);
   }
 
   private ProgramMethod generateCallbackMethod(
@@ -351,20 +349,22 @@
 
   public void reportInvalidInvoke(DexType type, DexMethod invokedMethod, String debugString) {
     DexType desugaredType = appView.rewritePrefix.rewrittenType(type, appView);
-    appView
-        .options()
-        .reporter
-        .info(
-            new StringDiagnostic(
-                "Invoke to "
-                    + invokedMethod.holder
-                    + "#"
-                    + invokedMethod.name
-                    + " may not work correctly at runtime (Cannot convert "
-                    + debugString
-                    + "type "
-                    + desugaredType
-                    + ")."));
+    StringDiagnostic diagnostic =
+        new StringDiagnostic(
+            "Invoke to "
+                + invokedMethod.holder
+                + "#"
+                + invokedMethod.name
+                + " may not work correctly at runtime (Cannot convert "
+                + debugString
+                + "type "
+                + desugaredType
+                + ").");
+    if (appView.options().isDesugaredLibraryCompilation()) {
+      throw appView.options().reporter.fatalError(diagnostic);
+    } else {
+      appView.options().reporter.info(diagnostic);
+    }
   }
 
   public static DexType vivifiedTypeFor(DexType type, AppView<?> appView) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java
index 1590484..5414791 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java
@@ -11,24 +11,32 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.desugar.PrefixRewritingMapper.DesugarPrefixRewritingMapper;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 public class DesugaredLibraryConfiguration {
 
   public static final String FALL_BACK_SYNTHESIZED_CLASSES_PACKAGE_PREFIX = "j$/";
 
-  // TODO(b/134732760): should use DexString, DexType, DexMethod or so on when possible.
+  // TODO(b/158632510): should use DexString, DexType, DexMethod or so on when possible.
   private final AndroidApiLevel requiredCompilationAPILevel;
   private final boolean libraryCompilation;
   private final String synthesizedLibraryClassesPackagePrefix;
@@ -40,9 +48,10 @@
   private final Map<DexType, DexType> customConversions;
   private final List<Pair<DexType, DexString>> dontRewriteInvocation;
   private final List<String> extraKeepRules;
+  private final Set<DexType> wrapperConversions;
 
-  public static Builder builder(DexItemFactory dexItemFactory) {
-    return new Builder(dexItemFactory);
+  public static Builder builder(DexItemFactory dexItemFactory, Reporter reporter, Origin origin) {
+    return new Builder(dexItemFactory, reporter, origin);
   }
 
   public static DesugaredLibraryConfiguration withOnlyRewritePrefixForTesting(
@@ -57,6 +66,7 @@
         ImmutableMap.of(),
         ImmutableMap.of(),
         ImmutableMap.of(),
+        ImmutableSet.of(),
         ImmutableList.of(),
         ImmutableList.of());
   }
@@ -72,11 +82,12 @@
         ImmutableMap.of(),
         ImmutableMap.of(),
         ImmutableMap.of(),
+        ImmutableSet.of(),
         ImmutableList.of(),
         ImmutableList.of());
   }
 
-  public DesugaredLibraryConfiguration(
+  private DesugaredLibraryConfiguration(
       AndroidApiLevel requiredCompilationAPILevel,
       boolean libraryCompilation,
       String packagePrefix,
@@ -86,6 +97,7 @@
       Map<DexString, Map<DexType, DexType>> retargetCoreLibMember,
       Map<DexType, DexType> backportCoreLibraryMember,
       Map<DexType, DexType> customConversions,
+      Set<DexType> wrapperConversions,
       List<Pair<DexType, DexString>> dontRewriteInvocation,
       List<String> extraKeepRules) {
     this.requiredCompilationAPILevel = requiredCompilationAPILevel;
@@ -97,6 +109,7 @@
     this.retargetCoreLibMember = retargetCoreLibMember;
     this.backportCoreLibraryMember = backportCoreLibraryMember;
     this.customConversions = customConversions;
+    this.wrapperConversions = wrapperConversions;
     this.dontRewriteInvocation = dontRewriteInvocation;
     this.extraKeepRules = extraKeepRules;
   }
@@ -157,6 +170,10 @@
     return customConversions;
   }
 
+  public Set<DexType> getWrapperConversions() {
+    return wrapperConversions;
+  }
+
   public List<Pair<DexType, DexString>> getDontRewriteInvocation() {
     return dontRewriteInvocation;
   }
@@ -168,6 +185,8 @@
   public static class Builder {
 
     private final DexItemFactory factory;
+    private final Reporter reporter;
+    private final Origin origin;
 
     private AndroidApiLevel requiredCompilationAPILevel;
     private boolean libraryCompilation = false;
@@ -175,15 +194,34 @@
         FALL_BACK_SYNTHESIZED_CLASSES_PACKAGE_PREFIX;
     private String identifier;
     private Map<String, String> rewritePrefix = new HashMap<>();
-    private Map<DexType, DexType> emulateLibraryInterface = new HashMap<>();
+    private Map<DexType, DexType> emulateLibraryInterface = new IdentityHashMap<>();
     private Map<DexString, Map<DexType, DexType>> retargetCoreLibMember = new IdentityHashMap<>();
-    private Map<DexType, DexType> backportCoreLibraryMember = new HashMap<>();
-    private Map<DexType, DexType> customConversions = new HashMap<>();
+    private Map<DexType, DexType> backportCoreLibraryMember = new IdentityHashMap<>();
+    private Map<DexType, DexType> customConversions = new IdentityHashMap<>();
+    private Set<DexType> wrapperConversions = Sets.newIdentityHashSet();
     private List<Pair<DexType, DexString>> dontRewriteInvocation = new ArrayList<>();
     private List<String> extraKeepRules = Collections.emptyList();
 
-    public Builder(DexItemFactory dexItemFactory) {
+    private Builder(DexItemFactory dexItemFactory, Reporter reporter, Origin origin) {
       this.factory = dexItemFactory;
+      this.reporter = reporter;
+      this.origin = origin;
+    }
+
+    // Utility to set values. Currently assumes the key is fresh.
+    private <K, V> void put(Map<K, V> map, K key, V value, String desc) {
+      if (map.containsKey(key)) {
+        throw reporter.fatalError(
+            new StringDiagnostic(
+                "Invalid desugared library configuration. "
+                    + " Duplicate assignment of key: '"
+                    + key
+                    + "' in sections for '"
+                    + desc
+                    + "'",
+                origin));
+      }
+      map.put(key, value);
     }
 
     public Builder setSynthesizedLibraryClassesPackagePrefix(String prefix) {
@@ -217,7 +255,11 @@
     }
 
     public Builder putRewritePrefix(String prefix, String rewrittenPrefix) {
-      rewritePrefix.put(prefix, rewrittenPrefix);
+      put(
+          rewritePrefix,
+          prefix,
+          rewrittenPrefix,
+          DesugaredLibraryConfigurationParser.REWRITE_PREFIX_KEY);
       return this;
     }
 
@@ -225,14 +267,28 @@
         String emulateLibraryItf, String rewrittenEmulateLibraryItf) {
       DexType interfaceType = stringClassToDexType(emulateLibraryItf);
       DexType rewrittenType = stringClassToDexType(rewrittenEmulateLibraryItf);
-      emulateLibraryInterface.put(interfaceType, rewrittenType);
+      put(
+          emulateLibraryInterface,
+          interfaceType,
+          rewrittenType,
+          DesugaredLibraryConfigurationParser.EMULATE_INTERFACE_KEY);
       return this;
     }
 
     public Builder putCustomConversion(String type, String conversionHolder) {
       DexType dexType = stringClassToDexType(type);
       DexType conversionType = stringClassToDexType(conversionHolder);
-      customConversions.put(dexType, conversionType);
+      put(
+          customConversions,
+          dexType,
+          conversionType,
+          DesugaredLibraryConfigurationParser.CUSTOM_CONVERSION_KEY);
+      return this;
+    }
+
+    public Builder addWrapperConversion(String type) {
+      DexType dexType = stringClassToDexType(type);
+      wrapperConversions.add(dexType);
       return this;
     }
 
@@ -244,14 +300,22 @@
       DexType originalType = stringClassToDexType(retarget.substring(0, index));
       DexType finalType = stringClassToDexType(rewrittenRetarget);
       assert !typeMap.containsKey(originalType);
-      typeMap.put(originalType, finalType);
+      put(
+          typeMap,
+          originalType,
+          finalType,
+          DesugaredLibraryConfigurationParser.RETARGET_LIB_MEMBER_KEY);
       return this;
     }
 
     public Builder putBackportCoreLibraryMember(String backport, String rewrittenBackport) {
       DexType backportType = stringClassToDexType(backport);
       DexType rewrittenBackportType = stringClassToDexType(rewrittenBackport);
-      backportCoreLibraryMember.put(backportType, rewrittenBackportType);
+      put(
+          backportCoreLibraryMember,
+          backportType,
+          rewrittenBackportType,
+          DesugaredLibraryConfigurationParser.BACKPORT_KEY);
       return this;
     }
 
@@ -278,6 +342,7 @@
     }
 
     public DesugaredLibraryConfiguration build() {
+      validate();
       return new DesugaredLibraryConfiguration(
           requiredCompilationAPILevel,
           libraryCompilation,
@@ -288,8 +353,22 @@
           ImmutableMap.copyOf(retargetCoreLibMember),
           ImmutableMap.copyOf(backportCoreLibraryMember),
           ImmutableMap.copyOf(customConversions),
+          ImmutableSet.copyOf(wrapperConversions),
           ImmutableList.copyOf(dontRewriteInvocation),
           ImmutableList.copyOf(extraKeepRules));
     }
+
+    private void validate() {
+      SetView<DexType> dups = Sets.intersection(customConversions.keySet(), wrapperConversions);
+      if (!dups.isEmpty()) {
+        throw reporter.fatalError(
+            new StringDiagnostic(
+                "Invalid desugared library configuration. "
+                    + "Duplicate types in custom conversions and wrapper conversions: "
+                    + String.join(
+                        ", ", dups.stream().map(DexType::toString).collect(Collectors.toSet())),
+                origin));
+      }
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfigurationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfigurationParser.java
index a659cc6..d687d19 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfigurationParser.java
@@ -6,8 +6,11 @@
 
 import com.android.tools.r8.StringResource;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.SemanticVersion;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
@@ -19,142 +22,184 @@
 
 public class DesugaredLibraryConfigurationParser {
 
-  private static final int MAX_SUPPORTED_VERSION = 4;
+  public static final int MAX_SUPPORTED_VERSION = 4;
+  public static final SemanticVersion MIN_SUPPORTED_VERSION = new SemanticVersion(1, 0, 9);
 
-  private final DesugaredLibraryConfiguration.Builder configurationBuilder;
+  static final String CONFIGURATION_FORMAT_VERSION_KEY = "configuration_format_version";
+  static final String VERSION_KEY = "version";
+  static final String GROUP_ID_KEY = "group_id";
+  static final String ARTIFACT_ID_KEY = "artifact_id";
+  static final String REQUIRED_COMPILATION_API_LEVEL_KEY = "required_compilation_api_level";
+  static final String SYNTHESIZED_LIBRARY_CLASSES_PACKAGE_PREFIX_KEY =
+      "synthesized_library_classes_package_prefix";
+
+  static final String COMMON_FLAGS_KEY = "common_flags";
+  static final String LIBRARY_FLAGS_KEY = "library_flags";
+  static final String PROGRAM_FLAGS_KEY = "program_flags";
+
+  static final String API_LEVEL_BELOW_OR_EQUAL_KEY = "api_level_below_or_equal";
+  static final String WRAPPER_CONVERSION_KEY = "wrapper_conversion";
+  static final String CUSTOM_CONVERSION_KEY = "custom_conversion";
+  static final String REWRITE_PREFIX_KEY = "rewrite_prefix";
+  static final String RETARGET_LIB_MEMBER_KEY = "retarget_lib_member";
+  static final String EMULATE_INTERFACE_KEY = "emulate_interface";
+  static final String DONT_REWRITE_KEY = "dont_rewrite";
+  static final String BACKPORT_KEY = "backport";
+  static final String SHRINKER_CONFIG_KEY = "shrinker_config";
+
+  private final DexItemFactory dexItemFactory;
   private final Reporter reporter;
   private final boolean libraryCompilation;
   private final int minAPILevel;
 
+  private DesugaredLibraryConfiguration.Builder configurationBuilder = null;
+  private Origin origin;
+
   public DesugaredLibraryConfigurationParser(
       DexItemFactory dexItemFactory,
       Reporter reporter,
       boolean libraryCompilation,
       int minAPILevel) {
+    this.dexItemFactory = dexItemFactory;
     this.reporter = reporter;
-    this.configurationBuilder = DesugaredLibraryConfiguration.builder(dexItemFactory);
     this.minAPILevel = minAPILevel;
     this.libraryCompilation = libraryCompilation;
+  }
+
+  private JsonElement required(JsonObject json, String key) {
+    if (!json.has(key)) {
+      throw reporter.fatalError(
+          new StringDiagnostic(
+              "Invalid desugared library configuration. Expected required key '" + key + "'",
+              origin));
+    }
+    return json.get(key);
+  }
+
+  public DesugaredLibraryConfiguration parse(StringResource stringResource) {
+    origin = stringResource.getOrigin();
+    assert origin != null;
+    configurationBuilder = DesugaredLibraryConfiguration.builder(dexItemFactory, reporter, origin);
     if (libraryCompilation) {
       configurationBuilder.setLibraryCompilation();
     } else {
       configurationBuilder.setProgramCompilation();
     }
-  }
-
-  public DesugaredLibraryConfiguration parse(StringResource stringResource) {
-    String jsonConfigString;
+    JsonObject jsonConfig;
     try {
-      jsonConfigString = stringResource.getString();
+      String jsonConfigString = stringResource.getString();
+      JsonParser parser = new JsonParser();
+      jsonConfig = parser.parse(jsonConfigString).getAsJsonObject();
     } catch (Exception e) {
-      throw reporter.fatalError(
-          new StringDiagnostic("Cannot access desugared library configuration."));
+      throw reporter.fatalError(new ExceptionDiagnostic(e, origin));
     }
-    JsonParser parser = new JsonParser();
-    JsonObject jsonConfig = parser.parse(jsonConfigString).getAsJsonObject();
 
-    int formatVersion = jsonConfig.get("configuration_format_version").getAsInt();
+    JsonElement formatVersionElement = required(jsonConfig, CONFIGURATION_FORMAT_VERSION_KEY);
+    int formatVersion = formatVersionElement.getAsInt();
     if (formatVersion > MAX_SUPPORTED_VERSION) {
       throw reporter.fatalError(
           new StringDiagnostic(
               "Unsupported desugared library configuration version, please upgrade the D8/R8"
-                  + " compiler."));
-    }
-    if (formatVersion == 1) {
-      reporter.warning(
-          new StringDiagnostic(
-              "You are using an experimental version of the desugared library configuration, "
-                  + "distributed only in the early canary versions. Please update for "
-                  + "production releases and to fix this warning."));
+                  + " compiler.",
+              origin));
     }
 
-    String version = jsonConfig.get("version").getAsString();
-    String groupID;
-    String artifactID;
-    if (formatVersion < 4) {
-      groupID = "com.tools.android";
-      artifactID = "desugar_jdk_libs";
-    } else {
-      groupID = jsonConfig.get("group_id").getAsString();
-      artifactID = jsonConfig.get("artifact_id").getAsString();
+    String version = required(jsonConfig, VERSION_KEY).getAsString();
+    SemanticVersion semanticVersion = SemanticVersion.parse(version);
+    if (!semanticVersion.isNewerOrEqual(MIN_SUPPORTED_VERSION)) {
+      throw reporter.fatalError(
+          new StringDiagnostic(
+              "Unsupported desugared library version: "
+                  + version
+                  + ", please upgrade the desugared library to at least version "
+                  + MIN_SUPPORTED_VERSION
+                  + ".",
+              origin));
     }
+
+    String groupID = required(jsonConfig, GROUP_ID_KEY).getAsString();
+    String artifactID = required(jsonConfig, ARTIFACT_ID_KEY).getAsString();
     String identifier = String.join(":", groupID, artifactID, version);
     configurationBuilder.setDesugaredLibraryIdentifier(identifier);
+    configurationBuilder.setSynthesizedLibraryClassesPackagePrefix(
+        required(jsonConfig, SYNTHESIZED_LIBRARY_CLASSES_PACKAGE_PREFIX_KEY).getAsString());
 
-    if (jsonConfig.has("synthesized_library_classes_package_prefix")) {
-      configurationBuilder.setSynthesizedLibraryClassesPackagePrefix(
-          jsonConfig.get("synthesized_library_classes_package_prefix").getAsString());
-    } else {
-      reporter.warning(
-          new StringDiagnostic(
-              "Missing package_prefix, falling back to "
-                  + DesugaredLibraryConfiguration.FALL_BACK_SYNTHESIZED_CLASSES_PACKAGE_PREFIX
-                  + " prefix, update desugared library configuration."));
-    }
     int required_compilation_api_level =
-        jsonConfig.get("required_compilation_api_level").getAsInt();
+        required(jsonConfig, REQUIRED_COMPILATION_API_LEVEL_KEY).getAsInt();
     configurationBuilder.setRequiredCompilationAPILevel(
         AndroidApiLevel.getAndroidApiLevel(required_compilation_api_level));
-    JsonArray jsonFlags =
-        libraryCompilation
-            ? jsonConfig.getAsJsonArray("library_flags")
-            : jsonConfig.getAsJsonArray("program_flags");
-    for (JsonElement jsonFlagSet : jsonFlags) {
-      int api_level_below_or_equal =
-          jsonFlagSet.getAsJsonObject().get("api_level_below_or_equal").getAsInt();
-      if (minAPILevel > api_level_below_or_equal) {
-        continue;
-      }
-      parseFlags(jsonFlagSet.getAsJsonObject());
-    }
-    if (jsonConfig.has("shrinker_config")) {
-      JsonArray jsonKeepRules = jsonConfig.get("shrinker_config").getAsJsonArray();
+    JsonElement commonFlags = required(jsonConfig, COMMON_FLAGS_KEY);
+    JsonElement libraryFlags = required(jsonConfig, LIBRARY_FLAGS_KEY);
+    JsonElement programFlags = required(jsonConfig, PROGRAM_FLAGS_KEY);
+    parseFlagsList(commonFlags.getAsJsonArray());
+    parseFlagsList(
+        libraryCompilation ? libraryFlags.getAsJsonArray() : programFlags.getAsJsonArray());
+    if (jsonConfig.has(SHRINKER_CONFIG_KEY)) {
+      JsonArray jsonKeepRules = jsonConfig.get(SHRINKER_CONFIG_KEY).getAsJsonArray();
       List<String> extraKeepRules = new ArrayList<>(jsonKeepRules.size());
       for (JsonElement keepRule : jsonKeepRules) {
         extraKeepRules.add(keepRule.getAsString());
       }
       configurationBuilder.setExtraKeepRules(extraKeepRules);
     }
-    return configurationBuilder.build();
+    DesugaredLibraryConfiguration config = configurationBuilder.build();
+    configurationBuilder = null;
+    origin = null;
+    return config;
+  }
+
+  private void parseFlagsList(JsonArray jsonFlags) {
+    for (JsonElement jsonFlagSet : jsonFlags) {
+      JsonObject flag = jsonFlagSet.getAsJsonObject();
+      int api_level_below_or_equal = required(flag, API_LEVEL_BELOW_OR_EQUAL_KEY).getAsInt();
+      if (minAPILevel <= api_level_below_or_equal) {
+        parseFlags(flag);
+      }
+    }
   }
 
   private void parseFlags(JsonObject jsonFlagSet) {
-    if (jsonFlagSet.has("rewrite_prefix")) {
+    if (jsonFlagSet.has(REWRITE_PREFIX_KEY)) {
       for (Map.Entry<String, JsonElement> rewritePrefix :
-          jsonFlagSet.get("rewrite_prefix").getAsJsonObject().entrySet()) {
+          jsonFlagSet.get(REWRITE_PREFIX_KEY).getAsJsonObject().entrySet()) {
         configurationBuilder.putRewritePrefix(
             rewritePrefix.getKey(), rewritePrefix.getValue().getAsString());
       }
     }
-    if (jsonFlagSet.has("retarget_lib_member")) {
+    if (jsonFlagSet.has(RETARGET_LIB_MEMBER_KEY)) {
       for (Map.Entry<String, JsonElement> retarget :
-          jsonFlagSet.get("retarget_lib_member").getAsJsonObject().entrySet()) {
+          jsonFlagSet.get(RETARGET_LIB_MEMBER_KEY).getAsJsonObject().entrySet()) {
         configurationBuilder.putRetargetCoreLibMember(
             retarget.getKey(), retarget.getValue().getAsString());
       }
     }
-    if (jsonFlagSet.has("backport")) {
+    if (jsonFlagSet.has(BACKPORT_KEY)) {
       for (Map.Entry<String, JsonElement> backport :
-          jsonFlagSet.get("backport").getAsJsonObject().entrySet()) {
+          jsonFlagSet.get(BACKPORT_KEY).getAsJsonObject().entrySet()) {
         configurationBuilder.putBackportCoreLibraryMember(
             backport.getKey(), backport.getValue().getAsString());
       }
     }
-    if (jsonFlagSet.has("emulate_interface")) {
+    if (jsonFlagSet.has(EMULATE_INTERFACE_KEY)) {
       for (Map.Entry<String, JsonElement> itf :
-          jsonFlagSet.get("emulate_interface").getAsJsonObject().entrySet()) {
+          jsonFlagSet.get(EMULATE_INTERFACE_KEY).getAsJsonObject().entrySet()) {
         configurationBuilder.putEmulateLibraryInterface(itf.getKey(), itf.getValue().getAsString());
       }
     }
-    if (jsonFlagSet.has("custom_conversion")) {
+    if (jsonFlagSet.has(CUSTOM_CONVERSION_KEY)) {
       for (Map.Entry<String, JsonElement> conversion :
-          jsonFlagSet.get("custom_conversion").getAsJsonObject().entrySet()) {
+          jsonFlagSet.get(CUSTOM_CONVERSION_KEY).getAsJsonObject().entrySet()) {
         configurationBuilder.putCustomConversion(
             conversion.getKey(), conversion.getValue().getAsString());
       }
     }
-    if (jsonFlagSet.has("dont_rewrite")) {
-      JsonArray dontRewrite = jsonFlagSet.get("dont_rewrite").getAsJsonArray();
+    if (jsonFlagSet.has(WRAPPER_CONVERSION_KEY)) {
+      for (JsonElement wrapper : jsonFlagSet.get(WRAPPER_CONVERSION_KEY).getAsJsonArray()) {
+        configurationBuilder.addWrapperConversion(wrapper.getAsString());
+      }
+    }
+    if (jsonFlagSet.has(DONT_REWRITE_KEY)) {
+      JsonArray dontRewrite = jsonFlagSet.get(DONT_REWRITE_KEY).getAsJsonArray();
       for (JsonElement rewrite : dontRewrite) {
         configurationBuilder.addDontRewriteInvocation(rewrite.getAsString());
       }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
index 5fbf3df..23a6465 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexApplication;
@@ -33,7 +34,6 @@
 import com.android.tools.r8.ir.synthetic.DesugaredLibraryAPIConversionCfCodeProvider.APIConverterWrapperCfCodeProvider;
 import com.android.tools.r8.ir.synthetic.DesugaredLibraryAPIConversionCfCodeProvider.APIConverterWrapperConversionCfCodeProvider;
 import com.android.tools.r8.origin.SynthesizedOrigin;
-import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
@@ -48,6 +48,8 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 // I am responsible for the generation of wrappers used to call library APIs when desugaring
 // libraries. Wrappers can be both ways, wrapping the desugarType as a type, or the type as
@@ -110,15 +112,19 @@
   private final Set<DexType> invalidWrappers = Sets.newConcurrentHashSet();
   private final DexItemFactory factory;
   private final DesugaredLibraryAPIConverter converter;
-  private final DexString vivifiedSourceFile;
 
   DesugaredLibraryWrapperSynthesizer(AppView<?> appView, DesugaredLibraryAPIConverter converter) {
     this.appView = appView;
     this.factory = appView.dexItemFactory();
-    dexWrapperPrefixString = "L" + appView.options().synthesizedClassPrefix + WRAPPER_PREFIX;
+    dexWrapperPrefixString =
+        "L"
+            + appView
+                .options()
+                .desugaredLibraryConfiguration
+                .getSynthesizedLibraryClassesPackagePrefix()
+            + WRAPPER_PREFIX;
     dexWrapperPrefixDexString = factory.createString(dexWrapperPrefixString);
     this.converter = converter;
-    this.vivifiedSourceFile = appView.dexItemFactory().createString("vivified");
   }
 
   boolean hasSynthesized(DexType type) {
@@ -126,11 +132,7 @@
   }
 
   boolean canGenerateWrapper(DexType type) {
-    DexClass dexClass = appView.definitionFor(type);
-    if (dexClass == null || dexClass.accessFlags.isFinal()) {
-      return false;
-    }
-    return dexClass.isLibraryClass() || appView.options().isDesugaredLibraryCompilation();
+    return appView.options().desugaredLibraryConfiguration.getWrapperConversions().contains(type);
   }
 
   DexType getTypeWrapper(DexType type) {
@@ -155,9 +157,10 @@
     return wrappers.computeIfAbsent(
         type,
         t -> {
+          assert canGenerateWrapper(type) : type;
           DexType wrapperType = createWrapperType(type, suffix);
           assert converter.canGenerateWrappersAndCallbacks()
-                  || appView.definitionForProgramType(wrapperType) != null
+                  || appView.definitionFor(wrapperType).isClasspathClass()
               : "Wrapper " + wrapperType + " should have been generated in the enqueuer.";
           return wrapperType;
         });
@@ -176,10 +179,12 @@
     return DesugaredLibraryAPIConverter.vivifiedTypeFor(type, appView);
   }
 
-  private DexProgramClass generateTypeWrapper(DexClass dexClass, DexType typeWrapperType) {
+  private DexClass generateTypeWrapper(
+      ClassKind classKind, DexClass dexClass, DexType typeWrapperType) {
     DexType type = dexClass.type;
     DexEncodedField wrapperField = synthesizeWrappedValueEncodedField(typeWrapperType, type);
     return synthesizeWrapper(
+        classKind,
         vivifiedTypeFor(type),
         dexClass,
         synthesizeVirtualMethodsForTypeWrapper(dexClass, wrapperField),
@@ -187,12 +192,13 @@
         wrapperField);
   }
 
-  private DexProgramClass generateVivifiedTypeWrapper(
-      DexClass dexClass, DexType vivifiedTypeWrapperType) {
+  private DexClass generateVivifiedTypeWrapper(
+      ClassKind classKind, DexClass dexClass, DexType vivifiedTypeWrapperType) {
     DexType type = dexClass.type;
     DexEncodedField wrapperField =
         synthesizeWrappedValueEncodedField(vivifiedTypeWrapperType, vivifiedTypeFor(type));
     return synthesizeWrapper(
+        classKind,
         type,
         dexClass,
         synthesizeVirtualMethodsForVivifiedTypeWrapper(dexClass, wrapperField),
@@ -200,7 +206,8 @@
         wrapperField);
   }
 
-  private DexProgramClass synthesizeWrapper(
+  private DexClass synthesizeWrapper(
+      ClassKind classKind,
       DexType wrappingType,
       DexClass clazz,
       DexEncodedMethod[] virtualMethods,
@@ -210,9 +217,9 @@
     DexType superType = isItf ? factory.objectType : wrappingType;
     DexTypeList interfaces =
         isItf ? new DexTypeList(new DexType[] {wrappingType}) : DexTypeList.empty();
-    return new DexProgramClass(
+    return classKind.create(
         wrapperField.holder(),
-        null,
+        Kind.CF,
         new SynthesizedOrigin("Desugared library API Converter", getClass()),
         ClassAccessFlags.fromSharedAccessFlags(
             Constants.ACC_FINAL | Constants.ACC_SYNTHETIC | Constants.ACC_PUBLIC),
@@ -229,8 +236,7 @@
         new DexEncodedMethod[] {synthesizeConstructor(wrapperField.field), conversionMethod},
         virtualMethods,
         factory.getSkipNameValidationForTesting(),
-        DexProgramClass::checksumFromType,
-        Collections.emptyList());
+        DexProgramClass::checksumFromType);
   }
 
   private DexEncodedMethod[] synthesizeVirtualMethodsForVivifiedTypeWrapper(
@@ -325,51 +331,6 @@
     return finalizeWrapperMethods(generatedMethods, finalMethods);
   }
 
-  private DexEncodedMethod[] synthesizeVirtualMethodsForClasspathMock(
-      DexClass dexClass, DexType mockType) {
-    List<DexEncodedMethod> dexMethods = allImplementedMethods(dexClass);
-    List<DexEncodedMethod> generatedMethods = new ArrayList<>();
-    // Generate only abstract methods for library override detection.
-    for (DexEncodedMethod dexEncodedMethod : dexMethods) {
-      DexClass holderClass = appView.definitionFor(dexEncodedMethod.holder());
-      assert holderClass != null || appView.options().isDesugaredLibraryCompilation();
-      if (!dexEncodedMethod.isFinal()) {
-        DexMethod methodToInstall =
-            DesugaredLibraryAPIConverter.methodWithVivifiedTypeInSignature(
-                dexEncodedMethod.method, mockType, appView);
-        DexEncodedMethod newDexEncodedMethod =
-            newSynthesizedMethod(methodToInstall, dexEncodedMethod, null);
-        generatedMethods.add(newDexEncodedMethod);
-      }
-    }
-    return generatedMethods.toArray(DexEncodedMethod.EMPTY_ARRAY);
-  }
-
-  DexClasspathClass synthesizeClasspathMock(
-      DexClass classToMock, DexType mockType, boolean mockIsInterface) {
-    return new DexClasspathClass(
-        mockType,
-        Kind.CF,
-        new SynthesizedOrigin("Desugared library wrapper super class ", getClass()),
-        ClassAccessFlags.fromDexAccessFlags(
-            Constants.ACC_SYNTHETIC
-                | Constants.ACC_PUBLIC
-                | (BooleanUtils.intValue(mockIsInterface) * Constants.ACC_INTERFACE)),
-        appView.dexItemFactory().objectType,
-        DexTypeList.empty(),
-        vivifiedSourceFile,
-        null,
-        Collections.emptyList(),
-        null,
-        Collections.emptyList(),
-        DexAnnotationSet.empty(),
-        DexEncodedField.EMPTY_ARRAY,
-        DexEncodedField.EMPTY_ARRAY,
-        DexEncodedMethod.EMPTY_ARRAY,
-        synthesizeVirtualMethodsForClasspathMock(classToMock, mockType),
-        appView.dexItemFactory().getSkipNameValidationForTesting());
-  }
-
   private DexEncodedMethod[] finalizeWrapperMethods(
       List<DexEncodedMethod> generatedMethods, Set<DexMethod> finalMethods) {
     if (finalMethods.isEmpty()) {
@@ -492,36 +453,69 @@
         true);
   }
 
-  void finalizeWrappersForD8(
+  void finalizeWrappersForL8(
       DexApplication.Builder<?> builder, IRConverter irConverter, ExecutorService executorService)
       throws ExecutionException {
-    List<DexProgramClass> synthesizedWrappers = synthesizeWrappers(new IdentityHashMap<>());
+    List<DexProgramClass> synthesizedWrappers = synthesizeWrappers();
     registerAndProcessWrappers(builder, irConverter, executorService, synthesizedWrappers);
   }
 
-  List<DexProgramClass> synthesizeWrappers(Map<DexType, DexProgramClass> synthesizedWrappers) {
+  private List<DexProgramClass> synthesizeWrappers() {
+    DesugaredLibraryConfiguration conf = appView.options().desugaredLibraryConfiguration;
+    for (DexType type : conf.getWrapperConversions()) {
+      assert !conf.getCustomConversions().containsKey(type);
+      getTypeWrapper(type);
+    }
+    Map<DexType, DexClass> synthesizedWrappers = new IdentityHashMap<>();
     List<DexProgramClass> additions = new ArrayList<>();
-    // Generating a wrapper may require other wrappers to be generated, iterate until fix point.
-    while (synthesizedWrappers.size() != typeWrappers.size() + vivifiedTypeWrappers.size()) {
+    int total = typeWrappers.size() + vivifiedTypeWrappers.size();
+    generateWrappers(
+        ClassKind.PROGRAM,
+        synthesizedWrappers.keySet(),
+        (type, wrapper) -> {
+          synthesizedWrappers.put(type, wrapper);
+          additions.add(wrapper.asProgramClass());
+        });
+    assert total == typeWrappers.size() + vivifiedTypeWrappers.size() : "unexpected additions";
+    return additions;
+  }
+
+  void synthesizeWrappersForClasspath(
+      Map<DexType, DexClasspathClass> synthesizedWrappers,
+      Consumer<DexClasspathClass> synthesizedCallback) {
+    generateWrappers(
+        ClassKind.CLASSPATH,
+        synthesizedWrappers.keySet(),
+        (type, wrapper) -> {
+          DexClasspathClass classpathWrapper = wrapper.asClasspathClass();
+          synthesizedWrappers.put(type, classpathWrapper);
+          synthesizedCallback.accept(classpathWrapper);
+        });
+  }
+
+  private void generateWrappers(
+      ClassKind classKind,
+      Set<DexType> synthesized,
+      BiConsumer<DexType, DexClass> generatedCallback) {
+    while (synthesized.size() != typeWrappers.size() + vivifiedTypeWrappers.size()) {
       for (DexType type : typeWrappers.keySet()) {
         DexType typeWrapperType = typeWrappers.get(type);
-        if (!synthesizedWrappers.containsKey(typeWrapperType)) {
-          DexProgramClass wrapper = generateTypeWrapper(getValidClassToWrap(type), typeWrapperType);
-          synthesizedWrappers.put(typeWrapperType, wrapper);
-          additions.add(wrapper);
+        if (!synthesized.contains(typeWrapperType)) {
+          DexClass wrapper =
+              generateTypeWrapper(classKind, getValidClassToWrap(type), typeWrapperType);
+          generatedCallback.accept(typeWrapperType, wrapper);
         }
       }
       for (DexType type : vivifiedTypeWrappers.keySet()) {
         DexType vivifiedTypeWrapperType = vivifiedTypeWrappers.get(type);
-        if (!synthesizedWrappers.containsKey(vivifiedTypeWrapperType)) {
-          DexProgramClass wrapper =
-              generateVivifiedTypeWrapper(getValidClassToWrap(type), vivifiedTypeWrapperType);
-          synthesizedWrappers.put(vivifiedTypeWrapperType, wrapper);
-          additions.add(wrapper);
+        if (!synthesized.contains(vivifiedTypeWrapperType)) {
+          DexClass wrapper =
+              generateVivifiedTypeWrapper(
+                  classKind, getValidClassToWrap(type), vivifiedTypeWrapperType);
+          generatedCallback.accept(vivifiedTypeWrapperType, wrapper);
         }
       }
     }
-    return additions;
   }
 
   private void registerAndProcessWrappers(
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 2bdfa26..ff06397 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -2687,7 +2687,10 @@
     Set<DexType> mainDexTypes = Sets.newIdentityHashSet();
 
     boolean isEmpty() {
-      boolean empty = syntheticInstantiations.isEmpty() && liveMethods.isEmpty();
+      boolean empty =
+          syntheticInstantiations.isEmpty()
+              && liveMethods.isEmpty()
+              && syntheticClasspathClasses.isEmpty();
       assert !empty || (pinnedMethods.isEmpty() && mainDexTypes.isEmpty());
       return empty;
     }
@@ -2747,6 +2750,7 @@
         enqueuer.markMethodAsTargeted(liveMethod, fakeReason);
         enqueuer.enqueueMarkMethodLiveAction(liveMethod, fakeReason);
       }
+      enqueuer.liveNonProgramTypes.addAll(syntheticClasspathClasses.values());
     }
   }
 
@@ -3059,21 +3063,8 @@
     ProgramMethodSet callbacks = desugaredLibraryWrapperAnalysis.generateCallbackMethods();
     callbacks.forEach(additions::addLiveMethod);
 
-    // Generate the wrappers.
-    List<DexProgramClass> wrappers = desugaredLibraryWrapperAnalysis.generateWrappers();
-    for (DexProgramClass wrapper : wrappers) {
-      additions.addInstantiatedClass(wrapper, null, false);
-      // Mark all methods on the wrapper as live and targeted.
-      for (DexEncodedMethod method : wrapper.methods()) {
-        additions.addLiveMethod(new ProgramMethod(wrapper, method));
-      }
-    }
-
-    // Add all vivified types as classpath classes.
-    // They will be available at runtime in the desugared library dex file.
-    desugaredLibraryWrapperAnalysis
-        .generateWrappersSuperTypeMock(wrappers)
-        .forEach(additions::addClasspathClass);
+    // Generate wrappers on classpath so types are defined.
+    desugaredLibraryWrapperAnalysis.generateWrappers(additions::addClasspathClass);
   }
 
   private void rewriteLambdaCallSites(
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 00853e0..8599b02 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -419,7 +419,8 @@
       if (outputMode == OutputMode.DexIndexed) {
         DexIndexedConsumer.ArchiveConsumer.writeResources(
             archive, getDexProgramResourcesForTesting(), getDataEntryResourcesForTesting());
-      } else if (outputMode == OutputMode.DexFilePerClassFile) {
+      } else if (outputMode == OutputMode.DexFilePerClassFile
+          || outputMode == OutputMode.DexFilePerClass) {
         List<ProgramResource> resources = getDexProgramResourcesForTesting();
         DexFilePerClassFileConsumer.ArchiveConsumer.writeResources(
             archive, resources, programResourcesMainDescriptor);
diff --git a/src/main/java/com/android/tools/r8/utils/SemanticVersion.java b/src/main/java/com/android/tools/r8/utils/SemanticVersion.java
new file mode 100644
index 0000000..977073d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/SemanticVersion.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2020, 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 java.util.Objects;
+
+public class SemanticVersion {
+
+  public static SemanticVersion parse(String version) {
+    int majorEnd = version.indexOf('.');
+    if (majorEnd <= 0) {
+      throw new IllegalArgumentException("Invalid semantic version: " + version);
+    }
+    int minorEnd = version.indexOf('.', majorEnd + 1);
+    if (minorEnd <= majorEnd) {
+      throw new IllegalArgumentException("Invalid semantic version: " + version);
+    }
+    // No current support for extensions.
+    int patchEnd = version.length();
+    int major;
+    int minor;
+    int patch;
+    try {
+      major = Integer.parseInt(version.substring(0, majorEnd));
+      minor = Integer.parseInt(version.substring(majorEnd + 1, minorEnd));
+      patch = Integer.parseInt(version.substring(minorEnd + 1, patchEnd));
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException("Invalid semantic version: " + version, e);
+    }
+    return new SemanticVersion(major, minor, patch);
+  }
+
+  private final int major;
+  private final int minor;
+  private final int patch;
+
+  public SemanticVersion(int major, int minor, int patch) {
+    this.major = major;
+    this.minor = minor;
+    this.patch = patch;
+  }
+
+  public int getMajor() {
+    return major;
+  }
+
+  public int getMinor() {
+    return minor;
+  }
+
+  public int getPatch() {
+    return patch;
+  }
+
+  public boolean isNewerOrEqual(SemanticVersion other) {
+    if (major != other.major) {
+      return major > other.major;
+    }
+    if (minor != other.minor) {
+      return minor > other.minor;
+    }
+    return patch >= other.patch;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof SemanticVersion)) {
+      return false;
+    }
+    SemanticVersion other = (SemanticVersion) obj;
+    return major == other.major && minor == other.minor && patch == other.patch;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(major, minor, patch);
+  }
+
+  @Override
+  public String toString() {
+    return "" + major + "." + minor + "." + patch;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java b/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java
index 70518d7..a800801 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
+import org.hamcrest.CoreMatchers;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.TypeSafeMatcher;
@@ -71,15 +72,20 @@
   }
 
   public static Matcher<Diagnostic> diagnosticPosition(Position position) {
+    return diagnosticPosition(CoreMatchers.equalTo(position));
+  }
+
+  public static Matcher<Diagnostic> diagnosticPosition(Matcher<Position> positionMatcher) {
     return new DiagnosticsMatcher() {
       @Override
       protected boolean eval(Diagnostic diagnostic) {
-        return diagnostic.getPosition().equals(position);
+        return positionMatcher.matches(diagnostic.getPosition());
       }
 
       @Override
       protected void explain(Description description) {
-        description.appendText("position ").appendText(position.getDescription());
+        description.appendText("position ");
+        positionMatcher.describeTo(description);
       }
     };
   }
diff --git a/src/test/java/com/android/tools/r8/JvmTestBuilder.java b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
index 8bc4820..18571d0 100644
--- a/src/test/java/com/android/tools/r8/JvmTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.debug.CfDebugTestConfig;
 import com.android.tools.r8.debug.DebugTestConfig;
 import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.testing.AndroidBuildVersion;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
 import com.google.common.collect.ObjectArrays;
@@ -150,4 +151,9 @@
   public JvmTestBuilder addVmArguments(String... arguments) {
     return addVmArguments(Arrays.asList(arguments));
   }
+
+  public JvmTestBuilder addAndroidBuildVersion() {
+    addVmArguments("-D" + AndroidBuildVersion.PROPERTY + "=10000");
+    return addProgramClasses(AndroidBuildVersion.class);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/PositionMatcher.java b/src/test/java/com/android/tools/r8/PositionMatcher.java
new file mode 100644
index 0000000..4fca1c6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/PositionMatcher.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2020, 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;
+
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+public abstract class PositionMatcher extends TypeSafeMatcher<Position> {
+
+  public static Matcher<Position> positionLine(int line) {
+    return new PositionMatcher() {
+      @Override
+      protected boolean matchesSafely(Position position) {
+        return position instanceof TextPosition && ((TextPosition) position).getLine() == line;
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with line " + line);
+      }
+    };
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index 39356e1..0816496 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -243,6 +243,11 @@
     return self();
   }
 
+  public CR setSystemProperty(String name, String value) {
+    vmArguments.add("-D" + name + "=" + value);
+    return self();
+  }
+
   public Path writeToZip() throws IOException {
     Path file = state.getNewTempFolder().resolve("out.zip");
     writeToZip(file);
@@ -429,6 +434,13 @@
         withArt6Plus64BitsLib && vm.getVersion().isAtLeast(DexVm.Version.V6_0_1)
             ? builder -> builder.appendArtOption("--64")
             : builder -> {};
+    commandConsumer =
+        commandConsumer.andThen(
+            builder -> {
+              for (String vmArgument : vmArguments) {
+                builder.appendArtOption(vmArgument);
+              }
+            });
     ProcessResult result =
         ToolHelper.runArtRaw(
             classPath, mainClass, commandConsumer, vm, withArtFrameworks, arguments);
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index ce03733..ae249ac 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.debug.DebugTestConfig;
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase.KeepRuleConsumer;
+import com.android.tools.r8.testing.AndroidBuildVersion;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
@@ -61,6 +62,14 @@
   private PrintStream oldStderr = null;
   protected OutputMode outputMode = OutputMode.DexIndexed;
 
+  private boolean isAndroidBuildVersionAdded = false;
+
+  public T addAndroidBuildVersion() {
+    addProgramClasses(AndroidBuildVersion.class);
+    isAndroidBuildVersionAdded = true;
+    return self();
+  }
+
   TestCompilerBuilder(TestState state, B builder, Backend backend) {
     super(state, builder);
     this.backend = backend;
@@ -132,6 +141,9 @@
       cr =
           internalCompile(builder, optionsConsumer, Suppliers.memoize(sink::build))
               .addRunClasspathFiles(additionalRunClassPath);
+      if (isAndroidBuildVersionAdded) {
+        cr.setSystemProperty(AndroidBuildVersion.PROPERTY, "" + builder.getMinApiLevel());
+      }
       return cr;
     } finally {
       if (stdout != null) {
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/BackwardsCompatibleSpecificationTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/BackwardsCompatibleSpecificationTest.java
new file mode 100644
index 0000000..54f3acb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/BackwardsCompatibleSpecificationTest.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2020, 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.desugar.desugaredlibrary;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.NoneRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class BackwardsCompatibleSpecificationTest extends TestBase {
+
+  private static final List<String> RELEASES = ImmutableList.of("2.0.74");
+
+  @Parameterized.Parameters(name = "{1}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withNoneRuntime().build(), RELEASES);
+  }
+
+  private final Path desugaredLib = ToolHelper.getDesugarJDKLibs();
+  private final Path desugaredSpec = ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING;
+  private final String release;
+
+  public BackwardsCompatibleSpecificationTest(TestParameters parameters, String release) {
+    assertEquals(NoneRuntime.getInstance(), parameters.getRuntime());
+    this.release = release;
+  }
+
+  private Path getReleaseJar() {
+    return Paths.get(ToolHelper.THIRD_PARTY_DIR, "r8-releases", release, "r8lib.jar");
+  }
+
+  @Test
+  public void test() throws Exception {
+    ProcessResult result =
+        ToolHelper.runJava(
+            getReleaseJar(),
+            "com.android.tools.r8.L8",
+            "--desugared-lib",
+            desugaredSpec.toString(),
+            desugaredLib.toString());
+    assertEquals(result.toString(), 0, result.exitCode);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibaryChecksumsTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibaryChecksumsTest.java
new file mode 100644
index 0000000..3e819e2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibaryChecksumsTest.java
@@ -0,0 +1,69 @@
+// Copyright (c) 2020, 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.desugar.desugaredlibrary;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.L8;
+import com.android.tools.r8.L8Command;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.NoneRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DesugaredLibaryChecksumsTest extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public DesugaredLibaryChecksumsTest(TestParameters parameters) {
+    assertEquals(NoneRuntime.getInstance(), parameters.getRuntime());
+  }
+
+  @Test
+  public void test() throws Exception {
+    Path out = temp.newFolder().toPath().resolve("out.jar");
+    L8.run(
+        L8Command.builder()
+            .setIncludeClassesChecksum(true)
+            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+            .addProgramFiles(ToolHelper.getDesugarJDKLibs())
+            .addProgramFiles(ToolHelper.DESUGAR_LIB_CONVERSIONS)
+            .setMode(CompilationMode.DEBUG)
+            .addDesugaredLibraryConfiguration(
+                StringResource.fromFile(ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING))
+            .setMinApiLevel(AndroidApiLevel.B.getLevel())
+            .setOutput(out, OutputMode.DexIndexed)
+            .build());
+
+    try {
+      CodeInspector inspector = new CodeInspector(out);
+      for (FoundClassSubject clazz : inspector.allClasses()) {
+        assertTrue(clazz.getDexProgramClass().getChecksum() > 0);
+      }
+    } catch (CompilationError e) {
+      // TODO(b/158746302): Desugared library should support checksums.
+      //  also, the failure should have occured in the L8.run above!
+      assertThat(e.getMessage(), containsString("has no checksum"));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryConfigurationParsingTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryConfigurationParsingTest.java
new file mode 100644
index 0000000..07c0703
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryConfigurationParsingTest.java
@@ -0,0 +1,319 @@
+// Copyright (c) 2020, 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.desugar.desugaredlibrary;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestDiagnosticMessagesImpl;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.NoneRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryConfigurationParser;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AbortException;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.SemanticVersion;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.StringUtils.BraceType;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DesugaredLibraryConfigurationParsingTest extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public DesugaredLibraryConfigurationParsingTest(TestParameters parameters) {
+    assertEquals(NoneRuntime.getInstance(), parameters.getRuntime());
+  }
+
+  final AndroidApiLevel minApi = AndroidApiLevel.B;
+  final boolean libraryCompilation = true;
+
+  final DexItemFactory factory = new DexItemFactory();
+  final Origin origin =
+      new Origin(Origin.root()) {
+        @Override
+        public String part() {
+          return "Test Origin";
+        }
+      };
+
+  final Map<String, Object> TEMPLATE =
+      ImmutableMap.<String, Object>builder()
+          .put(
+              "configuration_format_version",
+              DesugaredLibraryConfigurationParser.MAX_SUPPORTED_VERSION)
+          .put("group_id", "com.tools.android")
+          .put("artifact_id", "desugar_jdk_libs")
+          .put("version", DesugaredLibraryConfigurationParser.MIN_SUPPORTED_VERSION.toString())
+          .put("required_compilation_api_level", 1)
+          .put("synthesized_library_classes_package_prefix", "j$.")
+          .put("common_flags", Collections.emptyList())
+          .put("program_flags", Collections.emptyList())
+          .put("library_flags", Collections.emptyList())
+          .build();
+
+  private LinkedHashMap<String, Object> template() {
+    return new LinkedHashMap<>(TEMPLATE);
+  }
+
+  private DesugaredLibraryConfigurationParser parser(DiagnosticsHandler handler) {
+    return new DesugaredLibraryConfigurationParser(
+        factory, new Reporter(handler), libraryCompilation, minApi.getLevel());
+  }
+
+  private DesugaredLibraryConfiguration runPassing(String resource) {
+    return runPassing(StringResource.fromString(resource, origin));
+  }
+
+  private DesugaredLibraryConfiguration runPassing(StringResource resource) {
+    TestDiagnosticMessagesImpl handler = new TestDiagnosticMessagesImpl();
+    DesugaredLibraryConfiguration config = parser(handler).parse(resource);
+    handler.assertNoMessages();
+    return config;
+  }
+
+  private void runFailing(String json, Consumer<TestDiagnosticMessages> checker) {
+    TestDiagnosticMessagesImpl handler = new TestDiagnosticMessagesImpl();
+    try {
+      parser(handler).parse(StringResource.fromString(json, origin));
+      fail("Expected failure");
+    } catch (AbortException e) {
+      checker.accept(handler);
+    }
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    // Just test that the reference file parses without issues.
+    DesugaredLibraryConfiguration config =
+        runPassing(StringResource.fromFile(ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING));
+    assertEquals(libraryCompilation, config.isLibraryCompilation());
+  }
+
+  @Test
+  public void testEmpty() {
+    runFailing(
+        "",
+        diagnostics -> {
+          diagnostics.assertErrorsMatch(
+              allOf(
+                  diagnosticMessage(containsString("Not a JSON Object")),
+                  diagnosticOrigin(origin)));
+        });
+  }
+
+  @Test
+  public void testRequiredKeys() {
+    ImmutableList<String> requiredKeys =
+        ImmutableList.of(
+            "configuration_format_version",
+            "group_id",
+            "artifact_id",
+            "version",
+            "required_compilation_api_level",
+            "synthesized_library_classes_package_prefix",
+            "common_flags",
+            "program_flags",
+            "library_flags");
+    for (String key : requiredKeys) {
+      Map<String, Object> data = template();
+      data.remove(key);
+      runFailing(
+          toJson(data),
+          diagnostics ->
+              diagnostics.assertErrorsMatch(
+                  allOf(
+                      diagnosticMessage(containsString("Invalid desugared library configuration")),
+                      diagnosticMessage(containsString("Expected required key '" + key + "'")),
+                      diagnosticOrigin(origin))));
+    }
+  }
+
+  @Test
+  public void testUnsupportedVersion() {
+    LinkedHashMap<String, Object> data = template();
+    SemanticVersion minVersion = DesugaredLibraryConfigurationParser.MIN_SUPPORTED_VERSION;
+    data.put(
+        "version",
+        new SemanticVersion(minVersion.getMajor(), minVersion.getMinor(), minVersion.getPatch() - 1)
+            .toString());
+    runFailing(
+        toJson(data),
+        diagnostics ->
+            diagnostics.assertErrorsMatch(
+                allOf(
+                    diagnosticMessage(containsString("upgrade the desugared library")),
+                    diagnosticOrigin(origin))));
+  }
+
+  @Test
+  public void testUnsupportedAbove() {
+    LinkedHashMap<String, Object> data = template();
+    data.put("configuration_format_version", 100000);
+    runFailing(
+        toJson(data),
+        diagnostics ->
+            diagnostics.assertErrorsMatch(
+                allOf(
+                    diagnosticMessage(containsString("upgrade the D8/R8 compiler")),
+                    diagnosticOrigin(origin))));
+  }
+
+  @Test
+  public void testCustomAndWrapperOverlap() {
+    LinkedHashMap<String, Object> data = template();
+    data.put(
+        "common_flags",
+        ImmutableList.of(
+            ImmutableMap.of(
+                "api_level_below_or_equal",
+                100000,
+                "custom_conversion",
+                ImmutableMap.of("java.util.Foo", "j$.util.FooConv"),
+                "wrapper_conversion",
+                ImmutableList.of("java.util.Foo"))));
+    runFailing(
+        toJson(data),
+        diagnostics ->
+            diagnostics.assertErrorsMatch(
+                allOf(
+                    diagnosticMessage(containsString("Duplicate types")),
+                    diagnosticMessage(containsString("java.util.Foo")),
+                    diagnosticOrigin(origin))));
+  }
+
+  @Test
+  public void testRedefinition() {
+    LinkedHashMap<String, Object> data = template();
+    data.put(
+        "common_flags",
+        ImmutableList.of(
+            ImmutableMap.of(
+                "api_level_below_or_equal",
+                100000,
+                "custom_conversion",
+                ImmutableMap.of("java.util.Foo", "j$.util.FooConv1"))));
+    data.put(
+        "library_flags",
+        ImmutableList.of(
+            ImmutableMap.of(
+                "api_level_below_or_equal",
+                100000,
+                "custom_conversion",
+                ImmutableMap.of("java.util.Foo", "j$.util.FooConv2"))));
+    runFailing(
+        toJson(data),
+        diagnostics ->
+            diagnostics.assertErrorsMatch(
+                allOf(
+                    diagnosticMessage(containsString("Duplicate assignment of key")),
+                    diagnosticMessage(containsString("java.util.Foo")),
+                    diagnosticMessage(containsString("custom_conversion")),
+                    diagnosticOrigin(origin))));
+  }
+
+  @Test
+  public void testDuplicate() {
+    LinkedHashMap<String, Object> data = template();
+    data.put(
+        "common_flags",
+        ImmutableList.of(
+            ImmutableMap.of(
+                "api_level_below_or_equal",
+                100000,
+                "custom_conversion",
+                ImmutableMap.of(
+                    "java.util.Foo", "j$.util.FooConv1",
+                    "java.util.Foo2", "j$.util.FooConv2"))));
+    // The gson parser will overwrite the key in order during parsing, thus hiding potential issues.
+    DesugaredLibraryConfiguration config = runPassing(toJson(data).replace("Foo2", "Foo"));
+    assertEquals(
+        Collections.singletonList("java.util.Foo"),
+        config.getCustomConversions().keySet().stream()
+            .map(DexType::toString)
+            .collect(Collectors.toList()));
+    assertEquals(
+        Collections.singletonList("j$.util.FooConv2"),
+        config.getCustomConversions().values().stream()
+            .map(DexType::toString)
+            .collect(Collectors.toList()));
+  }
+
+  // JSON building helpers.
+  // This does not use gson to make the text input construction independent of gson.
+
+  private static String toJson(Map<String, Object> data) {
+    StringBuilder builder = new StringBuilder();
+    toJsonObject(data, builder);
+    return builder.toString();
+  }
+
+  private static void toJsonObject(Map<String, Object> data, StringBuilder builder) {
+    StringUtils.append(
+        builder,
+        ListUtils.map(
+            data.entrySet(),
+            entry -> "\n  " + quote(entry.getKey()) + ": " + toJsonElement(entry.getValue())),
+        ", ",
+        BraceType.TUBORG);
+  }
+
+  private static String toJsonElement(Object element) {
+    StringBuilder builder = new StringBuilder();
+    toJsonElement(element, builder);
+    return builder.toString();
+  }
+
+  private static void toJsonElement(Object element, StringBuilder builder) {
+    if (element instanceof String) {
+      builder.append(quote((String) element));
+    } else if (element instanceof Integer) {
+      builder.append(element);
+    } else if (element instanceof List) {
+      toJsonList((List<Object>) element, builder);
+    } else if (element instanceof Map) {
+      toJsonObject((Map<String, Object>) element, builder);
+    } else {
+      throw new IllegalStateException("Unexpected object type: " + element.getClass());
+    }
+  }
+
+  private static void toJsonList(List<Object> element, StringBuilder builder) {
+    StringUtils.append(
+        builder, ListUtils.map(element, o -> "\n  " + toJsonElement(o)), ", ", BraceType.SQUARE);
+  }
+
+  private static String quote(String str) {
+    return "\"" + str + "\"";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ExtractWrapperTypesTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ExtractWrapperTypesTest.java
new file mode 100644
index 0000000..846b65d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ExtractWrapperTypesTest.java
@@ -0,0 +1,304 @@
+// Copyright (c) 2020, 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.desugar.desugaredlibrary;
+
+import static com.android.tools.r8.utils.DescriptorUtils.descriptorToJavaType;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.GenerateLintFiles;
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.NoneRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryConfigurationParser;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.WorkList;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ExtractWrapperTypesTest extends TestBase {
+
+  // Filter on types that do not need to be considered for wrapping.
+  private static boolean doesNotNeedWrapper(String type, Set<String> customConversions) {
+    return excludePackage(type)
+        || NOT_NEEDED_NOT_IN_DOCS.contains(type)
+        || FINAL_CLASSES.contains(type)
+        || customConversions.contains(type);
+  }
+
+  private static boolean excludePackage(String type) {
+    return type.startsWith("java.lang.")
+        || type.startsWith("java.nio.")
+        || type.startsWith("java.security.")
+        || type.startsWith("java.net.")
+        || type.startsWith("java.awt.")
+        || type.startsWith("java.util.concurrent.");
+  }
+
+  // Types not picked up by the android.jar scan but for which wrappers are needed.
+  private static final Set<String> ADDITIONAL_WRAPPERS = ImmutableSet.of();
+
+  // Types not in API docs, referenced in android.jar and must be wrapped.
+  private static final Set<String> NEEDED_BUT_NOT_IN_DOCS = ImmutableSet.of();
+
+  // Types not in API docs, referenced in android.jar but need not be wrapped.
+  private static final Set<String> NOT_NEEDED_NOT_IN_DOCS =
+      ImmutableSet.of(
+          "java.util.Base64$Decoder",
+          "java.util.Base64$Encoder",
+          "java.util.Calendar$Builder",
+          "java.util.Locale$Builder",
+          "java.util.Locale$Category",
+          "java.util.Locale$FilteringMode",
+          "java.util.SplittableRandom");
+
+  // List of referenced final classes (cannot be wrapper converted) with no custom conversions.
+  private static final Set<String> FINAL_CLASSES =
+      ImmutableSet.of(
+          // TODO(b/159304624): Does this need custom conversion?
+          "java.time.Period");
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  // TODO: parameterize to check both api<=23 as well as 23<api<26 for which the spec differs.
+  private final AndroidApiLevel minApi = AndroidApiLevel.B;
+  private final AndroidApiLevel targetApi = AndroidApiLevel.Q;
+
+  public ExtractWrapperTypesTest(TestParameters parameters) {
+    assertEquals(NoneRuntime.getInstance(), parameters.getRuntime());
+  }
+
+  @Test
+  public void checkConsistency() {
+    List<Set<String>> sets =
+        ImmutableList.of(
+            ADDITIONAL_WRAPPERS, NEEDED_BUT_NOT_IN_DOCS, NOT_NEEDED_NOT_IN_DOCS, FINAL_CLASSES);
+    for (Set<String> set1 : sets) {
+      for (Set<String> set2 : sets) {
+        if (set1 != set2) {
+          assertEquals(Collections.emptySet(), Sets.intersection(set1, set2));
+        }
+      }
+    }
+    for (Set<String> set : sets) {
+      for (String type : set) {
+        assertFalse(excludePackage(type));
+      }
+    }
+  }
+
+  @Test
+  public void test() throws Exception {
+    CodeInspector desugaredApiJar = getDesugaredApiJar();
+    Set<ClassReference> preDesugarTypes = getPreDesugarTypes();
+
+    DesugaredLibraryConfiguration conf = getDesugaredLibraryConfiguration();
+    Set<String> wrappersInSpec =
+        conf.getWrapperConversions().stream().map(DexType::toString).collect(Collectors.toSet());
+    Set<String> customConversionsInSpec =
+        conf.getCustomConversions().keySet().stream()
+            .map(DexType::toString)
+            .collect(Collectors.toSet());
+    assertEquals(
+        Collections.emptySet(), Sets.intersection(wrappersInSpec, customConversionsInSpec));
+    assertEquals(Collections.emptySet(), Sets.intersection(FINAL_CLASSES, customConversionsInSpec));
+
+    CodeInspector nonDesugaredJar = new CodeInspector(ToolHelper.getAndroidJar(targetApi));
+    Map<ClassReference, Set<MethodReference>> directWrappers =
+        getDirectlyReferencedWrapperTypes(
+            desugaredApiJar, preDesugarTypes, nonDesugaredJar, customConversionsInSpec);
+    Map<ClassReference, Set<ClassReference>> indirectWrappers =
+        getIndirectlyReferencedWrapperTypes(
+            directWrappers, preDesugarTypes, nonDesugaredJar, customConversionsInSpec);
+
+    {
+      Set<String> missingWrappers = getMissingWrappers(directWrappers, wrappersInSpec);
+      assertTrue(
+          "Missing direct wrappers:\n" + String.join("\n", missingWrappers),
+          missingWrappers.isEmpty());
+    }
+
+    {
+      Set<String> missingWrappers = getMissingWrappers(indirectWrappers, wrappersInSpec);
+      assertTrue(
+          "Missing indirect wrappers:\n" + String.join("\n", missingWrappers),
+          missingWrappers.isEmpty());
+    }
+
+    Set<String> additionalWrappers = new TreeSet<>();
+    for (String wrapper : wrappersInSpec) {
+      ClassReference item = Reference.classFromTypeName(wrapper);
+      if (!directWrappers.containsKey(item)
+          && !indirectWrappers.containsKey(item)
+          && !ADDITIONAL_WRAPPERS.contains(wrapper)) {
+        additionalWrappers.add(wrapper);
+      }
+    }
+    assertTrue(
+        "Additional wrapper:\n" + String.join("\n", additionalWrappers),
+        additionalWrappers.isEmpty());
+
+    assertEquals(
+        directWrappers.size() + indirectWrappers.size() + ADDITIONAL_WRAPPERS.size(),
+        wrappersInSpec.size());
+  }
+
+  private static <T> Set<String> getMissingWrappers(
+      Map<ClassReference, Set<T>> expected, Set<String> wrappersInSpec) {
+    Set<String> missingWrappers = new TreeSet<>();
+    for (ClassReference addition : expected.keySet()) {
+      String item = descriptorToJavaType(addition.getDescriptor());
+      if (!wrappersInSpec.contains(item)) {
+        missingWrappers.add(item + " referenced from: " + expected.get(addition));
+      }
+    }
+    return missingWrappers;
+  }
+
+  private DesugaredLibraryConfiguration getDesugaredLibraryConfiguration() {
+    DesugaredLibraryConfigurationParser parser =
+        new DesugaredLibraryConfigurationParser(
+            new DexItemFactory(), null, true, minApi.getLevel());
+    return parser.parse(StringResource.fromFile(ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING));
+  }
+
+  private Map<ClassReference, Set<MethodReference>> getDirectlyReferencedWrapperTypes(
+      CodeInspector desugaredApiJar,
+      Set<ClassReference> preDesugarTypes,
+      CodeInspector nonDesugaredJar,
+      Set<String> customConversions) {
+    Map<ClassReference, Set<MethodReference>> directWrappers = new HashMap<>();
+    nonDesugaredJar.forAllClasses(
+        clazz -> {
+          clazz.forAllMethods(
+              method -> {
+                if (!method.isPublic() && !method.isProtected()) {
+                  return;
+                }
+                if (desugaredApiJar.method(method.asMethodReference()).isPresent()) {
+                  return;
+                }
+                Consumer<ClassReference> adder =
+                    t ->
+                        directWrappers
+                            .computeIfAbsent(t, k -> new HashSet<>())
+                            .add(method.asMethodReference());
+                MethodSignature signature = method.getFinalSignature().asMethodSignature();
+                addType(adder, signature.type, preDesugarTypes, customConversions);
+                for (String parameter : signature.parameters) {
+                  addType(adder, parameter, preDesugarTypes, customConversions);
+                }
+              });
+        });
+    return directWrappers;
+  }
+
+  private Map<ClassReference, Set<ClassReference>> getIndirectlyReferencedWrapperTypes(
+      Map<ClassReference, Set<MethodReference>> directWrappers,
+      Set<ClassReference> existing,
+      CodeInspector latest,
+      Set<String> customConversions) {
+    Map<ClassReference, Set<ClassReference>> indirectWrappers = new HashMap<>();
+    WorkList<ClassReference> worklist = WorkList.newEqualityWorkList(directWrappers.keySet());
+    while (worklist.hasNext()) {
+      ClassReference reference = worklist.next();
+      ClassSubject clazz = latest.clazz(reference);
+      clazz.forAllVirtualMethods(
+          method -> {
+            assertTrue(method.toString(), method.isPublic() || method.isProtected());
+            MethodSignature signature = method.getFinalSignature().asMethodSignature();
+            Consumer<ClassReference> adder =
+                t -> {
+                  if (worklist.addIfNotSeen(t)) {
+                    indirectWrappers.computeIfAbsent(t, k -> new HashSet<>()).add(reference);
+                  }
+                };
+            addType(adder, signature.type, existing, customConversions);
+            for (String parameter : signature.parameters) {
+              addType(adder, parameter, existing, customConversions);
+            }
+          });
+    }
+    return indirectWrappers;
+  }
+
+  private Set<ClassReference> getPreDesugarTypes() throws IOException {
+    Set<ClassReference> existing = new HashSet<>();
+    Path androidJar = ToolHelper.getAndroidJar(minApi);
+    new CodeInspector(androidJar)
+        .forAllClasses(
+            clazz -> {
+              if (clazz.getFinalName().startsWith("java.")) {
+                existing.add(Reference.classFromTypeName(clazz.getFinalName()));
+              }
+            });
+    return existing;
+  }
+
+  private CodeInspector getDesugaredApiJar() throws Exception {
+    Path out = temp.newFolder().toPath();
+    GenerateLintFiles desugaredApi =
+        new GenerateLintFiles(ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING.toString(), out.toString());
+    desugaredApi.run(targetApi.getLevel());
+    return new CodeInspector(
+        out.resolve("compile_api_level_" + targetApi.getLevel())
+            .resolve("desugared_apis_" + targetApi.getLevel() + "_" + minApi.getLevel() + ".jar"));
+  }
+
+  private void addType(
+      Consumer<ClassReference> additions,
+      String type,
+      Set<ClassReference> preDesugarTypes,
+      Set<String> customConversions) {
+    if (type.equals("void")) {
+      return;
+    }
+    TypeReference typeReference = Reference.typeFromTypeName(type);
+    if (typeReference.isArray()) {
+      typeReference = typeReference.asArray().getBaseType();
+    }
+    if (typeReference.isClass()) {
+      ClassReference clazz = typeReference.asClass();
+      String clazzType = descriptorToJavaType(clazz.getDescriptor());
+      if (clazzType.startsWith("java.")
+          && !doesNotNeedWrapper(clazzType, customConversions)
+          && !preDesugarTypes.contains(clazz)) {
+        additions.accept(clazz);
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/APIConversionTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/APIConversionTest.java
index 579b6f6..ad8e74c 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/APIConversionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/APIConversionTest.java
@@ -31,7 +31,7 @@
   private static final AndroidApiLevel MIN_SUPPORTED = AndroidApiLevel.N;
   private static final String EXPECTED_RESULT =
       StringUtils.lines(
-          "[5, 6, 7]", "$r8$wrapper$java$util$stream$IntStream$-V-WRP", "IntSummaryStatistics");
+          "[5, 6, 7]", "j$.$r8$wrapper$java$util$stream$IntStream$-V-WRP", "IntSummaryStatistics");
 
   @Parameters(name = "{0}, shrinkDesugaredLibrary: {1}")
   public static List<Object[]> data() {
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ClockAPIConversionTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ClockAPIConversionTest.java
index 4581211..382f55d 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ClockAPIConversionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ClockAPIConversionTest.java
@@ -57,6 +57,7 @@
         .addLibraryClasses(CustomLibClass.class)
         .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
         .compile()
+        .assertNoMessages()
         .addDesugaredCoreLibraryRunClassPath(
             this::buildDesugaredLibrary,
             parameters.getApiLevel(),
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/FunctionConversionTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/FunctionConversionTest.java
index 3d1b999..e8aa581 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/FunctionConversionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/FunctionConversionTest.java
@@ -6,7 +6,6 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
-import com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
@@ -21,8 +20,7 @@
 import java.util.function.IntSupplier;
 import java.util.function.LongConsumer;
 import java.util.function.LongSupplier;
-import org.junit.Assert;
-import org.junit.Assume;
+import java.util.function.Supplier;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -70,6 +68,7 @@
         .addLibraryClasses(CustomLibClass.class)
         .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
         .compile()
+        .assertNoMessages()
         .addDesugaredCoreLibraryRunClassPath(
             this::buildDesugaredLibrary,
             parameters.getApiLevel(),
@@ -101,43 +100,6 @@
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
 
-  @Test
-  public void testWrapperWithChecksum() throws Exception {
-    Assume.assumeTrue(
-        shrinkDesugaredLibrary && parameters.getApiLevel().getLevel() <= MIN_SUPPORTED.getLevel());
-    testForD8()
-        .addProgramClasses(
-            Executor.class, Executor.Object1.class, Executor.Object2.class, Executor.Object3.class)
-        .addLibraryClasses(CustomLibClass.class)
-        .setMinApi(parameters.getApiLevel())
-        .enableCoreLibraryDesugaring(parameters.getApiLevel())
-        .setIncludeClassesChecksum(true) // Compilation fails if some classes are missing checksum.
-        .compile()
-        .inspect(
-            inspector -> {
-              Assert.assertEquals(
-                  9,
-                  inspector.allClasses().stream()
-                      .filter(
-                          clazz ->
-                              clazz
-                                  .getFinalName()
-                                  .contains(DesugaredLibraryWrapperSynthesizer.TYPE_WRAPPER_SUFFIX))
-                      .count());
-              Assert.assertEquals(
-                  9,
-                  inspector.allClasses().stream()
-                      .filter(
-                          clazz ->
-                              clazz
-                                  .getFinalName()
-                                  .contains(
-                                      DesugaredLibraryWrapperSynthesizer
-                                          .VIVIFIED_TYPE_WRAPPER_SUFFIX))
-                      .count());
-            });
-  }
-
   static class Executor {
 
     public static void main(String[] args) {
@@ -146,16 +108,16 @@
       BiFunction<String, String, Character> biFunction =
           CustomLibClass.mixBiFunctions((String i, String j) -> i + j, (String s) -> s.charAt(1));
       System.out.println(biFunction.apply("1", "2"));
-      BooleanSupplier booleanSupplier = CustomLibClass.mixBoolSuppliers(() -> true, () -> false);
+      BooleanSupplier booleanSupplier =
+          () -> CustomLibClass.mixBoolSuppliers(() -> true, () -> false).get();
       System.out.println(booleanSupplier.getAsBoolean());
       LongConsumer longConsumer = CustomLibClass.mixLong(() -> 1L, System.out::println);
       longConsumer.accept(2L);
       DoublePredicate doublePredicate =
           CustomLibClass.mixPredicate(d -> d > 1.0, d -> d == 2.0, d -> d < 3.0);
       System.out.println(doublePredicate.test(2.0));
-      // Reverse wrapper should not exist.
       System.out.println(CustomLibClass.extractInt(() -> 5));
-      System.out.println(CustomLibClass.getDoubleSupplier().getAsDouble());
+      System.out.println(CustomLibClass.getDoubleSupplier().get());
     }
 
     static class Object1 {}
@@ -203,13 +165,18 @@
       return operator.andThen(function);
     }
 
-    public static BooleanSupplier mixBoolSuppliers(
-        BooleanSupplier supplier1, BooleanSupplier supplier2) {
-      return () -> supplier1.getAsBoolean() && supplier2.getAsBoolean();
+    // BooleanSupplier is not a wrapped type, so it can't be placed on the boundary.
+    public static Supplier<Boolean> mixBoolSuppliers(
+        Supplier<Boolean> supplier1, Supplier<Boolean> supplier2) {
+      BooleanSupplier wrap1 = supplier1::get;
+      BooleanSupplier wrap2 = supplier2::get;
+      return () -> wrap1.getAsBoolean() && wrap2.getAsBoolean();
     }
 
-    public static LongConsumer mixLong(LongSupplier supplier, LongConsumer consumer) {
-      return l -> consumer.accept(l + supplier.getAsLong());
+    // LongSupplier is not a wrapped type, so it can't be placed on the boundary.
+    public static LongConsumer mixLong(Supplier<Long> supplier, LongConsumer consumer) {
+      LongSupplier wrap = supplier::get;
+      return l -> consumer.accept(l + wrap.getAsLong());
     }
 
     public static DoublePredicate mixPredicate(
@@ -217,12 +184,16 @@
       return predicate1.and(predicate2).and(predicate3);
     }
 
-    public static int extractInt(IntSupplier supplier) {
-      return supplier.getAsInt();
+    // IntSupplier is not a wrapped type, so it can't be placed on the boundary.
+    public static int extractInt(Supplier<Integer> supplier) {
+      IntSupplier wrap = supplier::get;
+      return wrap.getAsInt();
     }
 
-    public static DoubleSupplier getDoubleSupplier() {
-      return () -> 42.0;
+    // DoubleSupplier is not a wrapped type, so it can't be placed on the boundary.
+    public static Supplier<Double> getDoubleSupplier() {
+      DoubleSupplier supplier = () -> 42.0;
+      return supplier::getAsDouble;
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/WrapperMergeTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/WrapperMergeTest.java
deleted file mode 100644
index 4c1caf7..0000000
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/WrapperMergeTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (c) 2019, 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.desugar.desugaredlibrary.conversiontests;
-
-import static org.junit.Assert.assertEquals;
-
-import com.android.tools.r8.TestRuntime.DexRuntime;
-import com.android.tools.r8.ToolHelper.DexVm;
-import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
-import com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import java.nio.file.Path;
-import java.util.Arrays;
-import org.junit.Test;
-
-public class WrapperMergeTest extends DesugaredLibraryTestBase {
-
-  @Test
-  public void testWrapperMerge() throws Exception {
-    // Multiple wrapper classes have to be merged here.
-    Path path1 = testForD8()
-        .addProgramClasses(Executor1.class)
-        .setMinApi(AndroidApiLevel.B)
-        .enableCoreLibraryDesugaring(AndroidApiLevel.B)
-        .compile()
-        .inspect(this::assertWrappers)
-        .writeToZip();
-    Path path2 = testForD8()
-        .addProgramClasses(Executor2.class)
-        .setMinApi(AndroidApiLevel.B)
-        .enableCoreLibraryDesugaring(AndroidApiLevel.B)
-        .compile()
-        .inspect(this::assertWrappers)
-        .writeToZip();
-    testForD8()
-        .addProgramFiles(path1, path2)
-        .compile()
-        .addDesugaredCoreLibraryRunClassPath(this::buildDesugaredLibrary, AndroidApiLevel.B)
-        .run(new DexRuntime(DexVm.ART_9_0_0_HOST), Executor1.class)
-        .assertSuccessWithOutput(StringUtils.lines("[1, 2, 3]"));
-  }
-
-  private void assertWrappers(CodeInspector inspector) {
-    assertEquals(2,inspector.allClasses().stream().filter(c -> c.getOriginalName().contains(
-        DesugaredLibraryWrapperSynthesizer.WRAPPER_PREFIX)).count());
-  }
-
-  static class Executor1 {
-
-    public static void main(String[] args) {
-      int[] ints = new int[3];
-      Arrays.setAll(ints,x->x+1);
-      System.out.println(Arrays.toString(ints));
-    }
-  }
-
-  static class Executor2 {
-
-    public static void main(String[] args) {
-      int[] ints = new int[3];
-      Arrays.setAll(ints,x->x+2);
-      System.out.println(Arrays.toString(ints));
-    }
-  }
-
-}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/WrapperPlacementTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/WrapperPlacementTest.java
new file mode 100644
index 0000000..cbab10c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/WrapperPlacementTest.java
@@ -0,0 +1,159 @@
+// Copyright (c) 2019, 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.desugar.desugaredlibrary.conversiontests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class WrapperPlacementTest extends DesugaredLibraryTestBase {
+
+  private static final String EXPECTED = StringUtils.lines("[1, 2, 3]", "[2, 3, 4]");
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private final TestParameters parameters;
+
+  public WrapperPlacementTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addAndroidBuildVersion()
+        .addProgramClassesAndInnerClasses(MyArrays1.class)
+        .addProgramClassesAndInnerClasses(MyArrays2.class)
+        .addProgramClasses(TestClass.class)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testNoWrappers() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    // No wrappers are made during program compilation.
+    Path path1 = compileWithCoreLibraryDesugaring(MyArrays1.class);
+    Path path2 = compileWithCoreLibraryDesugaring(MyArrays2.class);
+    testForD8()
+        .addProgramClasses(TestClass.class)
+        .addAndroidBuildVersion()
+        .enableCoreLibraryDesugaring(parameters.getApiLevel())
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::assertNoWrappers)
+        .apply(
+            b -> {
+              if (!hasNativeIntUnaryOperator()) {
+                Path coreLib = buildDesugaredLibrary(parameters.getApiLevel());
+                assertCoreLibContainsWrappers(coreLib);
+                b.addRunClasspathFiles(coreLib);
+              }
+            })
+        // The previous compilations are appended to the classpath (no merge).
+        .addRunClasspathFiles(path1, path2)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  private Path compileWithCoreLibraryDesugaring(Class<?> clazz) throws Exception {
+    return testForD8()
+        .addProgramClassesAndInnerClasses(clazz)
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel())
+        .compile()
+        .inspect(this::assertNoWrappers)
+        .writeToZip();
+  }
+
+  private boolean hasNativeIntUnaryOperator() {
+    return parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
+  }
+
+  private void assertCoreLibContainsWrappers(Path coreLib) throws IOException {
+    CodeInspector inspector = new CodeInspector(coreLib);
+    Stream<FoundClassSubject> wrappers = getWrappers(inspector);
+    assertNotEquals(0, wrappers.count());
+  }
+
+  private void assertNoWrappers(CodeInspector inspector) {
+    Stream<FoundClassSubject> wrappers = getWrappers(inspector);
+    assertEquals(0, wrappers.count());
+  }
+
+  private Stream<FoundClassSubject> getWrappers(CodeInspector inspector) {
+    return inspector.allClasses().stream()
+        .filter(
+            c -> c.getOriginalName().contains(DesugaredLibraryWrapperSynthesizer.WRAPPER_PREFIX));
+  }
+
+  static class MyArrays1 {
+
+    interface IntGenerator {
+      int generate(int index);
+    }
+
+    public static void setAll(int[] ints, IntGenerator generator) {
+      if (AndroidBuildVersion.VERSION >= 24) {
+        java.util.Arrays.setAll(ints, generator::generate);
+      } else {
+        for (int i = 0; i < ints.length; i++) {
+          ints[i] = generator.generate(i);
+        }
+      }
+    }
+  }
+
+  static class MyArrays2 {
+
+    interface IntGenerator {
+      int generate(int index);
+    }
+
+    public static void setAll(int[] ints, IntGenerator generator) {
+      if (AndroidBuildVersion.VERSION >= 24) {
+        java.util.Arrays.setAll(ints, generator::generate);
+      } else {
+        for (int i = 0; i < ints.length; i++) {
+          ints[i] = generator.generate(i);
+        }
+      }
+    }
+  }
+
+  public static class TestClass {
+
+    public static void main(String[] args) {
+      int[] ints = new int[3];
+      MyArrays1.setAll(ints, x -> x + 1);
+      System.out.println(Arrays.toString(ints));
+      MyArrays2.setAll(ints, x -> x + 2);
+      System.out.println(Arrays.toString(ints));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/testing/AndroidBuildVersion.java b/src/test/java/com/android/tools/r8/testing/AndroidBuildVersion.java
new file mode 100644
index 0000000..ab3ea7e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/testing/AndroidBuildVersion.java
@@ -0,0 +1,14 @@
+// Copyright (c) 2020, 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.testing;
+
+/**
+ * Stub class to simulate having a Build.VERSION property in headless tests.
+ *
+ * <p>Use test builder addAndroidBuildVersion() methods when used in tests.
+ */
+public class AndroidBuildVersion {
+  public static final String PROPERTY = "com.android.tools.r8.testing.AndroidBuildVersion.VERSION";
+  public static int VERSION = Integer.parseInt(System.getProperty(PROPERTY));
+}
diff --git a/src/test/java/com/android/tools/r8/utils/SemanticVersionTests.java b/src/test/java/com/android/tools/r8/utils/SemanticVersionTests.java
new file mode 100644
index 0000000..8f0dd61
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/SemanticVersionTests.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2020, 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 static com.android.tools.r8.utils.SemanticVersion.parse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+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.TestRuntime.NoneRuntime;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class SemanticVersionTests extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public SemanticVersionTests(TestParameters parameters) {
+    assertEquals(NoneRuntime.getInstance(), parameters.getRuntime());
+  }
+
+  @Test
+  public void test() throws Exception {
+    assertTrue(parse("1.0.1").isNewerOrEqual(parse("1.0.0")));
+    assertFalse(parse("1.0.1").isNewerOrEqual(parse("1.1.0")));
+    assertTrue(parse("1.1.0").isNewerOrEqual(parse("1.0.1")));
+    assertFalse(parse("1.1.1").isNewerOrEqual(parse("2.0.0")));
+
+    assertTrue(parse("2.0.0").isNewerOrEqual(parse("1.1.1")));
+    assertTrue(parse("42.42.42").isNewerOrEqual(parse("9.9.9")));
+    assertTrue(parse("9.42.42").isNewerOrEqual(parse("9.9.9")));
+    assertTrue(parse("9.9.42").isNewerOrEqual(parse("9.9.9")));
+
+    assertFalse(parse("1.1.1").isNewerOrEqual(parse("2.0.0")));
+    assertFalse(parse("9.9.9").isNewerOrEqual(parse("42.42.42")));
+    assertFalse(parse("9.9.9").isNewerOrEqual(parse("9.42.42")));
+    assertFalse(parse("9.9.9").isNewerOrEqual(parse("9.9.42")));
+  }
+}
diff --git a/third_party/r8-releases/2.0.74.tar.gz.sha1 b/third_party/r8-releases/2.0.74.tar.gz.sha1
new file mode 100644
index 0000000..7359e6d
--- /dev/null
+++ b/third_party/r8-releases/2.0.74.tar.gz.sha1
@@ -0,0 +1 @@
+6903a4fb98ef23da4d40e1b08998a390578dd0ef
\ No newline at end of file