blob: 9da37ab44eaa8bad588e7bd5fe5741764d6b6994 [file] [log] [blame]
/*
* Copyright (C) 2015 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.transforms;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent.ContentType;
import com.android.build.api.transform.QualifiedContent.Scope;
import com.android.build.api.transform.SecondaryFile;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.gradle.internal.aapt.AaptGeneration;
import com.android.build.gradle.internal.core.GradleVariantConfiguration;
import com.android.build.gradle.internal.dsl.AaptOptions;
import com.android.build.gradle.internal.pipeline.TransformManager;
import com.android.build.gradle.internal.scope.BuildElements;
import com.android.build.gradle.internal.scope.BuildOutput;
import com.android.build.gradle.internal.scope.ExistingBuildElements;
import com.android.build.gradle.internal.scope.GlobalScope;
import com.android.build.gradle.internal.scope.TaskOutputHolder.TaskOutputType;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.variant.BaseVariantData;
import com.android.build.gradle.internal.variant.MultiOutputPolicy;
import com.android.build.gradle.tasks.ResourceUsageAnalyzer;
import com.android.builder.core.VariantType;
import com.android.ide.common.build.ApkInfo;
import com.android.utils.FileUtils;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.ParserConfigurationException;
import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.logging.Logger;
import org.xml.sax.SAXException;
/**
* Implementation of Resource Shrinking as a transform.
*
* Since this transform only reads the data from the stream but does not output anything
* back into the stream, it is a no-op transform, asking only for referenced scopes, and not
* "consumed" scopes.
* <p>
* To run the tests specifically related to resource shrinking:
* <pre>
* ./gradlew :base:int:test -Dtest.single=ShrinkResourcesTest
* </pre>
*/
public class ShrinkResourcesTransform extends Transform {
/** 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 static boolean ourWarned = true; // Logging disabled until shrinking is on by default.
/**
* Associated variant data that the strip task will be run against. Used to locate not only
* locations the task needs (e.g. for resources and generated R classes) but also to obtain the
* resource merging task, since we will run it a second time here to generate a new .ap_ file
* with fewer resources
*/
@NonNull private final BaseVariantData variantData;
@NonNull private final Logger logger;
@NonNull private final File sourceDir;
@NonNull private final FileCollection resourceDir;
@Nullable private final FileCollection mappingFileSrc;
@NonNull private final FileCollection mergedManifests;
@NonNull private final FileCollection uncompressedResources;
@NonNull private final AaptGeneration aaptGeneration;
@NonNull private final AaptOptions aaptOptions;
@NonNull private final VariantType variantType;
private final boolean isDebuggableBuildType;
@NonNull private final MultiOutputPolicy multiOutputPolicy;
@NonNull private final File compressedResources;
public ShrinkResourcesTransform(
@NonNull BaseVariantData variantData,
@NonNull FileCollection uncompressedResources,
@NonNull File compressedResources,
@NonNull AaptGeneration aaptGeneration,
@NonNull Logger logger) {
VariantScope variantScope = variantData.getScope();
GlobalScope globalScope = variantScope.getGlobalScope();
GradleVariantConfiguration variantConfig = variantData.getVariantConfiguration();
this.variantData = variantData;
this.logger = logger;
this.sourceDir = variantScope.getRClassSourceOutputDir();
this.resourceDir = variantScope.getOutput(TaskOutputType.MERGED_NOT_COMPILED_RES);
this.mappingFileSrc =
variantScope.hasOutput(TaskOutputType.APK_MAPPING)
? variantScope.getOutput(TaskOutputType.APK_MAPPING)
: null;
this.mergedManifests = variantScope.getOutput(TaskOutputType.MERGED_MANIFESTS);
this.uncompressedResources = uncompressedResources;
this.aaptGeneration = aaptGeneration;
this.aaptOptions = globalScope.getExtension().getAaptOptions();
this.variantType = variantData.getType();
this.isDebuggableBuildType = variantConfig.getBuildType().isDebuggable();
this.multiOutputPolicy = variantData.getMultiOutputPolicy();
this.compressedResources = compressedResources;
}
@NonNull
@Override
public String getName() {
return "shrinkRes";
}
@NonNull
@Override
public Set<ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@NonNull
@Override
public Set<ContentType> getOutputTypes() {
return ImmutableSet.of();
}
@NonNull
@Override
public Set<Scope> getScopes() {
return TransformManager.EMPTY_SCOPES;
}
@NonNull
@Override
public Set<Scope> getReferencedScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@NonNull
@Override
public Collection<SecondaryFile> getSecondaryFiles() {
Collection<SecondaryFile> secondaryFiles = Lists.newLinkedList();
// FIXME use Task output to get FileCollection for sourceDir/resourceDir
secondaryFiles.add(SecondaryFile.nonIncremental(sourceDir));
secondaryFiles.add(SecondaryFile.nonIncremental(resourceDir));
if (mappingFileSrc != null) {
secondaryFiles.add(SecondaryFile.nonIncremental(mappingFileSrc));
}
secondaryFiles.add(SecondaryFile.nonIncremental(mergedManifests));
secondaryFiles.add(SecondaryFile.nonIncremental(uncompressedResources));
return secondaryFiles;
}
@NonNull
@Override
public Map<String, Object> getParameterInputs() {
Map<String, Object> params = Maps.newHashMapWithExpectedSize(7);
params.put("aaptGeneration", aaptGeneration.name());
params.put(
"aaptOptions",
Joiner.on(";")
.join(
aaptOptions.getIgnoreAssetsPattern() != null
? aaptOptions.getIgnoreAssetsPattern()
: "",
aaptOptions.getNoCompress() != null
? Joiner.on(":").join(aaptOptions.getNoCompress())
: "",
aaptOptions.getFailOnMissingConfigEntry(),
aaptOptions.getAdditionalParameters() != null
? Joiner.on(":").join(aaptOptions.getAdditionalParameters())
: "",
aaptOptions.getCruncherProcesses()));
params.put("variantType", variantType.name());
params.put("isDebuggableBuildType", isDebuggableBuildType);
params.put("splitHandlingPolicy", multiOutputPolicy);
return params;
}
@NonNull
@Override
public Collection<File> getSecondaryDirectoryOutputs() {
return ImmutableList.of(compressedResources);
}
@Override
public boolean isIncremental() {
return false;
}
@Override
public boolean isCacheable() {
return true;
}
@Override
public void transform(@NonNull TransformInvocation invocation)
throws IOException, TransformException, InterruptedException {
BuildElements mergedManifestsOutputs =
ExistingBuildElements.from(TaskOutputType.MERGED_MANIFESTS, mergedManifests);
ExistingBuildElements.from(TaskOutputType.PROCESSED_RES, uncompressedResources)
.transform(
(ApkInfo apkInfo, File buildInput) ->
splitAction(
apkInfo, buildInput, mergedManifestsOutputs, invocation))
.into(TaskOutputType.SHRUNK_PROCESSED_RES, compressedResources);
}
@Nullable
public File splitAction(
@NonNull ApkInfo apkInfo,
@Nullable File uncompressedResourceFile,
@NonNull BuildElements mergedManifests,
TransformInvocation invocation) {
if (uncompressedResourceFile == null) {
return null;
}
Collection<TransformInput> referencedInputs = invocation.getReferencedInputs();
List<File> classes = new ArrayList<>();
for (TransformInput transformInput : referencedInputs) {
for (DirectoryInput directoryInput : transformInput.getDirectoryInputs()) {
classes.add(directoryInput.getFile());
}
for (JarInput jarInput : transformInput.getJarInputs()) {
classes.add(jarInput.getFile());
}
}
File reportFile = null;
File mappingFile = mappingFileSrc != null ? mappingFileSrc.getSingleFile() : null;
if (mappingFile != null) {
File logDir = mappingFile.getParentFile();
if (logDir != null) {
reportFile = new File(logDir, "resources.txt");
}
}
File compressedResourceFile =
new File(
compressedResources,
"resources-" + apkInfo.getBaseName() + "-stripped.ap_");
FileUtils.mkdirs(compressedResourceFile.getParentFile());
BuildOutput mergedManifest = mergedManifests.element(apkInfo);
if (mergedManifest == null) {
try {
FileUtils.copyFile(uncompressedResourceFile, compressedResourceFile);
} catch (IOException e) {
logger.error("Failed to copy uncompressed resource file :", e);
throw new RuntimeException("Failed to copy uncompressed resource file", e);
}
return compressedResourceFile;
}
// Analyze resources and usages and strip out unused
ResourceUsageAnalyzer analyzer =
new ResourceUsageAnalyzer(
sourceDir,
classes,
mergedManifest.getOutputFile(),
mappingFile,
resourceDir.getSingleFile(),
reportFile);
try {
analyzer.setVerbose(logger.isEnabled(LogLevel.INFO));
analyzer.setDebug(logger.isEnabled(LogLevel.DEBUG));
try {
analyzer.analyze();
} catch (IOException | ParserConfigurationException | SAXException e) {
throw new RuntimeException(e);
}
// Just rewrite the .ap_ file to strip out the res/ files for unused resources
try {
analyzer.rewriteResourceZip(uncompressedResourceFile, compressedResourceFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
// Dump some stats
int unused = analyzer.getUnusedResourceCount();
if (unused > 0) {
StringBuilder sb = new 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 + ")")
long before = uncompressedResourceFile.length();
long after = compressedResourceFile.length();
long percent = (int) ((before - after) * 100 / before);
sb.append(": Binary resource data reduced from ").
append(toKbString(before)).
append("KB to ").
append(toKbString(after)).
append("KB: Removed ").append(percent).append("%");
if (!ourWarned) {
ourWarned = true;
String name = variantData.getVariantConfiguration().getBuildType().getName();
sb.append("\n")
.append(
"Note: If necessary, you can disable resource shrinking by adding\n")
.append("android {\n")
.append(" buildTypes {\n")
.append(" ")
.append(name)
.append(" {\n")
.append(" shrinkResources false\n")
.append(" }\n")
.append(" }\n")
.append("}");
}
System.out.println(sb.toString());
}
} finally {
analyzer.dispose();
}
return compressedResourceFile;
}
private static String toKbString(long size) {
return Integer.toString((int)size/1024);
}
}