/* * 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.builder.core; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.builder.dependency.SymbolFileProvider; import com.android.builder.model.AaptOptions; import com.android.ide.common.process.ProcessEnvBuilder; import com.android.ide.common.process.ProcessInfo; import com.android.ide.common.process.ProcessInfoBuilder; import com.android.resources.Density; import com.android.sdklib.BuildToolInfo; import com.android.sdklib.IAndroidTarget; import com.android.utils.ILogger; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * Builds the ProcessInfo necessary for an aapt package invocation */ public class AaptPackageProcessBuilder extends ProcessEnvBuilder<AaptPackageProcessBuilder> { @NonNull private final File mManifestFile; @NonNull private final AaptOptions mOptions; @Nullable private File mResFolder; @Nullable private File mAssetsFolder; private boolean mVerboseExec = false; @Nullable private String mSourceOutputDir; @Nullable private String mSymbolOutputDir; @Nullable private List<? extends SymbolFileProvider> mLibraries; @Nullable private String mResPackageOutput; @Nullable private String mProguardOutput; @Nullable private VariantType mType; private boolean mDebuggable = false; private boolean mPseudoLocalesEnabled = false; @Nullable private Collection<String> mResourceConfigs; @Nullable Collection<String> mSplits; @Nullable String mPackageForR; @Nullable String mPreferredDensity; /** * * @param manifestFile the location of the manifest file * @param options the {@link com.android.builder.model.AaptOptions} */ public AaptPackageProcessBuilder( @NonNull File manifestFile, @NonNull AaptOptions options) { checkNotNull(manifestFile, "manifestFile cannot be null."); checkNotNull(options, "options cannot be null."); mManifestFile = manifestFile; mOptions = options; } @NonNull public File getManifestFile() { return mManifestFile; } /** * @param resFolder the merged res folder * @return itself */ public AaptPackageProcessBuilder setResFolder(@NonNull File resFolder) { if (!resFolder.isDirectory()) { throw new RuntimeException("resFolder parameter is not a directory"); } mResFolder = resFolder; return this; } /** * @param assetsFolder the merged asset folder * @return itself */ public AaptPackageProcessBuilder setAssetsFolder(@NonNull File assetsFolder) { if (!assetsFolder.isDirectory()) { throw new RuntimeException("assetsFolder parameter is not a directory"); } mAssetsFolder = assetsFolder; return this; } /** * @param sourceOutputDir optional source folder to generate R.java * @return itself */ public AaptPackageProcessBuilder setSourceOutputDir(@Nullable String sourceOutputDir) { mSourceOutputDir = sourceOutputDir; return this; } @Nullable public String getSourceOutputDir() { return mSourceOutputDir; } /** * @param symbolOutputDir the folder to write symbols into * @ itself */ public AaptPackageProcessBuilder setSymbolOutputDir(@Nullable String symbolOutputDir) { mSymbolOutputDir = symbolOutputDir; return this; } @Nullable public String getSymbolOutputDir() { return mSymbolOutputDir; } /** * @param libraries the flat list of libraries * @return itself */ public AaptPackageProcessBuilder setLibraries( @NonNull List<? extends SymbolFileProvider> libraries) { mLibraries = libraries; return this; } @NonNull public List<? extends SymbolFileProvider> getLibraries() { return mLibraries == null ? ImmutableList.<SymbolFileProvider>of() : mLibraries; } /** * @param resPackageOutput optional filepath for packaged resources * @return itself */ public AaptPackageProcessBuilder setResPackageOutput(@Nullable String resPackageOutput) { mResPackageOutput = resPackageOutput; return this; } /** * @param proguardOutput optional filepath for proguard file to generate * @return itself */ public AaptPackageProcessBuilder setProguardOutput(@Nullable String proguardOutput) { mProguardOutput = proguardOutput; return this; } /** * @param type the type of the variant being built * @return itself */ public AaptPackageProcessBuilder setType(@NonNull VariantType type) { this.mType = type; return this; } @Nullable public VariantType getType() { return mType; } /** * @param debuggable whether the app is debuggable * @return itself */ public AaptPackageProcessBuilder setDebuggable(boolean debuggable) { this.mDebuggable = debuggable; return this; } /** * @param resourceConfigs a list of resource config filters to pass to aapt. * @return itself */ public AaptPackageProcessBuilder setResourceConfigs(@NonNull Collection<String> resourceConfigs) { this.mResourceConfigs = resourceConfigs; return this; } /** * @param splits optional list of split dimensions values (like a density or an abi). This * will be used by aapt to generate the corresponding pure split apks. * @return itself */ public AaptPackageProcessBuilder setSplits(@NonNull Collection<String> splits) { this.mSplits = splits; return this; } public AaptPackageProcessBuilder setVerbose() { mVerboseExec = true; return this; } /** * @param packageForR Package override to generate the R class in a different package. * @return itself */ public AaptPackageProcessBuilder setPackageForR(@NonNull String packageForR) { this.mPackageForR = packageForR; return this; } public AaptPackageProcessBuilder setPseudoLocalesEnabled(boolean pseudoLocalesEnabled) { mPseudoLocalesEnabled = pseudoLocalesEnabled; return this; } /** * Specifies a preference for a particular density. Resources that do not match this density * and have variants that are a closer match are removed. * @param density the preferred density * @return itself */ public AaptPackageProcessBuilder setPreferredDensity(String density) { mPreferredDensity = density; return this; } @Nullable String getPackageForR() { return mPackageForR; } public ProcessInfo build( @NonNull BuildToolInfo buildToolInfo, @NonNull IAndroidTarget target, @NonNull ILogger logger) { // if both output types are empty, then there's nothing to do and this is an error checkArgument(mSourceOutputDir != null || mResPackageOutput != null, "No output provided for aapt task"); if (mSymbolOutputDir != null || mSourceOutputDir != null) { checkNotNull(mLibraries, "libraries cannot be null if symbolOutputDir or sourceOutputDir is non-null"); } // check resConfigs and split settings coherence. checkResConfigsVersusSplitSettings(logger); ProcessInfoBuilder builder = new ProcessInfoBuilder(); builder.addEnvironments(mEnvironment); String aapt = buildToolInfo.getPath(BuildToolInfo.PathId.AAPT); if (aapt == null || !new File(aapt).isFile()) { throw new IllegalStateException("aapt is missing"); } builder.setExecutable(aapt); builder.addArgs("package"); if (mVerboseExec) { builder.addArgs("-v"); } builder.addArgs("-f"); builder.addArgs("--no-crunch"); // inputs builder.addArgs("-I", target.getPath(IAndroidTarget.ANDROID_JAR)); builder.addArgs("-M", mManifestFile.getAbsolutePath()); if (mResFolder != null) { builder.addArgs("-S", mResFolder.getAbsolutePath()); } if (mAssetsFolder != null) { builder.addArgs("-A", mAssetsFolder.getAbsolutePath()); } // outputs if (mSourceOutputDir != null) { builder.addArgs("-m"); builder.addArgs("-J", mSourceOutputDir); } if (mResPackageOutput != null) { builder.addArgs("-F", mResPackageOutput); } if (mProguardOutput != null) { builder.addArgs("-G", mProguardOutput); } if (mSplits != null) { for (String split : mSplits) { builder.addArgs("--split", split); } } // options controlled by build variants if (mDebuggable) { builder.addArgs("--debug-mode"); } if (mType != VariantType.ANDROID_TEST) { if (mPackageForR != null) { builder.addArgs("--custom-package", mPackageForR); logger.verbose("Custom package for R class: '%s'", mPackageForR); } } if (mPseudoLocalesEnabled) { if (buildToolInfo.getRevision().getMajor() >= 21) { builder.addArgs("--pseudo-localize"); } else { throw new RuntimeException( "Pseudolocalization is only available since Build Tools version 21.0.0," + " please upgrade or turn it off."); } } // library specific options if (mType == VariantType.LIBRARY) { builder.addArgs("--non-constant-id"); } // AAPT options String ignoreAssets = mOptions.getIgnoreAssets(); if (ignoreAssets != null) { builder.addArgs("--ignore-assets", ignoreAssets); } if (mOptions.getFailOnMissingConfigEntry()) { if (buildToolInfo.getRevision().getMajor() > 20) { builder.addArgs("--error-on-missing-config-entry"); } else { throw new IllegalStateException("aaptOptions:failOnMissingConfigEntry cannot be used" + " with SDK Build Tools revision earlier than 21.0.0"); } } // never compress apks. builder.addArgs("-0", "apk"); // add custom no-compress extensions Collection<String> noCompressList = mOptions.getNoCompress(); if (noCompressList != null) { for (String noCompress : noCompressList) { builder.addArgs("-0", noCompress); } } List<String> additionalParameters = mOptions.getAdditionalParameters(); if (!isNullOrEmpty(additionalParameters)) { builder.addArgs(additionalParameters); } List<String> resourceConfigs = new ArrayList<String>(); if (!isNullOrEmpty(mResourceConfigs)) { resourceConfigs.addAll(mResourceConfigs); } if (buildToolInfo.getRevision().getMajor() < 21 && mPreferredDensity != null) { resourceConfigs.add(mPreferredDensity); // when adding a density filter, also always add the nodpi option. resourceConfigs.add(Density.NODPI.getResourceValue()); } // separate the density and language resource configs, since starting in 21, the // density resource configs should be passed with --preferred-density to ensure packaging // of scalable resources when no resource for the preferred density is present. List<String> otherResourceConfigs = new ArrayList<String>(); List<String> densityResourceConfigs = new ArrayList<String>(); if (!resourceConfigs.isEmpty()) { if (buildToolInfo.getRevision().getMajor() >= 21) { for (String resourceConfig : resourceConfigs) { if (Density.getEnum(resourceConfig) != null) { densityResourceConfigs.add(resourceConfig); } else { otherResourceConfigs.add(resourceConfig); } } } else { // before 21, everything is passed with -c option. otherResourceConfigs = resourceConfigs; } } if (!otherResourceConfigs.isEmpty()) { Joiner joiner = Joiner.on(','); builder.addArgs("-c", joiner.join(otherResourceConfigs)); } for (String densityResourceConfig : densityResourceConfigs) { builder.addArgs("--preferred-density", densityResourceConfig); } if (buildToolInfo.getRevision().getMajor() >= 21 && mPreferredDensity != null) { if (!isNullOrEmpty(mResourceConfigs)) { Collection<String> densityResConfig = getDensityResConfigs(mResourceConfigs); if (!densityResConfig.isEmpty()) { throw new RuntimeException(String.format( "When using splits in tools 21 and above, resConfigs should not contain " + "any densities. Right now, it contains \"%1$s\"\n" + "Suggestion: remove these from resConfigs from build.gradle", Joiner.on("\",\"").join(densityResConfig))); } } builder.addArgs("--preferred-density", mPreferredDensity); } if (buildToolInfo.getRevision().getMajor() < 21 && mPreferredDensity != null) { logger.warning(String.format("Warning : Project is building density based multiple APKs" + " but using tools version %1$s, you should upgrade to build-tools 21 or above" + " to ensure proper packaging of resources.", buildToolInfo.getRevision().getMajor())); } if (mSymbolOutputDir != null && (mType == VariantType.LIBRARY || !mLibraries.isEmpty())) { builder.addArgs("--output-text-symbols", mSymbolOutputDir); } return builder.createProcess(); } private void checkResConfigsVersusSplitSettings(ILogger logger) { if (isNullOrEmpty(mResourceConfigs) || isNullOrEmpty(mSplits)) { return; } // only consider the Density related resConfig settings. Collection<String> resConfigs = getDensityResConfigs(mResourceConfigs); List<String> splits = new ArrayList<String>(mSplits); splits.removeAll(resConfigs); if (!splits.isEmpty()) { // some splits are required, yet the resConfigs do not contain the split density value // which mean that the resulting split file would be empty, flag this as an error. throw new RuntimeException(String.format( "Splits for densities \"%1$s\" were configured, yet the resConfigs settings does" + " not include such splits. The resulting split APKs would be empty.\n" + "Suggestion : exclude those splits in your build.gradle : \n" + "splits {\n" + " density {\n" + " enable true\n" + " exclude \"%2$s\"\n" + " }\n" + "}\n" + "OR add them to the resConfigs list.", Joiner.on(",").join(splits), Joiner.on("\",\"").join(splits))); } resConfigs.removeAll(mSplits); if (!resConfigs.isEmpty()) { // there are densities present in the resConfig but not in splits, which mean that those // densities will be packaged in the main APK throw new RuntimeException(String.format( "Inconsistent density configuration, with \"%1$s\" present on " + "resConfig settings, while only \"%2$s\" densities are requested " + "in splits APK density settings.\n" + "Suggestion : remove extra densities from the resConfig : \n" + "defaultConfig {\n" + " resConfigs \"%2$s\"\n" + "}\n" + "OR remove such densities from the split's exclude list.\n", Joiner.on(",").join(resConfigs), Joiner.on("\",\"").join(mSplits))); } } private static boolean isNullOrEmpty(@Nullable Collection<?> collection) { return collection == null || collection.isEmpty(); } private static Collection<String> getDensityResConfigs(Collection<String> resourceConfigs) { return Collections2.filter(new ArrayList<String>(resourceConfigs), new Predicate<String>() { @Override public boolean apply(@Nullable String input) { return Density.getEnum(input) != null; } }); } }