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