diff --git a/tools/android/BUILD b/tools/android/BUILD index 922e7e36..25c731e4 100644 --- a/tools/android/BUILD +++ b/tools/android/BUILD @@ -68,7 +68,11 @@ java_binary( java_binary( name = "resource_shrinker", - main_class = "com.android.build.shrinker.ResourceShrinkerCli", + main_class = "com.google.devtools.build.android.shrinker.BazelResourceShrinkerCli", + srcs = ["src/main/java/com/google/devtools/build/android/shrinker/BazelResourceShrinkerCli.java"], visibility = ["//visibility:public"], - runtime_deps = ["@rules_android_maven//:com_android_tools_build_gradle"], + deps = [ + "@rules_android_maven//:com_android_tools_common", + "@rules_android_maven//:com_android_tools_build_gradle", + ], ) diff --git a/tools/android/src/main/java/com/google/devtools/build/android/shrinker/BazelResourceShrinkerCli.java b/tools/android/src/main/java/com/google/devtools/build/android/shrinker/BazelResourceShrinkerCli.java new file mode 100644 index 00000000..eb1d4b2f --- /dev/null +++ b/tools/android/src/main/java/com/google/devtools/build/android/shrinker/BazelResourceShrinkerCli.java @@ -0,0 +1,292 @@ +/* + * Copyright 2023 The Bazel Authors. All rights reserved. + + * 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.google.devtools.build.android.shrinker; + +import com.android.build.shrinker.ResourceShrinkerImpl; +import com.android.build.shrinker.FileReporter; +import com.android.build.shrinker.NoDebugReporter; +import com.android.build.shrinker.LinkedResourcesFormat; +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.obfuscation.ProguardMappingsRecorder; +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 BazelResourceShrinkerCli { + + 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 PROGUARD_MAPPING_ARG = "--proguard_mapping"; + 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 dex_inputs = new ArrayList<>(); + private String output; + private String usageLog; + private final List rawResources = new ArrayList<>(); + private String proguardMapping; + 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(PROGUARD_MAPPING_ARG)) { + i++; + if (i == args.length) { + throw new ResourceShrinkingFailedException("No argument given for proguard_mapping"); + } + if (options.proguardMapping != null) { + throw new ResourceShrinkingFailedException( + "More than one proguard mapping file is not supported"); + } + options.proguardMapping = 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 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 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)); + ProguardMappingsRecorder proguardMappingsRecorder = null; + if (options.proguardMapping != null ) { + new ProguardMappingsRecorder(Paths.get(options.proguardMapping)); + } + ResourceShrinkerImpl resourceShrinker = + new ResourceShrinkerImpl( + List.of(gatherer), + proguardMappingsRecorder, + 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 , container with manifest, resources table and res"); + out.println(" folder. May contain dex."); + out.println(" --dex_input 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 "); + out.println(" --raw_resource "); + 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); + } + } +}