diff options
author | 2015-06-25 17:12:49 +0000 | |
---|---|---|
committer | 2015-06-26 15:29:53 +0000 | |
commit | d13716a201b2dcfe952e843ffcc566056519aaa5 (patch) | |
tree | 8f6fd28465854ed94d5915e73a746a2fa651f70d | |
parent | 643063d582dcf346f276680288b11f958f5c551d (diff) |
Open source AarGeneratorAction and AndroidResourceProcessingAction.
--
MOS_MIGRATED_REVID=96883818
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 <compatible-screens> 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( |