blob: 7804d984f2c42fe8a3283f53a4f6eee5c1fde708 [file] [log] [blame]
/*
* 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.
*/
package com.android.build.gradle.internal.tasks
import android.databinding.tool.util.Preconditions
import com.android.build.api.artifact.ArtifactTransformationRequest
import com.android.build.api.component.impl.ComponentPropertiesImpl
import com.android.build.api.transform.QualifiedContent.DefaultContentType
import com.android.build.api.variant.BuiltArtifact
import com.android.build.api.variant.impl.BuiltArtifactsLoaderImpl
import com.android.build.api.variant.impl.VariantOutputImpl
import com.android.build.gradle.internal.pipeline.ExtendedContentType
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.build.gradle.internal.res.shrinker.ApkFormat
import com.android.build.gradle.internal.res.shrinker.LoggerAndFileDebugReporter
import com.android.build.gradle.internal.res.shrinker.ResourceShrinker
import com.android.build.gradle.internal.res.shrinker.ResourceShrinkerImpl
import com.android.build.gradle.internal.res.shrinker.gatherer.ResourcesGathererFromRTxt
import com.android.build.gradle.internal.res.shrinker.graph.RawResourcesGraphBuilder
import com.android.build.gradle.internal.res.shrinker.obfuscation.ProguardMappingsRecorder
import com.android.build.gradle.internal.res.shrinker.usages.DexUsageRecorder
import com.android.build.gradle.internal.res.shrinker.usages.XmlAndroidManifestUsageRecorder
import com.android.build.gradle.internal.scope.InternalArtifactType
import com.android.build.gradle.internal.scope.InternalMultipleArtifactType
import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction
import com.android.build.gradle.internal.utils.setDisallowChanges
import com.android.build.gradle.options.BooleanOption
import com.android.build.gradle.tasks.ResourceUsageAnalyzer
import com.android.builder.model.CodeShrinker
import com.android.utils.FileUtils
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.LogLevel
import org.gradle.api.logging.Logging
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskProvider
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.xml.sax.SAXException
import java.io.File
import java.io.IOException
import java.io.Serializable
import javax.inject.Inject
import javax.xml.parsers.ParserConfigurationException
/**
* Implementation of Resource Shrinking as a task.
*/
@CacheableTask
abstract class ShrinkResourcesTask : NonIncrementalTask() {
private var buildTypeName: String? = null
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val uncompressedResources: DirectoryProperty
@get:Optional
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val lightRClasses: RegularFileProperty
@get:Optional
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val rTxtFile: RegularFileProperty
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val resourceDir: DirectoryProperty
@get:Optional
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val mappingFileSrc: RegularFileProperty
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val mergedManifests: DirectoryProperty
@get:Input
abstract val debuggableBuildType: Property<Boolean>
@get:Input
abstract val enableRTxtResourceShrinking: Property<Boolean>
@get:Input
abstract val useNewResourceShrinker: Property<Boolean>
@get:Nested
abstract val variantOutputs: ListProperty<VariantOutputImpl>
@get:Internal
abstract val artifactTransformationRequest: Property<ArtifactTransformationRequest<ShrinkResourcesTask>>
@get:Classpath
abstract val classes: ConfigurableFileCollection
@get:OutputDirectory
abstract val compressedResources: DirectoryProperty
override fun doTaskAction() {
if (useNewResourceShrinker.get()) {
Preconditions.check(
enableRTxtResourceShrinking.get(),
"New implementation of resource shrinker supports gathering resources from R " +
"text files only. Enable 'android.enableRTxtResourceShrinking' flag to " +
"use it."
)
}
val mergedManifestsOutputs = BuiltArtifactsLoaderImpl().load(mergedManifests)
?: throw RuntimeException("Cannoft load merged manifests from $mergedManifests")
artifactTransformationRequest.get().submit(
task = this,
workQueue = workerExecutor.noIsolation(),
actionType = SplitterRunnable::class.java,
parameterType = SplitterParams::class.java
) { builtArtifact: BuiltArtifact, directory: Directory, parameters: SplitterParams ->
val mergedManifest = mergedManifestsOutputs.getBuiltArtifact(builtArtifact)
?: throw RuntimeException("Cannot find manifest file for $builtArtifact")
val variantOutput = variantOutputs.get().find {
it.variantOutputConfiguration.outputType == builtArtifact.outputType
&& it.variantOutputConfiguration.filters == builtArtifact.filters
} ?: throw java.lang.RuntimeException("Cannot find variant output for $builtArtifact")
parameters.outputFile.set(File(
directory.asFile,
"resources-${variantOutput.baseName}-stripped.ap_"))
parameters.rSourceVariant.set(if (enableRTxtResourceShrinking.get()){
rTxtFile.get().asFile } else { lightRClasses.get().asFile })
parameters.uncompressedResourceFile.set(File(builtArtifact.outputFile))
parameters.resourceDir.set(resourceDir)
parameters.infoLoggingEnabled.set(logger.isEnabled(LogLevel.INFO))
parameters.debugLoggingEnabled.set(logger.isEnabled(LogLevel.DEBUG))
parameters.useNewResourceShrinker.set(useNewResourceShrinker)
parameters.classes.set(classes.toList())
parameters.mergedManifest.set(mergedManifest)
mappingFileSrc.orNull?.asFile?.also { parameters.mappingFile.set(it) }
buildTypeName?.also { parameters.buildTypeName.set(it) }
parameters.outputFile.get().asFile
}
}
class CreationAction(
componentProperties: ComponentPropertiesImpl
) : VariantTaskCreationAction<ShrinkResourcesTask, ComponentPropertiesImpl>(
componentProperties
) {
override val type = ShrinkResourcesTask::class.java
override val name = computeTaskName("shrink", "Res")
private val classes = componentProperties.transformManager
.getPipelineOutputAsFileCollection { contentTypes, scopes ->
scopes.intersect(TransformManager.SCOPE_FULL_PROJECT).isNotEmpty()
&& (contentTypes.contains(DefaultContentType.CLASSES)
|| contentTypes.contains(ExtendedContentType.DEX))
}
private lateinit var artifactTransformationRequest: ArtifactTransformationRequest<ShrinkResourcesTask>
override fun handleProvider(
taskProvider: TaskProvider<ShrinkResourcesTask>
) {
super.handleProvider(taskProvider)
artifactTransformationRequest = creationConfig.artifacts.use(taskProvider)
.wiredWithDirectories(
ShrinkResourcesTask::uncompressedResources,
ShrinkResourcesTask::compressedResources)
.toTransformMany(
InternalArtifactType.PROCESSED_RES,
InternalArtifactType.SHRUNK_PROCESSED_RES)
}
override fun configure(
task: ShrinkResourcesTask
) {
super.configure(task)
val artifacts = creationConfig.artifacts
if (creationConfig
.globalScope.projectOptions[BooleanOption.ENABLE_R_TXT_RESOURCE_SHRINKING]) {
artifacts.setTaskInputToFinalProduct(
InternalArtifactType.RUNTIME_SYMBOL_LIST,
task.rTxtFile
)
} else {
artifacts.setTaskInputToFinalProduct(
InternalArtifactType.COMPILE_AND_RUNTIME_NOT_NAMESPACED_R_CLASS_JAR,
task.lightRClasses
)
}
artifacts.setTaskInputToFinalProduct(
InternalArtifactType.MERGED_NOT_COMPILED_RES,
task.resourceDir
)
artifacts.setTaskInputToFinalProduct(InternalArtifactType.APK_MAPPING,
task.mappingFileSrc)
artifacts.setTaskInputToFinalProduct(
InternalArtifactType.PACKAGED_MANIFESTS,
task.mergedManifests
)
task.buildTypeName = creationConfig.variantDslInfo.componentIdentity.buildType
task.debuggableBuildType.setDisallowChanges(creationConfig.variantDslInfo.isDebuggable)
task.artifactTransformationRequest.setDisallowChanges(artifactTransformationRequest)
task.enableRTxtResourceShrinking.set(creationConfig
.globalScope.projectOptions[BooleanOption.ENABLE_R_TXT_RESOURCE_SHRINKING])
task.useNewResourceShrinker.set(creationConfig
.globalScope.projectOptions[BooleanOption.ENABLE_NEW_RESOURCE_SHRINKER])
creationConfig.outputs.getEnabledVariantOutputs().forEach(task.variantOutputs::add)
task.variantOutputs.disallowChanges()
// When R8 produces dex files, this task analyzes them. If R8 or Proguard produce
// class files, this task will analyze those. That is why both types are specified.
task.classes.from(
if (creationConfig.variantScope.codeShrinker == CodeShrinker.R8
&& creationConfig.variantType.isAar) {
creationConfig.artifacts.get(InternalArtifactType.SHRUNK_CLASSES)
} else {
artifacts.getAll(InternalMultipleArtifactType.DEX)
.map {
if (it.isEmpty()) { classes } else {
creationConfig.globalScope.project.files(it)
}
}
}
)
}
}
abstract class SplitterRunnable @Inject constructor() : WorkAction<SplitterParams> {
override fun execute() {
var reportFile: File? = null
if (parameters.mappingFile.isPresent) {
val logDir = parameters.mappingFile.get().asFile.parentFile
if (logDir != null) {
reportFile = File(logDir, "resources.txt")
}
}
FileUtils.mkdirs(parameters.outputFile.get().asFile.parentFile)
if (!parameters.mergedManifest.isPresent) {
try {
FileUtils.copyFile(
parameters.uncompressedResourceFile.get().asFile,
parameters.outputFile.get().asFile
)
} catch (e: IOException) {
Logging.getLogger(ShrinkResourcesTask::class.java)
.error("Failed to copy uncompressed resource file :", e)
throw RuntimeException("Failed to copy uncompressed resource file", e)
}
return
}
// Analyze resources and usages and strip out unused
val analyzer = createResourceShrinker(reportFile)
try {
try {
analyzer.analyze()
} catch (e: IOException) {
throw RuntimeException(e)
} catch (e: ParserConfigurationException) {
throw RuntimeException(e)
} catch (e: SAXException) {
throw RuntimeException(e)
}
// Just rewrite the .ap_ file to strip out the res/ files for unused resources
try {
analyzer.rewriteResourceZip(
parameters.uncompressedResourceFile.get().asFile,
parameters.outputFile.get().asFile
)
} catch (e: IOException) {
throw RuntimeException(e)
}
// Dump some stats
val unused = analyzer.unusedResourceCount
if (unused > 0) {
val sb = StringBuilder(200)
sb.append("Removed unused resources")
// This is a bit misleading until we can strip out all resource types:
// int total = analyzer.getTotalResourceCount()
// sb.append("(" + unused + "/" + total + ")")
val before = parameters.uncompressedResourceFile.get().asFile.length()
val after = parameters.outputFile.get().asFile.length()
val percent = ((before - after) * 100 / before).toInt().toLong()
sb.append(": Binary resource data reduced from ${toKbString(before)}")
.append("KB to ${toKbString(after)}")
.append("KB: Removed $percent%")
if (!ourWarned) {
ourWarned = true
sb.append(
"""
Note: If necessary, you can disable resource shrinking by adding
android {
buildTypes {
${parameters.buildTypeName} {
shrinkResources false
}
}
}""".trimIndent()
)
}
Logging.getLogger(SplitterRunnable::class.java)
.log(LogLevel.INFO, sb.toString())
}
} finally {
analyzer.close()
}
}
private fun createResourceShrinker(reportFile: File?): ResourceShrinker =
when {
parameters.useNewResourceShrinker.get() -> createNewResourceShrinker(reportFile)
else -> createLegacyResourceShrinker(reportFile)
}
private fun createLegacyResourceShrinker(reportFile: File?): ResourceUsageAnalyzer {
val analyzer = ResourceUsageAnalyzer(
parameters.rSourceVariant.get().asFile,
parameters.classes.get(),
File(parameters.mergedManifest.get().outputFile),
parameters.mappingFile.get().asFile,
parameters.resourceDir.get().asFile,
reportFile,
ApkFormat.BINARY
)
analyzer.isVerbose = parameters.infoLoggingEnabled.get()
analyzer.isDebug = parameters.debugLoggingEnabled.get()
return analyzer
}
private fun createNewResourceShrinker(reportFile: File?): ResourceShrinkerImpl {
val mergedManifestFile = File(parameters.mergedManifest.get().outputFile).toPath()
val classes = parameters.classes.get()
val manifestUsageRecorder = XmlAndroidManifestUsageRecorder(mergedManifestFile)
val dexClassesUsageRecorder = classes.map { DexUsageRecorder(it.toPath()) }
val reporter = LoggerAndFileDebugReporter(
Logging.getLogger(ShrinkResourcesTask::class.java),
reportFile
)
return ResourceShrinkerImpl(
ResourcesGathererFromRTxt(parameters.rSourceVariant.get().asFile, ""),
ProguardMappingsRecorder(parameters.mappingFile.get().asFile.toPath()),
listOf(manifestUsageRecorder) + dexClassesUsageRecorder,
RawResourcesGraphBuilder(parameters.resourceDir.get().asFile.toPath()),
reporter,
ApkFormat.BINARY
)
}
}
abstract class SplitterParams : WorkParameters, Serializable {
abstract val outputFile: RegularFileProperty
abstract val uncompressedResourceFile: RegularFileProperty
@get:Optional
abstract val mappingFile: RegularFileProperty
@get:Optional
abstract val mergedManifest: Property<BuiltArtifact>
@get:Optional
abstract val buildTypeName: Property<String>
abstract val classes: ListProperty<File>
abstract val rSourceVariant: RegularFileProperty
abstract val resourceDir: DirectoryProperty
abstract val infoLoggingEnabled: Property<Boolean>
abstract val debugLoggingEnabled: Property<Boolean>
abstract val useNewResourceShrinker: Property<Boolean>
}
companion object {
/** Whether we've already warned about how to turn off shrinking. Used to avoid
* repeating the same multi-line message for every repeated abi split. */
private var ourWarned = true // Logging disabled until shrinking is on by default.
private fun toKbString(size: Long): String {
return (size.toInt() / 1024).toString()
}
}
}