From b5fd760fc0ffed1025629fe76a061716e24fb9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Varga?= Date: Mon, 29 Jul 2024 14:55:14 +0200 Subject: [PATCH] build: Fix --- .idea/.gitignore | 1 + .idea/gradle.xml | 3 +- build.gradle | 2 +- buildSrc/build.gradle | 32 +++- .../org/robolectric/gradle/AarDepsPlugin.java | 11 +- .../gradle/agp/ExtractAarTransform.java | 172 ++++++++++++++++++ .../agp/GenericTransformParameters.java | 33 ++++ gradle/libs.versions.toml | 6 +- integration-tests/agp-groovy-dsl/build.gradle | 2 +- .../agp-kotlin-dsl/build.gradle.kts | 2 +- ...ectricExtensionCustomAndroidSdkSelfTest.kt | 14 +- .../RobolectricExtensionSelfTest.kt | 3 - settings.gradle | 2 +- 13 files changed, 254 insertions(+), 29 deletions(-) create mode 100644 buildSrc/src/main/groovy/org/robolectric/gradle/agp/ExtractAarTransform.java create mode 100644 buildSrc/src/main/groovy/org/robolectric/gradle/agp/GenericTransformParameters.java diff --git a/.idea/.gitignore b/.idea/.gitignore index 8b6e44d..71d8ab8 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -7,3 +7,4 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +/other.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml index b91666f..33f7bc8 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -24,7 +24,6 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index d8fa90a..e95dee6 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ subprojects { dependencies { kover(project(':integration-tests:agp-groovy-dsl')) - kover(project(':integration-tests:agp-kotlin-dsl')) + // kover(project(':integration-tests:agp-kotlin-dsl')) kover(project(':robolectric-extension')) kover(project(':robolectric-extension-gradle-plugin')) } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 272f92d..4693865 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -12,7 +12,8 @@ dependencies { compileOnly(gradleApi()) compileOnly(localGroovy()) implementation(libs.guava) - implementation(libs.androidGradle) + implementation(libs.androidGradleApi) + implementation(libs.androidToolsCommon) } java { @@ -22,18 +23,33 @@ java { } task downloadAarDepsPlugin { - final from = "https://raw.githubusercontent.com/robolectric/robolectric/robolectric-${libs.versions.robolectric.get()}/buildSrc/src/main/groovy/org/robolectric/gradle/AarDepsPlugin.java" + final baseUrl = "https://raw.githubusercontent.com/robolectric/robolectric/robolectric-${libs.versions.robolectric.get()}/" + final from = [ + "${baseUrl}buildSrc/src/main/groovy/org/robolectric/gradle/AarDepsPlugin.java", + "${baseUrl}buildSrc/src/main/groovy/org/robolectric/gradle/agp/ExtractAarTransform.java", + "${baseUrl}buildSrc/src/main/groovy/org/robolectric/gradle/agp/GenericTransformParameters.java", + + ] final groovySourceSet = sourceSets.named('main').get().allSource.sourceDirectories.find { it.name == 'groovy' } as File - final to = new File(groovySourceSet, '/org/robolectric/gradle/AarDepsPlugin.java') + final to = [ + new File(groovySourceSet, '/org/robolectric/gradle/AarDepsPlugin.java'), + new File(groovySourceSet, '/org/robolectric/gradle/agp/ExtractAarTransform.java'), + new File(groovySourceSet, '/org/robolectric/gradle/agp/GenericTransformParameters.java'), + ] + inputs.property("from", from) - outputs.file(to) + outputs.files(to) doLast { - try { - new URL(from).withInputStream { i -> to.withOutputStream { it << i } } - } catch (IOException e) { - logger.debug("Error during downloading AarDepsPlugin. Keep the stored version.\n$e") + from.indices.forEach { i -> + final url = from[i] + final targetFile = to[i] + try { + new URL(url).withInputStream { inputStream -> targetFile.withOutputStream { it << inputStream } } + } catch (IOException e) { + logger.debug("Error during downloading ${url.name}. Keep the stored version.\n$e") + } } } } diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/AarDepsPlugin.java b/buildSrc/src/main/groovy/org/robolectric/gradle/AarDepsPlugin.java index 9322d6c..e5d1934 100644 --- a/buildSrc/src/main/groovy/org/robolectric/gradle/AarDepsPlugin.java +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/AarDepsPlugin.java @@ -2,12 +2,12 @@ import static org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE; -import com.android.build.gradle.internal.dependency.ExtractAarTransform; -import com.google.common.base.Joiner; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import javax.inject.Inject; import org.gradle.api.Action; import org.gradle.api.Plugin; @@ -17,6 +17,7 @@ import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.compile.JavaCompile; import org.jetbrains.annotations.NotNull; +import org.robolectric.gradle.agp.ExtractAarTransform; /** Resolve aar dependencies into jars for non-Android projects. */ public class AarDepsPlugin implements Plugin { @@ -63,7 +64,11 @@ public void execute(@NotNull Task task) { List aarFiles = AarDepsPlugin.this.findAarFiles(t.getClasspath()); if (!aarFiles.isEmpty()) { throw new IllegalStateException( - "AARs on classpath: " + Joiner.on("\n ").join(aarFiles)); + "AARs on classpath: " + + aarFiles.stream() + .filter(Objects::nonNull) + .map(File::toString) + .collect(Collectors.joining("\n "))); } } })); diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/agp/ExtractAarTransform.java b/buildSrc/src/main/groovy/org/robolectric/gradle/agp/ExtractAarTransform.java new file mode 100644 index 0000000..571415c --- /dev/null +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/agp/ExtractAarTransform.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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. + */ + +/* + * This class comes from AGP internals: + * https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/ExtractAarTransform.kt;bpv=0 + */ + +package org.robolectric.gradle.agp; + +import com.android.SdkConstants; +import com.android.utils.FileUtils; +import com.google.common.io.Files; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +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.file.FileSystemLocation; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Classpath; +import org.gradle.work.DisableCachingByDefault; +import org.jetbrains.annotations.NotNull; + +// TODO Keep the original Kotlin implementation when `buildSrc` is migrated to Kotlin. +@DisableCachingByDefault(because = "Copy task") +public abstract class ExtractAarTransform implements TransformAction { + @Classpath + @InputArtifact + public abstract Provider getAarFile(); + + @Override + public void transform(@NotNull TransformOutputs outputs) { + // TODO(b/162813654) record transform execution span + File inputFile = getAarFile().get().getAsFile(); + String inputFileNameWithoutExtension = Files.getNameWithoutExtension(inputFile.getName()); + File outputDir = outputs.dir(inputFileNameWithoutExtension); + FileUtils.mkdirs(outputDir); + new AarExtractor().extract(inputFile, outputDir); + } +} + +class AarExtractor { + private static final String LIBS_PREFIX = SdkConstants.LIBS_FOLDER + '/'; + private static final int LIBS_PREFIX_LENGTH = LIBS_PREFIX.length(); + private static final int JARS_PREFIX_LENGTH = SdkConstants.FD_JARS.length() + 1; + + // Note: + // - A jar doesn't need a manifest entry, but if we ever want to create a manifest entry, be + // sure to set a fixed timestamp for it so that the jar is deterministic (see b/315336689). + // - This empty jar takes up only ~22 bytes, so we don't need to GC it at the end of the build. + private static final byte[] emptyJar; + + /** + * {@link StringBuilder} used to construct all paths. It gets truncated back to {@link + * JARS_PREFIX_LENGTH} on every calculation. + */ + private final StringBuilder stringBuilder = new StringBuilder(60); + + static { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + //noinspection EmptyTryBlock + try (JarOutputStream outputStream = new JarOutputStream(byteArrayOutputStream)) { + } catch (IOException e) { + throw new RuntimeException(e); + } + + emptyJar = byteArrayOutputStream.toByteArray(); + } + + AarExtractor() { + stringBuilder.append(SdkConstants.FD_JARS); + stringBuilder.append(File.separatorChar); + } + + private String choosePathInOutput(@NotNull String entryName) { + stringBuilder.setLength(JARS_PREFIX_LENGTH); + + if (entryName.equals(SdkConstants.FN_CLASSES_JAR) + || entryName.equals(SdkConstants.FN_LINT_JAR)) { + stringBuilder.append(entryName); + + return stringBuilder.toString(); + } else if (entryName.startsWith(LIBS_PREFIX)) { + // In case we have libs/classes.jar we are going to rename them, due an issue in + // Gradle. + // TODO: stop doing this once this is fixed in gradle. b/65298222 + String pathWithinLibs = entryName.substring(LIBS_PREFIX_LENGTH); + + if (pathWithinLibs.equals(SdkConstants.FN_CLASSES_JAR)) { + stringBuilder.append(LIBS_PREFIX).append("classes-2" + SdkConstants.DOT_JAR); + } else if (pathWithinLibs.equals(SdkConstants.FN_LINT_JAR)) { + stringBuilder.append(LIBS_PREFIX).append("lint-2" + SdkConstants.DOT_JAR); + } else { + stringBuilder.append(LIBS_PREFIX).append(pathWithinLibs); + } + + return stringBuilder.toString(); + } else { + return entryName; + } + } + + /** + * Extracts an AAR file into a directory. + * + *

Note: There are small adjustments made to the extracted contents. For example, classes.jar + * inside the AAR will be extracted to jars/classes.jar, and if the jar does not exist, we will + * create an empty classes.jar. + */ + void extract(@NotNull File aar, @NotNull File outputDir) { + try (ZipInputStream zipInputStream = + new ZipInputStream(java.nio.file.Files.newInputStream(aar.toPath()))) { + while (true) { + ZipEntry entry = zipInputStream.getNextEntry(); + if (entry == null) { + break; + } + + if (entry.isDirectory() || entry.getName().contains("../") || entry.getName().isEmpty()) { + continue; + } + + String path = FileUtils.toSystemDependentPath(choosePathInOutput(entry.getName())); + File outputFile = new File(outputDir, path); + Files.createParentDirs(outputFile); + Files.asByteSink(outputFile).writeFrom(zipInputStream); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + // If classes.jar does not exist, create an empty one + File classesJar = resolve(outputDir, SdkConstants.FD_JARS + "/" + SdkConstants.FN_CLASSES_JAR); + if (!classesJar.exists()) { + try { + Files.createParentDirs(classesJar); + Files.write(emptyJar, classesJar); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @NotNull + private File resolve(@NotNull File source, @NotNull String relative) { + Path baseDir = source.toPath(); + Path relativeFile = Paths.get(relative); + Path resolvedFile = baseDir.resolve(relativeFile); + + return resolvedFile.toFile(); + } +} diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/agp/GenericTransformParameters.java b/buildSrc/src/main/groovy/org/robolectric/gradle/agp/GenericTransformParameters.java new file mode 100644 index 0000000..8205323 --- /dev/null +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/agp/GenericTransformParameters.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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. + */ + +/* + * This class comes from AGP internals: + * https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/GenericTransformParameters.kt;bpv=0 + */ + +package org.robolectric.gradle.agp; + +import org.gradle.api.artifacts.transform.TransformParameters; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Internal; + +/** Generic {@link TransformParameters} for all of our Artifact Transforms. */ +// TODO Keep the original Kotlin implementation when `buildSrc` is migrated to Kotlin. +public interface GenericTransformParameters extends TransformParameters { + @Internal + Property getProjectName(); +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 954daec..1da7d1c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,9 @@ [versions] androidBuildTools = "34.0.0" +androidToolsCommon = "31.5.1" androidCompileSdk = "34" androidGradle = "8.5.1" -androidMinimumSdk = "14" +androidMinimumSdk = "19" androidxTestExtJunit = "1.2.1" buildConfig = "5.4.0" detekt = "1.23.6" @@ -21,8 +22,9 @@ robolectricExtensionGradlePlugin = "0.7.0" sources = "sources" [libraries] -androidGradle = { module = "com.android.tools.build:gradle", version.ref = "androidGradle" } +androidGradleApi = { module = "com.android.tools.build:gradle-api", version.ref = "androidGradle" } androidGradleJava11 = { module = "com.android.tools.build:gradle", version = { require = "[7.0.0,8.0.0[", prefer = "7.4.2" } } +androidToolsCommon = { module = "com.android.tools:common", version.ref = "androidToolsCommon" } androidxTestExtJunit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExtJunit" } detektFormatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detektRulesLibraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" } diff --git a/integration-tests/agp-groovy-dsl/build.gradle b/integration-tests/agp-groovy-dsl/build.gradle index 5ef4dec..468ce61 100644 --- a/integration-tests/agp-groovy-dsl/build.gradle +++ b/integration-tests/agp-groovy-dsl/build.gradle @@ -1,5 +1,5 @@ plugins { - id('com.android.library') + alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) alias(libs.plugins.kotlinxKover) alias(libs.plugins.detekt) diff --git a/integration-tests/agp-kotlin-dsl/build.gradle.kts b/integration-tests/agp-kotlin-dsl/build.gradle.kts index eb480cb..64c280b 100644 --- a/integration-tests/agp-kotlin-dsl/build.gradle.kts +++ b/integration-tests/agp-kotlin-dsl/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.android.library") + alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) alias(libs.plugins.kotlinxKover) alias(libs.plugins.detekt) diff --git a/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionCustomAndroidSdkSelfTest.kt b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionCustomAndroidSdkSelfTest.kt index 656fbcc..e9f188e 100644 --- a/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionCustomAndroidSdkSelfTest.kt +++ b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionCustomAndroidSdkSelfTest.kt @@ -12,21 +12,21 @@ import kotlin.test.assertEquals import kotlin.test.assertSame @ExtendWith(RobolectricExtension::class) -@Config(sdk = [Build.VERSION_CODES.KITKAT]) +@Config(sdk = [Build.VERSION_CODES.LOLLIPOP]) @Execution(ExecutionMode.SAME_THREAD) class RobolectricExtensionCustomAndroidSdkSelfTest { @Test fun `Given a test class configured with custom runtime SDK then SDK_INT should be the version set up`() { - assertEquals(Build.VERSION_CODES.KITKAT, Build.VERSION.SDK_INT) - assertEquals("4.4", Build.VERSION.RELEASE) + assertEquals(Build.VERSION_CODES.LOLLIPOP, Build.VERSION.SDK_INT) + assertEquals("5.0.2", Build.VERSION.RELEASE) } @Nested inner class NestedSelfTest { @Test fun `Given a test class configured with custom runtime SDK when call test from a nested test class then SDK_INT should be the version set up`() { - assertEquals(Build.VERSION_CODES.KITKAT, Build.VERSION.SDK_INT) - assertEquals("4.4", Build.VERSION.RELEASE) + assertEquals(Build.VERSION_CODES.LOLLIPOP, Build.VERSION.SDK_INT) + assertEquals("5.0.2", Build.VERSION.RELEASE) } @Test @@ -38,8 +38,8 @@ class RobolectricExtensionCustomAndroidSdkSelfTest { inner class TwoLevelNestedSelfTest { @Test fun `Given a test class configured with custom runtime SDK when call test from a nested test class then SDK_INT should be the version set up`() { - assertEquals(Build.VERSION_CODES.KITKAT, Build.VERSION.SDK_INT) - assertEquals("4.4", Build.VERSION.RELEASE) + assertEquals(Build.VERSION_CODES.LOLLIPOP, Build.VERSION.SDK_INT) + assertEquals("5.0.2", Build.VERSION.RELEASE) } @Test diff --git a/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt index 9809d7c..411a63d 100644 --- a/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt +++ b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt @@ -37,9 +37,6 @@ class RobolectricExtensionSelfTest { val application = assertDoesNotThrow { getApplicationContext() } assertIs(application, "application") assertTrue("onCreateCalled") { application.onCreateWasCalled } - if (RuntimeEnvironment.useLegacyResources()) { - assertNotNull(RuntimeEnvironment.getAppResourceTable(), "Application resource loader") - } } @Test diff --git a/settings.gradle b/settings.gradle index 656e9ce..627a6ce 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,7 +20,7 @@ dependencyResolutionManagement { rootProject.name = 'junit5-robolectric-extension' include('integration-tests:agp-groovy-dsl') -include('integration-tests:agp-kotlin-dsl') +//include('integration-tests:agp-kotlin-dsl') include('robolectric-extension') include('robolectric-extension-gradle-plugin')