blob: 5f3ed0bdb926c3542270130e6c84e0ca99809805 [file] [log] [blame]
Rico Winda6e4efc2023-08-03 07:51:44 +02001/*
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
17package com.android.build.shrinker;
18
19import com.android.build.shrinker.gatherer.ProtoResourceTableGatherer;
20import com.android.build.shrinker.gatherer.ResourcesGatherer;
21import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
22import com.android.build.shrinker.usages.DexUsageRecorder;
23import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorder;
24import com.android.build.shrinker.usages.ResourceUsageRecorder;
25import com.android.build.shrinker.usages.ToolsAttributeUsageRecorder;
26import com.android.utils.FileUtils;
27import java.io.IOException;
28import java.io.PrintStream;
29import java.nio.file.FileSystem;
30import java.nio.file.Path;
31import java.nio.file.Paths;
32import java.util.ArrayList;
33import java.util.List;
34import java.util.zip.ZipFile;
35import javax.xml.parsers.ParserConfigurationException;
36import org.xml.sax.SAXException;
37
38public 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 Wind6c8de572023-10-31 15:22:52 +0100177 for (String rawResource : options.getRawResources()) {
178 resourceUsageRecorders.add(new ToolsAttributeUsageRecorder(Paths.get(rawResource)));
179 }
Rico Winda6e4efc2023-08-03 07:51:44 +0200180 // 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}