From d13716a201b2dcfe952e843ffcc566056519aaa5 Mon Sep 17 00:00:00 2001 From: Andrew Pellegrini Date: Thu, 25 Jun 2015 17:12:49 +0000 Subject: Open source AarGeneratorAction and AndroidResourceProcessingAction. -- MOS_MIGRATED_REVID=96883818 --- .../devtools/build/android/AarGeneratorAction.java | 249 +++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java (limited to 'src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java') 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. + * + *

+ * 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
+ * 
+ */ +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 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 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 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 { + 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; + } + } +} -- cgit v1.2.3