blob: aae6091253c7506b6b19f71a3b40db07bc38c892 [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.tasks
import com.android.SdkConstants.FD_RES_VALUES
import com.android.build.gradle.internal.LoggerWrapper
import com.android.build.gradle.internal.aapt.SharedExecutorResourceCompilationService
import com.android.build.gradle.internal.res.getAapt2FromMavenAndVersion
import com.android.build.gradle.internal.res.namespaced.Aapt2ServiceKey
import com.android.build.gradle.internal.res.namespaced.registerAaptService
import com.android.build.gradle.internal.scope.BuildArtifactsHolder
import com.android.build.gradle.internal.scope.InternalArtifactType
import com.android.build.gradle.internal.scope.VariantScope
import com.android.build.gradle.internal.tasks.NewIncrementalTask
import com.android.build.gradle.internal.tasks.TaskInputHelper
import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction
import com.android.build.gradle.options.SyncOptions
import com.android.builder.internal.aapt.v2.Aapt2RenamingConventions
import com.android.builder.png.VectorDrawableRenderer
import com.android.ide.common.resources.CompileResourceRequest
import com.android.ide.common.resources.NoOpResourcePreprocessor
import com.android.ide.common.resources.ResourcePreprocessor
import com.android.ide.common.workers.WorkerExecutorFacade
import com.android.resources.Density
import com.android.utils.FileUtils
import com.google.common.collect.ImmutableList
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileType
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskProvider
import org.gradle.work.ChangeType
import org.gradle.work.FileChange
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
import java.util.function.Supplier
import javax.inject.Inject
@CacheableTask
abstract class CompileLibraryResourcesTask : NewIncrementalTask() {
private lateinit var errorFormatMode: SyncOptions.ErrorFormatMode
@get:InputFiles
@get:Incremental
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val mergedLibraryResourcesDir: DirectoryProperty
@get:Input
var pseudoLocalesEnabled: Boolean = false
private set
@get:Input
var crunchPng: Boolean = true
private set
@get:Input
lateinit var aapt2Version: String
private set
@get:Internal
abstract val aapt2FromMaven: ConfigurableFileCollection
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Input
lateinit var generatedDensities: Set<String>
private set
private lateinit var generatedPngsOutputDir: File
/**
* Used to load [generatedFilesMap] when running incrementally.
*/
@get:OutputFile
@get:Optional
lateinit var generatedFilesMapBlobFile: File
private set
/**
* Contains for each vector drawable file a list of generated pngs to help deleting previously
* generated files in incremental builds.
*
* We can identify the files uniquely using a combination of parent name and file name.
*/
private var generatedFilesMap: MutableMap<FileIdentifier, Collection<FileIdentifier>> =
HashMap()
@get:Input
var vectorSupportLibraryIsUsed: Boolean = false
private set
private lateinit var resourcePreprocessor: ResourcePreprocessor
private lateinit var minSdk: Supplier<Int>
@Input
fun getMinSdk(): Int {
return minSdk.get()
}
override fun doTaskAction(inputChanges: InputChanges) {
if (generatedDensities.isEmpty()) {
resourcePreprocessor = NoOpResourcePreprocessor.INSTANCE
} else {
resourcePreprocessor = VectorDrawableRenderer(
minSdk.get(),
vectorSupportLibraryIsUsed,
generatedPngsOutputDir,
generatedDensities.map { Density.getEnum(it) },
LoggerWrapper.supplierFor(CompileLibraryResourcesTask::class.java)
)
}
val aapt2ServiceKey = registerAaptService(
aapt2FromMaven, LoggerWrapper(logger)
)
getWorkerFacadeWithWorkers().use { workers ->
val requests = ImmutableList.builder<CompileResourceRequest>()
val generatedFilesRequests = ImmutableList.builder<CompileResourceRequest>()
if (inputChanges.isIncremental && loadGeneratedFilesMap()) {
doIncrementalTaskAction(
inputChanges.getFileChanges(mergedLibraryResourcesDir),
requests,
generatedFilesRequests,
workers
)
} else {
// do full task action
if (generatedPngsOutputDir.exists()) {
FileUtils.deleteDirectoryContents(generatedPngsOutputDir)
}
FileUtils.deleteDirectoryContents(outputDir.asFile.get())
// filter out the values files as they have to go through the resources merging
// pipeline.
mergedLibraryResourcesDir.asFile.get().listFiles()!!
.filter { it.isDirectory && !it.name.startsWith(FD_RES_VALUES) }
.forEach { dir ->
dir.listFiles()!!.forEach { file ->
submitFileToBeCompiled(file, requests, generatedFilesRequests, workers)
}
}
}
// Wait for the file generation to be completed
workers.await()
// Save the generated files map
generatedFilesMapBlobFile.parentFile.mkdirs()
ObjectOutputStream(FileOutputStream(generatedFilesMapBlobFile)).use {
it.writeObject(generatedFilesMap)
}
// Generated resources override normal resources so we need to add them at the end
requests.addAll(generatedFilesRequests.build())
workers.submit(
CompileLibraryResourcesRunnable::class.java,
CompileLibraryResourcesParams(
projectName,
path,
aapt2ServiceKey,
errorFormatMode,
requests.build()
)
)
}
}
private fun submitFileToBeCompiled(
file: File,
requests: ImmutableList.Builder<CompileResourceRequest>,
generatedFilesRequests: ImmutableList.Builder<CompileResourceRequest>,
workers: WorkerExecutorFacade
) {
val generatedFiles = resourcePreprocessor.getFilesToBeGenerated(file)
if (generatedFiles.isEmpty()) {
requests.add(
CompileResourceRequest(
file,
outputDir.asFile.get(),
isPseudoLocalize = pseudoLocalesEnabled,
isPngCrunching = crunchPng
)
)
} else {
generatedFilesMap[FileIdentifier(file)] = generatedFiles.map { FileIdentifier(it) }
generatedFiles.forEach {
workers.submit(
FileGenerationWorkAction::class.java,
FileGenerationParameters(it, file, resourcePreprocessor)
)
generatedFilesRequests.add(
CompileResourceRequest(
it,
outputDir.asFile.get(),
isPseudoLocalize = pseudoLocalesEnabled,
isPngCrunching = crunchPng
)
)
}
}
}
private fun deleteFile(file: File) {
val generatedFiles = generatedFilesMap[FileIdentifier(file)]
if (generatedFiles?.isEmpty() == false) {
generatedFiles.forEach {
FileUtils.deleteIfExists(
File(
outputDir.asFile.get(),
Aapt2RenamingConventions.compilationRename(File(it.parentName, it.fileName))
)
)
}
generatedFilesMap.remove(FileIdentifier(file))
} else {
FileUtils.deleteIfExists(
File(
outputDir.asFile.get(),
Aapt2RenamingConventions.compilationRename(file)
)
)
}
}
private fun loadGeneratedFilesMap(): Boolean {
return try {
ObjectInputStream(FileInputStream(generatedFilesMapBlobFile)).use {
@Suppress("UNCHECKED_CAST")
generatedFilesMap =
it.readObject() as MutableMap<FileIdentifier, Collection<FileIdentifier>>
}
true
} catch (e: Exception) {
false
}
}
private fun handleModifiedFile(
file: File,
changeType: ChangeType,
requests: ImmutableList.Builder<CompileResourceRequest>,
generatedFilesRequests: ImmutableList.Builder<CompileResourceRequest>,
workers: WorkerExecutorFacade
) {
if (changeType == ChangeType.MODIFIED || changeType == ChangeType.REMOVED) {
deleteFile(file)
}
if (changeType == ChangeType.ADDED || changeType == ChangeType.MODIFIED) {
submitFileToBeCompiled(file, requests, generatedFilesRequests, workers)
}
}
private fun doIncrementalTaskAction(
fileChanges: Iterable<FileChange>,
requests: ImmutableList.Builder<CompileResourceRequest>,
generatedFilesRequests: ImmutableList.Builder<CompileResourceRequest>,
workers: WorkerExecutorFacade
) {
fileChanges.filter {
it.fileType == FileType.FILE &&
!it.file.parentFile.name.startsWith(FD_RES_VALUES)
}
.forEach { fileChange ->
handleModifiedFile(
fileChange.file,
fileChange.changeType,
requests,
generatedFilesRequests,
workers
)
}
}
private data class FileIdentifier(val fileName: String, val parentName: String) {
constructor(file: File) : this(file.name, file.parentFile.name)
}
private data class CompileLibraryResourcesParams(
val projectName: String,
val owner: String,
val aapt2ServiceKey: Aapt2ServiceKey,
val errorFormatMode: SyncOptions.ErrorFormatMode,
val requests: List<CompileResourceRequest>
) : Serializable
private class CompileLibraryResourcesRunnable
@Inject constructor(private val params: CompileLibraryResourcesParams) : Runnable {
override fun run() {
SharedExecutorResourceCompilationService(
params.projectName,
params.owner,
params.aapt2ServiceKey,
params.errorFormatMode
).use {
it.submitCompile(params.requests)
}
}
}
class CreationAction(variantScope: VariantScope) :
VariantTaskCreationAction<CompileLibraryResourcesTask>(variantScope) {
override val name: String
get() = variantScope.getTaskName("compile", "LibraryResources")
override val type: Class<CompileLibraryResourcesTask>
get() = CompileLibraryResourcesTask::class.java
override fun handleProvider(taskProvider: TaskProvider<out CompileLibraryResourcesTask>) {
super.handleProvider(taskProvider)
variantScope.artifacts.producesDir(
InternalArtifactType.COMPILED_LOCAL_RESOURCES,
BuildArtifactsHolder.OperationType.INITIAL,
taskProvider,
CompileLibraryResourcesTask::outputDir
)
}
override fun configure(task: CompileLibraryResourcesTask) {
super.configure(task)
variantScope.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.PACKAGED_RES,
task.mergedLibraryResourcesDir
)
val (aapt2FromMaven, aapt2Version) = getAapt2FromMavenAndVersion(variantScope.globalScope)
task.aapt2FromMaven.from(aapt2FromMaven)
task.aapt2Version = aapt2Version
task.pseudoLocalesEnabled = variantScope
.variantData
.variantConfiguration
.buildType
.isPseudoLocalesEnabled
task.crunchPng = variantScope.isCrunchPngs
task.errorFormatMode =
SyncOptions.getErrorFormatMode(variantScope.globalScope.projectOptions)
val vectorDrawablesOptions =
variantScope.variantData.variantConfiguration.mergedFlavor.vectorDrawables
task.generatedDensities = vectorDrawablesOptions.generatedDensities ?: emptySet()
task.vectorSupportLibraryIsUsed = vectorDrawablesOptions.useSupportLibrary ?: false
task.minSdk = TaskInputHelper.memoize {
variantScope.variantData.variantConfiguration.minSdkVersion.apiLevel
}
task.generatedPngsOutputDir =
FileUtils.join(
variantScope.globalScope.generatedDir,
"compile-library-resources",
"pngs",
variantScope.variantConfiguration.dirName
)
task.generatedFilesMapBlobFile =
FileUtils.join(variantScope.getIncrementalDir(name), "generatedFilesMap")
}
}
data class FileGenerationParameters constructor(
val file: File, val sourceFile: File, val resourcePreprocessor: ResourcePreprocessor
) : Serializable
class FileGenerationWorkAction @Inject
constructor(private val params: FileGenerationParameters) : Runnable {
override fun run() {
try {
params.resourcePreprocessor.generateFile(
params.file,
params.sourceFile
)
} catch (e: Exception) {
throw RuntimeException(
"Error while processing "
+ params.sourceFile
+ " : "
+ e.message,
e
)
}
}
}
}