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