blob: cc3ea3dc35563c437828279a5316e2ea5b863c00 [file] [log] [blame]
/*
* Copyright (C) 2017 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.
*/
@file:JvmName("FeatureSplitUtils")
package com.android.build.gradle.internal.tasks.featuresplit
import com.android.build.api.component.impl.ComponentPropertiesImpl
import com.android.build.gradle.internal.publishing.AndroidArtifacts
import com.android.build.gradle.internal.scope.InternalArtifactType
import com.android.build.gradle.internal.tasks.NonIncrementalTask
import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction
import com.android.build.gradle.options.IntegerOption
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
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.tooling.BuildException
import java.io.File
import java.io.FileNotFoundException
import java.io.Serializable
import java.util.regex.Pattern
import javax.inject.Inject
/** Task to write the FeatureSetMetadata file. */
@CacheableTask
abstract class FeatureSetMetadataWriterTask : NonIncrementalTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.NONE)
lateinit var inputFiles: FileCollection
internal set
@get:OutputFile
abstract val outputFile: RegularFileProperty
@get:Input
var minSdkVersion: Int = 1
internal set
@get:Input
var maxNumberOfFeaturesBeforeOreo: Int = FeatureSetMetadata.MAX_NUMBER_OF_SPLITS_BEFORE_O
internal set
public override fun doTaskAction() {
getWorkerFacadeWithWorkers().use {
it.submit(
FeatureSetRunnable::class.java,
Params(inputFiles.asFileTree.files,
minSdkVersion,
maxNumberOfFeaturesBeforeOreo,
outputFile.get().asFile
)
)
}
}
private data class Params(
val featureFiles: Set<File>,
val minSdkVersion: Int,
val maxNumberOfFeaturesBeforeOreo: Int,
val outputFile: File
) : Serializable
private class FeatureSetRunnable @Inject constructor(private val params: Params): Runnable {
override fun run() {
val features = mutableListOf<FeatureSplitDeclaration>()
val featureMetadata = FeatureSetMetadata(params.maxNumberOfFeaturesBeforeOreo)
for (file in params.featureFiles) {
try {
features.add(FeatureSplitDeclaration.load(file))
} catch (e: FileNotFoundException) {
throw BuildException("Cannot read features split declaration file", e)
}
}
val featureNameMap = computeFeatureNames(features)
for (feature in features) {
featureMetadata.addFeatureSplit(
params.minSdkVersion, feature.modulePath, featureNameMap[feature.modulePath]!!)
}
// save the list.
featureMetadata.save(params.outputFile)
}
}
class CreationAction(componentProperties: ComponentPropertiesImpl) :
VariantTaskCreationAction<FeatureSetMetadataWriterTask, ComponentPropertiesImpl>(
componentProperties
) {
override val name: String
get() = computeTaskName("generate", "FeatureMetadata")
override val type: Class<FeatureSetMetadataWriterTask>
get() = FeatureSetMetadataWriterTask::class.java
override fun handleProvider(
taskProvider: TaskProvider<FeatureSetMetadataWriterTask>
) {
super.handleProvider(taskProvider)
creationConfig.artifacts.setInitialProvider(
taskProvider,
FeatureSetMetadataWriterTask::outputFile
).withName(FeatureSetMetadata.OUTPUT_FILE_NAME).on(InternalArtifactType.FEATURE_SET_METADATA)
}
override fun configure(
task: FeatureSetMetadataWriterTask
) {
super.configure(task)
task.minSdkVersion = creationConfig.minSdkVersion.apiLevel
task.inputFiles = creationConfig.variantDependencies.getArtifactFileCollection(
AndroidArtifacts.ConsumedConfigType.REVERSE_METADATA_VALUES,
AndroidArtifacts.ArtifactScope.PROJECT,
AndroidArtifacts.ArtifactType.REVERSE_METADATA_FEATURE_DECLARATION
)
val maxNumberOfFeaturesBeforeOreo = creationConfig.services.projectOptions
.get(IntegerOption.PRE_O_MAX_NUMBER_OF_FEATURES)
if (maxNumberOfFeaturesBeforeOreo != null) {
task.maxNumberOfFeaturesBeforeOreo =
Integer.min(100, maxNumberOfFeaturesBeforeOreo)
}
}
}
}
/** Regular expression defining valid characters for split names */
private val FEATURE_NAME_CHARS = Pattern.compile("[a-zA-Z0-9_]+")
/** Returns the feature name based on the module path. */
fun getFeatureName(modulePath: String): String =
if (modulePath == ":") modulePath else modulePath.substring(modulePath.lastIndexOf(':') + 1)
/**
* Converts from a list of [FeatureSplitDeclaration] to a map of (module-path -> feature name)
*
* This also performs validation to ensure all feature name are unique.
*/
internal fun computeFeatureNames(features: List<FeatureSplitDeclaration>): Map<String, String> {
val result = mutableMapOf<String, String>()
// build a map of (leaf -> list(full paths)). This will allow us to detect duplicates
// and properly display error messages with all the paths containing the same leaf.
val leafMap = features.groupBy({ getFeatureName(it.modulePath) }, { it.modulePath })
// check for root module name (root module)
if (leafMap.keys.contains(":")) {
throw RuntimeException("Root module ':' is used as a feature module. This is not supported.")
}
// check all the leaves for invalid characters.
val invalidNames = leafMap.keys.filter { name -> !FEATURE_NAME_CHARS.matcher(name).matches() }
if (!invalidNames.isEmpty()) {
throw RuntimeException(
invalidNames.joinTo(
StringBuilder(
"The following feature module names contain invalid characters. Feature " +
"module names can only contain letters, digits and underscores."
), separator = "\n\t-> ", prefix = "\n\t-> "
).toString()
)
}
// check for duplicate names while building the final (path, leaf) map.
// Dex splitting relies on all features having unique names.
for ((leaf, modules) in leafMap) {
val module = modules.singleOrNull() ?: throw RuntimeException(
modules.joinTo(
StringBuilder(
"Module name '$leaf' is used by multiple modules. All dynamic features must have a unique name."
), separator = "\n\t-> ", prefix = "\n\t-> "
).toString()
)
result[module] = leaf
}
return result
}