| // Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file | 
 | // for details. All rights reserved. Use of this source code is governed by a | 
 | // BSD-style license that can be found in the LICENSE file. | 
 |  | 
 | package com.android.tools.r8.dexsplitter; | 
 |  | 
 | import com.android.tools.r8.CompilationFailedException; | 
 | import com.android.tools.r8.D8Command; | 
 | import com.android.tools.r8.DexIndexedConsumer; | 
 | import com.android.tools.r8.DexSplitterHelper; | 
 | import com.android.tools.r8.Diagnostic; | 
 | import com.android.tools.r8.DiagnosticsHandler; | 
 | import com.android.tools.r8.Keep; | 
 | import com.android.tools.r8.origin.PathOrigin; | 
 | import com.android.tools.r8.utils.AbortException; | 
 | import com.android.tools.r8.utils.ExceptionDiagnostic; | 
 | import com.android.tools.r8.utils.ExceptionUtils; | 
 | import com.android.tools.r8.utils.FeatureClassMapping; | 
 | import com.android.tools.r8.utils.FeatureClassMapping.FeatureMappingException; | 
 | import com.android.tools.r8.utils.OptionsParsing; | 
 | import com.android.tools.r8.utils.OptionsParsing.ParseContext; | 
 | import com.android.tools.r8.utils.StringDiagnostic; | 
 | import com.android.tools.r8.utils.ZipUtils; | 
 | import com.google.common.collect.ImmutableList; | 
 | import java.io.IOException; | 
 | import java.io.InputStream; | 
 | import java.nio.charset.StandardCharsets; | 
 | import java.nio.file.Files; | 
 | import java.nio.file.Path; | 
 | import java.nio.file.Paths; | 
 | import java.util.ArrayList; | 
 | import java.util.Enumeration; | 
 | import java.util.List; | 
 | import java.util.zip.ZipEntry; | 
 | import java.util.zip.ZipFile; | 
 |  | 
 | @Keep | 
 | public final class DexSplitter { | 
 |  | 
 |   private static final String DEFAULT_OUTPUT_DIR = "output"; | 
 |   private static final String DEFAULT_BASE_NAME = "base"; | 
 |  | 
 |   private static final boolean PRINT_ARGS = false; | 
 |  | 
 |   public static class FeatureJar { | 
 |     private String jar; | 
 |     private String outputName; | 
 |  | 
 |     public FeatureJar(String jar, String outputName) { | 
 |       this.jar = jar; | 
 |       this.outputName = outputName; | 
 |     } | 
 |  | 
 |     public FeatureJar(String jar) { | 
 |       this(jar, featureNameFromJar(jar)); | 
 |     } | 
 |  | 
 |     public String getJar() { | 
 |       return jar; | 
 |     } | 
 |  | 
 |     public String getOutputName() { | 
 |       return outputName; | 
 |     } | 
 |  | 
 |     private static String featureNameFromJar(String jar) { | 
 |       Path jarPath = Paths.get(jar); | 
 |       String featureName = jarPath.getFileName().toString(); | 
 |       if (featureName.endsWith(".jar") || featureName.endsWith(".zip")) { | 
 |         featureName = featureName.substring(0, featureName.length() - 4); | 
 |       } | 
 |       return featureName; | 
 |     } | 
 |   } | 
 |  | 
 |   private static class ZipFileOrigin extends PathOrigin { | 
 |  | 
 |     public ZipFileOrigin(Path path) { | 
 |       super(path); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public String part() { | 
 |       return "splitting of file '" + super.part() + "'"; | 
 |     } | 
 |   } | 
 |  | 
 |   @Keep | 
 |   public static final class Options { | 
 |     private final DiagnosticsHandler diagnosticsHandler; | 
 |     private List<String> inputArchives = new ArrayList<>(); | 
 |     private List<FeatureJar> featureJars = new ArrayList<>(); | 
 |     private List<String> baseJars = new ArrayList<>(); | 
 |     private String baseOutputName = DEFAULT_BASE_NAME; | 
 |     private String output = DEFAULT_OUTPUT_DIR; | 
 |     private String featureSplitMapping; | 
 |     private String proguardMap; | 
 |     private String mainDexList; | 
 |     private boolean splitNonClassResources = false; | 
 |  | 
 |     public Options() { | 
 |       this(new DiagnosticsHandler() {}); | 
 |     } | 
 |  | 
 |     public Options(DiagnosticsHandler diagnosticsHandler) { | 
 |       this.diagnosticsHandler = diagnosticsHandler; | 
 |     } | 
 |  | 
 |     public DiagnosticsHandler getDiagnosticsHandler() { | 
 |       return diagnosticsHandler; | 
 |     } | 
 |  | 
 |     public String getMainDexList() { | 
 |       return mainDexList; | 
 |     } | 
 |  | 
 |     public void setMainDexList(String mainDexList) { | 
 |       this.mainDexList = mainDexList; | 
 |     } | 
 |  | 
 |     public String getOutput() { | 
 |       return output; | 
 |     } | 
 |  | 
 |     public void setOutput(String output) { | 
 |       this.output = output; | 
 |     } | 
 |  | 
 |     public String getFeatureSplitMapping() { | 
 |       return featureSplitMapping; | 
 |     } | 
 |  | 
 |     public void setFeatureSplitMapping(String featureSplitMapping) { | 
 |       this.featureSplitMapping = featureSplitMapping; | 
 |     } | 
 |  | 
 |     public String getProguardMap() { | 
 |       return proguardMap; | 
 |     } | 
 |  | 
 |     public void setProguardMap(String proguardMap) { | 
 |       this.proguardMap = proguardMap; | 
 |     } | 
 |  | 
 |     public String getBaseOutputName() { | 
 |       return baseOutputName; | 
 |     } | 
 |  | 
 |     public void setBaseOutputName(String baseOutputName) { | 
 |       this.baseOutputName = baseOutputName; | 
 |     } | 
 |  | 
 |     public void addInputArchive(String inputArchive) { | 
 |       inputArchives.add(inputArchive); | 
 |     } | 
 |  | 
 |     public void addBaseJar(String baseJar) { | 
 |       baseJars.add(baseJar); | 
 |     } | 
 |  | 
 |     private void addFeatureJar(FeatureJar featureJar) { | 
 |       featureJars.add(featureJar); | 
 |     } | 
 |  | 
 |     public void addFeatureJar(String jar) { | 
 |       featureJars.add(new FeatureJar(jar)); | 
 |     } | 
 |  | 
 |     public void addFeatureJar(String jar, String outputName) { | 
 |       featureJars.add(new FeatureJar(jar, outputName)); | 
 |     } | 
 |  | 
 |     public void setSplitNonClassResources(boolean value) { | 
 |       splitNonClassResources = value; | 
 |     } | 
 |  | 
 |     public ImmutableList<String> getInputArchives() { | 
 |       return ImmutableList.copyOf(inputArchives); | 
 |     } | 
 |  | 
 |     ImmutableList<FeatureJar> getFeatureJars() { | 
 |       return ImmutableList.copyOf(featureJars); | 
 |     } | 
 |  | 
 |     ImmutableList<String> getBaseJars() { | 
 |       return ImmutableList.copyOf(baseJars); | 
 |     } | 
 |  | 
 |     // Shorthand error messages. | 
 |     public Diagnostic error(String msg) { | 
 |       StringDiagnostic error = new StringDiagnostic(msg); | 
 |       diagnosticsHandler.error(error); | 
 |       return error; | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * Parse a feature jar argument and return the corresponding FeatureJar representation. | 
 |    * Default to use the name of the jar file if the argument contains no ':', if the argument | 
 |    * contains ':', then use the value after the ':' as the name. | 
 |    * @param argument | 
 |    */ | 
 |   private static FeatureJar parseFeatureJarArgument(String argument) { | 
 |     if (argument.contains(":")) { | 
 |       String[] parts = argument.split(":"); | 
 |       if (parts.length > 2) { | 
 |         throw new RuntimeException("--feature-jar argument contains more than one :"); | 
 |       } | 
 |       return new FeatureJar(parts[0], parts[1]); | 
 |     } | 
 |     return new FeatureJar(argument); | 
 |   } | 
 |  | 
 |   private static Options parseArguments(String[] args) { | 
 |     Options options = new Options(); | 
 |     ParseContext context = new ParseContext(args); | 
 |     while (context.head() != null) { | 
 |       List<String> inputs = OptionsParsing.tryParseMulti(context, "--input"); | 
 |       if (inputs != null) { | 
 |         inputs.forEach(options::addInputArchive); | 
 |         continue; | 
 |       } | 
 |       List<String> featureJars = OptionsParsing.tryParseMulti(context, "--feature-jar"); | 
 |       if (featureJars != null) { | 
 |         featureJars.forEach((feature) -> options.addFeatureJar(parseFeatureJarArgument(feature))); | 
 |         continue; | 
 |       } | 
 |       List<String> baseJars = OptionsParsing.tryParseMulti(context, "--base-jar"); | 
 |       if (baseJars != null) { | 
 |         baseJars.forEach(options::addBaseJar); | 
 |         continue; | 
 |       } | 
 |       String output = OptionsParsing.tryParseSingle(context, "--output", "-o"); | 
 |       if (output != null) { | 
 |         options.setOutput(output); | 
 |         continue; | 
 |       } | 
 |  | 
 |       String mainDexList= OptionsParsing.tryParseSingle(context, "--main-dex-list", null); | 
 |       if (mainDexList!= null) { | 
 |         options.setMainDexList(mainDexList); | 
 |         continue; | 
 |       } | 
 |  | 
 |       String proguardMap = OptionsParsing.tryParseSingle(context, "--proguard-map", null); | 
 |       if (proguardMap != null) { | 
 |         options.setProguardMap(proguardMap); | 
 |         continue; | 
 |       } | 
 |       String baseOutputName = OptionsParsing.tryParseSingle(context, "--base-output-name", null); | 
 |       if (baseOutputName != null) { | 
 |         options.setBaseOutputName(baseOutputName); | 
 |         continue; | 
 |       } | 
 |       String featureSplit = OptionsParsing.tryParseSingle(context, "--feature-splits", null); | 
 |       if (featureSplit != null) { | 
 |         options.setFeatureSplitMapping(featureSplit); | 
 |         continue; | 
 |       } | 
 |       Boolean b = OptionsParsing.tryParseBoolean(context, "--split-non-class-resources"); | 
 |       if (b != null) { | 
 |         options.setSplitNonClassResources(b); | 
 |         continue; | 
 |       } | 
 |       throw new RuntimeException(String.format("Unknown options: '%s'.", context.head())); | 
 |     } | 
 |     return options; | 
 |   } | 
 |  | 
 |   private static FeatureClassMapping createFeatureClassMapping(Options options) | 
 |       throws FeatureMappingException { | 
 |     if (options.getFeatureSplitMapping() != null) { | 
 |       return FeatureClassMapping.fromSpecification( | 
 |           Paths.get(options.getFeatureSplitMapping()), options.getDiagnosticsHandler()); | 
 |     } | 
 |     assert !options.getFeatureJars().isEmpty(); | 
 |     return FeatureClassMapping.Internal.fromJarFiles(options.getFeatureJars(), | 
 |         options.getBaseJars(), options.getBaseOutputName(), options.getDiagnosticsHandler()); | 
 |   } | 
 |  | 
 |   private static void run(String[] args) | 
 |       throws CompilationFailedException, FeatureMappingException { | 
 |     Options options = parseArguments(args); | 
 |     run(options); | 
 |   } | 
 |  | 
 |   public static void run(Options options) | 
 |       throws FeatureMappingException, CompilationFailedException { | 
 |     Diagnostic error = null; | 
 |     if (options.getInputArchives().isEmpty()) { | 
 |       error = options.error("Need at least one --input"); | 
 |     } | 
 |     if (options.getFeatureSplitMapping() == null && options.getFeatureJars().isEmpty()) { | 
 |       error = options.error("You must supply a feature split mapping or feature jars"); | 
 |     } | 
 |     if (options.getFeatureSplitMapping() != null && !options.getFeatureJars().isEmpty()) { | 
 |       error = options.error("You can't supply both a feature split mapping and feature jars"); | 
 |     } | 
 |     if (error != null) { | 
 |       throw new AbortException(error); | 
 |     } | 
 |  | 
 |     D8Command.Builder builder = D8Command.builder(options.diagnosticsHandler); | 
 |  | 
 |  | 
 |     for (String s : options.inputArchives) { | 
 |       builder.addProgramFiles(Paths.get(s)); | 
 |     } | 
 |     // We set the actual consumer on the ApplicationWriter when we have calculated the distribution | 
 |     // since we don't yet know the distribution. | 
 |     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer()); | 
 |     if (options.getMainDexList() != null) { | 
 |       builder.addMainDexListFiles(Paths.get(options.getMainDexList())); | 
 |     } | 
 |  | 
 |     FeatureClassMapping featureClassMapping = createFeatureClassMapping(options); | 
 |  | 
 |     DexSplitterHelper.run( | 
 |         builder.build(), featureClassMapping, options.getOutput(), options.getProguardMap()); | 
 |  | 
 |     if (options.splitNonClassResources) { | 
 |       splitNonClassResources(options, featureClassMapping); | 
 |     } | 
 |   } | 
 |  | 
 |   private static void splitNonClassResources(Options options, | 
 |       FeatureClassMapping featureClassMapping) { | 
 |     for (String s : options.inputArchives) { | 
 |       try (ZipFile zipFile = new ZipFile(s, StandardCharsets.UTF_8)) { | 
 |         Enumeration<? extends ZipEntry> entries = zipFile.entries(); | 
 |         while (entries.hasMoreElements()) { | 
 |           ZipEntry entry = entries.nextElement(); | 
 |           String name = entry.getName(); | 
 |           if (!ZipUtils.isDexFile(name) && !ZipUtils.isClassFile(name)) { | 
 |             String feature = featureClassMapping.featureForNonClass(name); | 
 |             Path outputDir = Paths.get(options.getOutput()).resolve(feature); | 
 |             try (InputStream stream = zipFile.getInputStream(entry)) { | 
 |               Path outputFile = outputDir.resolve(name); | 
 |               Path parent = outputFile.getParent(); | 
 |               if (parent != null) { | 
 |                 Files.createDirectories(parent); | 
 |               } | 
 |               Files.copy(stream, outputFile); | 
 |             } | 
 |           } | 
 |         } | 
 |       } catch (IOException e) { | 
 |         ExceptionDiagnostic error = new ExceptionDiagnostic(e, new ZipFileOrigin(Paths.get(s))); | 
 |         options.getDiagnosticsHandler().error(error); | 
 |         throw new AbortException(error); | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   public static void main(String[] args) { | 
 |     if (PRINT_ARGS) { | 
 |       printArgs(args); | 
 |     } | 
 |     ExceptionUtils.withMainProgramHandler( | 
 |         () -> { | 
 |           try { | 
 |             run(args); | 
 |           } catch (FeatureMappingException e) { | 
 |             // TODO(ricow): Report feature mapping errors via the reporter. | 
 |             System.err.println("Splitting failed: " + e.getMessage()); | 
 |             System.exit(1); | 
 |           } | 
 |         }); | 
 |   } | 
 |  | 
 |   private static void printArgs(String[] args) { | 
 |     System.err.printf("r8.DexSplitter"); | 
 |     for (String s : args) { | 
 |       System.err.printf(" %s", s); | 
 |     } | 
 |     System.err.println(""); | 
 |   } | 
 | } |