blob: 58eea553af280e10e82ef4f533c451f25deaba30 [file] [log] [blame]
/*
* Copyright (C) 2014 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.variant;
import static com.android.builder.core.BuilderConstants.DEBUG;
import static com.android.builder.core.BuilderConstants.RELEASE;
import com.android.annotations.NonNull;
import com.android.build.OutputFile;
import com.android.build.gradle.BaseExtension;
import com.android.build.gradle.internal.BuildTypeData;
import com.android.build.gradle.internal.ProductFlavorData;
import com.android.build.gradle.internal.TaskManager;
import com.android.build.gradle.internal.VariantModel;
import com.android.build.gradle.internal.api.ApplicationVariantImpl;
import com.android.build.gradle.internal.api.BaseVariantImpl;
import com.android.build.gradle.internal.core.GradleVariantConfiguration;
import com.android.build.gradle.internal.dsl.BuildType;
import com.android.build.gradle.internal.dsl.ProductFlavor;
import com.android.build.gradle.internal.dsl.SigningConfig;
import com.android.build.gradle.internal.scope.ApkData;
import com.android.build.gradle.internal.scope.GlobalScope;
import com.android.build.gradle.internal.scope.OutputFactory;
import com.android.build.gradle.options.BooleanOption;
import com.android.build.gradle.options.ProjectOptions;
import com.android.build.gradle.options.StringOption;
import com.android.builder.core.VariantType;
import com.android.builder.core.VariantTypeImpl;
import com.android.builder.errors.EvalIssueReporter;
import com.android.builder.errors.EvalIssueReporter.Type;
import com.android.builder.profile.Recorder;
import com.android.ide.common.build.SplitOutputMatcher;
import com.android.resources.Density;
import com.android.utils.Pair;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.gradle.api.NamedDomainObjectContainer;
/**
* An implementation of VariantFactory for a project that generates APKs.
*
* <p>This can be an app project, or a test-only project, though the default behavior is app.
*/
public class ApplicationVariantFactory extends BaseVariantFactory implements VariantFactory {
public ApplicationVariantFactory(@NonNull GlobalScope globalScope) {
super(globalScope);
}
@Override
@NonNull
public BaseVariantData createVariantData(
@NonNull GradleVariantConfiguration variantConfiguration,
@NonNull TaskManager taskManager,
@NonNull Recorder recorder) {
ApplicationVariantData variant =
new ApplicationVariantData(
globalScope, taskManager, variantConfiguration, recorder);
computeOutputs(variantConfiguration, variant, true);
return variant;
}
protected void computeOutputs(
@NonNull GradleVariantConfiguration variantConfiguration,
@NonNull ApplicationVariantData variant,
boolean includeMainApk) {
BaseExtension extension = globalScope.getExtension();
variant.calculateFilters(extension.getSplits());
Set<String> densities = variant.getFilters(OutputFile.FilterType.DENSITY);
Set<String> abis = variant.getFilters(OutputFile.FilterType.ABI);
checkSplitsConflicts(variant, abis);
if (!densities.isEmpty()) {
variant.setCompatibleScreens(extension.getSplits().getDensity()
.getCompatibleScreens());
}
OutputFactory outputFactory = variant.getOutputFactory();
populateMultiApkOutputs(abis, densities, outputFactory, includeMainApk);
restrictEnabledOutputs(variantConfiguration, variant.getOutputScope().getApkDatas());
}
private void populateMultiApkOutputs(
Set<String> abis,
Set<String> densities,
OutputFactory outputFactory,
boolean includeMainApk) {
if (densities.isEmpty() && abis.isEmpty()) {
// If both are empty, we will have only the main Apk.
if (includeMainApk) {
outputFactory.addMainApk();
}
return;
}
boolean universalApkForAbi =
globalScope.getExtension().getSplits().getAbi().isEnable()
&& globalScope.getExtension().getSplits().getAbi().isUniversalApk();
if (universalApkForAbi) {
outputFactory.addUniversalApk();
} else {
if (abis.isEmpty() && includeMainApk) {
outputFactory.addUniversalApk();
}
}
if (!abis.isEmpty()) {
// TODO(b/117973371): Check if this is still needed/used, as BundleTool don't do this.
// for each ABI, create a specific split that will contain all densities.
abis.forEach(
abi ->
outputFactory.addFullSplit(
ImmutableList.of(Pair.of(OutputFile.FilterType.ABI, abi))));
}
// create its outputs
for (String density : densities) {
if (!abis.isEmpty()) {
for (String abi : abis) {
outputFactory.addFullSplit(
ImmutableList.of(
Pair.of(OutputFile.FilterType.ABI, abi),
Pair.of(OutputFile.FilterType.DENSITY, density)));
}
} else {
outputFactory.addFullSplit(
ImmutableList.of(Pair.of(OutputFile.FilterType.DENSITY, density)));
}
}
}
private void checkSplitsConflicts(
@NonNull ApplicationVariantData variantData, @NonNull Set<String> abiFilters) {
// if we don't have any ABI splits, nothing is conflicting.
if (abiFilters.isEmpty()) {
return;
}
// if universalAPK is requested, abiFilter will control what goes into the universal APK.
if (globalScope.getExtension().getSplits().getAbi().isUniversalApk()) {
return;
}
// check supportedAbis in Ndk configuration versus ABI splits.
Set<String> ndkConfigAbiFilters =
variantData.getVariantConfiguration().getNdkConfig().getAbiFilters();
if (ndkConfigAbiFilters == null || ndkConfigAbiFilters.isEmpty()) {
return;
}
// if we have any ABI splits, whether it's a full or pure ABI splits, it's an error.
EvalIssueReporter issueReporter = globalScope.getErrorHandler();
issueReporter.reportError(
EvalIssueReporter.Type.GENERIC,
String.format(
"Conflicting configuration : '%1$s' in ndk abiFilters "
+ "cannot be present when splits abi filters are set : %2$s",
Joiner.on(",").join(ndkConfigAbiFilters), Joiner.on(",").join(abiFilters)));
}
private void restrictEnabledOutputs(
GradleVariantConfiguration configuration, List<ApkData> apkDataList) {
Set<String> supportedAbis = configuration.getSupportedAbis();
ProjectOptions projectOptions = globalScope.getProjectOptions();
String buildTargetAbi =
projectOptions.get(BooleanOption.BUILD_ONLY_TARGET_ABI)
|| globalScope.getExtension().getSplits().getAbi().isEnable()
? projectOptions.get(StringOption.IDE_BUILD_TARGET_ABI)
: null;
if (buildTargetAbi == null) {
return;
}
String buildTargetDensity = projectOptions.get(StringOption.IDE_BUILD_TARGET_DENSITY);
Density density = Density.getEnum(buildTargetDensity);
List<ApkData> apksToGenerate =
SplitOutputMatcher.computeBestOutput(
apkDataList,
supportedAbis,
density == null ? -1 : density.getDpiValue(),
Arrays.asList(Strings.nullToEmpty(buildTargetAbi).split(",")));
if (apksToGenerate.isEmpty()) {
List<String> splits =
apkDataList
.stream()
.map(ApkData::getFilterName)
.filter(Objects::nonNull)
.collect(Collectors.toList());
globalScope
.getErrorHandler()
.reportWarning(
EvalIssueReporter.Type.GENERIC,
String.format(
"Cannot build selected target ABI: %1$s, "
+ (splits.isEmpty()
? "no suitable splits configured: %2$s;"
: "supported ABIs are: %2$s"),
buildTargetAbi,
supportedAbis == null
? Joiner.on(", ").join(splits)
: Joiner.on(", ").join(supportedAbis)));
// do not disable anything, build all and let the apk install figure it out.
return;
}
apkDataList.forEach(
apkData -> {
if (!apksToGenerate.contains(apkData)) {
apkData.disable();
}
});
}
@Override
@NonNull
public Class<? extends BaseVariantImpl> getVariantImplementationClass(
@NonNull BaseVariantData variantData) {
return ApplicationVariantImpl.class;
}
@NonNull
@Override
public Collection<VariantType> getVariantConfigurationTypes() {
if (globalScope.getExtension().getBaseFeature()) {
return ImmutableList.of(VariantTypeImpl.BASE_APK);
}
return ImmutableList.of(VariantTypeImpl.OPTIONAL_APK);
}
@Override
public boolean hasTestScope() {
return true;
}
@Override
public void validateModel(@NonNull VariantModel model) {
super.validateModel(model);
validateVersionCodes(model);
if (getVariantConfigurationTypes().stream().noneMatch(VariantType::isDynamicFeature)) {
return;
}
EvalIssueReporter issueReporter = globalScope.getErrorHandler();
for (BuildTypeData buildType : model.getBuildTypes().values()) {
if (buildType.getBuildType().isMinifyEnabled()) {
issueReporter.reportError(
Type.GENERIC,
"Dynamic feature modules cannot set minifyEnabled to true. "
+ "minifyEnabled is set to true in build type '"
+ buildType.getBuildType().getName()
+ "'.\nTo enable minification for a dynamic feature "
+ "module, set minifyEnabled to true in the base module.");
}
}
}
@Override
public void createDefaultComponents(
@NonNull NamedDomainObjectContainer<BuildType> buildTypes,
@NonNull NamedDomainObjectContainer<ProductFlavor> productFlavors,
@NonNull NamedDomainObjectContainer<SigningConfig> signingConfigs) {
// must create signing config first so that build type 'debug' can be initialized
// with the debug signing config.
signingConfigs.create(DEBUG);
buildTypes.create(DEBUG);
buildTypes.create(RELEASE);
}
private void validateVersionCodes(@NonNull VariantModel model) {
EvalIssueReporter issueReporter = globalScope.getErrorHandler();
Integer versionCode = model.getDefaultConfig().getProductFlavor().getVersionCode();
if (versionCode != null && versionCode < 1) {
issueReporter.reportError(
Type.GENERIC,
"android.defaultConfig.versionCode is set to "
+ versionCode
+ ", but it should be a positive integer.\n"
+ "See https://developer.android.com/studio/publish/versioning#appversioning"
+ " for more information.");
return;
}
for (ProductFlavorData flavorData : model.getProductFlavors().values()) {
Integer flavorVersionCode = flavorData.getProductFlavor().getVersionCode();
if (flavorVersionCode == null || flavorVersionCode > 0) {
return;
}
issueReporter.reportError(
Type.GENERIC,
"versionCode is set to "
+ flavorVersionCode
+ " in product flavor "
+ flavorData.getProductFlavor().getName()
+ ", but it should be a positive integer.\n"
+ "See https://developer.android.com/studio/publish/versioning#appversioning"
+ " for more information.");
}
}
}