| /* |
| * 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))); |
| // 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); |
| } |
| } |
| } |