xref: /aosp_15_r20/external/robolectric/buildSrc/src/main/java/org/robolectric/gradle/agp/ExtractAarTransform.kt (revision e6ba16074e6af37d123cb567d575f496bf0a58ee)
1 /*
<lambda>null2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 /*
18  * This class comes from AGP internals:
19  * 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
20  */
21 
22 package org.robolectric.gradle.agp
23 
24 import com.android.SdkConstants
25 import com.android.utils.FileUtils
26 import com.google.common.annotations.VisibleForTesting
27 import com.google.common.io.Files
28 import java.io.ByteArrayOutputStream
29 import java.io.File
30 import java.util.jar.JarOutputStream
31 import java.util.zip.ZipInputStream
32 import org.gradle.api.artifacts.transform.InputArtifact
33 import org.gradle.api.artifacts.transform.TransformAction
34 import org.gradle.api.artifacts.transform.TransformOutputs
35 import org.gradle.api.file.FileSystemLocation
36 import org.gradle.api.provider.Provider
37 import org.gradle.api.tasks.Classpath
38 import org.gradle.work.DisableCachingByDefault
39 
40 /**
41  * Transform that extracts an AAR file into a directory.
42  *
43  * Note: There are small adjustments made to the extracted contents (see [AarExtractor.extract]).
44  */
45 @DisableCachingByDefault(because = "Copy task")
46 abstract class ExtractAarTransform : TransformAction<GenericTransformParameters> {
47 
48   @get:Classpath @get:InputArtifact abstract val aarFile: Provider<FileSystemLocation>
49 
50   override fun transform(outputs: TransformOutputs) {
51     // TODO: record transform execution span
52     val inputFile = aarFile.get().asFile
53     val outputDir = outputs.dir(inputFile.nameWithoutExtension)
54     FileUtils.mkdirs(outputDir)
55     AarExtractor().extract(inputFile, outputDir)
56   }
57 }
58 
59 private const val LIBS_PREFIX = SdkConstants.LIBS_FOLDER + '/'
60 private const val LIBS_PREFIX_LENGTH = LIBS_PREFIX.length
61 private const val JARS_PREFIX_LENGTH = SdkConstants.FD_JARS.length + 1
62 
63 @VisibleForTesting
64 internal class AarExtractor {
65 
66   /**
67    * [StringBuilder] used to construct all paths. It gets truncated back to [JARS_PREFIX_LENGTH] on
68    * every calculation.
69    */
70   private val stringBuilder =
<lambda>null71     StringBuilder(60).apply {
72       append(SdkConstants.FD_JARS)
73       append(File.separatorChar)
74     }
75 
choosePathInOutputnull76   private fun choosePathInOutput(entryName: String): String {
77     stringBuilder.setLength(JARS_PREFIX_LENGTH)
78 
79     return when {
80       entryName == SdkConstants.FN_CLASSES_JAR || entryName == SdkConstants.FN_LINT_JAR -> {
81         stringBuilder.append(entryName)
82         stringBuilder.toString()
83       }
84       entryName.startsWith(LIBS_PREFIX) -> {
85         // In case we have libs/classes.jar we are going to rename them, due an issue in
86         // Gradle.
87         // TODO: stop doing this once this is fixed in gradle.
88         when (val pathWithinLibs = entryName.substring(LIBS_PREFIX_LENGTH)) {
89           SdkConstants.FN_CLASSES_JAR ->
90             stringBuilder.append(LIBS_PREFIX).append("classes-2${SdkConstants.DOT_JAR}")
91           SdkConstants.FN_LINT_JAR ->
92             stringBuilder.append(LIBS_PREFIX).append("lint-2${SdkConstants.DOT_JAR}")
93           else -> stringBuilder.append(LIBS_PREFIX).append(pathWithinLibs)
94         }
95         stringBuilder.toString()
96       }
97       else -> entryName
98     }
99   }
100 
101   /**
102    * Extracts an AAR file into a directory.
103    *
104    * Note: There are small adjustments made to the extracted contents. For example, classes.jar
105    * inside the AAR will be extracted to jars/classes.jar, and if the jar does not exist, we will
106    * create an empty classes.jar.
107    */
extractnull108   fun extract(aar: File, outputDir: File) {
109     ZipInputStream(aar.inputStream().buffered()).use { zipInputStream ->
110       while (true) {
111         val entry = zipInputStream.nextEntry ?: break
112         if (entry.isDirectory || entry.name.contains("../") || entry.name.isEmpty()) {
113           continue
114         }
115         val path = FileUtils.toSystemDependentPath(choosePathInOutput(entry.name))
116         val outputFile = File(outputDir, path)
117         Files.createParentDirs(outputFile)
118         Files.asByteSink(outputFile).writeFrom(zipInputStream)
119       }
120     }
121 
122     // If classes.jar does not exist, create an empty one
123     val classesJar = outputDir.resolve("${SdkConstants.FD_JARS}/${SdkConstants.FN_CLASSES_JAR}")
124     if (!classesJar.exists()) {
125       Files.createParentDirs(classesJar)
126       classesJar.writeBytes(emptyJar)
127     }
128   }
129 }
130 
131 private val emptyJar: ByteArray =
132   // Note:
133   //  - A jar doesn't need a manifest entry, but if we ever want to create a manifest entry, be
134   //    sure to set a fixed timestamp for it so that the jar is deterministic.
135   //  - This empty jar takes up only ~22 bytes, so we don't need to GC it at the end of the build.
<lambda>null136   ByteArrayOutputStream().apply { JarOutputStream(this).use {} }.toByteArray()
137