| // Copyright (c) 2017, 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.utils; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.Writer; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| public class PackageDistribution { |
| |
| private static final String OLDFILE_PREFIX_TEXT = |
| "\n" |
| + "# Below follow the original package to file mapping rules. These have not been\n" |
| + "# changed by R8.\n" |
| + "\n"; |
| |
| private static final String APPENDED_PREFIX_TEXT = |
| "# The following packages had no mapping in the supplied package file. The\n" |
| + "# mapping rules provided below reflect the mapping that was used by R8. Please\n" |
| + "# use this updated map moving forward to ensure stability of package placement\n" |
| + "# in DEX files (and thus minimize patch size).\n" |
| + "#\n" |
| + "# Note that the updated package placement might not be optimal. Shifting the new\n" |
| + "# packages to DEX files that contain related packages might yield smaller DEX\n" |
| + "# file sizes.\n" |
| + "\n"; |
| |
| private static final String NEW_PACKAGE_MAP_PREFIX_TEXT = |
| "# This file provides a mapping of classes to DEX files in an Android multi-dex\n" |
| +"# application. It is used in conjunction with the R8 DEX file optimizer\n" |
| + "# to enforce a fixed distribution of classes to DEX files.\n" |
| + "#\n" |
| + "# Fixing the class distribution serves two purposes:\n" |
| + "#\n" |
| + "# 1. Keeping classes in the same DEX file reduces the size of patches between\n" |
| + "# two versions of an application.\n" |
| + "# 2. Co-locating classes with their uses can reduce DEX file size. For example,\n" |
| + "# one might want to place the helper classes for credit card processing in\n" |
| + "# the same DEX file that contains the payment related logic.\n" |
| + "#\n" |
| + "# Entries in this file have the following form:\n" |
| + "#\n" |
| + "# <packageSpec>:<file number>\n" |
| + "#\n" |
| + "# Where packageSpec is either the name of a package, e.g., 'com.google.foo', or\n" |
| + "# a package wildcard of the form 'com.google.bar.*'. The former matches exactly\n" |
| + "# the classes in the given package, whereas the latter also matches classes in\n" |
| + "# subpackages. PackageSpec entries may not overlap.\n" |
| + "#\n" |
| + "# Empty lines and lines starting with a '#' are ignored.\n" |
| + "\n"; |
| |
| private static final String NO_PACKAGE_MAP_REQUIRED_TEXT = |
| "\n" |
| + "# Intentionally empty, as the output only has a single DEX file.\n" |
| + "\n"; |
| |
| private final Map<String, Integer> map; |
| |
| private PackageDistribution(Map<String, Integer> map) { |
| this.map = map; |
| } |
| |
| public static PackageDistribution load(InputStream input) throws IOException { |
| return read(new BufferedReader(new InputStreamReader(input))); |
| } |
| |
| public static PackageDistribution load(Path path) { |
| try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { |
| return read(reader); |
| } catch (IOException e) { |
| throw new RuntimeException("Error reading file " + path, e); |
| } |
| } |
| |
| private static PackageDistribution read(BufferedReader reader) throws IOException { |
| String line = null; |
| try { |
| Map<String, Integer> result = new HashMap<>(); |
| while ((line = reader.readLine()) != null) { |
| if (line.length() == 0 || line.startsWith("#")) { |
| continue; |
| } |
| String[] parts = line.split(":"); |
| if (parts.length != 2) { |
| throw new RuntimeException("Error parsing package map line " + line); |
| } |
| String prefix = parts[0]; |
| if (result.containsKey(prefix)) { |
| throw new RuntimeException("Prefix is assigned twice: " + prefix); |
| } |
| int file = Integer.parseInt(parts[1]); |
| result.put(prefix, file); |
| } |
| return new PackageDistribution(result); |
| } catch (NumberFormatException e) { |
| throw new RuntimeException("Error parsing package map line " + line, e); |
| } |
| } |
| |
| public static void formatEntry(Entry<String, Integer> entry, Writer writer) throws IOException { |
| writer.write(entry.getKey()); |
| writer.write(":"); |
| writer.write(entry.getValue().toString()); |
| } |
| |
| public static void writePackageToFileMap( |
| Path target, Map<String, Integer> mappings, PackageDistribution original) throws IOException { |
| BufferedWriter writer = Files.newBufferedWriter(target, StandardCharsets.UTF_8); |
| if (mappings.isEmpty()) { |
| if (original != null) { |
| copyOriginalPackageMap(original, writer); |
| } else { |
| writer.write(NEW_PACKAGE_MAP_PREFIX_TEXT); |
| writer.write(NO_PACKAGE_MAP_REQUIRED_TEXT); |
| } |
| writer.close(); |
| return; |
| } |
| if (original == null) { |
| writer.write(NEW_PACKAGE_MAP_PREFIX_TEXT); |
| } else { |
| writer.write(APPENDED_PREFIX_TEXT); |
| } |
| for (Entry<String, Integer> entry : mappings.entrySet()) { |
| formatEntry(entry, writer); |
| writer.newLine(); |
| } |
| if (original != null) { |
| // Copy the original |
| writer.write(OLDFILE_PREFIX_TEXT); |
| copyOriginalPackageMap(original, writer); |
| } |
| writer.close(); |
| } |
| |
| private static void copyOriginalPackageMap(PackageDistribution original, BufferedWriter writer) |
| throws IOException { |
| for (Entry<String, Integer> entry : original.map.entrySet()) { |
| formatEntry(entry, writer); |
| writer.newLine(); |
| } |
| } |
| |
| public int maxReferencedIndex() { |
| return map.values().stream().max(Integer::compare).orElse(0); |
| } |
| |
| public Set<String> getFiles() { |
| return map.keySet(); |
| } |
| |
| public int get(String file) { |
| return map.getOrDefault(file, -1); |
| } |
| |
| public boolean containsFile(String file) { |
| return map.containsKey(file); |
| } |
| } |