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')