blob: 55265e4040d88818fdf47907f5cd226b0450753b [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.SecondaryInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.gradle.internal.pipeline.TransformManager;
import com.android.build.gradle.internal.variant.BaseVariantData;
import com.android.build.gradle.internal.variant.BaseVariantOutputData;
import com.android.build.gradle.tasks.ProcessAndroidResources;
import com.android.build.gradle.tasks.ResourceUsageAnalyzer;
import com.android.build.api.transform.Context;
import com.android.build.api.transform.QualifiedContent.ContentType;
import com.android.build.api.transform.QualifiedContent.Scope;
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.TransformOutputProvider;
import com.android.builder.core.AaptPackageProcessBuilder;
import com.android.builder.core.AndroidBuilder;
import com.android.ide.common.process.LoggedProcessOutputHandler;
import com.android.utils.FileUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.logging.Logger;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;
/**
* 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 BaseVariantOutputData variantOutputData;
@NonNull
private final File uncompressedResources;
@NonNull
private final File compressedResources;
@NonNull
private final AndroidBuilder androidBuilder;
@NonNull
private final Logger logger;
@NonNull
private final ImmutableList<File> secondaryInputs;
private final File sourceDir;
private final File resourceDir;
private final File mergedManifest;
private final File mappingFile;
public ShrinkResourcesTransform(
@NonNull BaseVariantOutputData variantOutputData,
@NonNull File uncompressedResources,
@NonNull File compressedResources,
@NonNull AndroidBuilder androidBuilder,
@NonNull Logger logger) {
this.variantOutputData = variantOutputData;
this.uncompressedResources = uncompressedResources;
this.compressedResources = compressedResources;
this.androidBuilder = androidBuilder;
this.logger = logger;
BaseVariantData<?> variantData = variantOutputData.variantData;
ProcessAndroidResources processResourcesTask = variantData.generateRClassTask;
sourceDir = processResourcesTask.getSourceOutputDir();
resourceDir = variantData.getScope().getFinalResourcesDir();
mergedManifest = variantOutputData.manifestProcessorTask.getManifestOutputFile();
mappingFile = variantData.getMappingFile();
if (mappingFile != null) {
secondaryInputs = ImmutableList.of(
uncompressedResources,
sourceDir,
resourceDir,
mergedManifest,
mappingFile);
} else {
secondaryInputs = ImmutableList.of(
uncompressedResources,
sourceDir,
resourceDir,
mergedManifest);
}
}
@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<File> getSecondaryFileInputs() {
return secondaryInputs;
}
@NonNull
@Override
public Collection<File> getSecondaryFileOutputs() {
return ImmutableList.of(compressedResources);
}
@Override
public boolean isIncremental() {
return false;
}
@Override
public void transform(TransformInvocation invocation)
throws IOException, TransformException, InterruptedException {
// there should be only one input since this transform is always applied after
// proguard.
TransformInput input = Iterables.getOnlyElement(invocation.getReferencedInputs());
File minifiedOutJar = Iterables.getOnlyElement(input.getJarInputs()).getFile();
BaseVariantData<?> variantData = variantOutputData.variantData;
ProcessAndroidResources processResourcesTask = variantData.generateRClassTask;
File reportFile = null;
if (mappingFile != null) {
File logDir = mappingFile.getParentFile();
if (logDir != null) {
reportFile = new File(logDir, "resources.txt");
}
}
// Analyze resources and usages and strip out unused
ResourceUsageAnalyzer analyzer = new ResourceUsageAnalyzer(
sourceDir,
minifiedOutJar,
mergedManifest,
mappingFile,
resourceDir,
reportFile);
try {
analyzer.setVerbose(logger.isEnabled(LogLevel.INFO));
analyzer.setDebug(logger.isEnabled(LogLevel.DEBUG));
analyzer.analyze();
if (ResourceUsageAnalyzer.TWO_PASS_AAPT) {
// This is currently not working; we need support from aapt to be able
// to assign a stable set of resources that it should use.
File destination = new File(resourceDir.getParentFile(), resourceDir.getName() + "-stripped");
analyzer.removeUnused(destination);
File sourceOutputs = new File(sourceDir.getParentFile(),
sourceDir.getName() + "-stripped");
FileUtils.mkdirs(sourceOutputs);
// We don't need to emit R files again, but we can do this here such that
// we can *verify* that the R classes generated in the second aapt pass
// matches those we saw the first time around.
//String sourceOutputPath = sourceOutputs?.getAbsolutePath();
String sourceOutputPath = null;
// Repackage the resources:
AaptPackageProcessBuilder aaptPackageCommandBuilder =
new AaptPackageProcessBuilder(
mergedManifest,
processResourcesTask.getAaptOptions())
.setAssetsFolder(processResourcesTask.getAssetsDir())
.setResFolder(destination)
.setLibraries(processResourcesTask.getLibraries())
.setPackageForR(processResourcesTask.getPackageForR())
.setSourceOutputDir(sourceOutputPath)
.setResPackageOutput(compressedResources.getAbsolutePath())
.setType(processResourcesTask.getType())
.setDebuggable(processResourcesTask.getDebuggable())
.setResourceConfigs(processResourcesTask.getResourceConfigs())
.setSplits(processResourcesTask.getSplits());
androidBuilder.processResources(
aaptPackageCommandBuilder,
processResourcesTask.getEnforceUniquePackageName(),
new LoggedProcessOutputHandler(androidBuilder.getLogger())
);
} else {
// Just rewrite the .ap_ file to strip out the res/ files for unused resources
analyzer.rewriteResourceZip(uncompressedResources, compressedResources);
}
// 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 = uncompressedResources.length();
long after = compressedResources.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;
sb.append(
"\nNote: If necessary, you can disable resource shrinking by adding\n" +
"android {\n" +
" buildTypes {\n" +
" " + variantData.getVariantConfiguration().getBuildType().getName() + " {\n" +
" shrinkResources false\n" +
" }\n" +
" }\n" +
"}");
}
System.out.println(sb.toString());
}
} catch (Exception e) {
System.out.println("Failed to shrink resources: " + e.toString() + "; ignoring");
logger.quiet("Failed to shrink resources: ignoring", e);
} finally {
analyzer.dispose();
}
}
private static String toKbString(long size) {
return Integer.toString((int)size/1024);
}
}