Rico Wind | a6e4efc | 2023-08-03 07:51:44 +0200 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2022 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.build.shrinker; |
| 18 | |
| 19 | import com.android.build.shrinker.gatherer.ProtoResourceTableGatherer; |
| 20 | import com.android.build.shrinker.gatherer.ResourcesGatherer; |
| 21 | import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder; |
| 22 | import com.android.build.shrinker.usages.DexUsageRecorder; |
| 23 | import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorder; |
| 24 | import com.android.build.shrinker.usages.ResourceUsageRecorder; |
| 25 | import com.android.build.shrinker.usages.ToolsAttributeUsageRecorder; |
| 26 | import com.android.utils.FileUtils; |
| 27 | import java.io.IOException; |
| 28 | import java.io.PrintStream; |
| 29 | import java.nio.file.FileSystem; |
| 30 | import java.nio.file.Path; |
| 31 | import java.nio.file.Paths; |
| 32 | import java.util.ArrayList; |
| 33 | import java.util.List; |
| 34 | import java.util.zip.ZipFile; |
| 35 | import javax.xml.parsers.ParserConfigurationException; |
| 36 | import org.xml.sax.SAXException; |
| 37 | |
| 38 | public class ResourceShrinkerCli { |
| 39 | |
| 40 | private static final String INPUT_ARG = "--input"; |
| 41 | private static final String DEX_INPUT_ARG = "--dex_input"; |
| 42 | private static final String OUTPUT_ARG = "--output"; |
| 43 | private static final String RES_ARG = "--raw_resources"; |
| 44 | private static final String HELP_ARG = "--help"; |
| 45 | private static final String PRINT_USAGE_LOG = "--print_usage_log"; |
| 46 | |
| 47 | private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml"; |
| 48 | private static final String RESOURCES_PB = "resources.pb"; |
| 49 | private static final String RES_FOLDER = "res"; |
| 50 | |
| 51 | private static class Options { |
| 52 | private String input; |
| 53 | private final List<String> dex_inputs = new ArrayList<>(); |
| 54 | private String output; |
| 55 | private String usageLog; |
| 56 | private final List<String> rawResources = new ArrayList<>(); |
| 57 | private boolean help; |
| 58 | |
| 59 | private Options() {} |
| 60 | |
| 61 | public static Options parseOptions(String[] args) { |
| 62 | Options options = new Options(); |
| 63 | for (int i = 0; i < args.length; i++) { |
| 64 | String arg = args[i]; |
| 65 | if (arg.startsWith(INPUT_ARG)) { |
| 66 | i++; |
| 67 | if (i == args.length) { |
| 68 | throw new ResourceShrinkingFailedException("No argument given for input"); |
| 69 | } |
| 70 | if (options.input != null) { |
| 71 | throw new ResourceShrinkingFailedException( |
| 72 | "More than one input not supported"); |
| 73 | } |
| 74 | options.input = args[i]; |
| 75 | } else if (arg.startsWith(OUTPUT_ARG)) { |
| 76 | i++; |
| 77 | if (i == args.length) { |
| 78 | throw new ResourceShrinkingFailedException("No argument given for output"); |
| 79 | } |
| 80 | if (options.output != null) { |
| 81 | throw new ResourceShrinkingFailedException( |
| 82 | "More than one output not supported"); |
| 83 | } |
| 84 | options.output = args[i]; |
| 85 | } else if (arg.startsWith(DEX_INPUT_ARG)) { |
| 86 | i++; |
| 87 | if (i == args.length) { |
| 88 | throw new ResourceShrinkingFailedException( |
| 89 | "No argument given for dex_input"); |
| 90 | } |
| 91 | options.dex_inputs.add(args[i]); |
| 92 | } else if (arg.startsWith(PRINT_USAGE_LOG)) { |
| 93 | i++; |
| 94 | if (i == args.length) { |
| 95 | throw new ResourceShrinkingFailedException( |
| 96 | "No argument given for usage log"); |
| 97 | } |
| 98 | if (options.usageLog != null) { |
| 99 | throw new ResourceShrinkingFailedException( |
| 100 | "More than usage log not supported"); |
| 101 | } |
| 102 | options.usageLog = args[i]; |
| 103 | } else if (arg.startsWith(RES_ARG)) { |
| 104 | i++; |
| 105 | if (i == args.length) { |
| 106 | throw new ResourceShrinkingFailedException( |
| 107 | "No argument given for raw_resources"); |
| 108 | } |
| 109 | options.rawResources.add(args[i]); |
| 110 | } else if (arg.equals(HELP_ARG)) { |
| 111 | options.help = true; |
| 112 | } else { |
| 113 | throw new ResourceShrinkingFailedException("Unknown argument " + arg); |
| 114 | } |
| 115 | } |
| 116 | return options; |
| 117 | } |
| 118 | |
| 119 | public String getInput() { |
| 120 | return input; |
| 121 | } |
| 122 | |
| 123 | public String getOutput() { |
| 124 | return output; |
| 125 | } |
| 126 | |
| 127 | public String getUsageLog() { |
| 128 | return usageLog; |
| 129 | } |
| 130 | |
| 131 | public List<String> getRawResources() { |
| 132 | return rawResources; |
| 133 | } |
| 134 | |
| 135 | public boolean isHelp() { |
| 136 | return help; |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | public static void main(String[] args) { |
| 141 | run(args); |
| 142 | } |
| 143 | |
| 144 | protected static ResourceShrinkerImpl run(String[] args) { |
| 145 | try { |
| 146 | Options options = Options.parseOptions(args); |
| 147 | if (options.isHelp()) { |
| 148 | printUsage(); |
| 149 | return null; |
| 150 | } |
| 151 | validateOptions(options); |
| 152 | ResourceShrinkerImpl resourceShrinker = runResourceShrinking(options); |
| 153 | return resourceShrinker; |
| 154 | } catch (IOException | ParserConfigurationException | SAXException e) { |
| 155 | throw new ResourceShrinkingFailedException( |
| 156 | "Failed running resource shrinking: " + e.getMessage(), e); |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | private static ResourceShrinkerImpl runResourceShrinking(Options options) |
| 161 | throws IOException, ParserConfigurationException, SAXException { |
| 162 | validateInput(options.getInput()); |
| 163 | List<ResourceUsageRecorder> resourceUsageRecorders = new ArrayList<>(); |
| 164 | for (String dexInput : options.dex_inputs) { |
| 165 | validateFileExists(dexInput); |
| 166 | resourceUsageRecorders.add( |
| 167 | new DexUsageRecorder( |
| 168 | FileUtils.createZipFilesystem(Paths.get(dexInput)).getPath(""))); |
| 169 | } |
| 170 | Path protoApk = Paths.get(options.getInput()); |
| 171 | Path protoApkOut = Paths.get(options.getOutput()); |
| 172 | FileSystem fileSystemProto = FileUtils.createZipFilesystem(protoApk); |
| 173 | resourceUsageRecorders.add(new DexUsageRecorder(fileSystemProto.getPath(""))); |
| 174 | resourceUsageRecorders.add( |
| 175 | new ProtoAndroidManifestUsageRecorder( |
| 176 | fileSystemProto.getPath(ANDROID_MANIFEST_XML))); |
Rico Wind | 6c8de57 | 2023-10-31 15:22:52 +0100 | [diff] [blame] | 177 | for (String rawResource : options.getRawResources()) { |
| 178 | resourceUsageRecorders.add(new ToolsAttributeUsageRecorder(Paths.get(rawResource))); |
| 179 | } |
Rico Wind | a6e4efc | 2023-08-03 07:51:44 +0200 | [diff] [blame] | 180 | // If the apk contains a raw folder, find keep rules in there |
| 181 | if (new ZipFile(options.getInput()) |
| 182 | .stream().anyMatch(zipEntry -> zipEntry.getName().startsWith("res/raw"))) { |
| 183 | Path rawPath = fileSystemProto.getPath("res", "raw"); |
| 184 | resourceUsageRecorders.add(new ToolsAttributeUsageRecorder(rawPath)); |
| 185 | } |
| 186 | ResourcesGatherer gatherer = |
| 187 | new ProtoResourceTableGatherer(fileSystemProto.getPath(RESOURCES_PB)); |
| 188 | ProtoResourcesGraphBuilder res = |
| 189 | new ProtoResourcesGraphBuilder( |
| 190 | fileSystemProto.getPath(RES_FOLDER), fileSystemProto.getPath(RESOURCES_PB)); |
| 191 | ResourceShrinkerImpl resourceShrinker = |
| 192 | new ResourceShrinkerImpl( |
| 193 | List.of(gatherer), |
| 194 | null, |
| 195 | resourceUsageRecorders, |
| 196 | List.of(res), |
| 197 | options.usageLog != null |
| 198 | ? new FileReporter(Paths.get(options.usageLog).toFile()) |
| 199 | : NoDebugReporter.INSTANCE, |
| 200 | false, // TODO(b/245721267): Add support for bundles |
| 201 | true); |
| 202 | resourceShrinker.analyze(); |
| 203 | |
| 204 | resourceShrinker.rewriteResourcesInApkFormat( |
| 205 | protoApk.toFile(), protoApkOut.toFile(), LinkedResourcesFormat.PROTO); |
| 206 | return resourceShrinker; |
| 207 | } |
| 208 | |
| 209 | private static void validateInput(String input) throws IOException { |
| 210 | ZipFile zipfile = new ZipFile(input); |
| 211 | if (zipfile.getEntry(ANDROID_MANIFEST_XML) == null) { |
| 212 | throw new ResourceShrinkingFailedException( |
| 213 | "Input must include " + ANDROID_MANIFEST_XML); |
| 214 | } |
| 215 | if (zipfile.getEntry(RESOURCES_PB) == null) { |
| 216 | throw new ResourceShrinkingFailedException( |
| 217 | "Input must include " |
| 218 | + RESOURCES_PB |
| 219 | + ". Did you not convert the input apk" |
| 220 | + " to proto?"); |
| 221 | } |
| 222 | if (zipfile.stream().noneMatch(zipEntry -> zipEntry.getName().startsWith(RES_FOLDER))) { |
| 223 | throw new ResourceShrinkingFailedException( |
| 224 | "Input must include a " + RES_FOLDER + " folder"); |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | private static void validateFileExists(String file) { |
| 229 | if (!Paths.get(file).toFile().exists()) { |
| 230 | throw new RuntimeException("Can't find file: " + file); |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | private static void validateOptions(Options options) { |
| 235 | if (options.getInput() == null) { |
| 236 | throw new ResourceShrinkingFailedException("No input given."); |
| 237 | } |
| 238 | if (options.getOutput() == null) { |
| 239 | throw new ResourceShrinkingFailedException("No output destination given."); |
| 240 | } |
| 241 | validateFileExists(options.getInput()); |
| 242 | for (String rawResource : options.getRawResources()) { |
| 243 | validateFileExists(rawResource); |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | private static void printUsage() { |
| 248 | PrintStream out = System.err; |
| 249 | out.println("Usage:"); |
| 250 | out.println(" resourceshrinker "); |
| 251 | out.println(" --input <input-file>, container with manifest, resources table and res"); |
| 252 | out.println(" folder. May contain dex."); |
| 253 | out.println(" --dex_input <input-file> Container with dex files (only dex will be "); |
| 254 | out.println(" handled if this contains other files. Several --dex_input arguments"); |
| 255 | out.println(" are supported"); |
| 256 | out.println(" --output <output-file>"); |
| 257 | out.println(" --raw_resource <xml-file or res directory>"); |
| 258 | out.println(" optional, more than one raw_resoures argument might be given"); |
| 259 | out.println(" --help prints this help message"); |
| 260 | } |
| 261 | |
| 262 | private static class ResourceShrinkingFailedException extends RuntimeException { |
| 263 | public ResourceShrinkingFailedException(String message) { |
| 264 | super(message); |
| 265 | } |
| 266 | |
| 267 | public ResourceShrinkingFailedException(String message, Exception e) { |
| 268 | super(message, e); |
| 269 | } |
| 270 | } |
| 271 | } |