blob: 5f3ed0bdb926c3542270130e6c84e0ca99809805 [file] [log] [blame]
/*
* Copyright (C) 2022 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.shrinker;
import com.android.build.shrinker.gatherer.ProtoResourceTableGatherer;
import com.android.build.shrinker.gatherer.ResourcesGatherer;
import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
import com.android.build.shrinker.usages.DexUsageRecorder;
import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorder;
import com.android.build.shrinker.usages.ResourceUsageRecorder;
import com.android.build.shrinker.usages.ToolsAttributeUsageRecorder;
import com.android.utils.FileUtils;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipFile;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
public class ResourceShrinkerCli {
private static final String INPUT_ARG = "--input";
private static final String DEX_INPUT_ARG = "--dex_input";
private static final String OUTPUT_ARG = "--output";
private static final String RES_ARG = "--raw_resources";
private static final String HELP_ARG = "--help";
private static final String PRINT_USAGE_LOG = "--print_usage_log";
private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml";
private static final String RESOURCES_PB = "resources.pb";
private static final String RES_FOLDER = "res";
private static class Options {
private String input;
private final List<String> dex_inputs = new ArrayList<>();
private String output;
private String usageLog;
private final List<String> rawResources = new ArrayList<>();
private boolean help;
private Options() {}
public static Options parseOptions(String[] args) {
Options options = new Options();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (arg.startsWith(INPUT_ARG)) {
i++;
if (i == args.length) {
throw new ResourceShrinkingFailedException("No argument given for input");
}
if (options.input != null) {
throw new ResourceShrinkingFailedException(
"More than one input not supported");
}
options.input = args[i];
} else if (arg.startsWith(OUTPUT_ARG)) {
i++;
if (i == args.length) {
throw new ResourceShrinkingFailedException("No argument given for output");
}
if (options.output != null) {
throw new ResourceShrinkingFailedException(
"More than one output not supported");
}
options.output = args[i];
} else if (arg.startsWith(DEX_INPUT_ARG)) {
i++;
if (i == args.length) {
throw new ResourceShrinkingFailedException(
"No argument given for dex_input");
}
options.dex_inputs.add(args[i]);
} else if (arg.startsWith(PRINT_USAGE_LOG)) {
i++;
if (i == args.length) {
throw new ResourceShrinkingFailedException(
"No argument given for usage log");
}
if (options.usageLog != null) {
throw new ResourceShrinkingFailedException(
"More than usage log not supported");
}
options.usageLog = args[i];
} else if (arg.startsWith(RES_ARG)) {
i++;
if (i == args.length) {
throw new ResourceShrinkingFailedException(
"No argument given for raw_resources");
}
options.rawResources.add(args[i]);
} else if (arg.equals(HELP_ARG)) {
options.help = true;
} else {
throw new ResourceShrinkingFailedException("Unknown argument " + arg);
}
}
return options;
}
public String getInput() {
return input;
}
public String getOutput() {
return output;
}
public String getUsageLog() {
return usageLog;
}
public List<String> getRawResources() {
return rawResources;
}
public boolean isHelp() {
return help;
}
}
public static void main(String[] args) {
run(args);
}
protected static ResourceShrinkerImpl run(String[] args) {
try {
Options options = Options.parseOptions(args);
if (options.isHelp()) {
printUsage();
return null;
}
validateOptions(options);
ResourceShrinkerImpl resourceShrinker = runResourceShrinking(options);
return resourceShrinker;
} catch (IOException | ParserConfigurationException | SAXException e) {
throw new ResourceShrinkingFailedException(
"Failed running resource shrinking: " + e.getMessage(), e);
}
}
private static ResourceShrinkerImpl runResourceShrinking(Options options)
throws IOException, ParserConfigurationException, SAXException {
validateInput(options.getInput());
List<ResourceUsageRecorder> resourceUsageRecorders = new ArrayList<>();
for (String dexInput : options.dex_inputs) {
validateFileExists(dexInput);
resourceUsageRecorders.add(
new DexUsageRecorder(
FileUtils.createZipFilesystem(Paths.get(dexInput)).getPath("")));
}
Path protoApk = Paths.get(options.getInput());
Path protoApkOut = Paths.get(options.getOutput());
FileSystem fileSystemProto = FileUtils.createZipFilesystem(protoApk);
resourceUsageRecorders.add(new DexUsageRecorder(fileSystemProto.getPath("")));
resourceUsageRecorders.add(
new ProtoAndroidManifestUsageRecorder(
fileSystemProto.getPath(ANDROID_MANIFEST_XML)));
for (String rawResource : options.getRawResources()) {
resourceUsageRecorders.add(new ToolsAttributeUsageRecorder(Paths.get(rawResource)));
}
// If the apk contains a raw folder, find keep rules in there
if (new ZipFile(options.getInput())
.stream().anyMatch(zipEntry -> zipEntry.getName().startsWith("res/raw"))) {
Path rawPath = fileSystemProto.getPath("res", "raw");
resourceUsageRecorders.add(new ToolsAttributeUsageRecorder(rawPath));
}
ResourcesGatherer gatherer =
new ProtoResourceTableGatherer(fileSystemProto.getPath(RESOURCES_PB));
ProtoResourcesGraphBuilder res =
new ProtoResourcesGraphBuilder(
fileSystemProto.getPath(RES_FOLDER), fileSystemProto.getPath(RESOURCES_PB));
ResourceShrinkerImpl resourceShrinker =
new ResourceShrinkerImpl(
List.of(gatherer),
null,
resourceUsageRecorders,
List.of(res),
options.usageLog != null
? new FileReporter(Paths.get(options.usageLog).toFile())
: NoDebugReporter.INSTANCE,
false, // TODO(b/245721267): Add support for bundles
true);
resourceShrinker.analyze();
resourceShrinker.rewriteResourcesInApkFormat(
protoApk.toFile(), protoApkOut.toFile(), LinkedResourcesFormat.PROTO);
return resourceShrinker;
}
private static void validateInput(String input) throws IOException {
ZipFile zipfile = new ZipFile(input);
if (zipfile.getEntry(ANDROID_MANIFEST_XML) == null) {
throw new ResourceShrinkingFailedException(
"Input must include " + ANDROID_MANIFEST_XML);
}
if (zipfile.getEntry(RESOURCES_PB) == null) {
throw new ResourceShrinkingFailedException(
"Input must include "
+ RESOURCES_PB
+ ". Did you not convert the input apk"
+ " to proto?");
}
if (zipfile.stream().noneMatch(zipEntry -> zipEntry.getName().startsWith(RES_FOLDER))) {
throw new ResourceShrinkingFailedException(
"Input must include a " + RES_FOLDER + " folder");
}
}
private static void validateFileExists(String file) {
if (!Paths.get(file).toFile().exists()) {
throw new RuntimeException("Can't find file: " + file);
}
}
private static void validateOptions(Options options) {
if (options.getInput() == null) {
throw new ResourceShrinkingFailedException("No input given.");
}
if (options.getOutput() == null) {
throw new ResourceShrinkingFailedException("No output destination given.");
}
validateFileExists(options.getInput());
for (String rawResource : options.getRawResources()) {
validateFileExists(rawResource);
}
}
private static void printUsage() {
PrintStream out = System.err;
out.println("Usage:");
out.println(" resourceshrinker ");
out.println(" --input <input-file>, container with manifest, resources table and res");
out.println(" folder. May contain dex.");
out.println(" --dex_input <input-file> Container with dex files (only dex will be ");
out.println(" handled if this contains other files. Several --dex_input arguments");
out.println(" are supported");
out.println(" --output <output-file>");
out.println(" --raw_resource <xml-file or res directory>");
out.println(" optional, more than one raw_resoures argument might be given");
out.println(" --help prints this help message");
}
private static class ResourceShrinkingFailedException extends RuntimeException {
public ResourceShrinkingFailedException(String message) {
super(message);
}
public ResourceShrinkingFailedException(String message, Exception e) {
super(message, e);
}
}
}