blob: b242b9dbc3843f1f56757a34ae2be9ec5d4ca26d [file] [log] [blame]
// 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);
}
}