diff --git a/.circleci/config.yml b/.circleci/config.yml index 8d0d3f5..297a1c1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,8 +17,6 @@ jobs: key: v1-gradle-wrapper-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }} - restore_cache: key: v1-gradle-cache-{{ checksum "build.gradle.kts" }} - - restore_cache: - key: v1-sdk-cache-{{ checksum "build.gradle.kts" }} - run: name: Build and test @@ -32,10 +30,6 @@ jobs: paths: - ~/.gradle/caches key: v1-gradle-cache-{{ checksum "build.gradle.kts" }} - - save_cache: - paths: - - build/sdk-archives - key: v1-sdk-cache-{{ checksum "build.gradle.kts" }} - run: name: Save test results @@ -49,6 +43,9 @@ jobs: - store_artifacts: path: ~/test-results/junit + - run: + name: Deploy locally + command: ./gradlew publishToMavenLocal - run: name: Deploy (if release) command: "if [[ \"$CIRCLE_BRANCH\" == master ]]; then ./gradlew publish closeAndReleaseRepository -Dorg.gradle.internal.http.socketTimeout=120000 -Dorg.gradle.internal.network.retry.max.attempts=1 -Dorg.gradle.internal.publish.checksums.insecure=true; else echo skipping publishing; fi" diff --git a/api/19/build.gradle.kts b/api/19/build.gradle.kts index cd85953..5a3063a 100644 --- a/api/19/build.gradle.kts +++ b/api/19/build.gradle.kts @@ -15,7 +15,6 @@ buildSignatures( apiLevel = name, - sdkDir = "android-4.4.2", - sdkFile = "android-19_r04.zip", + sdk = "android-19:r04", coreLibDesugaring = true ) diff --git a/api/20/build.gradle.kts b/api/20/build.gradle.kts index 24118d0..1a5b1ec 100644 --- a/api/20/build.gradle.kts +++ b/api/20/build.gradle.kts @@ -15,7 +15,6 @@ buildSignatures( apiLevel = name, - sdkDir = "android-4.4W.2", - sdkFile = "android-20_r02.zip", + sdk = "android-20:r02", coreLibDesugaring = true ) diff --git a/api/21/build.gradle.kts b/api/21/build.gradle.kts index 5c32532..1824a5a 100644 --- a/api/21/build.gradle.kts +++ b/api/21/build.gradle.kts @@ -15,7 +15,6 @@ buildSignatures( apiLevel = name, - sdkDir = "android-5.0.1", - sdkFile = "android-21_r02.zip", + sdk = "android-21:r02", coreLibDesugaring = true ) diff --git a/api/22/build.gradle.kts b/api/22/build.gradle.kts index 4d85a67..3688254 100644 --- a/api/22/build.gradle.kts +++ b/api/22/build.gradle.kts @@ -15,7 +15,6 @@ buildSignatures( apiLevel = name, - sdkDir = "android-5.1.1", - sdkFile = "android-22_r02.zip", + sdk = "android-22:r02", coreLibDesugaring = true ) diff --git a/api/23/build.gradle.kts b/api/23/build.gradle.kts index 3b14f48..441a475 100644 --- a/api/23/build.gradle.kts +++ b/api/23/build.gradle.kts @@ -15,7 +15,6 @@ buildSignatures( apiLevel = name, - sdkDir = "android-6.0", - sdkFile = "platform-23_r03.zip", + sdk = "platform-23:r03", coreLibDesugaring = true ) diff --git a/api/24/build.gradle.kts b/api/24/build.gradle.kts index 99aae2d..db3b44c 100644 --- a/api/24/build.gradle.kts +++ b/api/24/build.gradle.kts @@ -15,7 +15,6 @@ buildSignatures( apiLevel = name, - sdkDir = "android-7.0", - sdkFile = "platform-24_r02.zip", + sdk = "platform-24:r02", coreLibDesugaring = true ) diff --git a/api/25/build.gradle.kts b/api/25/build.gradle.kts index 0e4e26e..9e6fd57 100644 --- a/api/25/build.gradle.kts +++ b/api/25/build.gradle.kts @@ -15,7 +15,6 @@ buildSignatures( apiLevel = name, - sdkDir = "android-7.1.1", - sdkFile = "platform-25_r03.zip", + sdk = "platform-25:r03", coreLibDesugaring = true ) diff --git a/api/26/build.gradle.kts b/api/26/build.gradle.kts index f788f3f..1445610 100644 --- a/api/26/build.gradle.kts +++ b/api/26/build.gradle.kts @@ -15,7 +15,6 @@ buildSignatures( apiLevel = name, - sdkDir = "android-8.0.0", - sdkFile = "platform-26_r02.zip", + sdk = "platform-26:r02", coreLibDesugaring = true ) diff --git a/api/27/build.gradle.kts b/api/27/build.gradle.kts index 043b155..d026184 100644 --- a/api/27/build.gradle.kts +++ b/api/27/build.gradle.kts @@ -15,7 +15,6 @@ buildSignatures( apiLevel = name, - sdkDir = "android-8.1.0", - sdkFile = "platform-27_r03.zip", + sdk = "platform-27:r03", coreLibDesugaring = true ) diff --git a/api/28/build.gradle.kts b/api/28/build.gradle.kts index 62478d8..ac1d4cd 100644 --- a/api/28/build.gradle.kts +++ b/api/28/build.gradle.kts @@ -15,6 +15,5 @@ buildSignatures( apiLevel = name, - sdkDir = "android-9", - sdkFile = "platform-28_r06.zip" + sdk = "platform-28:r06" ) diff --git a/api/29/build.gradle.kts b/api/29/build.gradle.kts index 1a62c20..241b7f2 100644 --- a/api/29/build.gradle.kts +++ b/api/29/build.gradle.kts @@ -15,6 +15,5 @@ buildSignatures( apiLevel = name, - sdkDir = "android-10", - sdkFile = "platform-29_r04.zip" + sdk = "platform-29:r04" ) diff --git a/api/30/build.gradle.kts b/api/30/build.gradle.kts index 53b3d82..ad72e87 100644 --- a/api/30/build.gradle.kts +++ b/api/30/build.gradle.kts @@ -15,6 +15,5 @@ buildSignatures( apiLevel = name, - sdkDir = "android-11", - sdkFile = "platform-30_r03.zip" + sdk = "platform-30:r03" ) diff --git a/basic-sugar/README.md b/basic-sugar/README.md new file mode 100644 index 0000000..f1e6bf4 --- /dev/null +++ b/basic-sugar/README.md @@ -0,0 +1,5 @@ +# Standard Desugared APIs + +Contains stubs for APIs desugared by vanilla D8 in AGP 3.x+ without core library desugaring. + +Currently must be compiled with java 8. \ No newline at end of file diff --git a/sugar/build.gradle.kts b/basic-sugar/build.gradle.kts similarity index 100% rename from sugar/build.gradle.kts rename to basic-sugar/build.gradle.kts diff --git a/sugar/src/main/java/java/lang/Boolean.java b/basic-sugar/src/main/java/java/lang/Boolean.java similarity index 83% rename from sugar/src/main/java/java/lang/Boolean.java rename to basic-sugar/src/main/java/java/lang/Boolean.java index 2d91e23..c4f0b87 100644 --- a/sugar/src/main/java/java/lang/Boolean.java +++ b/basic-sugar/src/main/java/java/lang/Boolean.java @@ -1,6 +1,6 @@ package java.lang; -public abstract class Boolean implements java.io.Serializable, Comparable { +public abstract class Boolean { public static int hashCode(boolean b) { throw new RuntimeException(); } diff --git a/sugar/src/main/java/java/lang/Byte.java b/basic-sugar/src/main/java/java/lang/Byte.java similarity index 59% rename from sugar/src/main/java/java/lang/Byte.java rename to basic-sugar/src/main/java/java/lang/Byte.java index 164b78d..2e9b367 100644 --- a/sugar/src/main/java/java/lang/Byte.java +++ b/basic-sugar/src/main/java/java/lang/Byte.java @@ -1,6 +1,6 @@ package java.lang; -public abstract class Byte extends Number implements Comparable { +public abstract class Byte { public static int hashCode(byte i) { throw new RuntimeException(); } diff --git a/sugar/src/main/java/java/lang/Character.java b/basic-sugar/src/main/java/java/lang/Character.java similarity index 69% rename from sugar/src/main/java/java/lang/Character.java rename to basic-sugar/src/main/java/java/lang/Character.java index 10a9125..8f0d568 100644 --- a/sugar/src/main/java/java/lang/Character.java +++ b/basic-sugar/src/main/java/java/lang/Character.java @@ -1,6 +1,6 @@ package java.lang; -public abstract class Character implements java.io.Serializable, Comparable { +public abstract class Character { public static int compare(char a, char b) { throw new RuntimeException(); } diff --git a/sugar/src/main/java/java/lang/Double.java b/basic-sugar/src/main/java/java/lang/Double.java similarity index 86% rename from sugar/src/main/java/java/lang/Double.java rename to basic-sugar/src/main/java/java/lang/Double.java index 72cab85..1b06b92 100644 --- a/sugar/src/main/java/java/lang/Double.java +++ b/basic-sugar/src/main/java/java/lang/Double.java @@ -1,6 +1,6 @@ package java.lang; -public abstract class Double extends Number implements Comparable { +public abstract class Double { public static boolean isFinite(double d) { throw new RuntimeException(); } diff --git a/sugar/src/main/java/java/lang/Float.java b/basic-sugar/src/main/java/java/lang/Float.java similarity index 86% rename from sugar/src/main/java/java/lang/Float.java rename to basic-sugar/src/main/java/java/lang/Float.java index d47213a..d453cbb 100644 --- a/sugar/src/main/java/java/lang/Float.java +++ b/basic-sugar/src/main/java/java/lang/Float.java @@ -1,6 +1,6 @@ package java.lang; -public abstract class Float extends Number implements Comparable { +public abstract class Float { public static int hashCode(float d) { throw new RuntimeException(); } diff --git a/sugar/src/main/java/java/lang/Integer.java b/basic-sugar/src/main/java/java/lang/Integer.java similarity index 91% rename from sugar/src/main/java/java/lang/Integer.java rename to basic-sugar/src/main/java/java/lang/Integer.java index 58e30b5..1ebe4e2 100644 --- a/sugar/src/main/java/java/lang/Integer.java +++ b/basic-sugar/src/main/java/java/lang/Integer.java @@ -1,6 +1,6 @@ package java.lang; -public abstract class Integer extends Number implements Comparable { +public abstract class Integer { public static int hashCode(int value) { throw new RuntimeException(); } diff --git a/sugar/src/main/java/java/lang/Long.java b/basic-sugar/src/main/java/java/lang/Long.java similarity index 91% rename from sugar/src/main/java/java/lang/Long.java rename to basic-sugar/src/main/java/java/lang/Long.java index 4053c86..cdc4849 100644 --- a/sugar/src/main/java/java/lang/Long.java +++ b/basic-sugar/src/main/java/java/lang/Long.java @@ -1,6 +1,6 @@ package java.lang; -public abstract class Long extends Number implements Comparable { +public abstract class Long { public static int hashCode(long value) { throw new RuntimeException(); } diff --git a/sugar/src/main/java/java/lang/Math.java b/basic-sugar/src/main/java/java/lang/Math.java similarity index 100% rename from sugar/src/main/java/java/lang/Math.java rename to basic-sugar/src/main/java/java/lang/Math.java diff --git a/sugar/src/main/java/java/lang/Short.java b/basic-sugar/src/main/java/java/lang/Short.java similarity index 59% rename from sugar/src/main/java/java/lang/Short.java rename to basic-sugar/src/main/java/java/lang/Short.java index 0bb8477..5593510 100644 --- a/sugar/src/main/java/java/lang/Short.java +++ b/basic-sugar/src/main/java/java/lang/Short.java @@ -1,6 +1,6 @@ package java.lang; -public abstract class Short extends Number implements Comparable { +public abstract class Short { public static int hashCode(short i) { throw new RuntimeException(); } diff --git a/build.gradle.kts b/build.gradle.kts index e84e0a5..455e942 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,8 @@ allprojects { repositories { google() jcenter() + + androidSdk() } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 951f831..30ca070 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -23,7 +23,5 @@ plugins { } dependencies { - implementation("de.undercouch:gradle-download-task:4.0.2") - implementation("ru.vyarus:gradle-animalsniffer-plugin:1.5.0") implementation("io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.21.2") } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Scopes.kt b/buildSrc/src/main/kotlin/Scopes.kt new file mode 100644 index 0000000..804f9a4 --- /dev/null +++ b/buildSrc/src/main/kotlin/Scopes.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020. Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +object Scopes { + const val generator = "generator" + const val sdk = "sdk" + const val standardSugar = "sugar" + const val exerciseStandardSugar = "exerciseStandardSugar" + const val coreLibSugar = "coreLibSugar" +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/androidSdk.kt b/buildSrc/src/main/kotlin/androidSdk.kt new file mode 100644 index 0000000..3c9e796 --- /dev/null +++ b/buildSrc/src/main/kotlin/androidSdk.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020. Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.gradle.api.Project +import org.gradle.api.artifacts.dsl.RepositoryHandler +import org.gradle.api.artifacts.transform.InputArtifact +import org.gradle.api.artifacts.transform.TransformAction +import org.gradle.api.artifacts.transform.TransformOutputs +import org.gradle.api.artifacts.transform.TransformParameters +import org.gradle.api.attributes.Attribute +import org.gradle.api.file.FileSystemLocation +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.registerTransform +import java.util.zip.ZipFile + +const val SDK_GROUP = "com.google.android.sdk" + +private const val ANDROID_JAR = "android.jar" +private val ARTIFACT_TYPE_ATTRIBUTE = Attribute.of("artifactType", String::class.java) +private val UNPACKED_SDK_ATTRIBUTE = Attribute.of("unpackedSdk", Boolean::class.javaObjectType) + +/** + * This [artifact transform](https://docs.gradle.org/current/userguide/artifact_transforms.html) + * extracts android.jar from the Android SDK Platform zip archive. + */ +abstract class ExtractSdkTransform : TransformAction { + @get:InputArtifact + abstract val inputArtifact: Provider + + override fun transform(outputs: TransformOutputs) { + val sdkArchive = ZipFile(inputArtifact.get().asFile) + + val androidJarEntry = sdkArchive.entries().asSequence().find { it.name.endsWith(ANDROID_JAR) } + + sdkArchive.getInputStream(androidJarEntry).use { i -> + outputs.file(ANDROID_JAR).outputStream().use { o -> + i.copyTo(o) + } + } + } +} + +fun Project.extractSdk() { + configurations.named("sdk") { + attributes.attribute(UNPACKED_SDK_ATTRIBUTE, true) + } + + dependencies { + attributesSchema { + attribute(UNPACKED_SDK_ATTRIBUTE) + } + + artifactTypes.create("zip") { + attributes.attribute(UNPACKED_SDK_ATTRIBUTE, false) + } + + registerTransform(ExtractSdkTransform::class) { + from.attribute(UNPACKED_SDK_ATTRIBUTE, false).attribute(ARTIFACT_TYPE_ATTRIBUTE, "zip") + to.attribute(UNPACKED_SDK_ATTRIBUTE, true).attribute(ARTIFACT_TYPE_ATTRIBUTE, "jar") + } + } +} + + +/** + * Enables Gradle to download artifacts from the Android SDK (non-Maven) repository. + */ +fun RepositoryHandler.androidSdk() { + ivy { + setUrl("https://dl.google.com/android/repository") + + content { + includeGroup(SDK_GROUP) + } + + patternLayout { + artifact("/[module]_[revision].[ext]") + } + + metadataSources { + artifact() + } + } +} diff --git a/buildSrc/src/main/kotlin/dependencies.kt b/buildSrc/src/main/kotlin/dependencies.kt index 32f5c68..94ba7da 100644 --- a/buildSrc/src/main/kotlin/dependencies.kt +++ b/buildSrc/src/main/kotlin/dependencies.kt @@ -14,9 +14,11 @@ */ object versions { + const val animalSniffer = "1.16" + const val clikt = "3.0.1" const val desugarJdkLibs = "1.0.10" const val r8 = "1.5.68" - const val kotlin = "1.3.60" + const val kotlin = "1.4.10" const val javapoet = "1.11.1" const val javassist = "3.26.0-GA" const val junit = "4.12" @@ -24,6 +26,8 @@ object versions { } object libraries { + val animalSniffer = "org.codehaus.mojo:animal-sniffer:${versions.animalSniffer}" + val clikt = "com.github.ajalt.clikt:clikt:${versions.clikt}" val desugarJdkLibs = "com.android.tools:desugar_jdk_libs:${versions.desugarJdkLibs}" val r8 = "com.android.tools:r8:${versions.r8}" val javapoet = "com.squareup:javapoet:${versions.javapoet}" diff --git a/buildSrc/src/main/kotlin/signatures.kt b/buildSrc/src/main/kotlin/signatures.kt index 7318606..a142a5b 100644 --- a/buildSrc/src/main/kotlin/signatures.kt +++ b/buildSrc/src/main/kotlin/signatures.kt @@ -16,7 +16,7 @@ import org.gradle.api.Project import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure @@ -27,101 +27,87 @@ import org.gradle.kotlin.dsl.kotlin import org.gradle.kotlin.dsl.project import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.withType -import ru.vyarus.gradle.plugin.animalsniffer.signature.BuildSignatureTask - -private object Scopes { - const val sugar = "sugar" - const val sugarCalls = "sugarCalls" - const val coreLibDesugaring = "coreLibDesugaring" -} private object Tasks { - const val download = "downloadSdk" - const val unpack = "unpackSdk" - const val signatures = "buildSignatures" const val signaturesCoreLib = "buildSignaturesCoreLib" } private object Outputs { - const val signatures = "signatures" - const val signaturesCoreLib = "signaturesCoreLib" + const val signatures = "signatures.sig" + const val signaturesCoreLib = "signaturesCoreLib.sig" } fun Project.buildSignatures( apiLevel: String, - sdkFile: String, - sdkDir: String, + sdk: String, coreLibDesugaring: Boolean = false ) { apply(plugin = "kotlin") apply(plugin = "maven-publish") - apply(plugin = "ru.vyarus.animalsniffer") - apply(plugin = "de.undercouch.download") apply(plugin = "signing") configurations { - create(Scopes.sugar) - create(Scopes.sugarCalls) - create(Scopes.coreLibDesugaring) + create(Scopes.sdk) + create(Scopes.generator) + create(Scopes.standardSugar) + create(Scopes.exerciseStandardSugar) + create(Scopes.coreLibSugar).isTransitive = false } dependencies { - add("implementation", kotlin("stdlib-jdk8")) + extractSdk() + + add("testImplementation", kotlin("stdlib-jdk8")) - add(Scopes.sugar, project(":sugar")) - add(Scopes.sugarCalls, project(":test:sugar-calls")) - add(Scopes.coreLibDesugaring, libraries.desugarJdkLibs) + add(Scopes.generator, project(":signature-builder")) - add("testImplementation", project(":test:d8-common")) + add(Scopes.sdk, "$SDK_GROUP:$sdk@zip") + add(Scopes.standardSugar, project(":basic-sugar")) + add(Scopes.exerciseStandardSugar, project(":test:basic-sugar-treadmill")) + add(Scopes.coreLibSugar, libraries.desugarJdkLibs) + + add("testImplementation", project(":test:d8-runner")) add("testImplementation", libraries.junit) add("testImplementation", libraries.truth) } - val sdk = project.file("$buildDir/sdk/$sdkDir/android.jar") - tasks.withType { - dependsOn("unpackSdk") - dependsOn(":sugar:build") - dependsOn(":test:sugar-calls:build") + dependsOn(":basic-sugar:build") + dependsOn(":test:basic-sugar-treadmill:build") - systemProperty("sdk", sdk) - systemProperty("jar", configurations.getByName(Scopes.sugarCalls).asPath) + systemProperty("sdk", configurations.getByName(Scopes.sdk).asPath) + systemProperty("jar", configurations.getByName(Scopes.exerciseStandardSugar).asPath) systemProperty("dexout", project.buildDir) } - tasks.register(Tasks.download) { - tempAndMove(true) - onlyIfModified(true) - src("https://dl.google.com/android/repository/$sdkFile") - dest("${rootProject.buildDir}/sdk-archives/$sdkFile") - } - - tasks.register(Tasks.unpack) { - dependsOn(Tasks.download) - from(zipTree("${rootProject.buildDir}/sdk-archives/$sdkFile")) - into("$buildDir/sdk") - } - - tasks.register(Tasks.signatures) { - dependsOn(Tasks.unpack) - - files(configurations.getByName(Scopes.sugar).asPath) - files(sdk) - - outputName = Outputs.signatures + tasks.register(Tasks.signatures) { + classpath = configurations.getByName("generator").asFileTree + main = "com.toasttab.animalsniffer.AndroidSignatureBuilderKt" + args = listOf( + "--sdk", + configurations.getByName(Scopes.sdk).asPath, + "--desugared", + configurations.getByName(Scopes.standardSugar).asPath, + "--output", + "$buildDir/${Outputs.signatures}" + ) } if (coreLibDesugaring) { - tasks.register(Tasks.signaturesCoreLib) { - dependsOn(Tasks.unpack) - - files(configurations.getByName(Scopes.sugar).asPath) - files(configurations.getByName(Scopes.coreLibDesugaring).asPath) - files(sdk) - exclude("java.lang.*8") - - outputName = Outputs.signaturesCoreLib + tasks.register(Tasks.signaturesCoreLib) { + classpath = configurations.getByName("generator").asFileTree + main = "com.toasttab.animalsniffer.AndroidSignatureBuilderKt" + args = listOf( + "--sdk", + configurations.getByName(Scopes.sdk).asPath, + "--desugared", + configurations.getByName(Scopes.coreLibSugar).asPath, + "--desugared", + configurations.getByName(Scopes.standardSugar).asPath, + "--output", + "$buildDir/${Outputs.signaturesCoreLib}" + ) } } @@ -132,7 +118,7 @@ fun Project.buildSignatures( groupId = "${project.group}" version = "${project.version}" artifactId = "gummy-bears-api-$apiLevel" - artifact("${project.buildDir}/animalsniffer/${Tasks.signatures}/${Outputs.signatures}.sig") { + artifact("$buildDir/${Outputs.signatures}") { extension = "signature" builtBy(tasks.named(Tasks.signatures)) } @@ -145,7 +131,7 @@ fun Project.buildSignatures( groupId = "${project.group}" version = "${project.version}" artifactId = "gummy-bears-api-$apiLevel" - artifact("${project.buildDir}/animalsniffer/${Tasks.signaturesCoreLib}/${Outputs.signaturesCoreLib}.sig") { + artifact("$buildDir/${Outputs.signaturesCoreLib}") { extension = "signature" classifier = "coreLib" builtBy(tasks.named(Tasks.signaturesCoreLib)) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 12d38de..be52383 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index bb737a3..e4918a3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,10 +16,11 @@ rootProject.name = "gummybears" include( - "sugar", - "test:d8-common", - "test:sugar-call-generator", - "test:sugar-calls" + "signature-builder", + "basic-sugar", + "test:d8-runner", + "test:api-treadmill", + "test:basic-sugar-treadmill" ) (19..30).forEach { diff --git a/signature-builder/README.md b/signature-builder/README.md new file mode 100644 index 0000000..96d6b25 --- /dev/null +++ b/signature-builder/README.md @@ -0,0 +1,8 @@ +# Signature Builder + +This is a special-purpose wrapper around Animal Sniffer that generates Android signatures from the standard SDK jar +and auxiliary jars with desugared APIs such as Google's desugar_jdk_libs. + +The wrapper supports name retargeting in the auxiliary jars (e.g. `DesugarDate` to `Date` and `Long8` to `Long`) and +is less strict compared to vanilla Animal Sniffer when merging signatures (e.g. does not care that `Long8` extends +`Object`, not `Number`.) \ No newline at end of file diff --git a/signature-builder/build.gradle.kts b/signature-builder/build.gradle.kts new file mode 100644 index 0000000..1663ecc --- /dev/null +++ b/signature-builder/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020. Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + kotlin("jvm") +} + +dependencies { + implementation(kotlin("stdlib-jdk8")) + implementation(libraries.animalSniffer) + implementation(libraries.clikt) +} \ No newline at end of file diff --git a/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/AndroidSignatureBuilder.kt b/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/AndroidSignatureBuilder.kt new file mode 100644 index 0000000..8238aa1 --- /dev/null +++ b/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/AndroidSignatureBuilder.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020. Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.toasttab.animalsniffer + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.multiple +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import java.io.File + +class AndroidSignatureBuilder : CliktCommand() { + private val sdk: String by option(help = "SDK jar").required() + private val desugared: List by option(help = "desugared API jar(s)").multiple() + private val output: String by option(help = "output").required() + + override fun run() { + val signatures = MutableSignatures(SignatureIo.create(File(sdk))) + + for (more in desugared) { + SignatureIo.create(File(more)).forEach { + signatures.add(DesugarSignatureTransformer.transform(it)) + } + } + + File(output).absoluteFile.apply { + parentFile.mkdirs() + + outputStream().use { + SignatureIo.serialize(signatures.classes(), it) + } + } + } +} + +fun main(args: Array) { + AndroidSignatureBuilder().main(args) +} \ No newline at end of file diff --git a/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/DesugarSignatureTransformer.kt b/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/DesugarSignatureTransformer.kt new file mode 100644 index 0000000..ef81e07 --- /dev/null +++ b/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/DesugarSignatureTransformer.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020. Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.toasttab.animalsniffer + +import org.codehaus.mojo.animal_sniffer.Clazz + +sealed class ShouldTransform { + object No: ShouldTransform() + class Yes(val newName: String): ShouldTransform() +} + +object DesugarSignatureTransformer { + fun shouldTransform(name: String): ShouldTransform { + if (name.endsWith("8")) { + return ShouldTransform.Yes(name.removeSuffix("8")) + } else if (name.substringAfterLast("/").startsWith("Desugar")) { + return ShouldTransform.Yes(name.substringBeforeLast("/") + "/" + name.substringAfterLast("/Desugar")) + } else { + return ShouldTransform.No + } + } + + fun shouldTransform(clz: Clazz) = shouldTransform(clz.name) + + fun transform(clz: Clazz) = + when (val shouldTransform = shouldTransform(clz)) { + is ShouldTransform.No -> clz + is ShouldTransform.Yes -> Clazz( + shouldTransform.newName, + clz.signatures, + clz.superClass, + clz.superInterfaces + ) + } +} \ No newline at end of file diff --git a/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/MutableSignatures.kt b/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/MutableSignatures.kt new file mode 100644 index 0000000..4116063 --- /dev/null +++ b/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/MutableSignatures.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020. Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.toasttab.animalsniffer + +import org.codehaus.mojo.animal_sniffer.Clazz +import java.lang.IllegalArgumentException + +class MutableSignatures { + private val classes: MutableMap = hashMapOf() + + constructor(initial: Sequence) { + initial.associateByTo(classes) { + it.name + } + } + + fun add(clz: Clazz) { + classes.merge(clz.name, clz) { oldValue, newValue -> + val superClass = when (newValue.superClass) { + "java/lang/Object" -> oldValue.superClass + oldValue.superClass -> oldValue.superClass + else -> throw IllegalArgumentException("conflicting superclasses ${oldValue.superClass} and ${newValue.superClass} for ${clz.name}") + } + + Clazz( + clz.name, oldValue.signatures + newValue.signatures, superClass, ( + (oldValue.superInterfaces.toSet() + newValue.superInterfaces).toTypedArray() + ) + ) + } + } + + fun classes(): Sequence = classes.values.asSequence() +} \ No newline at end of file diff --git a/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/SignatureIo.kt b/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/SignatureIo.kt new file mode 100644 index 0000000..3e9867f --- /dev/null +++ b/signature-builder/src/main/kotlin/com/toasttab/animalsniffer/SignatureIo.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020. Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.toasttab.animalsniffer + +import org.codehaus.mojo.animal_sniffer.Clazz +import org.codehaus.mojo.animal_sniffer.SignatureBuilder +import org.codehaus.mojo.animal_sniffer.logging.PrintWriterLogger +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.InputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.OutputStream +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +object SignatureIo { + fun create(file: File): Sequence { + val out = ByteArrayOutputStream() + + SignatureBuilder(out, PrintWriterLogger(System.err)).apply { + process(file) + close() + } + + return deserialize(out.toByteArray().inputStream()) + } + + fun serialize(classes: Sequence, out: OutputStream) { + ObjectOutputStream(GZIPOutputStream(out)).use { + for (cls in classes) { + it.writeObject(cls) + } + + it.writeObject(null) + } + } + + fun deserialize(inputStream: InputStream) = sequence { + inputStream.use { + val objectInputStream = ObjectInputStream(GZIPInputStream(inputStream)) + do { + val cls = objectInputStream.readObject() as Clazz? + if (cls != null) { + yield(cls) + } + } while (cls != null) + } + } +} \ No newline at end of file diff --git a/test/api-treadmill/README.md b/test/api-treadmill/README.md new file mode 100644 index 0000000..b008bf5 --- /dev/null +++ b/test/api-treadmill/README.md @@ -0,0 +1,3 @@ +# Helps Exercise APIs + +Generates dummy code that exercises all public methods from a jar. diff --git a/test/sugar-call-generator/build.gradle.kts b/test/api-treadmill/build.gradle.kts similarity index 95% rename from test/sugar-call-generator/build.gradle.kts rename to test/api-treadmill/build.gradle.kts index 79bc9d2..ffc9f30 100644 --- a/test/sugar-call-generator/build.gradle.kts +++ b/test/api-treadmill/build.gradle.kts @@ -18,6 +18,7 @@ plugins { } dependencies { + implementation(libraries.clikt) implementation(libraries.javapoet) implementation(libraries.javassist) implementation(kotlin("stdlib-jdk8")) diff --git a/test/api-treadmill/src/main/java/com/toasttab/android/ApiUseGenerator.kt b/test/api-treadmill/src/main/java/com/toasttab/android/ApiUseGenerator.kt new file mode 100644 index 0000000..6e8ae17 --- /dev/null +++ b/test/api-treadmill/src/main/java/com/toasttab/android/ApiUseGenerator.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2020. Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.toasttab.android + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.squareup.javapoet.ClassName +import com.squareup.javapoet.FieldSpec +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.MethodSpec +import com.squareup.javapoet.TypeName +import com.squareup.javapoet.TypeSpec +import javassist.ClassPool +import javassist.CtClass +import javassist.CtPrimitiveType +import javassist.bytecode.ClassFile +import javassist.bytecode.Descriptor +import javassist.bytecode.MethodInfo +import java.io.DataInputStream +import java.io.File +import java.util.jar.JarFile +import javax.lang.model.element.Modifier + +class ApiUseGenerator : CliktCommand() { + private val jar: String by option(help = "jar with APIs").required() + private val output: String by option(help = "output directory for generated classes").required() + + private fun listClasses(jar: File): Sequence { + val jf = JarFile(jar) + return jf.entries().asSequence().filter { it.name.endsWith(".class") }.map { + jf.getInputStream(it).use { s -> + ClassFile(DataInputStream(s)) + } + } + } + + private fun CtClass.typeName(): TypeName { + if (this is CtPrimitiveType) { + return fqnToClassName(wrapperName).unbox() + } else if (this.isArray) { + return fqnToClassName(name) + } else { + return fqnToClassName(name) + } + } + + private fun fqnToClassName(fqn: String) = ClassName.get(fqn.substringBeforeLast("."), fqn.substringAfterLast(".")) + + private fun generateMethodStubCaller(method: MethodInfo): MethodSpec { + val paramTypes = Descriptor.getParameterTypes(method.descriptor, ClassPool.getDefault()) + val returnType = Descriptor.getReturnType(method.descriptor, ClassPool.getDefault()) + + val params = paramTypes.indices.joinToString { "arg$it" }; + val instruction = if (returnType == CtClass.voidType) "" else "return " + + return MethodSpec.methodBuilder(method.name) + .addModifiers(Modifier.PUBLIC) + .returns(returnType.typeName()) + .apply { + paramTypes.forEachIndexed { i, p -> + addParameter(p.typeName(), "arg$i") + } + } + .addCode( + "$instruction callee.${method.name}($params);\n" + ) + .build() + } + + private fun generateStubCallers(cls: ClassFile): JavaFile { + val className = cls.name.replace(".", "_"); + val builder = TypeSpec.classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addField( + FieldSpec.builder( + fqnToClassName(cls.name), "callee" + ).build() + ) + + cls.methods.filter { it.isMethod }.forEach { method -> + builder.addMethod(generateMethodStubCaller(method)) + } + + return JavaFile.builder("com.toasttab.android.stub", builder.build()).build() + } + + private fun write(cls: ClassFile, output: File) { + generateStubCallers(cls).writeTo(output) + } + + override fun run() { + listClasses(File(jar)).forEach { + write(it, File(output)) + } + } +} + +fun main(args: Array) { + ApiUseGenerator().main(args) +} diff --git a/test/basic-sugar-treadmill/README.md b/test/basic-sugar-treadmill/README.md new file mode 100644 index 0000000..e6a0f7b --- /dev/null +++ b/test/basic-sugar-treadmill/README.md @@ -0,0 +1,6 @@ +# Exercises Desugared APIs + +Exercises all public methods in [basic-sugar](../../basic-sugar) using [api-treadmill](../api-treadmill). + +Integration tests feed the output of this project through D8 to verify that [basic-sugar](../../basic-sugar)'s +API stubs can indeed be desugared. \ No newline at end of file diff --git a/test/sugar-calls/build.gradle.kts b/test/basic-sugar-treadmill/build.gradle.kts similarity index 57% rename from test/sugar-calls/build.gradle.kts rename to test/basic-sugar-treadmill/build.gradle.kts index fc00f6b..9df154f 100644 --- a/test/sugar-calls/build.gradle.kts +++ b/test/basic-sugar-treadmill/build.gradle.kts @@ -14,27 +14,32 @@ */ plugins { - java + `java-library` } configurations { - create("sugar") - create("generator") + create(Scopes.standardSugar) + create(Scopes.generator) } dependencies { - add("sugar", project(":sugar")) - add("generator", project(":test:sugar-call-generator")) + add(Scopes.standardSugar, project(":basic-sugar")) + add(Scopes.generator, project(":test:api-treadmill")) } -tasks.register("generateStubCalls") { - classpath = configurations.getByName("generator").asFileTree - main = "com.toasttab.android.StubCallGeneratorKt" - args = listOf(configurations.getByName("sugar").asPath, "${project.buildDir}/generated-sources/java/main") +tasks.register("generateClasses") { + classpath = configurations.getByName(Scopes.generator).asFileTree + main = "com.toasttab.android.ApiUseGeneratorKt" + args = listOf( + "--jar", + configurations.getByName(Scopes.standardSugar).asPath, + "--output", + "${project.buildDir}/generated-sources/java/main" + ) } tasks.named("compileJava") { - dependsOn("generateStubCalls") + dependsOn("generateClasses") } sourceSets.main { diff --git a/test/d8-runner/README.md b/test/d8-runner/README.md new file mode 100644 index 0000000..94b8644 --- /dev/null +++ b/test/d8-runner/README.md @@ -0,0 +1,3 @@ +# D8 Runner + +Runs D8 and captures diagnostic output. \ No newline at end of file diff --git a/test/d8-common/build.gradle.kts b/test/d8-runner/build.gradle.kts similarity index 100% rename from test/d8-common/build.gradle.kts rename to test/d8-runner/build.gradle.kts diff --git a/test/d8-common/src/main/java/com/toasttab/android/test/D8Runner.kt b/test/d8-runner/src/main/java/com/toasttab/android/test/D8Runner.kt similarity index 100% rename from test/d8-common/src/main/java/com/toasttab/android/test/D8Runner.kt rename to test/d8-runner/src/main/java/com/toasttab/android/test/D8Runner.kt diff --git a/test/sugar-call-generator/src/main/java/com/toasttab/android/StubCallGenerator.kt b/test/sugar-call-generator/src/main/java/com/toasttab/android/StubCallGenerator.kt deleted file mode 100644 index 01d5693..0000000 --- a/test/sugar-call-generator/src/main/java/com/toasttab/android/StubCallGenerator.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2020. Toast Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.toasttab.android - -import com.squareup.javapoet.ClassName -import com.squareup.javapoet.FieldSpec -import com.squareup.javapoet.JavaFile -import com.squareup.javapoet.MethodSpec -import com.squareup.javapoet.TypeName -import com.squareup.javapoet.TypeSpec -import javassist.ClassPool -import javassist.CtClass -import javassist.CtPrimitiveType -import javassist.bytecode.ClassFile -import javassist.bytecode.Descriptor -import javassist.bytecode.MethodInfo -import java.io.DataInputStream -import java.io.File -import java.util.jar.JarFile -import javax.lang.model.element.Modifier - -fun main(args: Array) { - val jarLocation = args[0] - val output = File(args[1]) - - listClasses(File(jarLocation)).forEach { - write(it, output) - } -} - -private fun listClasses(jar: File): Sequence { - val jf = JarFile(jar) - return jf.entries().asSequence().filter { it.name.endsWith(".class") }.map { - jf.getInputStream(it).use { s -> - ClassFile(DataInputStream(s)) - } - } -} - -private fun CtClass.typeName(): TypeName { - if (this is CtPrimitiveType) { - return fqnToClassName(wrapperName).unbox() - } else if (this.isArray) { - return fqnToClassName(name) - } else { - return fqnToClassName(name) - } -} - -private fun fqnToClassName(fqn: String) = ClassName.get(fqn.substringBeforeLast("."), fqn.substringAfterLast(".")) - -private fun generateMethodStubCaller(method: MethodInfo): MethodSpec { - val paramTypes = Descriptor.getParameterTypes(method.descriptor, ClassPool.getDefault()) - val returnType = Descriptor.getReturnType(method.descriptor, ClassPool.getDefault()) - - val params = paramTypes.indices.joinToString{ "arg$it" }; - val instruction = if (returnType == CtClass.voidType) "" else "return " - - return MethodSpec.methodBuilder(method.name) - .addModifiers(Modifier.PUBLIC) - .returns(returnType.typeName()) - .apply { - paramTypes.forEachIndexed { i, p -> - addParameter(p.typeName(), "arg$i") - } - } - .addCode( - "$instruction callee.${method.name}($params);\n" - ) - .build() -} - -private fun generateStubCallers(cls: ClassFile): JavaFile { - val className = cls.name.replace(".", "_"); - val builder = TypeSpec.classBuilder(className) - .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) - .addField(FieldSpec.builder( - fqnToClassName(cls.name), "callee" - ).build()) - - cls.methods.filter { it.isMethod }.forEach { method -> - builder.addMethod(generateMethodStubCaller(method)) - } - - return JavaFile.builder("com.toasttab.android.stub", builder.build()).build() -} - -private fun write(cls: ClassFile, output: File) { - generateStubCallers(cls).writeTo(output) -} \ No newline at end of file