diff options
Diffstat (limited to 'src/tools/android/java/com')
8 files changed, 546 insertions, 73 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/AaptCommandBuilder.java b/src/tools/android/java/com/google/devtools/build/android/AaptCommandBuilder.java index df4f899d34..4bfe5cb10c 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AaptCommandBuilder.java +++ b/src/tools/android/java/com/google/devtools/build/android/AaptCommandBuilder.java @@ -27,7 +27,7 @@ import javax.annotation.Nullable; * Builder for AAPT command lines, with support for making flags conditional on build tools version * and variant type. */ -class AaptCommandBuilder { +public class AaptCommandBuilder { private final ImmutableList.Builder<String> flags = new ImmutableList.Builder<>(); private Revision buildToolsVersion; private VariantType variantType; @@ -37,7 +37,7 @@ class AaptCommandBuilder { } /** Sets the build tools version to be used for {@link #whenVersionIsAtLeast}. */ - AaptCommandBuilder forBuildToolsVersion(@Nullable Revision buildToolsVersion) { + public AaptCommandBuilder forBuildToolsVersion(@Nullable Revision buildToolsVersion) { Preconditions.checkState( this.buildToolsVersion == null, "A build tools version was already specified."); this.buildToolsVersion = buildToolsVersion; @@ -45,7 +45,7 @@ class AaptCommandBuilder { } /** Sets the variant type to be used for {@link #whenVariantIs}. */ - AaptCommandBuilder forVariantType(VariantType variantType) { + public AaptCommandBuilder forVariantType(VariantType variantType) { Preconditions.checkNotNull(variantType); Preconditions.checkState(this.variantType == null, "A variant type was already specified."); this.variantType = variantType; diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java index 935ffa0f3a..6840ac8cc4 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java @@ -265,11 +265,13 @@ public class AndroidResourceMergingAction { if (options.resourcesOutput != null) { Path resourcesDir = AndroidResourceProcessor.processDataBindings( + tmp.resolve("res_no_binding"), mergedData.getResourceDir(), options.dataBindingInfoOut, packageType, options.packageForR, - options.primaryManifest); + options.primaryManifest, + true); // For now, try compressing the library resources that we pass to the validator. This takes // extra CPU resources to pack and unpack (~2x), but can reduce the zip size (~4x). diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceOutputs.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceOutputs.java index 608309f556..3cad94c83f 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceOutputs.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceOutputs.java @@ -16,9 +16,12 @@ package com.google.devtools.build.android; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; import com.google.common.collect.Ordering; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.FileVisitResult; @@ -29,6 +32,8 @@ import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.Collection; +import java.util.GregorianCalendar; +import java.util.List; import java.util.Objects; import java.util.jar.Attributes; import java.util.jar.JarFile; @@ -42,10 +47,71 @@ import java.util.zip.ZipOutputStream; /** Collects all the functionationality for an action to create the final output artifacts. */ public class AndroidResourceOutputs { + static class ZipBuilder implements Closeable { + // 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; + + // The earliest date representable in a zip file, 1-1-1980 (the DOS epoch). + private static final long ZIP_EPOCH = + new GregorianCalendar(1980, 01, 01, 0, 0).getTimeInMillis(); + + private final ZipOutputStream zip; + + private ZipBuilder(ZipOutputStream zip) { + this.zip = zip; + } + + public static ZipBuilder createFor(Path archivePath) throws IOException { + return wrap( + new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(archivePath)))); + } + + public static ZipBuilder wrap(ZipOutputStream zip) { + return new ZipBuilder(zip); + } + + /** + * 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(String rawName, byte[] content, int storageMethod) throws IOException { + // Fix the path for windows. + String relativeName = rawName.replace('\\', '/'); + // Make sure the zip entry is not absolute. + Preconditions.checkArgument(!relativeName.startsWith("/")); + ZipEntry entry = new ZipEntry(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 void close() throws IOException { + zip.close(); + } + } + /** A FileVisitor that will add all R class files to be stored in a zip archive. */ static final class ClassJarBuildingVisitor extends ZipBuilderVisitor { - ClassJarBuildingVisitor(ZipOutputStream zip, Path root) { + ClassJarBuildingVisitor(ZipBuilder zip, Path root) { super(zip, root, null); } @@ -89,8 +155,8 @@ public class AndroidResourceOutputs { private final boolean staticIds; - private SymbolFileSrcJarBuildingVisitor(ZipOutputStream zip, Path root, boolean staticIds) { - super(zip, root, null); + private SymbolFileSrcJarBuildingVisitor(ZipBuilder zipBuilder, Path root, boolean staticIds) { + super(zipBuilder, root, null); this.staticIds = staticIds; } @@ -139,51 +205,21 @@ public class AndroidResourceOutputs { /** A FileVisitor that will add all files to be stored in a zip archive. */ static class ZipBuilderVisitor extends SimpleFileVisitor<Path> { - // 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; - // The earliest date representable in a zip file, 1-1-1980 (the DOS epoch). - private static final long ZIP_EPOCH = 315561600000L; - - private final String directoryPrefix; + protected final String directoryPrefix; private final Collection<Path> paths = new ArrayList<>(); protected final Path root; private int storageMethod = ZipEntry.STORED; - private final ZipOutputStream zip; + private ZipBuilder zipBuilder; - ZipBuilderVisitor(ZipOutputStream zip, Path root, String directory) { - this.zip = zip; + ZipBuilderVisitor(ZipBuilder zipBuilder, Path root, String directory) { this.root = root; - this.directoryPrefix = directory; + this.directoryPrefix = directory != null ? (directory + File.separator) : ""; + this.zipBuilder = zipBuilder; } 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).replace('\\', '/')); - 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(); - } - - /** - * 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; - } + Preconditions.checkArgument(file.startsWith(root), "%s does not start with %s", file, root); + zipBuilder.addEntry(directoryPrefix + root.relativize(file), content, storageMethod); } public void setCompress(boolean compress) { @@ -270,10 +306,8 @@ public class AndroidResourceOutputs { public static 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); + try (final ZipBuilder zip = ZipBuilder.createFor(classJar)) { + ClassJarBuildingVisitor visitor = new ClassJarBuildingVisitor(zip, generatedClassesRoot); Files.walkFileTree(generatedClassesRoot, visitor); visitor.writeEntries(); visitor.writeManifestContent(); @@ -285,6 +319,23 @@ public class AndroidResourceOutputs { } } + /** Creates a zip archive from all files under the provided root. */ + public static void archiveDirectory(Path root, Path archive) { + try { + Files.createDirectories(archive.getParent()); + try (final ZipBuilder zip = ZipBuilder.createFor(archive)) { + ZipBuilderVisitor visitor = new ZipBuilderVisitor(zip, root, null); + visitor.setCompress(false); + Files.walkFileTree(root, visitor); + visitor.writeEntries(); + } + // Set to the epoch for caching purposes. + Files.setLastModifiedTime(archive, FileTime.fromMillis(0L)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + /** * Creates a zip file containing the provided android resources and assets. * @@ -296,18 +347,15 @@ public class AndroidResourceOutputs { */ public static void createResourcesZip( Path resourcesRoot, Path assetsRoot, Path output, boolean compress) throws IOException { - try (ZipOutputStream zout = - new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(output)))) { + try (final ZipBuilder zip = ZipBuilder.createFor(output)) { if (Files.exists(resourcesRoot)) { - ZipBuilderVisitor visitor = - new ZipBuilderVisitor(zout, resourcesRoot, "res"); + ZipBuilderVisitor visitor = new ZipBuilderVisitor(zip, resourcesRoot, "res"); visitor.setCompress(compress); Files.walkFileTree(resourcesRoot, visitor); visitor.writeEntries(); } if (Files.exists(assetsRoot)) { - ZipBuilderVisitor visitor = - new ZipBuilderVisitor(zout, assetsRoot, "assets"); + ZipBuilderVisitor visitor = new ZipBuilderVisitor(zip, assetsRoot, "assets"); visitor.setCompress(compress); Files.walkFileTree(assetsRoot, visitor); visitor.writeEntries(); @@ -319,11 +367,9 @@ public class AndroidResourceOutputs { public static void createSrcJar(Path generatedSourcesRoot, Path srcJar, boolean staticIds) { try { Files.createDirectories(srcJar.getParent()); - try (final ZipOutputStream zip = - new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(srcJar)))) { + try (final ZipBuilder zip = ZipBuilder.createFor(srcJar)) { SymbolFileSrcJarBuildingVisitor visitor = - new SymbolFileSrcJarBuildingVisitor( - zip, generatedSourcesRoot, staticIds); + new SymbolFileSrcJarBuildingVisitor(zip, generatedSourcesRoot, staticIds); Files.walkFileTree(generatedSourcesRoot, visitor); visitor.writeEntries(); } @@ -333,4 +379,33 @@ public class AndroidResourceOutputs { throw new RuntimeException(e); } } + + /** Collects all the compiled resources into an archive, normalizing the paths to the root. */ + public static void archiveCompiledResources( + final Path archiveOut, + final Path databindingResourcesRoot, + final Path compiledRoot, + final List<Path> compiledArtifacts) + throws IOException { + final Path relativeDatabindingProcessedResources = + databindingResourcesRoot.getRoot().relativize(databindingResourcesRoot); + try (ZipBuilder builder = ZipBuilder.createFor(archiveOut)) { + for (Path artifact : compiledArtifacts) { + Path relativeName = artifact; + // remove compiled resources prefix + if (artifact.startsWith(compiledRoot)) { + relativeName = compiledRoot.relativize(relativeName); + } + // remove databinding prefix + if (relativeName.startsWith(relativeDatabindingProcessedResources)) { + relativeName = + relativeName.subpath( + relativeDatabindingProcessedResources.getNameCount(), + relativeName.getNameCount()); + } + + builder.addEntry(relativeName.toString(), Files.readAllBytes(artifact), ZipEntry.STORED); + } + } + } } diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java index 68eed92c55..58ca71bd4f 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java @@ -307,8 +307,15 @@ public class AndroidResourceProcessor { @Nullable Path dataBindingInfoOut) throws IOException, InterruptedException, LoggedErrorException, UnrecognizedSplitsException { Path androidManifest = primaryData.getManifest(); - final Path resourceDir = processDataBindings(primaryData.getResourceDir(), dataBindingInfoOut, - variantType, customPackageForR, androidManifest); + final Path resourceDir = + processDataBindings( + primaryData.getResourceDir().resolveSibling("res_no_binding"), + primaryData.getResourceDir(), + dataBindingInfoOut, + variantType, + customPackageForR, + androidManifest, + true /* shouldZipDataBindingInfo */); final Path assetsDir = primaryData.getAssetDir(); if (publicResourcesOut != null) { @@ -495,13 +502,19 @@ public class AndroidResourceProcessor { /** * 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). + * 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. */ - static Path processDataBindings(Path resourceDir, Path dataBindingInfoOut, - VariantType variantType, String packagePath, Path androidManifest) + static Path processDataBindings( + Path workingDirectory, + Path resourceDir, + Path dataBindingInfoOut, + VariantType variantType, + String packagePath, + Path androidManifest, + boolean shouldZipDataBindingInfo) throws IOException { if (dataBindingInfoOut == null) { @@ -514,15 +527,20 @@ public class AndroidResourceProcessor { // 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); + if (shouldZipDataBindingInfo) { + dataBindingInfoOut = dataBindingInfoOut.getParent(); + if (Files.notExists(dataBindingInfoOut)) { + Files.createDirectory(dataBindingInfoOut); + } } - Path processedResourceDir = resourceDir.resolveSibling("res_without_databindings"); - if (Files.notExists(processedResourceDir)) { - Files.createDirectory(processedResourceDir); - } + // Create a directory for the resources, namespaced with the old resource path + Path processedResourceDir = + Files.createDirectories( + workingDirectory.resolve( + resourceDir.isAbsolute() + ? resourceDir.getRoot().relativize(resourceDir) + : resourceDir)); ProcessXmlOptions options = new ProcessXmlOptions(); options.setAppId(packagePath); @@ -530,7 +548,8 @@ public class AndroidResourceProcessor { options.setResInput(resourceDir.toFile()); options.setResOutput(processedResourceDir.toFile()); options.setLayoutInfoOutput(dataBindingInfoOut.toFile()); - options.setZipLayoutInfo(true); // Aggregate data-bound .xml files into a single .zip. + // Whether or not to aggregate data-bound .xml files into a single .zip. + options.setZipLayoutInfo(shouldZipDataBindingInfo); try { Object minSdk = AndroidManifest.getMinSdkVersion(new FileWrapper(androidManifest.toFile())); diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD index 1d6aef86e3..834483aa98 100644 --- a/src/tools/android/java/com/google/devtools/build/android/BUILD +++ b/src/tools/android/java/com/google/devtools/build/android/BUILD @@ -34,6 +34,7 @@ java_library( srcs = glob([ "*.java", "xml/*.java", + "aapt2/*.java", ]), deps = [ "//src/main/java/com/google/devtools/common/options", diff --git a/src/tools/android/java/com/google/devtools/build/android/CompileLibraryResourcesAction.java b/src/tools/android/java/com/google/devtools/build/android/CompileLibraryResourcesAction.java new file mode 100644 index 0000000000..a6afad5ad0 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/CompileLibraryResourcesAction.java @@ -0,0 +1,211 @@ +// Copyright 2017 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 com.android.builder.core.VariantType; +import com.android.repository.Revision; +import com.google.common.base.Preconditions; +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.PathConverter; +import com.google.devtools.build.android.Converters.RevisionConverter; +import com.google.devtools.build.android.aapt2.ResourceCompiler; +import com.google.devtools.common.options.Option; +import com.google.devtools.common.options.OptionDocumentationCategory; +import com.google.devtools.common.options.OptionEffectTag; +import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParser; +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +/** Compiles resources using aapt2 and archives them to zip. */ +public class CompileLibraryResourcesAction { + /** Flag specifications for this action. */ + public static final class Options extends OptionsBase { + + @Option( + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + name = "resource", + defaultValue = "", + allowMultiple = true, + converter = ExistingPathConverter.class, + category = "input", + help = "The resources to compile with aapt2." + ) + public List<Path> resources; + + @Option( + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + name = "output", + defaultValue = "null", + converter = PathConverter.class, + category = "output", + help = "Path to write the zip of compiled resources." + ) + public Path output; + + @Option( + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + name = "aapt2", + defaultValue = "null", + converter = ExistingPathConverter.class, + category = "tool", + help = "Aapt2 tool location for resource compilation." + ) + public Path aapt2; + + @Option( + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + 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( + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + name = "packagePath", + defaultValue = "null", + category = "input", + help = + "The package path of the library being processed." + + " This value is required for processing data binding." + ) + public String packagePath; + + @Option( + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + name = "manifest", + defaultValue = "null", + category = "input", + converter = ExistingPathConverter.class, + help = + "The manifest of the library being processed." + + " This value is required for processing data binding." + ) + public Path manifest; + + @Option( + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + name = "dataBindingInfoOut", + defaultValue = "null", + category = "output", + converter = PathConverter.class, + help = + "Path for the derived data binding metadata." + + " This value is required for processing data binding." + ) + public Path dataBindingInfoOut; + } + + static final Logger logger = Logger.getLogger(CompileLibraryResourcesAction.class.getName()); + + public static void main(String[] args) throws Exception { + OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class); + optionsParser.enableParamsFileSupport(FileSystems.getDefault()); + optionsParser.parseAndExitUponError(args); + + Options options = optionsParser.getOptions(Options.class); + + Preconditions.checkNotNull(options.resources); + Preconditions.checkNotNull(options.output); + Preconditions.checkNotNull(options.aapt2); + + try (ScopedTemporaryDirectory scopedTmp = + new ScopedTemporaryDirectory("android_resources_tmp")) { + final Path tmp = scopedTmp.getPath(); + final Path databindingResourcesRoot = + Files.createDirectories(tmp.resolve("android_data_binding_resources")); + final Path databindingMetaData = + Files.createDirectories(tmp.resolve("android_data_binding_metadata")); + final Path compiledResources = Files.createDirectories(tmp.resolve("compiled")); + // 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); + final ListeningExecutorService executorService = + MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(numThreads)); + try (final Closeable closeable = ExecutorServiceCloser.createWith(executorService)) { + final ResourceCompiler compiler = + ResourceCompiler.create( + executorService, compiledResources, options.aapt2, options.buildToolsVersion); + for (final Path resource : + maybeProcessDataBindings( + databindingResourcesRoot, + databindingMetaData, + options.dataBindingInfoOut, + options.manifest, + options.packagePath, + options.resources)) { + compiler.queueDirectoryForCompilation(resource); + } + AndroidResourceOutputs.archiveCompiledResources( + options.output, + databindingResourcesRoot, + compiledResources, + compiler.getCompiledArtifacts()); + } + } + } + + private static List<Path> maybeProcessDataBindings( + Path resourceRoot, + Path databindingMetaData, + Path dataBindingInfoOut, + Path manifest, + String packagePath, + List<Path> resources) + throws IOException { + if (dataBindingInfoOut == null) { + return resources; + } + + Preconditions.checkNotNull(manifest); + Preconditions.checkNotNull(packagePath); + + List<Path> processed = new ArrayList<>(); + for (Path resource : resources) { + processed.add( + AndroidResourceProcessor.processDataBindings( + resourceRoot, + resource, + databindingMetaData, + VariantType.LIBRARY, + packagePath, + manifest, + false)); + } + + AndroidResourceOutputs.archiveDirectory(databindingMetaData, dataBindingInfoOut); + return processed; + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java b/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java index e4a62c8004..5e89dcdec5 100644 --- a/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java +++ b/src/tools/android/java/com/google/devtools/build/android/ResourceProcessorBusyBox.java @@ -108,6 +108,12 @@ public class ResourceProcessorBusyBox { void call(String[] args) throws Exception { ManifestMergerAction.main(args); } + }, + COMPILE_LIBRARY_RESOURCES() { + @Override + void call(String[] args) throws Exception { + CompileLibraryResourcesAction.main(args); + } }; abstract void call(String[] args) throws Exception; @@ -133,7 +139,7 @@ public class ResourceProcessorBusyBox { help = "The processing tool to execute. " + "Valid tools: PACKAGE, VALIDATE, GENERATE_BINARY_R, GENERATE_LIBRARY_R, PARSE, " - + "MERGE, GENERATE_AAR, SHRINK, MERGE_MANIFEST." + + "MERGE, GENERATE_AAR, SHRINK, MERGE_MANIFEST, COMPILE_LIBRARY_RESOURCES." ) public Tool tool; } diff --git a/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceCompiler.java b/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceCompiler.java new file mode 100644 index 0000000000..240f8e0d04 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceCompiler.java @@ -0,0 +1,159 @@ +// Copyright 2017 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.aapt2; + +import com.android.builder.core.VariantType; +import com.android.repository.Revision; +import com.google.common.base.Joiner; +import com.google.common.io.CharStreams; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.devtools.build.android.AaptCommandBuilder; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +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.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +/** Invokes aapt2 to compile resources. */ +public class ResourceCompiler { + + private final CompilingVisitor compilingVisitor; + + private static class CompileTask implements Callable<Path> { + + private final Path file; + private final Path compiledResourcesOut; + private final Path aapt2; + private final Revision buildToolsVersion; + + public CompileTask( + Path file, Path compiledResourcesOut, Path aapt2, Revision buildToolsVersion) { + this.file = file; + this.compiledResourcesOut = compiledResourcesOut; + this.aapt2 = aapt2; + this.buildToolsVersion = buildToolsVersion; + } + + @Override + public Path call() throws Exception { + List<String> processLog = new ArrayList<>(); + AaptCommandBuilder commandBuilder = + new AaptCommandBuilder(aapt2) + .forBuildToolsVersion(buildToolsVersion) + .forVariantType(VariantType.LIBRARY) + .add("compile") + .add("-v") + .add("--legacy") + .add("-o", compiledResourcesOut.toString()) + .add(file.toString()); + final Process process = + new ProcessBuilder().command(commandBuilder.build()).redirectErrorStream(true).start(); + processLog.add("Command:"); + processLog.add(commandBuilder.build().toString()); + final InputStreamReader stdout = + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8); + while (process.isAlive()) { + processLog.add(CharStreams.toString(stdout)); + } + if (process.exitValue() != 0) { + throw new RuntimeException( + "Error compiling " + file + "\n" + Joiner.on("\n").join(processLog)); + } + String type = file.getParent().getFileName().toString(); + String filename = file.getFileName().toString(); + if (type.startsWith("values")) { + filename = filename.substring(0, filename.indexOf('.')) + ".arsc"; + } + return compiledResourcesOut.resolve(type + "_" + filename + ".flat"); + } + } + + private static class CompilingVisitor extends SimpleFileVisitor<Path> { + + private final ListeningExecutorService executorService; + private final Path compiledResources; + private final List<ListenableFuture<Path>> tasks = new ArrayList<>(); + private final Path aapt2; + private final Revision buildToolsVersion; + + public CompilingVisitor( + ListeningExecutorService executorService, + Path compiledResources, + Path aapt2, + Revision buildToolsVersion) { + this.executorService = executorService; + this.compiledResources = compiledResources; + this.aapt2 = aapt2; + this.buildToolsVersion = buildToolsVersion; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!Files.isDirectory(file)) { + tasks.add( + executorService.submit( + new CompileTask( + file, + // Creates a relative output path based on the input path under the + // compiledResources path. + Files.createDirectories( + compiledResources.resolve( + (file.isAbsolute() ? file.getRoot().relativize(file) : file) + .getParent() + .getParent())), + aapt2, + buildToolsVersion))); + } + return super.visitFile(file, attrs); + } + + List<Path> getCompiledArtifacts() throws InterruptedException, ExecutionException { + return Futures.allAsList(tasks).get(); + } + } + + /** Creates a new {@link ResourceCompiler}. */ + public static ResourceCompiler create( + ListeningExecutorService executorService, + Path compiledResources, + Path aapt2, + Revision buildToolsVersion) { + return new ResourceCompiler( + new CompilingVisitor(executorService, compiledResources, aapt2, buildToolsVersion)); + } + + private ResourceCompiler(CompilingVisitor compilingVisitor) { + this.compilingVisitor = compilingVisitor; + } + + /** Adds a task to compile the directory using aapt2. */ + public void queueDirectoryForCompilation(Path resource) throws IOException { + Files.walkFileTree(resource, compilingVisitor); + } + + /** Returns all paths of the aapt2 compiled resources. */ + public List<Path> getCompiledArtifacts() throws InterruptedException, ExecutionException { + return compilingVisitor.getCompiledArtifacts(); + } +} |