blob: c01367fa3d97f6271082d177c4005fd53613d7ca [file] [log] [blame]
// Copyright 2015 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;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.databinding.AndroidDataBinding;
import android.databinding.cli.ProcessXmlOptions;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.core.VariantConfiguration;
import com.android.builder.core.VariantType;
import com.android.builder.dependency.SymbolFileProvider;
import com.android.builder.internal.SymbolLoader;
import com.android.builder.internal.SymbolWriter;
import com.android.builder.model.AaptOptions;
import com.android.ide.common.internal.CommandLineRunner;
import com.android.ide.common.internal.ExecutorSingleton;
import com.android.ide.common.internal.LoggedErrorException;
import com.android.ide.common.internal.PngCruncher;
import com.android.ide.common.res2.MergingException;
import com.android.io.FileWrapper;
import com.android.io.StreamException;
import com.android.manifmerger.ManifestMerger2;
import com.android.manifmerger.ManifestMerger2.Invoker;
import com.android.manifmerger.ManifestMerger2.Invoker.Feature;
import com.android.manifmerger.ManifestMerger2.MergeFailureException;
import com.android.manifmerger.ManifestMerger2.MergeType;
import com.android.manifmerger.ManifestMerger2.SystemProperty;
import com.android.manifmerger.MergingReport;
import com.android.manifmerger.MergingReport.MergedManifestKind;
import com.android.manifmerger.PlaceholderHandler;
import com.android.repository.Revision;
import com.android.utils.ILogger;
import com.android.utils.Pair;
import com.android.utils.StdLogger;
import com.android.xml.AndroidManifest;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.devtools.build.android.Converters.ExistingPathConverter;
import com.google.devtools.build.android.Converters.RevisionConverter;
import com.google.devtools.build.android.SplitConfigurationFilter.UnrecognizedSplitsException;
import com.google.devtools.build.android.resources.RClassGenerator;
import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.TriState;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import javax.xml.xpath.XPathExpressionException;
import org.xml.sax.SAXException;
/**
* Provides a wrapper around the AOSP build tools for resource processing.
*/
public class AndroidResourceProcessor {
private static final Logger logger = Logger.getLogger(AndroidResourceProcessor.class.getName());
/**
* Options class containing flags for Aapt setup.
*/
public static final class AaptConfigOptions extends OptionsBase {
@Option(name = "buildToolsVersion",
defaultValue = "null",
converter = RevisionConverter.class,
category = "config",
help = "Version of the build tools (e.g. aapt) being used, e.g. 23.0.2")
public Revision buildToolsVersion;
@Option(name = "aapt",
defaultValue = "null",
converter = ExistingPathConverter.class,
category = "tool",
help = "Aapt tool location for resource packaging.")
public Path aapt;
@Option(name = "annotationJar",
defaultValue = "null",
converter = ExistingPathConverter.class,
category = "tool",
help = "Annotation Jar for builder invocations.")
public Path annotationJar;
@Option(name = "androidJar",
defaultValue = "null",
converter = ExistingPathConverter.class,
category = "tool",
help = "Path to the android jar for resource packaging and building apks.")
public Path androidJar;
@Option(name = "useAaptCruncher",
defaultValue = "auto",
category = "config",
help = "Use the legacy aapt cruncher, defaults to true for non-LIBRARY packageTypes. "
+ " LIBRARY packages do not benefit from the additional processing as the resources"
+ " will need to be reprocessed during the generation of the final apk. See"
+ " https://code.google.com/p/android/issues/detail?id=67525 for a discussion of the"
+ " different png crunching methods.")
public TriState useAaptCruncher;
@Option(name = "uncompressedExtensions",
defaultValue = "",
converter = CommaSeparatedOptionListConverter.class,
category = "config",
help = "A list of file extensions not to compress.")
public List<String> uncompressedExtensions;
@Option(name = "assetsToIgnore",
defaultValue = "",
converter = CommaSeparatedOptionListConverter.class,
category = "config",
help = "A list of assets extensions to ignore.")
public List<String> assetsToIgnore;
@Option(name = "debug",
defaultValue = "false",
category = "config",
help = "Indicates if it is a debug build.")
public boolean debug;
@Option(name = "resourceConfigs",
defaultValue = "",
converter = CommaSeparatedOptionListConverter.class,
category = "config",
help = "A list of resource config filters to pass to aapt.")
public List<String> resourceConfigs;
private static final String ANDROID_SPLIT_DOCUMENTATION_URL =
"https://developer.android.com/guide/topics/resources/providing-resources.html"
+ "#QualifierRules";
@Option(
name = "split",
defaultValue = "required but ignored due to allowMultiple",
category = "config",
allowMultiple = true,
help =
"An individual split configuration to pass to aapt."
+ " Each split is a list of configuration filters separated by commas."
+ " Configuration filters are lists of configuration qualifiers separated by dashes,"
+ " as used in resource directory names and described on the Android developer site: "
+ ANDROID_SPLIT_DOCUMENTATION_URL
+ " For example, a split might be 'en-television,en-xxhdpi', containing English"
+ " assets which either are for TV screens or are extra extra high resolution."
+ " Multiple splits can be specified by passing this flag multiple times."
+ " Each split flag will produce an additional output file, named by replacing the"
+ " commas in the split specification with underscores, and appending the result to"
+ " the output package name following an underscore."
)
public List<String> splits;
}
/**
* {@link AaptOptions} backed by an {@link AaptConfigOptions}.
*/
public static final class FlagAaptOptions implements AaptOptions {
private final AaptConfigOptions options;
public FlagAaptOptions(AaptConfigOptions options) {
this.options = options;
}
@Override
public Collection<String> getNoCompress() {
if (!options.uncompressedExtensions.isEmpty()) {
return options.uncompressedExtensions;
}
return ImmutableList.of();
}
@Override
public String getIgnoreAssets() {
if (!options.assetsToIgnore.isEmpty()) {
return Joiner.on(":").join(options.assetsToIgnore);
}
return null;
}
@Override
public boolean getFailOnMissingConfigEntry() {
return false;
}
@Override
public List<String> getAdditionalParameters() {
return ImmutableList.of();
}
}
/** Shutdowns and verifies that no tasks are running in the executor service. */
private static final class ExecutorServiceCloser implements Closeable {
private final ListeningExecutorService executorService;
private ExecutorServiceCloser(ListeningExecutorService executorService) {
this.executorService = executorService;
}
@Override
public void close() throws IOException {
List<Runnable> unfinishedTasks = executorService.shutdownNow();
if (!unfinishedTasks.isEmpty()) {
throw new IOException(
"Shutting down the executor with unfinished tasks:" + unfinishedTasks);
}
}
public static Closeable createWith(ListeningExecutorService executorService) {
return new ExecutorServiceCloser(executorService);
}
}
private static final ImmutableMap<SystemProperty, String> SYSTEM_PROPERTY_NAMES = Maps.toMap(
Arrays.asList(SystemProperty.values()), new Function<SystemProperty, String>() {
@Override
public String apply(SystemProperty property) {
if (property == SystemProperty.PACKAGE) {
return "applicationId";
} else {
return property.toCamelCase();
}
}
});
private static final Pattern HEX_REGEX = Pattern.compile("0x[0-9A-Fa-f]{8}");
private final StdLogger stdLogger;
public AndroidResourceProcessor(StdLogger stdLogger) {
this.stdLogger = stdLogger;
}
/**
* Copies the R.txt to the expected place.
*
* @param generatedSourceRoot The path to the generated R.txt.
* @param rOutput The Path to write the R.txt.
* @param staticIds Boolean that indicates if the ids should be set to 0x1 for caching purposes.
*/
public void copyRToOutput(Path generatedSourceRoot, Path rOutput, boolean staticIds) {
try {
Files.createDirectories(rOutput.getParent());
final Path source = generatedSourceRoot.resolve("R.txt");
if (Files.exists(source)) {
if (staticIds) {
String contents =
HEX_REGEX
.matcher(Joiner.on("\n").join(Files.readAllLines(source, UTF_8)))
.replaceAll("0x1");
Files.write(rOutput, contents.getBytes(UTF_8));
} else {
Files.copy(source, rOutput);
}
} else {
// The R.txt wasn't generated, create one for future inheritance, as Bazel always requires
// outputs. This state occurs when there are no resource directories.
Files.createFile(rOutput);
}
// Set to the epoch for caching purposes.
Files.setLastModifiedTime(rOutput, FileTime.fromMillis(0L));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Creates a zip archive from all found R.java files.
*/
public void createSrcJar(Path generatedSourcesRoot, Path srcJar, boolean staticIds) {
try {
Files.createDirectories(srcJar.getParent());
try (final ZipOutputStream zip = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(srcJar)))) {
SymbolFileSrcJarBuildingVisitor visitor =
new SymbolFileSrcJarBuildingVisitor(zip, generatedSourcesRoot, staticIds);
Files.walkFileTree(generatedSourcesRoot, visitor);
}
// Set to the epoch for caching purposes.
Files.setLastModifiedTime(srcJar, FileTime.fromMillis(0L));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Creates a zip archive from all found R.class (and inner class) files.
*/
public void createClassJar(Path generatedClassesRoot, Path classJar) {
try {
Files.createDirectories(classJar.getParent());
try (final ZipOutputStream zip = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(classJar)))) {
ClassJarBuildingVisitor visitor = new ClassJarBuildingVisitor(zip, generatedClassesRoot);
Files.walkFileTree(generatedClassesRoot, visitor);
visitor.writeManifestContent();
}
// Set to the epoch for caching purposes.
Files.setLastModifiedTime(classJar, FileTime.fromMillis(0L));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Copies the AndroidManifest.xml to the specified output location.
*
* @param androidData The MergedAndroidData which contains the manifest to be written to
* manifestOut.
* @param manifestOut The Path to write the AndroidManifest.xml.
*/
public void copyManifestToOutput(MergedAndroidData androidData, Path manifestOut) {
try {
Files.createDirectories(manifestOut.getParent());
Files.copy(androidData.getManifest(), manifestOut);
// Set to the epoch for caching purposes.
Files.setLastModifiedTime(manifestOut, FileTime.fromMillis(0L));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Creates a zip file containing the provided android resources and assets.
*
* @param resourcesRoot The root containing android resources to be written.
* @param assetsRoot The root containing android assets to be written.
* @param output The path to write the zip file
* @param compress Whether or not to compress the content
* @throws IOException
*/
public void createResourcesZip(Path resourcesRoot, Path assetsRoot, Path output, boolean compress)
throws IOException {
try (ZipOutputStream zout = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(output)))) {
if (Files.exists(resourcesRoot)) {
ZipBuilderVisitor visitor = new ZipBuilderVisitor(zout, resourcesRoot, "res");
visitor.setCompress(compress);
Files.walkFileTree(resourcesRoot, visitor);
}
if (Files.exists(assetsRoot)) {
ZipBuilderVisitor visitor = new ZipBuilderVisitor(zout, assetsRoot, "assets");
visitor.setCompress(compress);
Files.walkFileTree(assetsRoot, visitor);
}
}
}
// TODO(bazel-team): Clean up this method call -- 13 params is too many.
/** Processes resources for generated sources, configs and packaging resources. */
public void processResources(
Path aapt,
Path androidJar,
@Nullable Revision buildToolsVersion,
VariantType variantType,
boolean debug,
String customPackageForR,
AaptOptions aaptOptions,
Collection<String> resourceConfigs,
Collection<String> splits,
MergedAndroidData primaryData,
List<DependencyAndroidData> dependencyData,
@Nullable Path sourceOut,
@Nullable Path packageOut,
@Nullable Path proguardOut,
@Nullable Path mainDexProguardOut,
@Nullable Path publicResourcesOut,
@Nullable Path dataBindingInfoOut)
throws IOException, InterruptedException, LoggedErrorException, UnrecognizedSplitsException {
Path androidManifest = primaryData.getManifest();
final Path resourceDir = processDataBindings(primaryData.getResourceDir(), dataBindingInfoOut,
variantType, customPackageForR, androidManifest);
final Path assetsDir = primaryData.getAssetDir();
if (publicResourcesOut != null) {
prepareOutputPath(publicResourcesOut.getParent());
}
runAapt(aapt,
androidJar,
buildToolsVersion,
variantType,
debug,
customPackageForR,
aaptOptions,
resourceConfigs,
splits,
androidManifest,
resourceDir,
assetsDir,
sourceOut,
packageOut,
proguardOut,
mainDexProguardOut,
publicResourcesOut);
// The R needs to be created for each library in the dependencies,
// but only if the current project is not a library.
if (sourceOut != null && variantType != VariantType.LIBRARY) {
writeDependencyPackageRJavaFiles(
dependencyData, customPackageForR, androidManifest, sourceOut);
}
// Reset the output date stamps.
if (proguardOut != null) {
Files.setLastModifiedTime(proguardOut, FileTime.fromMillis(0L));
}
if (mainDexProguardOut != null) {
Files.setLastModifiedTime(mainDexProguardOut, FileTime.fromMillis(0L));
}
if (packageOut != null) {
Files.setLastModifiedTime(packageOut, FileTime.fromMillis(0L));
if (!splits.isEmpty()) {
Iterable<Path> splitFilenames = findAndRenameSplitPackages(packageOut, splits);
for (Path splitFilename : splitFilenames) {
Files.setLastModifiedTime(splitFilename, FileTime.fromMillis(0L));
}
}
}
if (publicResourcesOut != null && Files.exists(publicResourcesOut)) {
Files.setLastModifiedTime(publicResourcesOut, FileTime.fromMillis(0L));
}
}
public void runAapt(
Path aapt,
Path androidJar,
@Nullable Revision buildToolsVersion,
VariantType variantType,
boolean debug,
String customPackageForR,
AaptOptions aaptOptions,
Collection<String> resourceConfigs,
Collection<String> splits,
Path androidManifest,
Path resourceDir,
Path assetsDir,
Path sourceOut,
@Nullable Path packageOut,
@Nullable Path proguardOut,
@Nullable Path mainDexProguardOut,
@Nullable Path publicResourcesOut)
throws InterruptedException, LoggedErrorException, IOException {
AaptCommandBuilder commandBuilder =
new AaptCommandBuilder(aapt)
.forBuildToolsVersion(buildToolsVersion)
.forVariantType(variantType)
// first argument is the command to be executed, "package"
.add("package")
// If the logger is verbose, set aapt to be verbose
.when(stdLogger.getLevel() == StdLogger.Level.VERBOSE).thenAdd("-v")
// Overwrite existing files, if they exist.
.add("-f")
// Resources are precrunched in the merge process.
.add("--no-crunch")
// Do not automatically generate versioned copies of vector XML resources.
.whenVersionIsAtLeast(new Revision(23)).thenAdd("--no-version-vectors")
// Add the android.jar as a base input.
.add("-I", androidJar)
// Add the manifest for validation.
.add("-M", androidManifest.toAbsolutePath())
// Maybe add the resources if they exist
.when(Files.isDirectory(resourceDir)).thenAdd("-S", resourceDir)
// Maybe add the assets if they exist
.when(Files.isDirectory(assetsDir)).thenAdd("-A", assetsDir)
// Outputs
.when(sourceOut != null).thenAdd("-m")
.add("-J", prepareOutputPath(sourceOut))
.add("--output-text-symbols", prepareOutputPath(sourceOut))
.add("-F", packageOut)
.add("-G", proguardOut)
.whenVersionIsAtLeast(new Revision(24)).thenAdd("-D", mainDexProguardOut)
.add("-P", publicResourcesOut)
.when(debug).thenAdd("--debug-mode")
.add("--custom-package", customPackageForR)
// If it is a library, do not generate final java ids.
.whenVariantIs(VariantType.LIBRARY).thenAdd("--non-constant-id")
.add("--ignore-assets", aaptOptions.getIgnoreAssets())
.when(aaptOptions.getFailOnMissingConfigEntry()).thenAdd("--error-on-missing-config-entry")
// Never compress apks.
.add("-0", "apk")
// Add custom no-compress extensions.
.addRepeated("-0", aaptOptions.getNoCompress())
// Filter by resource configuration type.
.add("-c", Joiner.on(',').join(resourceConfigs))
// Split APKs if any splits were specified.
.whenVersionIsAtLeast(new Revision(23)).thenAddRepeated("--split", splits);
try {
new CommandLineRunner(stdLogger).runCmdLine(commandBuilder.build(), null);
} catch (LoggedErrorException e) {
// Add context and throw the error to resume processing.
throw new LoggedErrorException(
e.getCmdLineError(), getOutputWithSourceContext(aapt, e.getOutput()), e.getCmdLine());
}
}
/** Adds 10 lines of source to each syntax error. Very useful for debugging. */
private List<String> getOutputWithSourceContext(Path aapt, List<String> lines)
throws IOException {
List<String> outputWithSourceContext = new ArrayList<>();
for (String line : lines) {
if (line.contains("Duplicate file") || line.contains("Original")) {
String[] parts = line.split(":");
String fileName = parts[0].trim();
outputWithSourceContext.add("\n" + fileName + ":\n\t");
outputWithSourceContext.add(
Joiner.on("\n\t")
.join(
Files.readAllLines(
aapt.getFileSystem().getPath(fileName), StandardCharsets.UTF_8)));
} else if (line.contains("error")) {
String[] parts = line.split(":");
String fileName = parts[0].trim();
try {
int lineNumber = Integer.valueOf(parts[1].trim());
StringBuilder expandedError =
new StringBuilder("\nError at " + lineNumber + " : " + line);
List<String> errorSource =
Files.readAllLines(aapt.getFileSystem().getPath(fileName), StandardCharsets.UTF_8);
for (int i = Math.max(lineNumber - 5, 0);
i < Math.min(lineNumber + 5, errorSource.size());
i++) {
expandedError.append("\n").append(i).append("\t: ").append(errorSource.get(i));
}
outputWithSourceContext.add(expandedError.toString());
} catch (IOException | NumberFormatException formatError) {
outputWithSourceContext.add("error parsing line" + line);
stdLogger.error(formatError, "error during reading source %s", fileName);
}
} else {
outputWithSourceContext.add(line);
}
}
return outputWithSourceContext;
}
/**
* If resources exist and a data binding layout info file is requested: processes data binding
* declarations over those resources, populates the output file, and creates a new resources
* directory with data binding expressions stripped out (so aapt, which doesn't understand
* data binding, can properly read them).
*
* <p>Returns the resources directory that aapt should read.
*/
private Path processDataBindings(Path resourceDir, Path dataBindingInfoOut,
VariantType variantType, String packagePath, Path androidManifest)
throws IOException {
if (dataBindingInfoOut == null) {
return resourceDir;
} else if (!Files.isDirectory(resourceDir)) {
// No resources: no data binding needed. Create a dummy file to satisfy declared outputs.
Files.createFile(dataBindingInfoOut);
return resourceDir;
}
// Strip the file name (the data binding library automatically adds it back in).
// ** The data binding library assumes this file is called "layout-info.zip". **
dataBindingInfoOut = dataBindingInfoOut.getParent();
if (Files.notExists(dataBindingInfoOut)) {
Files.createDirectory(dataBindingInfoOut);
}
Path processedResourceDir = resourceDir.resolveSibling("res_without_databindings");
if (Files.notExists(processedResourceDir)) {
Files.createDirectory(processedResourceDir);
}
ProcessXmlOptions options = new ProcessXmlOptions();
options.setAppId(packagePath);
options.setLibrary(variantType == VariantType.LIBRARY);
options.setResInput(resourceDir.toFile());
options.setResOutput(processedResourceDir.toFile());
options.setLayoutInfoOutput(dataBindingInfoOut.toFile());
options.setZipLayoutInfo(true); // Aggregate data-bound .xml files into a single .zip.
try {
Object minSdk = AndroidManifest.getMinSdkVersion(new FileWrapper(androidManifest.toFile()));
if (minSdk instanceof Integer) {
options.setMinSdk(((Integer) minSdk).intValue());
} else {
// TODO(bazel-team): Enforce the minimum SDK check.
options.setMinSdk(15);
}
} catch (XPathExpressionException | StreamException e) {
// TODO(bazel-team): Enforce the minimum SDK check.
options.setMinSdk(15);
}
try {
AndroidDataBinding.doRun(options);
} catch (Throwable t) {
throw new RuntimeException(t);
}
return processedResourceDir;
}
/** Task to parse java package from AndroidManifest.xml */
private static final class PackageParsingTask implements Callable<String> {
private final File manifest;
PackageParsingTask(File manifest) {
this.manifest = manifest;
}
@Override
public String call() throws Exception {
return VariantConfiguration.getManifestPackage(manifest);
}
}
/** Task to load and parse R.txt symbols */
private static final class SymbolLoadingTask implements Callable<Object> {
private final SymbolLoader symbolLoader;
SymbolLoadingTask(SymbolLoader symbolLoader) {
this.symbolLoader = symbolLoader;
}
@Override
public Object call() throws Exception {
symbolLoader.load();
return null;
}
}
@Nullable
public SymbolLoader loadResourceSymbolTable(
List<SymbolFileProvider> libraries,
String appPackageName,
Path primaryRTxt,
Multimap<String, SymbolLoader> libMap) throws IOException {
// The reported availableProcessors may be higher than the actual resources
// (on a shared system). On the other hand, a lot of the work is I/O, so it's not completely
// CPU bound. As a compromise, divide by 2 the reported availableProcessors.
int numThreads = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(
Executors.newFixedThreadPool(numThreads));
try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) {
// Load the package names from the manifest files.
Map<SymbolFileProvider, ListenableFuture<String>> packageJobs = new HashMap<>();
for (final SymbolFileProvider lib : libraries) {
packageJobs.put(lib, executorService.submit(new PackageParsingTask(lib.getManifest())));
}
Map<SymbolFileProvider, String> packageNames = new HashMap<>();
try {
for (Map.Entry<SymbolFileProvider, ListenableFuture<String>> entry : packageJobs
.entrySet()) {
packageNames.put(entry.getKey(), entry.getValue().get());
}
} catch (InterruptedException | ExecutionException e) {
throw new IOException("Failed to load package name: ", e);
}
// Associate the packages with symbol files.
for (SymbolFileProvider lib : libraries) {
String packageName = packageNames.get(lib);
// If the library package matches the app package skip -- the final app resource IDs are
// stored in the primaryRTxt file.
if (appPackageName.equals(packageName)) {
continue;
}
File rFile = lib.getSymbolFile();
// If the library has no resource, this file won't exist.
if (rFile.isFile()) {
SymbolLoader libSymbols = new SymbolLoader(rFile, stdLogger);
libMap.put(packageName, libSymbols);
}
}
// Even if there are no libraries, load fullSymbolValues, in case we only have resources
// defined for the binary.
File primaryRTxtFile = primaryRTxt.toFile();
SymbolLoader fullSymbolValues = null;
if (primaryRTxtFile.isFile()) {
fullSymbolValues = new SymbolLoader(primaryRTxtFile, stdLogger);
}
// Now load the symbol files in parallel.
List<ListenableFuture<?>> loadJobs = new ArrayList<>();
Iterable<SymbolLoader> toLoad = fullSymbolValues != null
? Iterables.concat(libMap.values(), ImmutableList.of(fullSymbolValues))
: libMap.values();
for (final SymbolLoader loader : toLoad) {
loadJobs.add(executorService.submit(new SymbolLoadingTask(loader)));
}
try {
Futures.allAsList(loadJobs).get();
} catch (InterruptedException | ExecutionException e) {
throw new IOException("Failed to load SymbolFile: ", e);
}
return fullSymbolValues;
}
}
void writeDependencyPackageRJavaFiles(
List<DependencyAndroidData> dependencyData,
String customPackageForR,
Path androidManifest,
Path sourceOut) throws IOException {
List<SymbolFileProvider> libraries = new ArrayList<>();
for (DependencyAndroidData dataDep : dependencyData) {
SymbolFileProvider library = dataDep.asSymbolFileProvider();
libraries.add(library);
}
String appPackageName = customPackageForR;
if (appPackageName == null) {
appPackageName = VariantConfiguration.getManifestPackage(androidManifest.toFile());
}
Multimap<String, SymbolLoader> libSymbolMap = ArrayListMultimap.create();
Path primaryRTxt = sourceOut != null ? sourceOut.resolve("R.txt") : null;
if (primaryRTxt != null && !libraries.isEmpty()) {
SymbolLoader fullSymbolValues = loadResourceSymbolTable(libraries,
appPackageName, primaryRTxt, libSymbolMap);
if (fullSymbolValues != null) {
writePackageRJavaFiles(libSymbolMap, fullSymbolValues, sourceOut);
}
}
}
private void writePackageRJavaFiles(
Multimap<String, SymbolLoader> libMap,
SymbolLoader fullSymbolValues,
Path sourceOut) throws IOException {
// Loop on all the package name, merge all the symbols to write, and write.
for (String packageName : libMap.keySet()) {
Collection<SymbolLoader> symbols = libMap.get(packageName);
SymbolWriter writer = new SymbolWriter(sourceOut.toString(), packageName, fullSymbolValues);
for (SymbolLoader symbolLoader : symbols) {
writer.addSymbolsToWrite(symbolLoader);
}
writer.write();
}
}
void writePackageRClasses(
Multimap<String, SymbolLoader> libMap,
SymbolLoader fullSymbolValues,
String appPackageName,
Path classesOut,
boolean finalFields) throws IOException {
for (String packageName : libMap.keySet()) {
Collection<SymbolLoader> symbols = libMap.get(packageName);
RClassGenerator classWriter = RClassGenerator.fromSymbols(
classesOut, packageName, fullSymbolValues, symbols, finalFields);
classWriter.write();
}
// Unlike the R.java generation, we also write the app's R.class file so that the class
// jar file can be complete (aapt doesn't generate it for us).
RClassGenerator classWriter = RClassGenerator.fromSymbols(classesOut, appPackageName,
fullSymbolValues, ImmutableList.of(fullSymbolValues), finalFields);
classWriter.write();
}
/** Finds aapt's split outputs and renames them according to the input flags. */
private Iterable<Path> findAndRenameSplitPackages(Path packageOut, Iterable<String> splits)
throws UnrecognizedSplitsException, IOException {
String prefix = packageOut.getFileName().toString() + "_";
// The regex java string literal below is received as [\\{}\[\]*?] by the regex engine,
// which produces a character class containing \{}[]*?
// The replacement string literal is received as \\$0 by the regex engine, which places
// a backslash before the match.
String prefixGlob = prefix.replaceAll("[\\\\{}\\[\\]*?]", "\\\\$0") + "*";
Path outputDirectory = packageOut.getParent();
ImmutableList.Builder<String> filenameSuffixes = new ImmutableList.Builder<>();
try (DirectoryStream<Path> glob = Files.newDirectoryStream(outputDirectory, prefixGlob)) {
for (Path file : glob) {
filenameSuffixes.add(file.getFileName().toString().substring(prefix.length()));
}
}
Map<String, String> outputs =
SplitConfigurationFilter.mapFilenamesToSplitFlags(filenameSuffixes.build(), splits);
ImmutableList.Builder<Path> outputPaths = new ImmutableList.Builder<>();
for (Map.Entry<String, String> splitMapping : outputs.entrySet()) {
Path resultPath = packageOut.resolveSibling(prefix + splitMapping.getValue());
outputPaths.add(resultPath);
if (!splitMapping.getKey().equals(splitMapping.getValue())) {
Path sourcePath = packageOut.resolveSibling(prefix + splitMapping.getKey());
Files.move(sourcePath, resultPath);
}
}
return outputPaths.build();
}
public MergedAndroidData processManifest(
VariantType variantType,
String customPackageForR,
String applicationId,
int versionCode,
String versionName,
MergedAndroidData primaryData,
Path processedManifest)
throws IOException {
ManifestMerger2.MergeType mergeType =
variantType == VariantType.DEFAULT
? ManifestMerger2.MergeType.APPLICATION
: ManifestMerger2.MergeType.LIBRARY;
String newManifestPackage =
variantType == VariantType.DEFAULT ? applicationId : customPackageForR;
if (versionCode != -1 || versionName != null || newManifestPackage != null) {
Files.createDirectories(processedManifest.getParent());
// The generics on Invoker don't make sense, so ignore them.
@SuppressWarnings("unchecked")
Invoker<?> manifestMergerInvoker =
ManifestMerger2.newMerger(primaryData.getManifest().toFile(), stdLogger, mergeType);
// Stamp new package
if (newManifestPackage != null) {
manifestMergerInvoker.setOverride(SystemProperty.PACKAGE, newManifestPackage);
}
// Stamp version and applicationId (if provided) into the manifest
if (versionCode > 0) {
manifestMergerInvoker.setOverride(SystemProperty.VERSION_CODE, String.valueOf(versionCode));
}
if (versionName != null) {
manifestMergerInvoker.setOverride(SystemProperty.VERSION_NAME, versionName);
}
MergedManifestKind mergedManifestKind = MergedManifestKind.MERGED;
if (mergeType == ManifestMerger2.MergeType.APPLICATION) {
manifestMergerInvoker.withFeatures(Invoker.Feature.REMOVE_TOOLS_DECLARATIONS);
}
try {
MergingReport mergingReport = manifestMergerInvoker.merge();
switch (mergingReport.getResult()) {
case WARNING:
mergingReport.log(stdLogger);
writeMergedManifest(mergedManifestKind, mergingReport, processedManifest);
break;
case SUCCESS:
writeMergedManifest(mergedManifestKind, mergingReport, processedManifest);
break;
case ERROR:
mergingReport.log(stdLogger);
throw new RuntimeException(mergingReport.getReportString());
default:
throw new RuntimeException("Unhandled result type : " + mergingReport.getResult());
}
} catch (IOException
| SAXException
| ParserConfigurationException
| MergeFailureException e) {
throw new RuntimeException(e);
}
return new MergedAndroidData(
primaryData.getResourceDir(), primaryData.getAssetDir(), processedManifest);
}
return primaryData;
}
/**
* A logger that will print messages to a target OutputStream.
*/
private static final class PrintStreamLogger implements ILogger {
private final PrintStream out;
public PrintStreamLogger(PrintStream stream) {
this.out = stream;
}
@Override
public void error(@Nullable Throwable t, @Nullable String msgFormat, Object... args) {
if (msgFormat != null) {
out.println(String.format("Error: " + msgFormat, args));
}
if (t != null) {
out.printf("Error: %s%n", t.getMessage());
}
}
@Override
public void warning(@NonNull String msgFormat, Object... args) {
out.println(String.format("Warning: " + msgFormat, args));
}
@Override
public void info(@NonNull String msgFormat, Object... args) {
out.println(String.format("Info: " + msgFormat, args));
}
@Override
public void verbose(@NonNull String msgFormat, Object... args) {
out.println(String.format(msgFormat, args));
}
}
/**
* Merge several manifests into one and perform placeholder substitutions. This operation uses
* Gradle semantics.
*
* @param manifest The primary manifest of the merge.
* @param mergeeManifests Manifests to be merged into {@code manifest}.
* @param mergeType Whether the merger should operate in application or library mode.
* @param values A map of strings to be used as manifest placeholders and overrides. packageName
* is the only disallowed value and will be ignored.
* @param output The path to write the resultant manifest to.
* @param logFile The path to write the merger log to.
* @return The path of the resultant manifest, either {@code output}, or {@code manifest} if no
* merging was required.
* @throws IOException if there was a problem writing the merged manifest.
*/
public Path mergeManifest(
Path manifest,
Map<Path, String> mergeeManifests,
MergeType mergeType,
Map<String, String> values,
Path output,
Path logFile)
throws IOException {
if (mergeeManifests.isEmpty() && values.isEmpty()) {
return manifest;
}
Invoker<?> manifestMerger = ManifestMerger2.newMerger(manifest.toFile(), stdLogger, mergeType);
MergedManifestKind mergedManifestKind = MergedManifestKind.MERGED;
if (mergeType == MergeType.APPLICATION) {
manifestMerger.withFeatures(Feature.REMOVE_TOOLS_DECLARATIONS);
}
// Add mergee manifests
List<Pair<String, File>> libraryManifests = new ArrayList<>();
for (Entry<Path, String> mergeeManifest : mergeeManifests.entrySet()) {
libraryManifests.add(Pair.of(mergeeManifest.getValue(), mergeeManifest.getKey().toFile()));
}
manifestMerger.addLibraryManifests(libraryManifests);
// Extract SystemProperties from the provided values.
Map<String, Object> placeholders = new HashMap<>();
placeholders.putAll(values);
for (SystemProperty property : SystemProperty.values()) {
if (values.containsKey(SYSTEM_PROPERTY_NAMES.get(property))) {
manifestMerger.setOverride(property, values.get(SYSTEM_PROPERTY_NAMES.get(property)));
// The manifest merger does not allow explicitly specifying either applicationId or
// packageName as placeholders if SystemProperty.PACKAGE is specified. It forces these
// placeholders to have the same value as specified by SystemProperty.PACKAGE.
if (property == SystemProperty.PACKAGE) {
placeholders.remove(PlaceholderHandler.APPLICATION_ID);
placeholders.remove(PlaceholderHandler.PACKAGE_NAME);
}
}
}
// Add placeholders for all values.
// packageName is populated from either the applicationId override or from the manifest itself;
// it cannot be manually specified.
placeholders.remove(PlaceholderHandler.PACKAGE_NAME);
manifestMerger.setPlaceHolderValues(placeholders);
try {
MergingReport mergingReport = manifestMerger.merge();
if (logFile != null) {
logFile.getParent().toFile().mkdirs();
try (PrintStream stream = new PrintStream(logFile.toFile())) {
mergingReport.log(new PrintStreamLogger(stream));
}
}
switch (mergingReport.getResult()) {
case WARNING:
mergingReport.log(stdLogger);
Files.createDirectories(output.getParent());
writeMergedManifest(mergedManifestKind, mergingReport, output);
break;
case SUCCESS:
Files.createDirectories(output.getParent());
writeMergedManifest(mergedManifestKind, mergingReport, output);
break;
case ERROR:
mergingReport.log(stdLogger);
throw new RuntimeException(mergingReport.getReportString());
default:
throw new RuntimeException("Unhandled result type : " + mergingReport.getResult());
}
} catch (SAXException | ParserConfigurationException | MergeFailureException e) {
throw new RuntimeException(e);
}
return output;
}
private void writeMergedManifest(
MergedManifestKind mergedManifestKind,
MergingReport mergingReport,
Path manifestOut)
throws IOException, SAXException, ParserConfigurationException {
String manifestContents = mergingReport.getMergedDocument(mergedManifestKind);
String annotatedDocument = mergingReport.getMergedDocument(MergedManifestKind.BLAME);
stdLogger.verbose(annotatedDocument);
Files.write(manifestOut, manifestContents.getBytes(UTF_8));
}
public void writeDummyManifestForAapt(Path dummyManifest, String packageForR) throws IOException {
Files.createDirectories(dummyManifest.getParent());
Files.write(dummyManifest, String.format(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\""
+ " package=\"%s\">"
+ "</manifest>", packageForR).getBytes(UTF_8));
}
/**
* Overwrite the package attribute of {@code <manifest>} in an AndroidManifest.xml file.
*
* @param manifest The input manifest.
* @param customPackage The package to write to the manifest.
* @param output The output manifest to generate.
* @return The output manifest if generated or the input manifest if no overwriting is required.
*/
/* TODO(apell): switch from custom xml parsing to Gradle merger with NO_PLACEHOLDER_REPLACEMENT
* set when android common is updated to version 2.5.0.
*/
public Path writeManifestPackage(Path manifest, String customPackage, Path output) {
if (Strings.isNullOrEmpty(customPackage)) {
return manifest;
}
try {
Files.createDirectories(output.getParent());
XMLEventReader reader =
XMLInputFactory.newInstance()
.createXMLEventReader(Files.newInputStream(manifest), UTF_8.name());
XMLEventWriter writer =
XMLOutputFactory.newInstance()
.createXMLEventWriter(Files.newOutputStream(output), UTF_8.name());
XMLEventFactory eventFactory = XMLEventFactory.newInstance();
while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
if (event.isStartElement()
&& event.asStartElement().getName().toString().equalsIgnoreCase("manifest")) {
StartElement element = event.asStartElement();
@SuppressWarnings("unchecked")
Iterator<Attribute> attributes = element.getAttributes();
ImmutableList.Builder<Attribute> newAttributes = ImmutableList.builder();
while (attributes.hasNext()) {
Attribute attr = attributes.next();
if (attr.getName().toString().equalsIgnoreCase("package")) {
newAttributes.add(eventFactory.createAttribute("package", customPackage));
} else {
newAttributes.add(attr);
}
}
writer.add(
eventFactory.createStartElement(
element.getName(), newAttributes.build().iterator(), element.getNamespaces()));
} else {
writer.add(event);
}
}
writer.flush();
} catch (XMLStreamException | FactoryConfigurationError | IOException e) {
throw new RuntimeException(e);
}
return output;
}
/**
* Merges all secondary resources with the primary resources, given that the primary resources
* have not yet been parsed and serialized.
*/
public MergedAndroidData mergeData(
final UnvalidatedAndroidData primary,
final List<? extends SerializedAndroidData> direct,
final List<? extends SerializedAndroidData> transitive,
final Path resourcesOut,
final Path assetsOut,
@Nullable final PngCruncher cruncher,
final VariantType type,
@Nullable final Path symbolsOut)
throws MergingException {
try {
final ParsedAndroidData parsedPrimary = ParsedAndroidData.from(primary);
return mergeData(parsedPrimary, primary.getManifest(), direct, transitive,
resourcesOut, assetsOut, cruncher, type, symbolsOut, null /* rclassWriter */);
} catch (IOException e) {
throw MergingException.wrapException(e).build();
}
}
/**
* Merges all secondary resources with the primary resources, given that the primary resources
* have been separately parsed and serialized.
*/
public MergedAndroidData mergeData(
final SerializedAndroidData primary,
final Path primaryManifest,
final List<? extends SerializedAndroidData> direct,
final List<? extends SerializedAndroidData> transitive,
final Path resourcesOut,
final Path assetsOut,
@Nullable final PngCruncher cruncher,
final VariantType type,
@Nullable AndroidResourceClassWriter rclassWriter)
throws MergingException {
final ParsedAndroidData.Builder primaryBuilder = ParsedAndroidData.Builder.newBuilder();
final AndroidDataSerializer serializer = AndroidDataSerializer.create();
primary.deserialize(serializer, primaryBuilder.consumers());
ParsedAndroidData primaryData = primaryBuilder.build();
return mergeData(primaryData, primaryManifest, direct, transitive,
resourcesOut, assetsOut, cruncher, type, null /* symbolsOut */, rclassWriter);
}
/**
* Merges all secondary resources with the primary resources.
*/
private MergedAndroidData mergeData(
final ParsedAndroidData primary,
final Path primaryManifest,
final List<? extends SerializedAndroidData> direct,
final List<? extends SerializedAndroidData> transitive,
final Path resourcesOut,
final Path assetsOut,
@Nullable final PngCruncher cruncher,
final VariantType type,
@Nullable final Path symbolsOut,
@Nullable AndroidResourceClassWriter rclassWriter)
throws MergingException {
Stopwatch timer = Stopwatch.createStarted();
final ListeningExecutorService executorService =
MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(15));
try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) {
AndroidDataMerger merger = AndroidDataMerger.createWithPathDeduplictor(executorService);
UnwrittenMergedAndroidData merged =
merger.loadAndMerge(
transitive,
direct,
primary,
primaryManifest,
type != VariantType.LIBRARY);
logger.fine(String.format("merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
timer.reset().start();
if (symbolsOut != null) {
AndroidDataSerializer serializer = AndroidDataSerializer.create();
merged.serializeTo(serializer);
serializer.flushTo(symbolsOut);
logger.fine(
String.format(
"serialize merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
timer.reset().start();
}
if (rclassWriter != null) {
merged.writeResourceClass(rclassWriter);
logger.fine(
String.format("write classes finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
timer.reset().start();
}
AndroidDataWriter writer =
AndroidDataWriter.createWith(
resourcesOut.getParent(), resourcesOut, assetsOut, cruncher, executorService);
return merged.write(writer);
} catch (IOException e) {
throw MergingException.wrapException(e).build();
} finally {
logger.fine(
String.format("write merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
}
}
/**
* Shutdown AOSP utilized thread-pool.
*/
public void shutdown() {
FullyQualifiedName.logCacheUsage(logger);
// AOSP code never shuts down its singleton executor and leaves the process hanging.
ExecutorSingleton.getExecutor().shutdownNow();
}
@Nullable
private Path prepareOutputPath(@Nullable Path out) throws IOException {
if (out == null) {
return null;
}
return Files.createDirectories(out);
}
/**
* A FileVisitor that will add all files to be stored in a zip archive.
*/
private static class ZipBuilderVisitor extends SimpleFileVisitor<Path> {
// The earliest date representable in a zip file, 1-1-1980 (the DOS epoch).
private static final long ZIP_EPOCH = 315561600000L;
// ZIP timestamps have a resolution of 2 seconds.
// see http://www.info-zip.org/FAQ.html#limits
private static final long MINIMUM_TIMESTAMP_INCREMENT = 2000L;
private final ZipOutputStream zip;
protected final Path root;
private final String directoryPrefix;
private int storageMethod = ZipEntry.STORED;
ZipBuilderVisitor(ZipOutputStream zip, Path root, String directory) {
this.zip = zip;
this.root = root;
this.directoryPrefix = directory;
}
public void setCompress(boolean compress) {
storageMethod = compress ? ZipEntry.DEFLATED : ZipEntry.STORED;
}
/**
* Normalize timestamps for deterministic builds. Stamp .class files to be a bit newer
* than .java files. See:
* {@link com.google.devtools.build.buildjar.jarhelper.JarHelper#normalizedTimestamp(String)}
*/
protected long normalizeTime(String filename) {
if (filename.endsWith(".class")) {
return ZIP_EPOCH + MINIMUM_TIMESTAMP_INCREMENT;
} else {
return ZIP_EPOCH;
}
}
protected void addEntry(Path file, byte[] content) throws IOException {
String prefix = directoryPrefix != null ? (directoryPrefix + "/") : "";
String relativeName = root.relativize(file).toString();
ZipEntry entry = new ZipEntry(prefix + relativeName);
entry.setMethod(storageMethod);
entry.setTime(normalizeTime(relativeName));
entry.setSize(content.length);
CRC32 crc32 = new CRC32();
crc32.update(content);
entry.setCrc(crc32.getValue());
zip.putNextEntry(entry);
zip.write(content);
zip.closeEntry();
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
byte[] content = Files.readAllBytes(file);
addEntry(file, content);
return FileVisitResult.CONTINUE;
}
}
/**
* A FileVisitor that will add all R.java files to be stored in a zip archive.
*/
private static final class SymbolFileSrcJarBuildingVisitor extends ZipBuilderVisitor {
static final Pattern PACKAGE_PATTERN =
Pattern.compile("\\s*package ([a-zA-Z_$][a-zA-Z\\d_$]*(?:\\.[a-zA-Z_$][a-zA-Z\\d_$]*)*)");
static final Pattern ID_PATTERN =
Pattern.compile("public static int ([\\w\\.]+)=0x[0-9A-fa-f]+;");
static final Pattern INNER_CLASS =
Pattern.compile("public static class ([a-z_]*) \\{(.*?)\\}", Pattern.DOTALL);
private final boolean staticIds;
private SymbolFileSrcJarBuildingVisitor(ZipOutputStream zip, Path root, boolean staticIds) {
super(zip, root, null);
this.staticIds = staticIds;
}
private String replaceIdsWithStaticIds(String contents) {
Matcher packageMatcher = PACKAGE_PATTERN.matcher(contents);
if (!packageMatcher.find()) {
return contents;
}
String pkg = packageMatcher.group(1);
StringBuffer out = new StringBuffer();
Matcher innerClassMatcher = INNER_CLASS.matcher(contents);
while (innerClassMatcher.find()) {
String resourceType = innerClassMatcher.group(1);
Matcher idMatcher = ID_PATTERN.matcher(innerClassMatcher.group(2));
StringBuffer resourceIds = new StringBuffer();
while (idMatcher.find()) {
String javaId = idMatcher.group(1);
idMatcher.appendReplacement(
resourceIds,
String.format(
"public static int %s=0x%08X;", javaId, Objects.hash(pkg, resourceType, javaId)));
}
idMatcher.appendTail(resourceIds);
innerClassMatcher.appendReplacement(
out,
String.format("public static class %s {%s}", resourceType, resourceIds.toString()));
}
innerClassMatcher.appendTail(out);
return out.toString();
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.getFileName().endsWith("R.java")) {
byte[] content = Files.readAllBytes(file);
if (staticIds) {
content =
replaceIdsWithStaticIds(UTF_8.decode(ByteBuffer.wrap(content)).toString())
.getBytes(UTF_8);
}
addEntry(file, content);
}
return FileVisitResult.CONTINUE;
}
}
/**
* A FileVisitor that will add all R class files to be stored in a zip archive.
*/
private static final class ClassJarBuildingVisitor extends ZipBuilderVisitor {
ClassJarBuildingVisitor(ZipOutputStream zip, Path root) {
super(zip, root, null);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path filename = file.getFileName();
String name = filename.toString();
if (name.endsWith(".class")) {
byte[] content = Files.readAllBytes(file);
addEntry(file, content);
}
return FileVisitResult.CONTINUE;
}
private byte[] manifestContent() throws IOException {
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
Attributes.Name createdBy = new Attributes.Name("Created-By");
if (attributes.getValue(createdBy) == null) {
attributes.put(createdBy, "bazel");
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
manifest.write(out);
return out.toByteArray();
}
void writeManifestContent() throws IOException {
addEntry(root.resolve(JarFile.MANIFEST_NAME), manifestContent());
}
}
}