aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Andrew Pellegrini <apell@google.com>2015-06-25 17:12:49 +0000
committerGravatar Han-Wen Nienhuys <hanwen@google.com>2015-06-26 15:29:53 +0000
commitd13716a201b2dcfe952e843ffcc566056519aaa5 (patch)
tree8f6fd28465854ed94d5915e73a746a2fa651f70d
parent643063d582dcf346f276680288b11f958f5c551d (diff)
Open source AarGeneratorAction and AndroidResourceProcessingAction.
-- MOS_MIGRATED_REVID=96883818
-rwxr-xr-xsrc/test/shell/bazel/test-setup.sh25
-rwxr-xr-xsrc/test/shell/bazel/testenv.sh2
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java249
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java393
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java288
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidSdkTools.java130
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/BUILD36
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/BazelPlatformTarget.java235
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/BuildToolInfoBuilder.java65
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/Converters.java157
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/DensityFilteredAndroidData.java26
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/DensitySpecificManifestProcessor.java145
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/DensitySpecificResourceFilter.java294
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java201
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/DirectoryModifier.java30
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/FileDeDuplicator.java137
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/ManifestProcessingException.java28
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/MergedAndroidData.java60
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/PackedResourceTarExpander.java147
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/README1
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java163
-rw-r--r--third_party/BUILD17
-rw-r--r--third_party/README.md14
-rw-r--r--tools/android/BUILD8
24 files changed, 2846 insertions, 5 deletions
diff --git a/src/test/shell/bazel/test-setup.sh b/src/test/shell/bazel/test-setup.sh
index 1ea5b151a8..6d882affd8 100755
--- a/src/test/shell/bazel/test-setup.sh
+++ b/src/test/shell/bazel/test-setup.sh
@@ -45,6 +45,29 @@ function bazel() {
${bazel} --bazelrc=$TEST_TMPDIR/bazelrc "$@"
}
+function setup_android_support() {
+ mkdir -p src/tools/android/java/com/google/devtools/build/android
+ cat <<EOF > src/tools/android/java/com/google/devtools/build/android/BUILD
+sh_binary(
+ name = "AarGeneratorAction",
+ srcs = ["fail.sh"],
+)
+
+sh_binary(
+ name = "AndroidResourceProcessingAction",
+ srcs = ["fail.sh"],
+)
+EOF
+
+ cat <<EOF > src/tools/android/java/com/google/devtools/build/android/fail.sh
+#!/bin/bash
+
+exit 1
+EOF
+
+ chmod +x src/tools/android/java/com/google/devtools/build/android/fail.sh
+}
+
function setup_protoc_support() {
mkdir -p third_party
[ -e third_party/protoc ] || ln -s ${protoc_compiler} third_party/protoc
@@ -122,6 +145,8 @@ function create_new_workspace() {
ln -s "${singlejar_path}" tools/jdk/SingleJar_deploy.jar
ln -s "${ijar_path}" tools/jdk/ijar
+ setup_android_support
+
touch WORKSPACE
}
diff --git a/src/test/shell/bazel/testenv.sh b/src/test/shell/bazel/testenv.sh
index c84e839197..12afe3635f 100755
--- a/src/test/shell/bazel/testenv.sh
+++ b/src/test/shell/bazel/testenv.sh
@@ -90,7 +90,7 @@ EOF
# Report whether a given directory name corresponds to a tools directory.
function is_tools_directory() {
case "$1" in
- third_party|tools)
+ third_party|tools|src)
true
;;
*)
diff --git a/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java b/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java
new file mode 100644
index 0000000000..032aefd0ab
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java
@@ -0,0 +1,249 @@
+// Copyright 2015 Google Inc. 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.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Stopwatch;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.hash.Hashing;
+import com.google.devtools.build.android.Converters.DependencyAndroidDataListConverter;
+import com.google.devtools.build.android.Converters.ExistingPathConverter;
+import com.google.devtools.build.android.Converters.PathConverter;
+import com.google.devtools.build.android.Converters.UnvalidatedAndroidDataConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+
+import com.android.ide.common.res2.MergingException;
+import com.android.utils.StdLogger;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+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.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Action to generate an AAR archive for an Android library.
+ *
+ * <p><pre>
+ * Example Usage:
+ * java/com/google/build/android/AarGeneratorAction\
+ * --primaryData path/to/resources:path/to/assets:path/to/manifest\
+ * --data p/t/res1:p/t/assets1:p/t/1/AndroidManifest.xml:p/t/1/R.txt,\
+ * p/t/res2:p/t/assets2:p/t/2/AndroidManifest.xml:p/t/2/R.txt\
+ * --manifest path/to/manifest\
+ * --rtxt path/to/rtxt\
+ * --classes path/to/classes.jar\
+ * --strictMerge\
+ * --aarOutput path/to/write/archive.aar
+ * </pre>
+ */
+public class AarGeneratorAction {
+ private static final Long EPOCH = 0L;
+
+ private static final Logger logger = Logger.getLogger(AarGeneratorAction.class.getName());
+
+ /** Flag specifications for this action. */
+ public static final class Options extends OptionsBase {
+ @Option(name = "mainData",
+ defaultValue = "null",
+ converter = UnvalidatedAndroidDataConverter.class,
+ category = "input",
+ help = "The directory containing the primary resource directory."
+ + "The contents will override the contents of any other resource directories during "
+ + "merging. The expected format is resources[#resources]:assets[#assets]:manifest")
+ public UnvalidatedAndroidData mainData;
+
+ @Option(name = "dependencyData",
+ defaultValue = "",
+ converter = DependencyAndroidDataListConverter.class,
+ category = "input",
+ help = "Additional Data dependencies. These values will be used if not defined in "
+ + "the primary resources. The expected format is "
+ + "resources[#resources]:assets[#assets]:manifest:r.txt"
+ + "[,resources[#resources]:assets[#assets]:manifest:r.txt]")
+ public List<DependencyAndroidData> dependencyData;
+
+ @Option(name = "manifest",
+ defaultValue = "null",
+ converter = ExistingPathConverter.class,
+ category = "input",
+ help = "Path to AndroidManifest.xml.")
+ public Path manifest;
+
+ @Option(name = "rtxt",
+ defaultValue = "null",
+ converter = ExistingPathConverter.class,
+ category = "input",
+ help = "Path to R.txt.")
+ public Path rtxt;
+
+ @Option(name = "classes",
+ defaultValue = "null",
+ converter = ExistingPathConverter.class,
+ category = "input",
+ help = "Path to classes.jar.")
+ public Path classes;
+
+ @Option(name = "aarOutput",
+ defaultValue = "null",
+ converter = PathConverter.class,
+ category = "output",
+ help = "Path to write the archive.")
+ public Path aarOutput;
+
+ @Option(name = "strictMerge",
+ defaultValue = "true",
+ category = "option",
+ help = "Merge strategy for resources.")
+ public boolean strictMerge;
+ }
+
+ public static void main(String[] args) {
+ Stopwatch timer = Stopwatch.createStarted();
+ OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class);
+ optionsParser.parseAndExitUponError(args);
+ Options options = optionsParser.getOptions(Options.class);
+
+ checkFlags(options);
+
+ FileSystem fileSystem = FileSystems.getDefault();
+ Path working = fileSystem.getPath("").toAbsolutePath();
+
+ AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor(
+ new StdLogger(com.android.utils.StdLogger.Level.VERBOSE));
+
+ try {
+ Path resourcesOut = Files.createTempDirectory("tmp-resources");
+ resourcesOut.toFile().deleteOnExit();
+ Path assetsOut = Files.createTempDirectory("tmp-assets");
+ assetsOut.toFile().deleteOnExit();
+ logger.fine(String.format("Setup finished at %dms", timer.elapsed(TimeUnit.MILLISECONDS)));
+
+ ImmutableList<DirectoryModifier> modifiers = ImmutableList.of(
+ new PackedResourceTarExpander(working.resolve("expanded"), working),
+ new FileDeDuplicator(Hashing.murmur3_128(), working.resolve("deduplicated"), working));
+ MergedAndroidData mergedData = resourceProcessor.mergeData(options.mainData,
+ options.dependencyData,
+ resourcesOut,
+ assetsOut,
+ modifiers,
+ null,
+ options.strictMerge);
+ logger.info(String.format("Merging finished at %dms", timer.elapsed(TimeUnit.MILLISECONDS)));
+
+ writeAar(options.aarOutput, mergedData, options.manifest, options.rtxt, options.classes);
+ logger.info(
+ String.format("Packaging finished at %dms", timer.elapsed(TimeUnit.MILLISECONDS)));
+
+ } catch (IOException | MergingException e) {
+ throw Throwables.propagate(e);
+ }
+ System.exit(0);
+ }
+
+ @VisibleForTesting
+ static void checkFlags(Options options) throws IllegalArgumentException {
+ List<String> nullFlags = new LinkedList<>();
+ if (options.manifest == null) {
+ nullFlags.add("manifest");
+ }
+ if (options.rtxt == null) {
+ nullFlags.add("rtxt");
+ }
+ if (options.classes == null) {
+ nullFlags.add("classes");
+ }
+ if (!nullFlags.isEmpty()) {
+ throw new IllegalArgumentException(String.format("%s must be specified. Building an .aar "
+ + "without %s is unsupported.",
+ Joiner.on(", ").join(nullFlags), Joiner.on(", ").join(nullFlags)));
+ }
+ }
+
+ @VisibleForTesting
+ static void writeAar(Path aar, final MergedAndroidData data, Path manifest, Path rtxt,
+ Path classes) throws IOException {
+ try (final ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(aar.toFile()))) {
+ ZipEntry manifestEntry = new ZipEntry("AndroidManifest.xml");
+ zipOut.putNextEntry(manifestEntry);
+ zipOut.write(Files.readAllBytes(manifest));
+ zipOut.closeEntry();
+
+ ZipEntry classJar = new ZipEntry("classes.jar");
+ zipOut.putNextEntry(classJar);
+ zipOut.write(Files.readAllBytes(classes));
+ zipOut.closeEntry();
+
+ Files.walkFileTree(data.getResourceDirFile().toPath(),
+ new ZipDirectoryWriter(zipOut, data.getResourceDirFile().toPath(), "res"));
+
+ ZipEntry r = new ZipEntry("R.txt");
+ zipOut.putNextEntry(r);
+ zipOut.write(Files.readAllBytes(rtxt));
+ zipOut.closeEntry();
+
+ if (data.getAssetDirFile().exists() && data.getAssetDirFile().list().length > 0) {
+ Files.walkFileTree(data.getAssetDirFile().toPath(),
+ new ZipDirectoryWriter(zipOut, data.getAssetDirFile().toPath(), "assets"));
+ }
+ }
+ aar.toFile().setLastModified(EPOCH);
+ }
+
+ private static class ZipDirectoryWriter extends SimpleFileVisitor<Path> {
+ private final ZipOutputStream zipOut;
+ private final Path root;
+ private final String dirName;
+
+ public ZipDirectoryWriter(ZipOutputStream zipOut, Path root, String dirName) {
+ this.zipOut = zipOut;
+ this.root = root;
+ this.dirName = dirName;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ ZipEntry entry = new ZipEntry(new File(dirName, root.relativize(file).toString()).toString());
+ zipOut.putNextEntry(entry);
+ zipOut.write(Files.readAllBytes(file));
+ zipOut.closeEntry();
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+ throws IOException {
+ ZipEntry entry = new ZipEntry(new File(dirName, root.relativize(dir).toString())
+ .toString() + "/");
+ zipOut.putNextEntry(entry);
+ zipOut.closeEntry();
+ return FileVisitResult.CONTINUE;
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java
new file mode 100644
index 0000000000..7d52289ed4
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java
@@ -0,0 +1,393 @@
+// Copyright 2015 Google Inc. 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.google.common.base.Joiner;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.hash.Hashing;
+import com.google.devtools.build.android.Converters.DependencyAndroidDataListConverter;
+import com.google.devtools.build.android.Converters.ExistingPathConverter;
+import com.google.devtools.build.android.Converters.FullRevisionConverter;
+import com.google.devtools.build.android.Converters.PathConverter;
+import com.google.devtools.build.android.Converters.UnvalidatedAndroidDataConverter;
+import com.google.devtools.build.android.Converters.VariantConfigurationTypeConverter;
+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.OptionsParser;
+import com.google.devtools.common.options.TriState;
+
+import com.android.builder.core.AndroidBuilder;
+import com.android.builder.core.VariantConfiguration;
+import com.android.builder.model.AaptOptions;
+import com.android.ide.common.internal.LoggedErrorException;
+import com.android.ide.common.res2.MergingException;
+import com.android.sdklib.repository.FullRevision;
+import com.android.utils.StdLogger;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+
+/**
+ * Provides an entry point for the resource processing using the AOSP build tools.
+ *
+ * <pre>
+ * Example Usage:
+ * java/com/google/build/android/AndroidResourceProcessingAction\
+ * --sdkRoot path/to/sdk\
+ * --aapt path/to/sdk/aapt\
+ * --annotationJar path/to/sdk/annotationJar\
+ * --adb path/to/sdk/adb\
+ * --zipAlign path/to/sdk/zipAlign\
+ * --androidJar path/to/sdk/androidJar\
+ * --manifest path/to/manifest\
+ * --primaryData path/to/resources:path/to/assets:path/to/manifest:path/to/R.txt
+ * --data p/t/res1:p/t/assets1:p/t/1/AndroidManifest.xml:p/t/1/R.txt,\
+ * p/t/res2:p/t/assets2:p/t/2/AndroidManifest.xml:p/t/2/R.txt
+ * --generatedSourcePath path/to/write/generated/sources
+ * --packagePath path/to/write/archive.ap_
+ * --srcJarOutput path/to/write/archive.srcjar
+ * </pre>
+ */
+public class AndroidResourceProcessingAction {
+
+ private static final StdLogger STD_LOGGER =
+ new StdLogger(com.android.utils.StdLogger.Level.WARNING);
+
+ private static final Logger LOGGER =
+ Logger.getLogger(AndroidResourceProcessingAction.class.getName());
+
+ /** Flag specifications for this action. */
+ public static final class Options extends OptionsBase {
+ @Option(name = "apiVersion",
+ defaultValue = "21.0.0",
+ converter = FullRevisionConverter.class,
+ category = "config",
+ help = "ApiVersion indicates the version passed to the AndroidBuilder. ApiVersion must be"
+ + " > 19.10 when defined.")
+ // TODO(bazel-team): Determine what the API version changes in AndroidBuilder.
+ public FullRevision apiVersion;
+
+ @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 = "adb",
+ defaultValue = "null",
+ converter = ExistingPathConverter.class,
+ category = "tool",
+ help = "Path to adb for builder functions.")
+ //TODO(bazel-team): Determine if this is completely necessary for running AndroidBuilder.
+ public Path adb;
+
+ @Option(name = "zipAlign",
+ defaultValue = "null",
+ converter = ExistingPathConverter.class,
+ category = "tool",
+ help = "Path to zipAlign for building apks.")
+ public Path zipAlign;
+
+ @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 = "primaryData",
+ defaultValue = "null",
+ converter = UnvalidatedAndroidDataConverter.class,
+ category = "input",
+ help = "The directory containing the primary resource directory. The contents will override"
+ + " the contents of any other resource directories during merging. The expected format"
+ + " is resources[|resources]:assets[|assets]:manifest")
+ public UnvalidatedAndroidData primaryData;
+
+ @Option(name = "data",
+ defaultValue = "",
+ converter = DependencyAndroidDataListConverter.class,
+ category = "input",
+ help = "Additional Data dependencies. These values will be used if not defined in the "
+ + "primary resources. The expected format is "
+ + "resources[#resources]:assets[#assets]:manifest:r.txt:symbols.txt"
+ + "[,resources[#resources]:assets[#assets]:manifest:r.txt:symbols.txt]")
+ public List<DependencyAndroidData> data;
+
+ @Option(name = "generatedSourcePath",
+ defaultValue = "null",
+ converter = PathConverter.class,
+ category = "output",
+ help = "Path for generated sources.")
+ public Path generatedSourcePath;
+
+ @Option(name = "rOutput",
+ defaultValue = "null",
+ converter = PathConverter.class,
+ category = "output",
+ help = "Path to where the R.txt should be written.")
+ public Path rOutput;
+
+ @Option(name = "symbolsTxtOut",
+ defaultValue = "null",
+ converter = PathConverter.class,
+ category = "output",
+ help = "Path to where the symbolsTxt should be written.")
+ public Path symbolsTxtOut;
+
+ @Option(name = "packagePath",
+ defaultValue = "null",
+ converter = PathConverter.class,
+ category = "output",
+ help = "Path to the write the archive.")
+ public Path packagePath;
+
+ @Option(name = "proguardOutput",
+ defaultValue = "null",
+ converter = PathConverter.class,
+ category = "output",
+ help = "Path for the proguard file.")
+ public Path proguardOutput;
+
+ @Option(name = "srcJarOutput",
+ defaultValue = "null",
+ converter = PathConverter.class,
+ category = "output",
+ help = "Path for the generated java source jar.")
+ public Path srcJarOutput;
+
+ @Option(name = "packageType",
+ defaultValue = "DEFAULT",
+ converter = VariantConfigurationTypeConverter.class,
+ category = "config",
+ help = "Variant configuration type for packaging the resources."
+ + " Acceptible values DEFAULT, LIBRARY, TEST")
+ public VariantConfiguration.Type packageType;
+
+ @Option(name = "densities",
+ defaultValue = "",
+ converter = CommaSeparatedOptionListConverter.class,
+ category = "config",
+ help = "A list densities to filter the resource drawables by.")
+ public List<String> densities;
+
+ @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;
+
+ @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 = "packageForR",
+ defaultValue = "null",
+ category = "config",
+ help = "Custom java package to generate the R symbols files.")
+ public String packageForR;
+
+ @Option(name = "applicationId",
+ defaultValue = "null",
+ category = "config",
+ help = "Custom application id (package manifest) for the packaged manifest.")
+ public String applicationId;
+
+ @Option(name = "versionName",
+ defaultValue = "null",
+ category = "config",
+ help = "Version name to stamp into the packaged manifest.")
+ public String versionName;
+
+ @Option(name = "versionCode",
+ defaultValue = "-1",
+ category = "config",
+ help = "Version code to stamp into the packaged manifest.")
+ public int versionCode;
+
+ @Option(name = "assetsToIgnore",
+ defaultValue = "",
+ converter = CommaSeparatedOptionListConverter.class,
+ category = "config",
+ help = "A list of assets extensions to ignore.")
+ public List<String> assetsToIgnore;
+ }
+
+ private static Options options;
+
+ public static void main(String[] args) {
+ final Stopwatch timer = Stopwatch.createStarted();
+ OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class);
+ optionsParser.parseAndExitUponError(args);
+ options = optionsParser.getOptions(Options.class);
+ FileSystem fileSystem = FileSystems.getDefault();
+ Path working = fileSystem.getPath("").toAbsolutePath();
+ Path mergedAssets = working.resolve("merged_assets");
+ Path mergedResources = working.resolve("merged_resources");
+
+ final AndroidResourceProcessor resourceProcessor =
+ new AndroidResourceProcessor(STD_LOGGER);
+
+ final AndroidSdkTools sdkTools = new AndroidSdkTools(options.apiVersion,
+ options.aapt,
+ options.annotationJar,
+ options.adb,
+ options.zipAlign,
+ options.androidJar,
+ STD_LOGGER);
+ try {
+ LOGGER.fine(String.format("Setup finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
+ final ImmutableList<DirectoryModifier> modifiers = ImmutableList.of(
+ new PackedResourceTarExpander(working.resolve("expanded"), working),
+ new FileDeDuplicator(Hashing.murmur3_128(), working.resolve("deduplicated"), working));
+
+ final AndroidBuilder builder = sdkTools.createAndroidBuilder();
+
+ final MergedAndroidData mergedData = resourceProcessor.mergeData(
+ options.primaryData,
+ options.data,
+ mergedResources,
+ mergedAssets,
+ modifiers,
+ useAaptCruncher() ? builder.getAaptCruncher() : null,
+ true);
+
+ LOGGER.info(String.format("Merging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
+ final Path filteredResources = fileSystem.getPath("resources-filtered");
+ final Path densityManifest = fileSystem.getPath("manifest-filtered/AndroidManifest.xml");
+ final DensityFilteredAndroidData filteredData = mergedData.filter(
+ new DensitySpecificResourceFilter(options.densities, filteredResources, working),
+ new DensitySpecificManifestProcessor(options.densities, densityManifest));
+ LOGGER.info(
+ String.format("Density filtering finished at %sms",
+ timer.elapsed(TimeUnit.MILLISECONDS)));
+ resourceProcessor.processResources(
+ builder,
+ options.packageType,
+ options.debug,
+ options.packageForR,
+ new FlagAaptOptions(),
+ options.resourceConfigs,
+ options.applicationId,
+ options.versionCode,
+ options.versionName,
+ filteredData,
+ options.data,
+ working.resolve("manifest"),
+ options.generatedSourcePath,
+ options.packagePath,
+ options.proguardOutput);
+ LOGGER.fine(String.format("appt finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
+ if (options.srcJarOutput != null) {
+ resourceProcessor.createSrcJar(options.generatedSourcePath, options.srcJarOutput);
+ }
+ if (options.rOutput != null) {
+ resourceProcessor.copyRToOutput(options.generatedSourcePath, options.rOutput);
+ }
+ if (options.symbolsTxtOut != null) {
+ resourceProcessor.copyRToOutput(options.generatedSourcePath, options.symbolsTxtOut);
+ }
+ LOGGER.fine(String.format("Packaging finished at %sms",
+ timer.elapsed(TimeUnit.MILLISECONDS)));
+ } catch (MergingException e) {
+ LOGGER.log(java.util.logging.Level.SEVERE, "Error during merging resources", e);
+ System.exit(1);
+ } catch (IOException | InterruptedException | LoggedErrorException e) {
+ LOGGER.log(java.util.logging.Level.SEVERE, "Error during processing resources", e);
+ System.exit(2);
+ } catch (Exception e) {
+ LOGGER.log(java.util.logging.Level.SEVERE, "Unexpected", e);
+ System.exit(3);
+ }
+ LOGGER.info(String.format("Resources processed in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
+ // AOSP code can leave dangling threads.
+ System.exit(0);
+ }
+
+ private static boolean useAaptCruncher() {
+ // If the value was set, use that.
+ if (options.useAaptCruncher != TriState.AUTO) {
+ return options.useAaptCruncher == TriState.YES;
+ }
+ // By default png cruncher shouldn't be invoked on a library -- the work is just thrown away.
+ return options.packageType != VariantConfiguration.Type.LIBRARY;
+ }
+
+ private static final class FlagAaptOptions implements AaptOptions {
+ @Override
+ public boolean getUseAaptPngCruncher() {
+ return options.useAaptCruncher != TriState.NO;
+ }
+
+ @Override
+ public Collection<String> getNoCompress() {
+ if (!options.uncompressedExtensions.isEmpty()) {
+ return options.uncompressedExtensions;
+ }
+ return null;
+ }
+
+ @Override
+ public String getIgnoreAssets() {
+ if (!options.assetsToIgnore.isEmpty()) {
+ return Joiner.on(":").join(options.assetsToIgnore);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean getFailOnMissingConfigEntry() {
+ return false;
+ }
+ }
+}
+
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
new file mode 100644
index 0000000000..2cb964ae65
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java
@@ -0,0 +1,288 @@
+// Copyright 2015 Google Inc. 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.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import com.android.annotations.Nullable;
+import com.android.builder.core.AndroidBuilder;
+import com.android.builder.core.VariantConfiguration;
+import com.android.builder.dependency.ManifestDependency;
+import com.android.builder.dependency.SymbolFileProvider;
+import com.android.builder.model.AaptOptions;
+import com.android.ide.common.internal.LoggedErrorException;
+import com.android.ide.common.internal.PngCruncher;
+import com.android.ide.common.res2.AssetMerger;
+import com.android.ide.common.res2.AssetSet;
+import com.android.ide.common.res2.MergedAssetWriter;
+import com.android.ide.common.res2.MergedResourceWriter;
+import com.android.ide.common.res2.MergingException;
+import com.android.ide.common.res2.ResourceMerger;
+import com.android.ide.common.res2.ResourceSet;
+import com.android.manifmerger.ManifestMerger2;
+import com.android.utils.StdLogger;
+
+import java.io.File;
+import java.io.IOException;
+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.Collection;
+import java.util.List;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Provides a wrapper around the AOSP build tools for resource processing.
+ */
+public class AndroidResourceProcessor {
+ private final StdLogger stdLogger;
+
+ public AndroidResourceProcessor(StdLogger stdLogger) {
+ this.stdLogger = stdLogger;
+ }
+
+ /**
+ * Copies the R.txt to the expected place.
+ */
+ public void copyRToOutput(Path generatedSourceRoot, Path rOutput) {
+ try {
+ Files.createDirectories(rOutput.getParent());
+ final Path source = generatedSourceRoot.resolve("R.txt");
+ if (Files.exists(source)) {
+ 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);
+ }
+ } catch (IOException e) {
+ Throwables.propagate(e);
+ }
+ }
+
+ /**
+ * Creates a zip archive from all found R.java files.
+ */
+ public void createSrcJar(Path generatedSourcesRoot, Path srcJar) {
+ try {
+ Files.createDirectories(srcJar.getParent());
+ try (final ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(srcJar))) {
+ Files.walkFileTree(generatedSourcesRoot, new SymbolFileSrcJarBuildingVisitor(zip));
+ }
+ } catch (IOException e) {
+ Throwables.propagate(e);
+ }
+ }
+
+ /**
+ * Processes resources for generated sources, configs and packaging resources.
+ */
+ public void processResources(
+ AndroidBuilder builder,
+ VariantConfiguration.Type variantType,
+ boolean debug,
+ String customPackageForR,
+ AaptOptions aaptOptions,
+ Collection<String> resourceConfigs,
+ String applicationId,
+ int versionCode,
+ String versionName,
+ MergedAndroidData primaryData,
+ List<DependencyAndroidData> dependencyData,
+ Path workingDirectory,
+ @Nullable Path sourceOut,
+ @Nullable Path packageOut,
+ @Nullable Path proguardOut) throws IOException, InterruptedException, LoggedErrorException {
+ ImmutableList.Builder<SymbolFileProvider> libraries = ImmutableList.builder();
+ for (DependencyAndroidData dataDep : dependencyData) {
+ libraries.add(dataDep.asSymbolFileProvider());
+ }
+
+ File androidManifest = processManifest(
+ applicationId,
+ versionCode,
+ versionName,
+ primaryData,
+ workingDirectory,
+ builder);
+
+ builder.processResources(
+ androidManifest,
+ primaryData.getResourceDirFile(),
+ primaryData.getAssetDirFile(),
+ libraries.build(),
+ customPackageForR,
+ prepareOutputPath(sourceOut),
+ prepareOutputPath(sourceOut),
+ packageOut != null ? packageOut.toString() : null,
+ proguardOut != null ? proguardOut.toString() : null,
+ variantType,
+ debug,
+ aaptOptions,
+ resourceConfigs,
+ true // boolean enforceUniquePackageName
+ );
+ }
+
+ private File processManifest(
+ String applicationId,
+ int versionCode,
+ String versionName,
+ MergedAndroidData primaryData,
+ Path workingDirectory,
+ AndroidBuilder builder) throws IOException {
+ if (versionCode != -1 || versionName != null || applicationId != null) {
+ Path androidManifest =
+ Files.createDirectories(workingDirectory).resolve("AndroidManifest.xml");
+ // stamp version and applicationId (if provided) into the manifest
+ builder.mergeManifests(
+ primaryData.getManifestFile(), // mainManifest,
+ ImmutableList.<File>of(),
+ ImmutableList.<ManifestDependency>of(),
+ applicationId,
+ versionCode,
+ versionName,
+ null, // String minSdkVersion
+ null, // String targetSdkVersion
+ null, // int maxSdkVersion
+ androidManifest.toString(),
+ ManifestMerger2.MergeType.APPLICATION,
+ ImmutableMap.<String, String>of());
+ return androidManifest.toFile();
+ }
+ return primaryData.getManifestFile();
+ }
+
+ /**
+ * Merges all secondary resources with the primary resources.
+ */
+ public MergedAndroidData mergeData(
+ final UnvalidatedAndroidData primary,
+ final List<DependencyAndroidData> secondary,
+ final Path resourcesOut,
+ final Path assetsOut,
+ final ImmutableList<DirectoryModifier> modifiers,
+ @Nullable final PngCruncher cruncher,
+ final boolean strict) throws MergingException {
+
+ List<ResourceSet> resourceSets = new ArrayList<>();
+ List<AssetSet> assetSets = new ArrayList<>();
+
+ if (strict) {
+ androidDataToStrictMergeSet(primary, secondary, modifiers, resourceSets, assetSets);
+ } else {
+ androidDataToRelaxedMergeSet(primary, secondary, modifiers, resourceSets, assetSets);
+ }
+ ResourceMerger merger = new ResourceMerger();
+ for (ResourceSet set : resourceSets) {
+ set.loadFromFiles(stdLogger);
+ merger.addDataSet(set);
+ }
+
+ AssetMerger assetMerger = new AssetMerger();
+ for (AssetSet set : assetSets) {
+ set.loadFromFiles(stdLogger);
+ assetMerger.addDataSet(set);
+ }
+
+ MergedResourceWriter resourceWriter = new MergedResourceWriter(resourcesOut.toFile(), cruncher);
+ MergedAssetWriter assetWriter = new MergedAssetWriter(assetsOut.toFile());
+
+ merger.mergeData(resourceWriter, false);
+ assetMerger.mergeData(assetWriter, false);
+
+ return new MergedAndroidData(resourcesOut, assetsOut, primary.getManifest());
+ }
+
+ private void androidDataToRelaxedMergeSet(UnvalidatedAndroidData primary,
+ List<DependencyAndroidData> secondary, ImmutableList<DirectoryModifier> modifiers,
+ List<ResourceSet> resourceSets, List<AssetSet> assetSets) {
+
+ for (DependencyAndroidData dependency : secondary) {
+ DependencyAndroidData modifiedDependency = dependency.modify(modifiers);
+ modifiedDependency.addAsResourceSets(resourceSets);
+ modifiedDependency.addAsAssetSets(assetSets);
+ }
+ UnvalidatedAndroidData modifiedPrimary = primary.modify(modifiers);
+ modifiedPrimary.addAsResourceSets(resourceSets);
+ modifiedPrimary.addAsAssetSets(assetSets);
+
+ }
+
+ private void androidDataToStrictMergeSet(UnvalidatedAndroidData primary,
+ List<DependencyAndroidData> secondary, ImmutableList<DirectoryModifier> modifiers,
+ List<ResourceSet> resourceSets, List<AssetSet> assetSets) {
+ UnvalidatedAndroidData modifiedPrimary = primary.modify(modifiers);
+ ResourceSet mainResources = modifiedPrimary.addToResourceSet(new ResourceSet("main"));
+ AssetSet mainAssets = modifiedPrimary.addToAssets(new AssetSet("main"));
+ ResourceSet dependentResources = new ResourceSet("deps");
+ AssetSet dependentAssets = new AssetSet("deps");
+ for (DependencyAndroidData dependency : secondary) {
+ DependencyAndroidData modifiedDependency = dependency.modify(modifiers);
+ modifiedDependency.addToResourceSet(dependentResources);
+ modifiedDependency.addToAssets(dependentAssets);
+ }
+ resourceSets.add(dependentResources);
+ resourceSets.add(mainResources);
+ assetSets.add(dependentAssets);
+ assetSets.add(mainAssets);
+ }
+
+ private String prepareOutputPath(@Nullable Path out) throws IOException {
+ if (out == null) {
+ return null;
+ }
+ return Files.createDirectories(out).toString();
+ }
+
+ /**
+ * A FileVisitor that will add all R.java files to be stored in a zip archive.
+ */
+ private final class SymbolFileSrcJarBuildingVisitor extends SimpleFileVisitor<Path> {
+
+ // The earliest date representable in a zip file, 1-1-1980.
+ private static final long ZIP_EPOCH = 315561600000L;
+ private final ZipOutputStream zip;
+
+ private SymbolFileSrcJarBuildingVisitor(ZipOutputStream zip) {
+ this.zip = zip;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ if (file.getFileName().endsWith("R.java")) {
+ byte[] content = Files.readAllBytes(file);
+ ZipEntry entry = new ZipEntry(file.toString());
+
+ entry.setMethod(ZipEntry.STORED);
+ entry.setTime(ZIP_EPOCH);
+ entry.setSize(content.length);
+ CRC32 crc32 = new CRC32();
+ crc32.update(content);
+ entry.setCrc(crc32.getValue());
+ zip.putNextEntry(entry);
+ zip.write(content);
+ zip.closeEntry();
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidSdkTools.java b/src/tools/android/java/com/google/devtools/build/android/AndroidSdkTools.java
new file mode 100644
index 0000000000..50d899448b
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/AndroidSdkTools.java
@@ -0,0 +1,130 @@
+// Copyright 2015 Google Inc. 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.google.common.base.Preconditions;
+
+import com.android.builder.core.AndroidBuilder;
+import com.android.builder.sdk.SdkInfo;
+import com.android.builder.sdk.TargetInfo;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.repository.FullRevision;
+import com.android.utils.StdLogger;
+
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Path;
+
+import javax.annotation.Nullable;
+
+/**
+ * Encapsulates the sdk related tools necessary for creating an AndroidBuilder.
+ */
+public class AndroidSdkTools {
+ private final FullRevision apiVersion;
+
+ private final Path aaptLocation;
+
+ private final Path annotationJar;
+
+ private final Path adbLocation;
+
+ private final Path zipAlign;
+ private final Path androidJar;
+
+ private StdLogger stdLogger;
+
+ public AndroidSdkTools(FullRevision apiVersion,
+ Path aaptLocation,
+ Path annotationJar,
+ @Nullable Path adbLocation,
+ @Nullable Path zipAlign,
+ Path androidJar,
+ StdLogger stdLogger) {
+ this.stdLogger = stdLogger;
+ this.apiVersion = Preconditions.checkNotNull(apiVersion, "apiVersion");
+ this.aaptLocation = Preconditions.checkNotNull(aaptLocation, "aapt");
+ this.annotationJar = Preconditions.checkNotNull(annotationJar, "annotationJar");
+ this.adbLocation = adbLocation;
+ this.zipAlign = zipAlign;
+ this.androidJar = Preconditions.checkNotNull(androidJar, "androidJar");
+ }
+
+ /** Creates an AndroidBuilder from the provided sdk tools. */
+ public AndroidBuilder createAndroidBuilder() {
+ // BuildInfoTool contains the paths to all tools that the AndroidBuilder uses.
+ BuildToolInfo buildToolInfo =
+ new BuildToolInfoBuilder(apiVersion).setZipAlign(zipAlign).setAapt(aaptLocation).build();
+
+ BazelPlatformTarget bazelPlatformTarget = new BazelPlatformTarget(androidJar,
+ new AndroidVersion(apiVersion.getMajor(), ""), buildToolInfo);
+
+ AndroidBuilder builder = new AndroidBuilder(
+ "bazel", /* project id */
+ "bazel", /* created by */
+ stdLogger,
+ false /* verbose */);
+ TargetInfo targetInfo = createTargetInfo(buildToolInfo, bazelPlatformTarget);
+ SdkInfo sdkInfo = createSdkInfo(annotationJar, adbLocation);
+
+ // TargetInfo and sdk info provide links to all the tools.
+ builder.setTargetInfo(sdkInfo, targetInfo);
+ return builder;
+ }
+
+ private static SdkInfo createSdkInfo(Path annotationJar, Path adbLocation) {
+ try {
+ // necessary hack because SdkInfo doesn't declare a public constructor.
+ Constructor<SdkInfo> sdkInfoConstructor =
+ SdkInfo.class.getDeclaredConstructor(File.class, File.class);
+ sdkInfoConstructor.setAccessible(true);
+ return sdkInfoConstructor.newInstance(maybeToFile(annotationJar), maybeToFile(adbLocation));
+ } catch (NoSuchMethodException
+ | SecurityException
+ | InstantiationException
+ | IllegalAccessException
+ | IllegalArgumentException
+ | InvocationTargetException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static TargetInfo createTargetInfo(BuildToolInfo buildToolInfo,
+ BazelPlatformTarget bazelPlatformTarget) {
+ try {
+ // necessary hack because TargetInfo doesn't declare a public constructor.
+ Constructor<TargetInfo> targetInfoConstructor =
+ TargetInfo.class.getDeclaredConstructor(IAndroidTarget.class, BuildToolInfo.class);
+ targetInfoConstructor.setAccessible(true);
+ return targetInfoConstructor.newInstance(bazelPlatformTarget, buildToolInfo);
+ } catch (NoSuchMethodException
+ | SecurityException
+ | InstantiationException
+ | IllegalAccessException
+ | IllegalArgumentException
+ | InvocationTargetException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static File maybeToFile(Path path) {
+ if (path == null) {
+ return null;
+ }
+ return path.toFile();
+ }
+} \ No newline at end of file
diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD
new file mode 100644
index 0000000000..4ff57d7dc4
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/BUILD
@@ -0,0 +1,36 @@
+# Actions for Android rules.
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+ name = "srcs",
+ srcs = glob(["**"]),
+)
+
+java_binary(
+ name = "AndroidResourceProcessingAction",
+ main_class = "com.google.devtools.build.android.AndroidResourceProcessingAction",
+ runtime_deps = [
+ ":android_builder_lib",
+ ],
+)
+
+java_binary(
+ name = "AarGeneratorAction",
+ main_class = "com.google.devtools.build.android.AarGeneratorAction",
+ runtime_deps = [
+ ":android_builder_lib",
+ ],
+)
+
+java_library(
+ name = "android_builder_lib",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/main/java:options",
+ "//third_party:android_common",
+ "//third_party:apache_commons_compress",
+ "//third_party:guava",
+ "//third_party:jsr305",
+ ],
+)
diff --git a/src/tools/android/java/com/google/devtools/build/android/BazelPlatformTarget.java b/src/tools/android/java/com/google/devtools/build/android/BazelPlatformTarget.java
new file mode 100644
index 0000000000..a767660556
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/BazelPlatformTarget.java
@@ -0,0 +1,235 @@
+// Copyright 2015 Google Inc. 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.SdkConstants;
+import com.android.sdklib.AndroidTargetHash;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.ISystemImage;
+import com.android.sdklib.repository.descriptors.IdDisplay;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Defines a target platform used by Bazel builds.
+ */
+public class BazelPlatformTarget implements IAndroidTarget {
+
+ private final BuildToolInfo buildToolInfo;
+ private final Map<Integer, Path> paths = new HashMap<>();
+ private final AndroidVersion version;
+ private final Path sdkRoot;
+
+ public BazelPlatformTarget(
+ Path androidJar,
+ AndroidVersion version,
+ BuildToolInfo buildToolInfo) {
+ this.version = version;
+ this.buildToolInfo = buildToolInfo;
+
+ sdkRoot = new File("unused/tool/from/sdk/root").toPath();
+ // pre-build the path to the platform components with default values
+ // TODO(bazel-team): Allow overrides of the default values.
+ paths.put(ANDROID_JAR, androidJar);
+ paths.put(UI_AUTOMATOR_JAR, sdkRoot.resolve(SdkConstants.FN_UI_AUTOMATOR_LIBRARY));
+ paths.put(SOURCES, sdkRoot.resolve(SdkConstants.FD_ANDROID_SOURCES));
+ paths.put(ANDROID_AIDL, sdkRoot.resolve(SdkConstants.FN_FRAMEWORK_AIDL));
+ paths.put(SAMPLES, sdkRoot.resolve(SdkConstants.OS_PLATFORM_SAMPLES_FOLDER));
+ paths.put(SKINS, sdkRoot.resolve(SdkConstants.OS_SKINS_FOLDER));
+ paths.put(TEMPLATES, sdkRoot.resolve(SdkConstants.OS_PLATFORM_TEMPLATES_FOLDER));
+ paths.put(DATA, sdkRoot.resolve(SdkConstants.OS_PLATFORM_DATA_FOLDER));
+ paths.put(ATTRIBUTES, sdkRoot.resolve(SdkConstants.OS_PLATFORM_ATTRS_XML));
+ paths.put(MANIFEST_ATTRIBUTES, sdkRoot.resolve(SdkConstants.OS_PLATFORM_ATTRS_MANIFEST_XML));
+ paths.put(RESOURCES, sdkRoot.resolve(SdkConstants.OS_PLATFORM_RESOURCES_FOLDER));
+ paths.put(FONTS, sdkRoot.resolve(SdkConstants.OS_PLATFORM_FONTS_FOLDER));
+ paths.put(LAYOUT_LIB,
+ sdkRoot.resolve(SdkConstants.OS_PLATFORM_DATA_FOLDER + SdkConstants.FN_LAYOUTLIB_JAR));
+ paths.put(WIDGETS,
+ sdkRoot.resolve(SdkConstants.OS_PLATFORM_DATA_FOLDER + SdkConstants.FN_WIDGETS));
+ paths.put(ACTIONS_ACTIVITY, sdkRoot.resolve(
+ SdkConstants.OS_PLATFORM_DATA_FOLDER + SdkConstants.FN_INTENT_ACTIONS_ACTIVITY));
+ paths.put(ACTIONS_BROADCAST, sdkRoot.resolve(
+ SdkConstants.OS_PLATFORM_DATA_FOLDER + SdkConstants.FN_INTENT_ACTIONS_BROADCAST));
+ paths.put(ACTIONS_SERVICE, sdkRoot.resolve(
+ SdkConstants.OS_PLATFORM_DATA_FOLDER + SdkConstants.FN_INTENT_ACTIONS_SERVICE));
+ paths.put(CATEGORIES,
+ sdkRoot.resolve(SdkConstants.OS_PLATFORM_DATA_FOLDER + SdkConstants.FN_INTENT_CATEGORIES));
+ }
+
+ @Override
+ public int compareTo(IAndroidTarget o) {
+ if (o.isPlatform() == false) {
+ return -1;
+ }
+ return version.compareTo(o.getVersion());
+ }
+
+ @Override
+ public String getLocation() {
+ return sdkRoot.toFile().getPath();
+ }
+
+ @Override
+ public String getVendor() {
+ return "Android";
+ }
+
+ @Override
+ public String getName() {
+ return "Android [Platform Version Name] (Bazel)";
+ }
+
+ @Override
+ public String getFullName() {
+ return "Android [Platform Version Name] (Bazel)";
+ }
+
+ @Override
+ public String getClasspathName() {
+ return "Android [Platform Version Name] (Bazel)";
+ }
+
+ @Override
+ public String getShortClasspathName() {
+ return "Android [Platform Version Name] (Bazel)";
+ }
+
+ @Override
+ public String getDescription() {
+ return String.format("Standard Android platform %s", "[Platform Version Name] (Bazel)");
+ }
+
+ @Override
+ public AndroidVersion getVersion() {
+ return version;
+ }
+
+ @Override
+ public String getVersionName() {
+ return version.getCodename();
+ }
+
+ @Override
+ public int getRevision() {
+ return 0;
+ }
+
+ @Override
+ public boolean isPlatform() {
+ return true;
+ }
+
+ @Override
+ public IAndroidTarget getParent() {
+ return null;
+ }
+
+ @Override
+ public String getPath(int pathId) {
+ return paths.get(pathId).toFile().getPath();
+ }
+
+ @Override
+ public File getFile(int pathId) {
+ return new File(getPath(pathId));
+ }
+
+ @Override
+ public BuildToolInfo getBuildToolInfo() {
+ return buildToolInfo;
+ }
+
+ @Override
+ public List<String> getBootClasspath() {
+ return Collections.singletonList(getPath(IAndroidTarget.ANDROID_JAR));
+ }
+
+ @Override
+ public boolean hasRenderingLibrary() {
+ return true;
+ }
+
+ @Override
+ public File[] getSkins() {
+ return new File[0];
+ }
+
+ @Override
+ public File getDefaultSkin() {
+ return null;
+ }
+
+ @Override
+ public IOptionalLibrary[] getOptionalLibraries() {
+ return new IOptionalLibrary[0];
+ }
+
+ @Override
+ public String[] getPlatformLibraries() {
+ return new String[] { SdkConstants.ANDROID_TEST_RUNNER_LIB };
+ }
+
+ @Override
+ public String getProperty(String name) {
+ return null;
+ }
+
+ @Override
+ public Integer getProperty(String name, Integer defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public Boolean getProperty(String name, Boolean defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public Map<String, String> getProperties() {
+ return null;
+ }
+
+ @Override
+ public int getUsbVendorId() {
+ return NO_USB_ID;
+ }
+
+ @Override
+ public ISystemImage[] getSystemImages() {
+ return new ISystemImage[0];
+ }
+
+ @Override
+ public ISystemImage getSystemImage(IdDisplay tag, String abiType) {
+ return null;
+ }
+
+ @Override
+ public boolean canRunOn(IAndroidTarget target) {
+ // TODO(bazel-team): Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public String hashString() {
+ return AndroidTargetHash.getPlatformHashString(version);
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/BuildToolInfoBuilder.java b/src/tools/android/java/com/google/devtools/build/android/BuildToolInfoBuilder.java
new file mode 100644
index 0000000000..329e94ea6e
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/BuildToolInfoBuilder.java
@@ -0,0 +1,65 @@
+// Copyright 2015 Google Inc. 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.SdkConstants;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.repository.FullRevision;
+
+import java.io.File;
+import java.nio.file.Path;
+
+import javax.annotation.Nullable;
+
+/**
+ * Simplifies the creation of a {@link BuildToolInfo}.
+ */
+public class BuildToolInfoBuilder {
+ private File aaptLocation;
+ private FullRevision fullRevision;
+ private File zipAlign;
+
+ public BuildToolInfoBuilder(FullRevision fullRevision) {
+ this.fullRevision = fullRevision;
+ }
+
+ public BuildToolInfoBuilder setAapt(@Nullable Path aaptLocation) {
+ this.aaptLocation = aaptLocation != null ? aaptLocation.toFile() : null;
+ return this;
+ }
+
+ public BuildToolInfoBuilder setZipAlign(@Nullable Path zipAlign) {
+ this.zipAlign = zipAlign != null ? zipAlign.toFile() : null;
+ return this;
+ }
+
+ public BuildToolInfo build() {
+ // Fill in the unused tools with fakes that will make sense if unexpectedly called.
+ Path platformToolsRoot = new File("unused/path/to/sdk/root").toPath();
+ return new BuildToolInfo(fullRevision,
+ platformToolsRoot.toFile(),
+ aaptLocation,
+ platformToolsRoot.resolve(SdkConstants.FN_AIDL).toFile(),
+ platformToolsRoot.resolve(SdkConstants.FN_DX).toFile(),
+ platformToolsRoot.resolve(SdkConstants.FN_DX_JAR).toFile(),
+ platformToolsRoot.resolve(SdkConstants.FN_RENDERSCRIPT).toFile(),
+ platformToolsRoot.resolve(SdkConstants.FN_FRAMEWORK_INCLUDE).toFile(),
+ platformToolsRoot.resolve(SdkConstants.FN_FRAMEWORK_INCLUDE_CLANG).toFile(),
+ platformToolsRoot.resolve(SdkConstants.FN_BCC_COMPAT).toFile(),
+ platformToolsRoot.resolve(SdkConstants.FN_LD_ARM).toFile(),
+ platformToolsRoot.resolve(SdkConstants.FN_LD_X86).toFile(),
+ platformToolsRoot.resolve(SdkConstants.FN_LD_MIPS).toFile(),
+ zipAlign == null ? platformToolsRoot.resolve(SdkConstants.FN_ZIPALIGN).toFile() : zipAlign);
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/Converters.java b/src/tools/android/java/com/google/devtools/build/android/Converters.java
new file mode 100644
index 0000000000..0cb7c3d7bb
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/Converters.java
@@ -0,0 +1,157 @@
+// Copyright 2015 Google Inc. 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.google.common.collect.ImmutableList;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.EnumConverter;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import com.android.builder.core.VariantConfiguration;
+import com.android.builder.core.VariantConfiguration.Type;
+import com.android.sdklib.repository.FullRevision;
+
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Some convenient converters used by android actions. Note: These are specific to android actions.
+ */
+public final class Converters {
+ /**
+ * Converter for {@link UnvalidatedAndroidData}. Relies on
+ * {@code UnvalidatedAndroidData#valueOf(String)} to perform conversion and validation.
+ */
+ public static class UnvalidatedAndroidDataConverter implements Converter<UnvalidatedAndroidData> {
+
+ @Override
+ public UnvalidatedAndroidData convert(String input) throws OptionsParsingException {
+ try {
+ return UnvalidatedAndroidData.valueOf(input);
+ } catch (IllegalArgumentException e) {
+ throw new OptionsParsingException("invalid UnvalidatedAndroidData specification", e);
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "unvalidated android data in the format "
+ + "resources[#resources]:assets[#assets]:manifest";
+ }
+ }
+
+ /**
+ * Converter for a list of {@link DependencyAndroidData}. Relies on
+ * {@code DependencyAndroidData#valueOf(String)} to perform conversion and validation.
+ */
+ public static class DependencyAndroidDataListConverter
+ implements Converter<List<DependencyAndroidData>> {
+
+ @Override
+ public List<DependencyAndroidData> convert(String input) throws OptionsParsingException {
+ if (input.isEmpty()) {
+ return ImmutableList.<DependencyAndroidData>of();
+ }
+ try {
+ ImmutableList.Builder<DependencyAndroidData> builder = ImmutableList.builder();
+ for (String item : input.split(",")) {
+ builder.add(DependencyAndroidData.valueOf(item));
+ }
+ return builder.build();
+ } catch (IllegalArgumentException e) {
+ throw new OptionsParsingException("invalid DependencyAndroidData", e);
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a list of dependency android data in the format "
+ + "resources[#resources]:assets[#assets]:manifest:r.txt"
+ + "[,resources[#resources]:assets[#assets]:manifest:r.txt]";
+ }
+ }
+
+ /**
+ * Converter for {@link FullRevision}. Relies on {@code FullRevision#parseRevision(String)} to
+ * perform conversion and validation.
+ */
+ public static class FullRevisionConverter implements Converter<FullRevision> {
+
+ @Override
+ public FullRevision convert(String input) throws OptionsParsingException {
+ try {
+ return FullRevision.parseRevision(input);
+ } catch (NumberFormatException e) {
+ throw new OptionsParsingException(e.getMessage());
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a revision number";
+ }
+ }
+
+ /** Validating converter for Paths. A Path is considered valid if it resolves to a file. */
+ public static class PathConverter implements Converter<Path> {
+
+ private final boolean mustExist;
+
+ public PathConverter() {
+ this.mustExist = false;
+ }
+
+ protected PathConverter(boolean mustExist) {
+ this.mustExist = mustExist;
+ }
+
+ @Override
+ public Path convert(String input) throws OptionsParsingException {
+ try {
+ Path path = FileSystems.getDefault().getPath(input);
+ if (mustExist && !Files.exists(path)) {
+ throw new OptionsParsingException(String.format("%s is not a valid path.", input));
+ }
+ return path;
+ } catch (InvalidPathException e) {
+ throw new OptionsParsingException(String.format("%s is not a valid path.", input), e);
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a valid filesystem path";
+ }
+ }
+
+ /**
+ * Validating converter for Paths. A Path is considered valid if it resolves to a file and exists.
+ */
+ public static class ExistingPathConverter extends PathConverter {
+ public ExistingPathConverter() {
+ super(true);
+ }
+ }
+
+ /** Converter for {@link VariantConfiguration}.{@link Type}. */
+ public static class VariantConfigurationTypeConverter
+ extends EnumConverter<VariantConfiguration.Type> {
+ public VariantConfigurationTypeConverter() {
+ super(VariantConfiguration.Type.class, "variant configuration type");
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/DensityFilteredAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/DensityFilteredAndroidData.java
new file mode 100644
index 0000000000..84e30534cc
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/DensityFilteredAndroidData.java
@@ -0,0 +1,26 @@
+// Copyright 2015 Google Inc. 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 java.nio.file.Path;
+
+/**
+ * Represents a MergedData that has been filtered for density content.
+ */
+public class DensityFilteredAndroidData extends MergedAndroidData {
+
+ public DensityFilteredAndroidData(Path resources, Path assets, Path manifest) {
+ super(resources, assets, manifest);
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/DensitySpecificManifestProcessor.java b/src/tools/android/java/com/google/devtools/build/android/DensitySpecificManifestProcessor.java
new file mode 100644
index 0000000000..ae094eaec4
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/DensitySpecificManifestProcessor.java
@@ -0,0 +1,145 @@
+// Copyright 2015 Google Inc. 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.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.FactoryConfigurationError;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+/**
+ * Modifies a {@link MergedAndroidData} manifest for the specified densities.
+ */
+public class DensitySpecificManifestProcessor {
+
+ static final ImmutableList<String> SCREEN_SIZES = ImmutableList.of(
+ "small", "normal", "large", "xlarge");
+ static final ImmutableMap<String, String> SCREEN_DENSITIES =
+ ImmutableMap.<String, String>builder()
+ .put("ldpi", "ldpi")
+ .put("mdpi", "mdpi")
+ .put("tvdpi", "213")
+ .put("hdpi", "hdpi")
+ .put("xhdpi", "xhdpi")
+ .put("400dpi", "400")
+ .put("xxhdpi", "480")
+ .put("560dpi", "560")
+ .put("xxxhdpi", "640").build();
+
+ private static final ImmutableMap<String, Boolean> SECURE_XML_FEATURES = ImmutableMap.of(
+ XMLConstants.FEATURE_SECURE_PROCESSING, true,
+ "http://xml.org/sax/features/external-general-entities", false,
+ "http://xml.org/sax/features/external-parameter-entities", false,
+ "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+
+ private static DocumentBuilder getSecureDocumentBuilder() throws ParserConfigurationException {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(
+ "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", null);
+ factory.setValidating(false);
+ factory.setXIncludeAware(false);
+ for (Map.Entry<String, Boolean> featureAndValue : SECURE_XML_FEATURES.entrySet()) {
+ try {
+ factory.setFeature(featureAndValue.getKey(), featureAndValue.getValue());
+ } catch (ParserConfigurationException e) {
+ throw new FactoryConfigurationError(e,
+ "Xerces DocumentBuilderFactory doesn't support the required security features: "
+ + e.getMessage());
+ }
+ }
+ return factory.newDocumentBuilder();
+ }
+
+ private final List<String> densities;
+ private final Path out;
+
+ /**
+ * @param densities An array of string densities to use for filtering resources.
+ * @param out The path to use for the generated manifest.
+ */
+ public DensitySpecificManifestProcessor(List<String> densities, Path out) {
+ this.densities = densities;
+ this.out = out;
+ }
+
+ /**
+ * Modifies the manifest to contain a &lt;compatible-screens&gt; section corresponding to the
+ * specified densities.
+ *
+ * @throws ManifestProcessingException when the manifest cannot be properly modified.
+ */
+ public Path process(Path manifest) throws ManifestProcessingException {
+ if (densities.isEmpty()) {
+ return manifest;
+ }
+ try {
+ DocumentBuilder db = getSecureDocumentBuilder();
+ Document doc = db.parse(Files.newInputStream(manifest));
+
+ NodeList manifestElements = doc.getElementsByTagName("manifest");
+ if (manifestElements.getLength() != 1) {
+ throw new ManifestProcessingException(
+ String.format("Manifest %s does not contain exactly one <manifest> tag. "
+ + "It contains %d.", manifest, manifestElements.getLength()));
+ }
+ Node manifestElement = manifestElements.item(0);
+
+ NodeList compatibleScreensElements = doc.getElementsByTagName("compatible-screens");
+ for (int i = 0; i < compatibleScreensElements.getLength(); i++) {
+ Node compatibleScreensElement = compatibleScreensElements.item(i);
+ compatibleScreensElement.getParentNode().removeChild(compatibleScreensElement);
+ }
+
+ Node compatibleScreens = doc.createElement("compatible-screens");
+ manifestElement.appendChild(compatibleScreens);
+
+ for (String density : densities) {
+ for (String screenSize : SCREEN_SIZES) {
+ Element screen = doc.createElement("screen");
+ screen.setAttribute("android:screenSize", screenSize);
+ screen.setAttribute("android:screenDensity", SCREEN_DENSITIES.get(density));
+ compatibleScreens.appendChild(screen);
+ }
+ }
+
+ Files.createDirectories(out.getParent());
+ TransformerFactory transformerFactory = TransformerFactory.newInstance();
+ transformerFactory.newTransformer().transform(
+ new DOMSource(doc), new StreamResult(Files.newOutputStream(out)));
+ return out;
+
+ } catch (ParserConfigurationException | SAXException | IOException | TransformerException e) {
+ throw new ManifestProcessingException(e.getMessage());
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/DensitySpecificResourceFilter.java b/src/tools/android/java/com/google/devtools/build/android/DensitySpecificResourceFilter.java
new file mode 100644
index 0000000000..dc9239db86
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/DensitySpecificResourceFilter.java
@@ -0,0 +1,294 @@
+// Copyright 2015 Google Inc. 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.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Ordering;
+
+import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Filters a {@link MergedAndroidData} resource drawables to the specified densities.
+ */
+public class DensitySpecificResourceFilter {
+ private static class ResourceInfo {
+ /** Path to an actual file resource, instead of a directory. */
+ private Path resource;
+ private String restype;
+ private String qualifiers;
+ private String density;
+ private String resid;
+
+ public ResourceInfo(Path resource, String restype, String qualifiers, String density,
+ String resid) {
+ this.resource = resource;
+ this.restype = restype;
+ this.qualifiers = qualifiers;
+ this.density = density;
+ this.resid = resid;
+ }
+
+ public Path getResource() {
+ return this.resource;
+ }
+
+ public String getRestype() {
+ return this.restype;
+ }
+
+ public String getQualifiers() {
+ return this.qualifiers;
+ }
+
+ public String getDensity() {
+ return this.density;
+ }
+
+ public String getResid() {
+ return this.resid;
+ }
+ }
+
+ private static class RecursiveFileCopier extends SimpleFileVisitor<Path> {
+ private final Path copyToPath;
+ private final List<Path> copiedSourceFiles = new ArrayList<>();
+ private Path root;
+
+ public RecursiveFileCopier(final Path copyToPath, final Path root) {
+ this.copyToPath = copyToPath;
+ this.root = root;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
+ Path copyTo = copyToPath.resolve(root.relativize(path));
+ Files.createDirectories(copyTo.getParent());
+ Files.copy(path, copyTo, LinkOption.NOFOLLOW_LINKS);
+ copiedSourceFiles.add(copyTo);
+ return FileVisitResult.CONTINUE;
+ }
+
+ public List<Path> getCopiedFiles() {
+ return copiedSourceFiles;
+ }
+ }
+
+ private final List<String> densities;
+ private final Path out;
+ private final Path working;
+
+ private static final Map<String, Integer> DENSITY_MAP =
+ new ImmutableMap.Builder<String, Integer>()
+ .put("nodpi", 0)
+ .put("ldpi", 120)
+ .put("mdpi", 160)
+ .put("tvdpi", 213)
+ .put("hdpi", 240)
+ .put("xhdpi", 320)
+ .put("400dpi", 400)
+ .put("xxhdpi", 480)
+ .put("xxxhdpi", 640)
+ .build();
+
+ private static final Function<ResourceInfo, String> GET_RESOURCE_ID =
+ new Function<ResourceInfo, String>() {
+ @Override
+ public String apply(ResourceInfo info) {
+ return info.getResid();
+ }
+ };
+
+ private static final Function<ResourceInfo, String> GET_RESOURCE_QUALIFIERS =
+ new Function<ResourceInfo, String>() {
+ @Override
+ public String apply(ResourceInfo info) {
+ return info.getQualifiers();
+ }
+ };
+
+ private static final Function<ResourceInfo, Path> GET_RESOURCE_PATH =
+ new Function<ResourceInfo, Path>() {
+ @Override
+ public Path apply(ResourceInfo info) {
+ return info.getResource();
+ }
+ };
+
+ /**
+ * @param densities An array of string densities to use for filtering resources
+ * @param out The path to use for name spacing the final resource directory.
+ * @param working The path of the working directory for the filtering
+ */
+ public DensitySpecificResourceFilter(List<String> densities, Path out, Path working) {
+ this.densities = densities;
+ this.out = out;
+ this.working = working;
+ }
+
+ @VisibleForTesting
+ List<Path> getResourceToRemove(List<Path> resourcePaths) {
+ List<ResourceInfo> resourceInfos = getResourceInfos(resourcePaths);
+ List<ResourceInfo> densityResourceInfos = filterDensityResourceInfos(resourceInfos);
+ List<ResourceInfo> resourceInfoToRemove = new ArrayList<>();
+
+ Multimap<String, ResourceInfo> fileGroups = groupResourceInfos(densityResourceInfos,
+ GET_RESOURCE_ID);
+
+ for (String key : fileGroups.keySet()) {
+ Multimap<String, ResourceInfo> qualifierGroups = groupResourceInfos(fileGroups.get(key),
+ GET_RESOURCE_QUALIFIERS);
+
+ for (String qualifiers : qualifierGroups.keySet()) {
+ Collection<ResourceInfo> qualifierResourceInfos = qualifierGroups.get(qualifiers);
+
+ if (qualifierResourceInfos.size() != 1) {
+ for (final String density : densities) {
+ List<ResourceInfo> sortedResourceInfos = Ordering.natural().onResultOf(
+ new Function<ResourceInfo, Double>() {
+ @Override
+ public Double apply(ResourceInfo info) {
+ return matchScore(info, density);
+ }
+ }).immutableSortedCopy(qualifierResourceInfos);
+
+ resourceInfoToRemove.addAll(sortedResourceInfos.subList(1, sortedResourceInfos.size()));
+ }
+ }
+ }
+ }
+
+ return ImmutableList.copyOf(Lists.transform(resourceInfoToRemove, GET_RESOURCE_PATH));
+ }
+
+ private static void removeResources(List<Path> resourceInfoToRemove) {
+ for (Path resource : resourceInfoToRemove) {
+ resource.toFile().delete();
+ }
+ }
+
+ private static Multimap<String, ResourceInfo> groupResourceInfos(
+ final Collection<ResourceInfo> resourceInfos, Function<ResourceInfo, String> keyFunction) {
+ Multimap<String, ResourceInfo> resourceGroups = ArrayListMultimap.create();
+
+ for (ResourceInfo resourceInfo : resourceInfos) {
+ resourceGroups.put(keyFunction.apply(resourceInfo), resourceInfo);
+ }
+
+ return ImmutableMultimap.copyOf(resourceGroups);
+ }
+
+ private static List<ResourceInfo> getResourceInfos(final List<Path> resourcePaths) {
+ List<ResourceInfo> resourceInfos = new ArrayList<>();
+
+ for (Path resourcePath : resourcePaths) {
+ String qualifiers = resourcePath.getParent().getFileName().toString();
+ String density = "";
+
+ for (String densityName : DENSITY_MAP.keySet()) {
+ if (qualifiers.contains("-" + densityName)) {
+ qualifiers = qualifiers.replace("-" + densityName, "");
+ density = densityName;
+ }
+ }
+
+ String[] qualifierArray = qualifiers.split("-");
+ String restype = qualifierArray[0];
+ qualifiers = (qualifierArray.length) > 0 ? Joiner.on("-").join(Arrays.copyOfRange(
+ qualifierArray, 1, qualifierArray.length)) : "";
+ resourceInfos.add(new ResourceInfo(resourcePath, restype, qualifiers, density,
+ resourcePath.getFileName().toString()));
+ }
+
+ return ImmutableList.copyOf(resourceInfos);
+ }
+
+ private static List<ResourceInfo> filterDensityResourceInfos(
+ final List<ResourceInfo> resourceInfos) {
+ List<ResourceInfo> densityResourceInfos = new ArrayList<>();
+
+ for (ResourceInfo info : resourceInfos) {
+ if (info.getRestype().equals("drawable") && !info.getDensity().equals("")
+ && !info.getDensity().equals("nodpi") && !info.getResid().endsWith(".xml")) {
+ densityResourceInfos.add(info);
+ }
+ }
+
+ return ImmutableList.copyOf(densityResourceInfos);
+ }
+
+ private static double matchScore(ResourceInfo resource, String density) {
+ if (resource.getDensity().equals(density)) {
+ return -2;
+ }
+
+ double affinity =
+ Math.log((double) (DENSITY_MAP.get(density)) / DENSITY_MAP.get(resource.getDensity()))
+ / Math.log(2);
+
+ if (affinity == -1) {
+ // It's very efficient to downsample an image that's exactly 2x the screen
+ // density, so we prefer that over other non-perfect matches
+ return affinity;
+ } else if (affinity < 0) {
+ // We give a slight bump to images that have the same multiplier but are
+ // higher quality.
+ affinity = Math.abs(affinity + 0.01);
+ }
+
+ return affinity;
+ }
+
+ /** Filters the contents of a resource directory. */
+ public Path filter(Path unFilteredResourceDir) {
+ // no densities to filter, so skip.
+ if (densities.isEmpty()) {
+ return unFilteredResourceDir;
+ }
+ final Path filteredResourceDir =
+ out.resolve(working.relativize(unFilteredResourceDir));
+ RecursiveFileCopier fileVisitor =
+ new RecursiveFileCopier(filteredResourceDir, unFilteredResourceDir);
+ try {
+ Files.walkFileTree(unFilteredResourceDir, EnumSet.of(FileVisitOption.FOLLOW_LINKS),
+ Integer.MAX_VALUE, fileVisitor);
+ } catch (IOException e) {
+ throw Throwables.propagate(e);
+ }
+ removeResources(getResourceToRemove(fileVisitor.getCopiedFiles()));
+ return filteredResourceDir;
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java
new file mode 100644
index 0000000000..26d9fa3b92
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java
@@ -0,0 +1,201 @@
+// Copyright 2015 Google Inc. 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.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import com.android.builder.dependency.SymbolFileProvider;
+import com.android.ide.common.res2.AssetSet;
+import com.android.ide.common.res2.ResourceSet;
+
+import java.io.File;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * Contains the assets, resources, manifest and resource symbols for an android_library dependency.
+ *
+ * <p>
+ * This class serves the role of both a processed MergedAndroidData and a dependency exported from
+ * another invocation of the AndroidResourcesProcessorAction. Since it's presumed to be cheaper to
+ * only pass the derived artifact (rTxt) rather that the entirety of the processed dependencies (png
+ * crunching and resource processing should be saved for the final AndroidResourcesProcessorAction
+ * invocation) AndroidData can have multiple roots for resources and assets.
+ * </p>
+ */
+class DependencyAndroidData {
+ static final Pattern VALID_REGEX = Pattern.compile(".*:.*:.+:.+(:.*)?");
+
+ public static DependencyAndroidData valueOf(String text) {
+ return valueOf(text, FileSystems.getDefault());
+ }
+
+ @VisibleForTesting
+ static DependencyAndroidData valueOf(String text, FileSystem fileSystem) {
+ if (!VALID_REGEX.matcher(text).find()) {
+ throw new IllegalArgumentException(text
+ + " is not in the format 'resources[#resources]:assets[#assets]:manifest:"
+ + "r.txt:symbols.txt'");
+ }
+ String[] parts = text.split("\\:");
+ // TODO(bazel-team): Handle the local-r.txt file.
+ // The local R is optional -- if it is missing, we'll use the full R.txt
+ return new DependencyAndroidData(splitPaths(parts[0], fileSystem),
+ parts[1].length() == 0 ? ImmutableList.<Path>of() : splitPaths(parts[1], fileSystem),
+ exists(fileSystem.getPath(parts[2])), exists(fileSystem.getPath(parts[3])),
+ parts.length == 5 ? fileSystem.getPath(parts[4]) : null);
+ }
+
+ private static ImmutableList<Path> splitPaths(String pathsString, FileSystem fileSystem) {
+ if (pathsString.trim().isEmpty()) {
+ return ImmutableList.<Path>of();
+ }
+ ImmutableList.Builder<Path> paths = new ImmutableList.Builder<>();
+ for (String pathString : pathsString.split("#")) {
+ Preconditions.checkArgument(!pathString.trim().isEmpty());
+ paths.add(exists(fileSystem.getPath(pathString)));
+ }
+ return paths.build();
+ }
+
+ private static Path exists(Path path) {
+ if (!Files.exists(path)) {
+ throw new IllegalArgumentException(path + " does not exist");
+ }
+ return path;
+ }
+
+ private final Path rTxt;
+ private final Path manifest;
+ private final ImmutableList<Path> assetDirs;
+ private final ImmutableList<Path> resourceDirs;
+ private final Path symbolsTxt;
+
+ public DependencyAndroidData(ImmutableList<Path> resourceDirs, ImmutableList<Path> assetDirs,
+ Path manifest, Path rTxt, Path symbolsTxt) {
+ this.resourceDirs = resourceDirs;
+ this.assetDirs = assetDirs;
+ this.manifest = manifest;
+ this.rTxt = rTxt;
+ this.symbolsTxt = symbolsTxt;
+ }
+
+ public SymbolFileProvider asSymbolFileProvider() {
+ return new SymbolFileProvider() {
+ @Override
+ public File getManifest() {
+ return manifest.toFile();
+ }
+ @Override
+ public File getSymbolFile() {
+ return rTxt == null ? null : rTxt.toFile();
+ }
+ };
+ }
+
+ public Path getManifest() {
+ return manifest;
+ }
+
+ public AssetSet addToAssets(AssetSet assets) {
+ for (Path assetDir : assetDirs) {
+ assets.addSource(assetDir.toFile());
+ }
+ return assets;
+ }
+
+ public ResourceSet addToResourceSet(ResourceSet resources) {
+ for (Path resourceDir : resourceDirs) {
+ resources.addSource(resourceDir.toFile());
+ }
+ return resources;
+ }
+
+ /**
+ * Adds all the resource directories as ResourceSets. This acts a loose merge
+ * strategy as it does not test for overrides.
+ * @param resourceSets A list of resource sets to append to.
+ */
+ void addAsResourceSets(List<ResourceSet> resourceSets) {
+ for (Path resourceDir : resourceDirs) {
+ ResourceSet set = new ResourceSet("dependency:" + resourceDir.toString());
+ set.addSource(resourceDir.toFile());
+ resourceSets.add(set);
+ }
+ }
+
+ /**
+ * Adds all the asset directories as AssetSets. This acts a loose merge
+ * strategy as it does not test for overrides.
+ * @param assetSets A list of asset sets to append to.
+ */
+ void addAsAssetSets(List<AssetSet> assetSets) {
+ for (Path assetDir : assetDirs) {
+ AssetSet set = new AssetSet("dependency:" + assetDir.toString());
+ set.addSource(assetDir.toFile());
+ assetSets.add(set);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("AndroidData(%s, %s, %s, %s, %s)",
+ resourceDirs,
+ assetDirs,
+ manifest,
+ rTxt,
+ symbolsTxt);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(resourceDirs, assetDirs, manifest, rTxt, symbolsTxt);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof DependencyAndroidData)) {
+ return false;
+ }
+ DependencyAndroidData other = (DependencyAndroidData) obj;
+ return Objects.equals(other.resourceDirs, resourceDirs)
+ && Objects.equals(other.assetDirs, assetDirs)
+ && Objects.equals(other.rTxt, rTxt)
+ && Objects.equals(other.symbolsTxt, symbolsTxt)
+ && Objects.equals(other.manifest, manifest);
+ }
+
+ public DependencyAndroidData modify(ImmutableList<DirectoryModifier> modifiers) {
+ ImmutableList<Path> modifiedResources = resourceDirs;
+ ImmutableList<Path> modifiedAssets = assetDirs;
+ for (DirectoryModifier modifier : modifiers) {
+ modifiedAssets = modifier.modify(modifiedAssets);
+ modifiedResources = modifier.modify(modifiedResources);
+ }
+ return new DependencyAndroidData(modifiedResources, modifiedAssets, manifest, rTxt, null);
+ }
+} \ No newline at end of file
diff --git a/src/tools/android/java/com/google/devtools/build/android/DirectoryModifier.java b/src/tools/android/java/com/google/devtools/build/android/DirectoryModifier.java
new file mode 100644
index 0000000000..9c6ebef7cd
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/DirectoryModifier.java
@@ -0,0 +1,30 @@
+// Copyright 2015 Google Inc. 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.google.common.collect.ImmutableList;
+
+import java.nio.file.Path;
+
+
+/**
+ * And interface for apply modifiers to lists of resource directories.
+ *
+ * <p>
+ * This is a common entry point for resource hacks such as the files deduplication and
+ * the resource unpacking.
+ */
+interface DirectoryModifier {
+ public abstract ImmutableList<Path> modify(ImmutableList<Path> directories);
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/FileDeDuplicator.java b/src/tools/android/java/com/google/devtools/build/android/FileDeDuplicator.java
new file mode 100644
index 0000000000..a87455f51b
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/FileDeDuplicator.java
@@ -0,0 +1,137 @@
+// Copyright 2015 Google Inc. 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.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hasher;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.FileVisitOption;
+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.logging.Logger;
+
+/**
+ * Deduplicates identical files in the provided directories.
+ * <p>
+ * This is necessary for the andorid_resources deprecation -- the old style of inheritance
+ * required all relevant resources to be copied from each dependency. This means each resource is
+ * duplicated for each resource set. This modifier creates a sym link forest for each unique file
+ * on a first come, first serve basis. Which makes aapt and the merging code loads happier.
+ */
+public class FileDeDuplicator implements DirectoryModifier {
+ private static final Logger LOGGER = Logger.getLogger(FileDeDuplicator.class.getName());
+
+ private static final class ConditionalCopyVisitor extends SimpleFileVisitor<Path> {
+ private final Path newRoot;
+ private final Path workingDir;
+ private Multimap<Path, HashCode> seen;
+ private HashFunction hashFunction;
+
+ private ConditionalCopyVisitor(Path newRoot, Path workingDir,
+ Multimap<Path, HashCode> seen, HashFunction hashFunction) {
+ this.newRoot = newRoot;
+ this.workingDir = workingDir;
+ this.seen = seen;
+ this.hashFunction = hashFunction;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+ throws IOException {
+ Files.createDirectories(newRoot.resolve(workingDir.relativize(dir)));
+ return super.preVisitDirectory(dir, attrs);
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Path relativePath = workingDir.relativize(file);
+ final HashCode fileHash = hashPath(file, hashFunction.newHasher());
+ if (!seen.get(relativePath).contains(fileHash)) {
+ seen.get(relativePath).add(fileHash);
+ // TODO(bazel-team): Change to a symlink when the AOSP merge code supports symlinks.
+ Files.copy(file, newRoot.resolve(relativePath));
+ // Files.createSymbolicLink(newRoot.resolve(workingDir.relativize(file)), file);
+ } else {
+ LOGGER.warning(String.format("Duplicated file %s [%s]", relativePath, file));
+ }
+ return super.visitFile(file, attrs);
+ }
+ }
+
+ private static HashCode hashPath(Path file, final Hasher hasher) throws IOException {
+ byte[] tmpBuffer = new byte[512];
+ final InputStream in = Files.newInputStream(file);
+ for (int read = in.read(tmpBuffer); read > 0; read = in.read(tmpBuffer)) {
+ hasher.putBytes(tmpBuffer, 0, read);
+ }
+ final HashCode fileHash = hasher.hash();
+ in.close();
+ return fileHash;
+ }
+
+ private final Multimap<Path, HashCode> seen;
+ private final HashFunction hashFunction;
+ private final Path out;
+ private final Path workingDirectory;
+
+ public FileDeDuplicator(HashFunction hashFunction, Path out, Path workingDirectory) {
+ this.hashFunction = hashFunction;
+ this.workingDirectory = workingDirectory;
+ this.seen = HashMultimap.create();
+ this.out = out;
+ }
+
+ private ImmutableList<Path> conditionallyCopy(ImmutableList<Path> roots)
+ throws IOException {
+ final Builder<Path> builder = ImmutableList.builder();
+ for (Path root : roots) {
+ Preconditions.checkArgument(root.startsWith(workingDirectory),
+ root + " must start with root " + workingDirectory + " from " + roots);
+ Preconditions.checkArgument(!root.equals(workingDirectory),
+ "Cannot deduplicate root directory: " + root + " from " + roots);
+ if (!seen.containsKey(root)) {
+ seen.put(root, null);
+ final Path newRoot = out.resolve(workingDirectory.relativize(root));
+ Files.walkFileTree(root, ImmutableSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
+ new ConditionalCopyVisitor(newRoot, root, seen, hashFunction));
+ builder.add(newRoot);
+ } else {
+ LOGGER.warning(String.format("Duplicated directory %s", root));
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public ImmutableList<Path> modify(ImmutableList<Path> directories) {
+ try {
+ return conditionallyCopy(directories);
+ } catch (IOException e) {
+ throw Throwables.propagate(e);
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/ManifestProcessingException.java b/src/tools/android/java/com/google/devtools/build/android/ManifestProcessingException.java
new file mode 100644
index 0000000000..b465acd536
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/ManifestProcessingException.java
@@ -0,0 +1,28 @@
+// Copyright 2015 Google Inc. 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;
+
+/**
+ * Indicates an error parsing or modifying an AndroidManifest.xml.
+ */
+public class ManifestProcessingException extends Exception {
+
+ public ManifestProcessingException() {
+ super();
+ }
+
+ public ManifestProcessingException(String message) {
+ super(message);
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/MergedAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/MergedAndroidData.java
new file mode 100644
index 0000000000..1b7eee001a
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/MergedAndroidData.java
@@ -0,0 +1,60 @@
+// Copyright 2015 Google Inc. 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 java.io.File;
+import java.nio.file.Path;
+
+/**
+ * Represents the AndroidData before processing, after merging.
+ *
+ * <p>
+ * The life cycle of AndroidData goes:
+ * <pre>
+ * UnvalidatedAndroidData -> MergedAndroidData -> DensityFilteredAndroidData
+ * -> DependencyAndroidData
+ * </pre>
+ */
+class MergedAndroidData {
+
+ private Path resourceDir;
+ private Path assetDir;
+ private Path manifest;
+
+ public MergedAndroidData(Path resources, Path assets, Path manifest) {
+ this.resourceDir = resources;
+ this.assetDir = assets;
+ this.manifest = manifest;
+ }
+
+ public File getResourceDirFile() {
+ return resourceDir.toFile();
+ }
+
+ public File getAssetDirFile() {
+ return assetDir != null ? assetDir.toFile() : null;
+ }
+
+ public File getManifestFile() {
+ return manifest.toFile();
+ }
+
+ public DensityFilteredAndroidData filter(
+ DensitySpecificResourceFilter resourceFilter,
+ DensitySpecificManifestProcessor manifestProcessor)
+ throws ManifestProcessingException {
+ return new DensityFilteredAndroidData(resourceFilter.filter(resourceDir),
+ assetDir, manifestProcessor.process(manifest));
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/PackedResourceTarExpander.java b/src/tools/android/java/com/google/devtools/build/android/PackedResourceTarExpander.java
new file mode 100644
index 0000000000..1fbbc7ca51
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/PackedResourceTarExpander.java
@@ -0,0 +1,147 @@
+// Copyright 2015 Google Inc. 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.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.FileVisitOption;
+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.logging.Logger;
+
+/**
+ * Unpacks specially named tar files in a resource file tree.
+ *
+ * <p>Scans a list of Resource directories looking for "raw/blaze_internal_packed_resources.tar".
+ * When found, it is unpacked into a new resource directory.</p>
+ */
+// TODO(bazel-team): Remove when Android support library version is handled by configurable
+// attribute.
+class PackedResourceTarExpander implements DirectoryModifier {
+ private static final Logger LOGGER = Logger.getLogger(PackedResourceTarExpander.class.getName());
+
+ private static final class ConditionallyLinkingVisitor extends SimpleFileVisitor<Path> {
+
+ private final Path fileToexclude;
+ private Path out;
+ private Path workingDirectory;
+
+ private ConditionallyLinkingVisitor(Path fileToExclude, Path out, Path workingDirectory) {
+ this.fileToexclude = fileToExclude;
+ this.out = out;
+ this.workingDirectory = workingDirectory;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+ throws IOException {
+ Files.createDirectories(out.resolve(workingDirectory.relativize(dir)));
+ return super.preVisitDirectory(dir, attrs);
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+ throws IOException {
+ if (!fileToexclude.equals(file)) {
+ // TODO(bazel-team): Change to a symlink when the merge code supports symlinks.
+ Files.copy(file, out.resolve(workingDirectory.relativize(file)));
+ //Files.createSymbolicLink(out.resolve(workingDirectory.relativize(file)), file);
+ }
+ return super.visitFile(file, attrs);
+ }
+ }
+
+ private final Path out;
+ private Path workingDirectory;
+
+ public PackedResourceTarExpander(Path out, Path workingDirectory) {
+ this.out = out;
+ this.workingDirectory = workingDirectory;
+ }
+
+ @Override
+ public ImmutableList<Path> modify(ImmutableList<Path> resourceRoots) {
+ final Builder<Path> outDirs = ImmutableList.builder();
+ for (final Path unresolvedRoot : resourceRoots) {
+ Path root = unresolvedRoot.toAbsolutePath();
+ try {
+ final Path packedResources =
+ root.resolve("raw/blaze_internal_packed_resources.tar");
+ if (Files.exists(packedResources)) {
+ Preconditions.checkArgument(root.startsWith(workingDirectory),
+ "%s is not under %s", root, workingDirectory);
+ final Path resourcePrefix = workingDirectory.relativize(root);
+ final Path targetDirectory = out.resolve(resourcePrefix);
+ outDirs.add(targetDirectory);
+ copyRemainingResources(root, packedResources);
+ // Group the unpacked resource by the path they came from.
+ final Path tarOut =
+ out.resolve("blaze_internal_packed_resources").resolve(resourcePrefix);
+ unTarPackedResources(tarOut, packedResources);
+ outDirs.add(tarOut);
+ } else {
+ outDirs.add(root);
+ }
+ } catch (IOException e) {
+ Throwables.propagate(e);
+ }
+ }
+ return outDirs.build();
+ }
+
+ private void unTarPackedResources(final Path tarOut, final Path packedResources)
+ throws IOException {
+ LOGGER.fine(String.format("Found packed resources: %s", packedResources));
+ try (InputStream inputStream = Files.newInputStream(packedResources);
+ TarArchiveInputStream tarStream = new TarArchiveInputStream(inputStream)) {
+ byte[] temp = new byte[4 * 1024];
+ for (TarArchiveEntry entry = tarStream.getNextTarEntry(); entry != null;
+ entry = tarStream.getNextTarEntry()) {
+ if (!entry.isFile()) {
+ continue;
+ }
+ int read = tarStream.read(temp);
+ // packed tars can start with a ./. This can cause issues, so remove it.
+ final Path entryPath = tarOut.resolve(entry.getName().replace("^\\./", ""));
+ Files.createDirectories(entryPath.getParent());
+ final OutputStream entryOutStream = Files.newOutputStream(entryPath);
+ while (read > -1) {
+ entryOutStream.write(temp, 0, read);
+ read = tarStream.read(temp);
+ }
+ entryOutStream.flush();
+ entryOutStream.close();
+ }
+ }
+ }
+
+ private void copyRemainingResources(final Path resourcePath, final Path packedResources)
+ throws IOException {
+ Files.walkFileTree(resourcePath, ImmutableSet.of(FileVisitOption.FOLLOW_LINKS),
+ Integer.MAX_VALUE, new ConditionallyLinkingVisitor(packedResources, out, workingDirectory));
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/README b/src/tools/android/java/com/google/devtools/build/android/README
new file mode 100644
index 0000000000..b6da2c3ad8
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/README
@@ -0,0 +1 @@
+Utilities and actions for Bazel Android rules. \ No newline at end of file
diff --git a/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java
new file mode 100644
index 0000000000..52cfdac8ff
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java
@@ -0,0 +1,163 @@
+// Copyright 2015 Google Inc. 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.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+
+import com.android.ide.common.res2.AssetSet;
+import com.android.ide.common.res2.ResourceSet;
+
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * Android data that has yet to be merged and validated, the primary data for the Processor.
+ *
+ * <p>The life cycle of AndroidData goes:
+ * {@link UnvalidatedAndroidData} -> {@link MergedAndroidData} -> {@link DensityFilteredAndroidData}
+ * -> {@link DependencyAndroidData}
+ */
+class UnvalidatedAndroidData {
+ static final Pattern VALID_REGEX = Pattern.compile(".*:.*:.+");
+
+ public static UnvalidatedAndroidData valueOf(String text) {
+ return valueOf(text, FileSystems.getDefault());
+ }
+
+ @VisibleForTesting
+ static UnvalidatedAndroidData valueOf(String text, FileSystem fileSystem) {
+ if (!VALID_REGEX.matcher(text).find()) {
+ throw new IllegalArgumentException(
+ text + " is not in the format 'resources[#resources]:assets[#assets]:manifest'");
+ }
+ String[] parts = text.split(":");
+ return new UnvalidatedAndroidData(
+ splitPaths(parts[0], fileSystem),
+ splitPaths(parts[1], fileSystem),
+ exists(fileSystem.getPath(parts[2])));
+ }
+
+ private static ImmutableList<Path> splitPaths(String pathsString, FileSystem fileSystem) {
+ if (pathsString.length() == 0) {
+ return ImmutableList.of();
+ }
+ ImmutableList.Builder<Path> paths = new ImmutableList.Builder<>();
+ for (String pathString : pathsString.split("#")) {
+ paths.add(exists(fileSystem.getPath(pathString)));
+ }
+ return paths.build();
+ }
+
+ private static Path exists(Path path) {
+ if (!Files.exists(path)) {
+ throw new IllegalArgumentException(path + " does not exist");
+ }
+ return path;
+ }
+
+ private final Path manifest;
+ private final ImmutableList<Path> assetDirs;
+ private final ImmutableList<Path> resourceDirs;
+
+ public UnvalidatedAndroidData(ImmutableList<Path> resourceDirs, ImmutableList<Path> assetDirs,
+ Path manifest) {
+ this.resourceDirs = resourceDirs;
+ this.assetDirs = assetDirs;
+ this.manifest = manifest;
+ }
+
+ public Path getManifest() {
+ return manifest;
+ }
+
+ public AssetSet addToAssets(AssetSet assets) {
+ for (Path assetDir : assetDirs) {
+ assets.addSource(assetDir.toFile());
+ }
+ return assets;
+ }
+
+ public ResourceSet addToResourceSet(ResourceSet resources) {
+ for (Path resourceDir : resourceDirs) {
+ resources.addSource(resourceDir.toFile());
+ }
+ return resources;
+ }
+
+ public UnvalidatedAndroidData modify(ImmutableList<DirectoryModifier> modifiers) {
+ ImmutableList<Path> modifiedResources = resourceDirs;
+ ImmutableList<Path> modifiedAssets = assetDirs;
+ for (DirectoryModifier modifier : modifiers) {
+ modifiedAssets = modifier.modify(modifiedAssets);
+ modifiedResources = modifier.modify(modifiedResources);
+ }
+ return new UnvalidatedAndroidData(modifiedResources, modifiedAssets, manifest);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UnvalidatedAndroidData(%s, %s, %s)", resourceDirs, assetDirs, manifest);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(resourceDirs, assetDirs, manifest);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof UnvalidatedAndroidData)) {
+ return false;
+ }
+ UnvalidatedAndroidData other = (UnvalidatedAndroidData) obj;
+ return Objects.equals(other.resourceDirs, resourceDirs)
+ && Objects.equals(other.assetDirs, assetDirs)
+ && Objects.equals(other.manifest, manifest);
+ }
+
+ /**
+ * Adds all the resource directories as ResourceSets. This acts a loose merge
+ * strategy as it does not test for overrides.
+ * @param resourceSets A list of resource sets to append to.
+ */
+ void addAsResourceSets(List<ResourceSet> resourceSets) {
+ for (Path resourceDir : resourceDirs) {
+ ResourceSet set = new ResourceSet("primary:" + resourceDir.toString());
+ set.addSource(resourceDir.toFile());
+ resourceSets.add(set);
+ }
+ }
+
+ /**
+ * Adds all the asset directories as AssetSets. This acts a loose merge
+ * strategy as it does not test for overrides.
+ * @param assetSets A list of asset sets to append to.
+ */
+ void addAsAssetSets(List<AssetSet> assetSets) {
+ for (Path assetDir : assetDirs) {
+ AssetSet set = new AssetSet("primary:" + assetDir.toString());
+ set.addSource(assetDir.toFile());
+ assetSets.add(set);
+ }
+ }
+}
diff --git a/third_party/BUILD b/third_party/BUILD
index 512b40e649..086d087550 100644
--- a/third_party/BUILD
+++ b/third_party/BUILD
@@ -43,11 +43,28 @@ java_import(
)
java_import(
+ name = "android_common",
+ jars = [
+ "android_common/com.android.tools.build_builder_0.13.3.jar",
+ "android_common/com.android.tools.build_builder-model_0.13.3.jar",
+ "android_common/com.android.tools.build_manifest-merger_23.1.3.jar",
+ "android_common/com.android.tools_common_23.1.3.jar",
+ "android_common/com.android.tools_sdk-common_23.1.3.jar",
+ "android_common/com.android.tools_sdklib_23.1.3.jar",
+ ],
+)
+
+java_import(
name = "apache_commons_collections",
jars = ["apache_commons_collections/commons-collections-3.2.1.jar"],
)
java_import(
+ name = "apache_commons_compress",
+ jars = ["apache_commons_compress/apache-commons-compress-1.9.jar"],
+)
+
+java_import(
name = "apache_commons_logging",
jars = ["apache_commons_logging/commons-logging-1.1.1.jar"],
)
diff --git a/third_party/README.md b/third_party/README.md
index 0c64d3752d..0f6bfdc4c7 100644
--- a/third_party/README.md
+++ b/third_party/README.md
@@ -10,6 +10,13 @@ a minimal set of extra dependencies.
* License: Eclipse Public License
+[android_common](http://mvnrepository.com/artifact/com.android.tools/sdk-common)
+----------------------------
+
+* Version: 22.8.4
+* License: Apache License 2.0.
+
+
[apache_commons_collections](http://commons.apache.org/proper/commons-collections/)
----------------------------
@@ -17,6 +24,13 @@ a minimal set of extra dependencies.
* License: Apache License 2.0.
+[apache_commons_compress](http://commons.apache.org/proper/commons-compress/)
+----------------------------
+
+* Version: 1.9
+* License: Apache License 2.0.
+
+
[apache_commons_logging](http://commons.apache.org/proper/commons-logging/)
------------------------
diff --git a/tools/android/BUILD b/tools/android/BUILD
index 8c1e1352e2..ec55eeb2b3 100644
--- a/tools/android/BUILD
+++ b/tools/android/BUILD
@@ -12,14 +12,14 @@ filegroup(
srcs = [],
)
-sh_binary(
+filegroup(
name = "aar_generator",
- srcs = ["fail.sh"],
+ srcs = ["//src/tools/android/java/com/google/devtools/build/android:AarGeneratorAction"],
)
-sh_binary(
+filegroup(
name = "resources_processor",
- srcs = ["fail.sh"],
+ srcs = ["//src/tools/android/java/com/google/devtools/build/android:AndroidResourceProcessingAction"],
)
sh_binary(