Skip to content

Commit

Permalink
Add limited support for core library desugaring 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
ogolberg authored Nov 19, 2023
1 parent 0d778e6 commit 08bae31
Show file tree
Hide file tree
Showing 21 changed files with 220 additions and 69 deletions.
32 changes: 25 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,18 @@ When the APK is assembled, D8 (the Android Dexer) transforms java bytecode into

### Gummy Bears

This project provides a safe and more accurate set of signatures for Android 4.4-13 (API 19-33). The additional _sugary_ signatures are generated from hand-written stubs. The reference for the stubs is the [D8 source code](https://r8.googlesource.com/r8/+/master/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java).
This project provides a safe and more accurate set of signatures for Android 4.4-13 (API 19-34). The additional _sugary_ signatures are generated from hand-written stubs. The reference for the stubs is the [D8 source code](https://r8.googlesource.com/r8/+/master/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java).

This project also provides experimental sets of signatures for APIs available via _core library desugaring_, including `java.time`, `ConcurrentHashMap`, etc. The artifacts are tagged with the `coreLib` classifier and are available for Android 4.4-8.1 (API 19-27).
### Core library desugaring

This project also provides _experimental_ sets of signatures for APIs available via [core library desugaring](https://developer.android.com/studio/write/java8-support), including `java.time`, `ConcurrentHashMap`, etc.

Two versions of core library desugaring signatures are provided: v1, which requires `desugar_jdk_libs:1.2.3` or above and is published under the `coreLib`
classifier, and v2, which requires `desugar_jdk_libs:2.0.4` and is published under the `coreLib2` classifier. Note that `desugar_jdk_libs` version `2`
comes in three flavors: minimal, nio, and full. Currently, only the full flavor is supported.

Using signatures with core library desugaring to validate a library effectively implies that all Android projects consuming the library
must have core library desugaring enabled at build time and bring in the appropriate version of `desugar_jdk_libs`.

## How to use

Expand All @@ -41,15 +50,23 @@ plugins {
}

dependencies {
signature('com.toasttab.android:gummy-bears-api-24:0.3.0@signature')
signature('com.toasttab.android:gummy-bears-api-24:0.7.0@signature')
}
```

With core library desugaring:

```groovy
dependencies {
signature('com.toasttab.android:gummy-bears-api-24:0.5.0:coreLib@signature')
signature('com.toasttab.android:gummy-bears-api-24:0.7.0:coreLib@signature')
}
```

With core library desugaring v2:

```groovy
dependencies {
signature('com.toasttab.android:gummy-bears-api-24:0.7.0:coreLib2@signature')
}
```

Expand All @@ -61,7 +78,7 @@ plugins {
}

dependencies {
add("signature", "com.toasttab.android:gummy-bears-api-24:0.5.0@signature")
add("signature", "com.toasttab.android:gummy-bears-api-24:0.7.0@signature")
}
```

Expand All @@ -76,15 +93,16 @@ dependencies {
<signature>
<groupId>com.toasttab.android</groupId>
<artifactId>gummy-bears-api-21</artifactId>
<version>0.5.0</version>
<version>0.7.0</version>
</signature>
</configuration>
</plugin>
```

## Expediter

As of version 0.6.0, this project also publishes [Expediter](/~https://github.com/open-toast/expediter) type descriptors.
As of version 0.6.0, this project also publishes native [Expediter](/~https://github.com/open-toast/expediter) type descriptors. Expediter provides a superset
of the Animal Sniffer binary compatibility checks and comes with its own Gradle plugin.

## License

Expand Down
13 changes: 13 additions & 0 deletions api/19/src/test/java/com/toasttab/android/Api19SignaturesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ class Api19SignaturesTest {
}
}

@Test
fun `core lib v2 signatures include Base64$Decoder#decode`() {
val desc = ObjectInputStream(GZIPInputStream(File(System.getProperty("coreLibSignatures2")).inputStream())).use {
generateSequence { it.readObject() as Clazz? }.toList()
}

val stream = desc.find { it.name == "java/util/Base64\$Decoder" }

expectThat(stream).isNotNull().and {
get { signatures }.contains("decode([B)[B")
}
}

@Test
fun `signatures use HashSet`() {
val desc = ObjectInputStream(GZIPInputStream(File(System.getProperty("signatures")).inputStream())).use {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ import protokt.v1.toasttab.expediter.v1.AccessProtection
import protokt.v1.toasttab.expediter.v1.MemberDescriptor
import protokt.v1.toasttab.expediter.v1.SymbolicReference
import protokt.v1.toasttab.expediter.v1.TypeDescriptors
import protokt.v1.toasttab.expediter.v1.TypeExtensibility
import protokt.v1.toasttab.expediter.v1.TypeFlavor
import strikt.api.expectThat
import strikt.assertions.contains
import strikt.assertions.isEqualTo
import strikt.assertions.isNotNull
import java.io.File
import java.util.zip.GZIPInputStream
Expand Down Expand Up @@ -51,7 +54,7 @@ class Api19TypeDescriptorsTest {
}

@Test
fun `type descriptors include Stream#count()`() {
fun `core lib type descriptors include Stream#count()`() {
val desc = GZIPInputStream(File(System.getProperty("platformCoreLibDescriptors")).inputStream()).use {
TypeDescriptors.deserialize(it)
}
Expand All @@ -71,4 +74,98 @@ class Api19TypeDescriptorsTest {
)
}
}

/**
* java.util.Base64 is included in desugar_jdk_libs starting with version 2.0.4
*/
@Test
fun `core lib v2 type descriptors include Base64$Decoder#decode`() {
val desc = GZIPInputStream(File(System.getProperty("platformCoreLibDescriptors2")).inputStream()).use {
TypeDescriptors.deserialize(it)
}

val decoder = desc.types.find { it.name == "java/util/Base64\$Decoder" }

expectThat(decoder).isNotNull().and {
get { methods }.contains(
MemberDescriptor {
ref = SymbolicReference {
name = "decode"
signature = "([B)[B"
}
protection = AccessProtection.PUBLIC
declaration = AccessDeclaration.INSTANCE
}
)
}
}

/**
* Tests an edge case in merging type descriptors, where multiple versions of `LinuxFileSystemProvider` are provided,
* one extending `UnixFileSystemProvider` and another extending `FileSystem`
*/
@Test
fun `core lib v2 LinuxFileSystemProvider extends UnixFileSystemProvider`() {
val desc = GZIPInputStream(File(System.getProperty("platformCoreLibDescriptors2")).inputStream()).use {
TypeDescriptors.deserialize(it)
}

val provider = desc.types.find { it.name == "sun/nio/fs/LinuxFileSystemProvider" }

expectThat(provider).isNotNull().and {
get { superName }.isEqualTo("sun/nio/fs/UnixFileSystemProvider")
}
}

/**
* Tests an edge case in merging type descriptors, where multiple versions of `MimeTypesFileTypeDetector` are
* provided, one with the transformed `desugar/sun/nio/fs/DesugarAbstractFileTypeDetector` name
*/
@Test
fun `core lib v2 MimeTypesFileTypeDetector extends AbstractFileTypeDetector`() {
val desc = GZIPInputStream(File(System.getProperty("platformCoreLibDescriptors2")).inputStream()).use {
TypeDescriptors.deserialize(it)
}

val detector = desc.types.find { it.name == "sun/nio/fs/MimeTypesFileTypeDetector" }

expectThat(detector).isNotNull().and {
get { superName }.isEqualTo("sun/nio/fs/AbstractFileTypeDetector")
}
}

/**
* Tests an edge case in merging type descriptors, where multiple versions of `IntStream`
* are provided; one is an interface, and another one is a class
*/
@Test
fun `core lib v2 IntStream is an interface`() {
val desc = GZIPInputStream(File(System.getProperty("platformCoreLibDescriptors2")).inputStream()).use {
TypeDescriptors.deserialize(it)
}

val stream = desc.types.find { it.name == "java/util/stream/IntStream" }

expectThat(stream).isNotNull().and {
get { flavor }.isEqualTo(TypeFlavor.INTERFACE)
get { extensibility }.isEqualTo(TypeExtensibility.NOT_FINAL)
}
}

/**
* Tests an edge case in merging type descriptors, where multiple versions of `Character`
* are provided; one is final, and another one is not
*/
@Test
fun `core lib v2 Character is final`() {
val desc = GZIPInputStream(File(System.getProperty("platformCoreLibDescriptors2")).inputStream()).use {
TypeDescriptors.deserialize(it)
}

val character = desc.types.find { it.name == "java/lang/Character" }

expectThat(character).isNotNull().and {
get { extensibility }.isEqualTo(TypeExtensibility.FINAL)
}
}
}
6 changes: 0 additions & 6 deletions basic-sugar/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,3 @@
plugins {
`java-conventions`
}

java {
// necessary to compile classes in java.lang
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package java.lang;
package desugar.java.lang;

public final class DesugarBoolean {
public static int hashCode(boolean b) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package java.lang;
package desugar.java.lang;

public final class DesugarByte {
public static int hashCode(byte i) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package java.lang;
package desugar.java.lang;

public final class DesugarCharacter {
public static int compare(char a, char b) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package java.lang;
package desugar.java.lang;

public final class DesugarDouble {
public static boolean isFinite(double d) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package java.lang;
package desugar.java.lang;

public final class DesugarFloat {
public static int hashCode(float d) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package java.lang;
package desugar.java.lang;

public final class DesugarInteger {
public static int hashCode(int value) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package java.lang;
package desugar.java.lang;

public final class DesugarLong {
public static int hashCode(long value) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package java.lang;
package desugar.java.lang;

public final class DesugarMath {
public static int addExact(int x, int y) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package java.lang;
package desugar.java.lang;

public final class DesugarShort {
public static int hashCode(short i) {
Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/Common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ object Configurations {
const val STANDARD_SUGAR = "sugar"
const val EXERCISE_STANDARD_SUGAR = "exerciseStandardSugar"
const val CORE_LIB_SUGAR = "coreLibSugar"
const val CORE_LIB_SUGAR_2 = "coreLibSugar_2"
}

object Publications {
Expand Down
3 changes: 2 additions & 1 deletion buildSrc/src/main/kotlin/signatures-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
*/

import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.invoke
import org.gradle.kotlin.dsl.named
Expand Down Expand Up @@ -49,6 +48,7 @@ configurations {
create(Configurations.STANDARD_SUGAR)
create(Configurations.EXERCISE_STANDARD_SUGAR)
create(Configurations.CORE_LIB_SUGAR).isTransitive = false
create(Configurations.CORE_LIB_SUGAR_2).isTransitive = false
}

dependencies {
Expand All @@ -59,6 +59,7 @@ dependencies {
add(Configurations.STANDARD_SUGAR, project(":basic-sugar"))
add(Configurations.EXERCISE_STANDARD_SUGAR, project(":test:basic-sugar-treadmill"))
add(Configurations.CORE_LIB_SUGAR, libs.desugarJdkLibs)
add(Configurations.CORE_LIB_SUGAR_2, libs.desugarJdkLibs2)

testImplementation(project(":test:d8-runner"))
testImplementation(libs.junit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import org.gradle.kotlin.dsl.register

private object Tasks {
const val signaturesCoreLib = "buildSignaturesCoreLib"
const val signaturesCoreLib2 = "buildSignaturesCoreLib2"
}

private object Outputs {
const val signaturesCoreLib = "signaturesCoreLib.sig"
const val expediterCoreLib = "platformCoreLib.expediter"
const val signaturesCoreLib2 = "signaturesCoreLib-2.sig"
const val expediterCoreLib2 = "platformCoreLib-2.expediter"
}

plugins {
Expand All @@ -35,9 +38,19 @@ tasks.register<TypeDescriptorsTask>(Tasks.signaturesCoreLib) {
desugar = configurations.getByName(Configurations.STANDARD_SUGAR) + configurations.getByName(Configurations.CORE_LIB_SUGAR)
animalSnifferOutput = project.layout.buildDirectory.file(Outputs.signaturesCoreLib)
expediterOutput = project.layout.buildDirectory.file(Outputs.expediterCoreLib)
outputDescription = "Android API ${project.name} with Core Library Desugaring"
outputDescription = "Android API ${project.name} with Core Library Desugaring 1.x"
}

tasks.register<TypeDescriptorsTask>(Tasks.signaturesCoreLib2) {
classpath = configurations.getByName(Configurations.GENERATOR)
sdk = configurations.getByName(Configurations.SDK)
desugar = configurations.getByName(Configurations.STANDARD_SUGAR) + configurations.getByName(Configurations.CORE_LIB_SUGAR_2)
animalSnifferOutput = project.layout.buildDirectory.file(Outputs.signaturesCoreLib2)
expediterOutput = project.layout.buildDirectory.file(Outputs.expediterCoreLib2)
outputDescription = "Android API ${project.name} with Core Library Desugaring 2.x"
}


publishing.publications.named<MavenPublication>(Publications.MAIN) {
artifact(layout.buildDirectory.file(Outputs.signaturesCoreLib)) {
extension = "signature"
Expand All @@ -50,13 +63,29 @@ publishing.publications.named<MavenPublication>(Publications.MAIN) {
classifier = "coreLib"
builtBy(tasks.named(Tasks.signaturesCoreLib))
}

artifact(layout.buildDirectory.file(Outputs.signaturesCoreLib2)) {
extension = "signature"
classifier = "coreLib2"
builtBy(tasks.named(Tasks.signaturesCoreLib2))
}

artifact(layout.buildDirectory.file(Outputs.expediterCoreLib2)) {
extension = "expediter"
classifier = "coreLib2"
builtBy(tasks.named(Tasks.signaturesCoreLib2))
}
}

tasks {
test {
fileProperty("platformCoreLibDescriptors", layout.buildDirectory.file(Outputs.expediterCoreLib))
fileProperty("coreLibSignatures", layout.buildDirectory.file(Outputs.signaturesCoreLib))

fileProperty("platformCoreLibDescriptors2", layout.buildDirectory.file(Outputs.expediterCoreLib2))
fileProperty("coreLibSignatures2", layout.buildDirectory.file(Outputs.signaturesCoreLib2))

dependsOn(Tasks.signaturesCoreLib)
dependsOn(Tasks.signaturesCoreLib2)
}
}
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ kotlin = "1.9.20"
animalSniffer = "1.22"
clikt = "4.2.1"
expediter = "0.0.7"
desugarJdkLibs = "1.2.2"
desugarJdkLibs = "1.2.3"
desugarJdkLibs2 = "2.0.4"
r8 = "1.5.68"
javapoet = "1.13.0"
javassist = "3.29.2-GA"
Expand All @@ -16,6 +17,7 @@ strikt = "0.34.1"
animalSniffer = { module = "org.codehaus.mojo:animal-sniffer", version.ref = "animalSniffer" }
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" }
desugarJdkLibs2 = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs2" }
expediter-core = { module = "com.toasttab.expediter:core", version.ref = "expediter" }
r8 = { module = "com.android.tools:r8", version.ref = "r8" }
javapoet = { module = "com.squareup:javapoet", version.ref = "javapoet" }
Expand Down
Loading

0 comments on commit 08bae31

Please sign in to comment.