| // 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()); |
| } |
| } |
| |
| } |