blob: 6b5794b5b4e9158300e32e36c94ce3c837be0f99 [file] [log] [blame]
// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.android;
import com.android.build.gradle.tasks.ResourceUsageAnalyzer;
import com.android.builder.core.VariantType;
import com.android.ide.common.xml.AndroidManifestParser;
import com.android.ide.common.xml.ManifestData;
import com.android.io.StreamException;
import com.android.utils.StdLogger;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.android.AndroidResourceProcessor.AaptConfigOptions;
import com.google.devtools.build.android.AndroidResourceProcessor.FlagAaptOptions;
import com.google.devtools.build.android.Converters.ExistingPathConverter;
import com.google.devtools.build.android.Converters.PathConverter;
import com.google.devtools.build.android.Converters.VariantTypeConverter;
import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
/**
* An action to perform resource shrinking using the Gradle resource shrinker.
*
* <pre>
* Example Usage:
* java/com/google/build/android/ResourceShrinkerAction
* --aapt path to sdk/aapt
* --androidJar path to sdk/androidJar
* --shrunkJar path to proguard dead code removal jar
* --resources path to processed resources zip
* --rTxt path to processed resources R.txt
* --primaryManifest path to processed resources AndroidManifest.xml
* --dependencyManifest path to dependency library manifest (repeated flag)
* --shrunkResourceApk path to write shrunk ap_
* --shrunkResources path to write shrunk resources zip
* </pre>
*/
public class ResourceShrinkerAction {
private static final StdLogger stdLogger = new StdLogger(StdLogger.Level.WARNING);
private static final Logger logger = Logger.getLogger(ResourceShrinkerAction.class.getName());
/** Flag specifications for this action. */
public static final class Options extends OptionsBase {
@Option(
name = "shrunkJar",
defaultValue = "null",
category = "input",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
converter = ExistingPathConverter.class,
help = "Path to the shrunk jar from a Proguard run with shrinking enabled."
)
public Path shrunkJar;
@Option(
name = "proguardMapping",
defaultValue = "null",
category = "input",
converter = PathConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Path to the Proguard obfuscation mapping of shrunkJar."
)
public Path proguardMapping;
@Option(
name = "resources",
defaultValue = "null",
category = "input",
converter = ExistingPathConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Path to the resources zip to be shrunk."
)
public Path resourcesZip;
@Option(
name = "rTxt",
defaultValue = "null",
category = "input",
converter = ExistingPathConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Path to the R.txt of the complete resource tree."
)
public Path rTxt;
@Option(
name = "primaryManifest",
defaultValue = "null",
category = "input",
converter = ExistingPathConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Path to the primary manifest for the resources to be shrunk."
)
public Path primaryManifest;
@Option(
name = "dependencyManifest",
allowMultiple = true,
defaultValue = "",
category = "input",
converter = PathConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Paths to the manifests of the dependencies. Specify one path per flag."
)
public List<Path> dependencyManifests;
@Option(
name = "resourcePackages",
defaultValue = "",
category = "input",
converter = CommaSeparatedOptionListConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "A list of packages that resources have been generated for."
)
public List<String> resourcePackages;
@Option(
name = "shrunkResourceApk",
defaultValue = "null",
category = "output",
converter = PathConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Path to where the shrunk resource.ap_ should be written."
)
public Path shrunkApk;
@Option(
name = "shrunkResources",
defaultValue = "null",
category = "output",
converter = PathConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Path to where the shrunk resource.ap_ should be written."
)
public Path shrunkResources;
@Option(
name = "rTxtOutput",
defaultValue = "null",
converter = PathConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
category = "output",
help = "Path to where the R.txt should be written."
)
public Path rTxtOutput;
@Option(
name = "log",
defaultValue = "null",
category = "output",
converter = PathConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Path to where the shrinker log should be written."
)
public Path log;
@Option(
name = "packageType",
defaultValue = "DEFAULT",
converter = VariantTypeConverter.class,
category = "config",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"Variant configuration type for packaging the resources."
+ " Acceptable values DEFAULT, LIBRARY, ANDROID_TEST, UNIT_TEST")
public VariantType packageType;
}
private static AaptConfigOptions aaptConfigOptions;
private static Options options;
private static String getManifestPackage(Path manifest)
throws SAXException, IOException, StreamException, ParserConfigurationException {
ManifestData manifestData = AndroidManifestParser.parse(Files.newInputStream(manifest));
return manifestData.getPackage();
}
private static Set<String> getManifestPackages(Path primaryManifest, List<Path> otherManifests)
throws SAXException, IOException, StreamException, ParserConfigurationException {
Set<String> manifestPackages = new HashSet<>();
manifestPackages.add(getManifestPackage(primaryManifest));
for (Path manifest : otherManifests) {
manifestPackages.add(getManifestPackage(manifest));
}
return manifestPackages;
}
public static void main(String[] args) throws Exception {
final Stopwatch timer = Stopwatch.createStarted();
// Parse arguments.
OptionsParser optionsParser =
OptionsParser.builder()
.optionsClasses(Options.class, AaptConfigOptions.class)
.argsPreProcessor(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()))
.build();
optionsParser.parseAndExitUponError(args);
aaptConfigOptions = optionsParser.getOptions(AaptConfigOptions.class);
options = optionsParser.getOptions(Options.class);
AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor(stdLogger);
// Setup temporary working directories.
try (ScopedTemporaryDirectory scopedTmp =
new ScopedTemporaryDirectory("resource_shrinker_tmp")) {
Path working = scopedTmp.getPath();
final Path resourceFiles = working.resolve("resource_files");
final Path shrunkResources = working.resolve("shrunk_resources");
// Gather package list from manifests.
Set<String> resourcePackages =
getManifestPackages(options.primaryManifest, options.dependencyManifests);
resourcePackages.addAll(options.resourcePackages);
// Expand resource files zip into working directory.
try (ZipInputStream zin =
new ZipInputStream(new FileInputStream(options.resourcesZip.toFile()))) {
ZipEntry entry;
while ((entry = zin.getNextEntry()) != null) {
if (!entry.isDirectory()) {
Path output = resourceFiles.resolve(entry.getName());
Files.createDirectories(output.getParent());
try (FileOutputStream fos = new FileOutputStream(output.toFile())) {
ByteStreams.copy(zin, fos);
}
}
}
}
// Shrink resources.
ResourceUsageAnalyzer resourceShrinker =
new ResourceUsageAnalyzer(
resourcePackages,
options.rTxt,
options.shrunkJar,
options.primaryManifest,
options.proguardMapping,
resourceFiles.resolve("res"),
options.log);
resourceShrinker.shrink(shrunkResources);
logger.fine(
String.format(
"Shrinking resources finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
Path generatedSources = null;
if (options.rTxtOutput != null) {
generatedSources = working.resolve("generated_resources");
}
// Build ap_ with shrunk resources.
resourceProcessor.processResources(
working,
aaptConfigOptions.aapt,
aaptConfigOptions.androidJar,
aaptConfigOptions.buildToolsVersion,
VariantType.DEFAULT,
aaptConfigOptions.debug,
null /* packageForR */,
new FlagAaptOptions(aaptConfigOptions),
aaptConfigOptions.resourceConfigs,
aaptConfigOptions.splits,
new MergedAndroidData(
shrunkResources, resourceFiles.resolve("assets"), options.primaryManifest),
ImmutableList.<DependencyAndroidData>of() /* libraries */,
generatedSources,
options.shrunkApk,
null /* proguardOutput */,
null /* mainDexProguardOutput */,
/* publicResourcesOut= */ null,
/* dataBindingInfoOut= */ null);
if (options.shrunkResources != null) {
ResourcesZip.from(shrunkResources, resourceFiles.resolve("assets"))
.writeTo(options.shrunkResources, /* compress= */ false);
}
if (options.rTxtOutput != null) {
AndroidResourceOutputs.copyRToOutput(
generatedSources, options.rTxtOutput, options.packageType == VariantType.LIBRARY);
}
logger.fine(
String.format(
"Packing resources finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
} catch (Exception e) {
logger.log(Level.SEVERE, "Error shrinking resources", e);
throw e;
} finally {
resourceProcessor.shutdown();
}
}
}