// Copyright 2015 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.build.android; import static java.nio.charset.StandardCharsets.UTF_8; import com.android.builder.core.VariantType; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; import com.google.devtools.build.android.AndroidDataMerger.MergeConflictException; import com.google.devtools.build.android.AndroidResourceMerger.MergingException; 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.OptionDocumentationCategory; import com.google.devtools.common.options.OptionEffectTag; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; 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.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; 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\
 *      --mainData path/to/resources:path/to/assets:path/to/manifest\
 *      --manifest path/to/manifest\
 *      --rtxt path/to/rtxt\
 *      --classes path/to/classes.jar\
 *      --aarOutput path/to/write/archive.aar
 * 
*/ public class AarGeneratorAction { public static final long DEFAULT_TIMESTAMP = LocalDateTime.of(2010, 1, 1, 0, 0, 0) .atZone(ZoneId.systemDefault()) .toInstant() .toEpochMilli(); private static final Logger logger = Logger.getLogger(AarGeneratorAction.class.getName()); /** Flag specifications for this action. */ public static final class AarGeneratorOptions extends OptionsBase { @Option( name = "mainData", defaultValue = "null", converter = UnvalidatedAndroidDataConverter.class, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, 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 = "manifest", defaultValue = "null", converter = ExistingPathConverter.class, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path to AndroidManifest.xml." ) public Path manifest; @Option( name = "rtxt", defaultValue = "null", converter = ExistingPathConverter.class, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path to R.txt." ) public Path rtxt; @Option( name = "classes", defaultValue = "null", converter = ExistingPathConverter.class, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path to classes.jar." ) public Path classes; @Option( name = "proguardSpec", defaultValue = "", converter = ExistingPathConverter.class, allowMultiple = true, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path to proguard spec file." ) public List proguardSpecs; @Option( name = "aarOutput", defaultValue = "null", converter = PathConverter.class, category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path to write the archive." ) public Path aarOutput; @Option( name = "throwOnResourceConflict", defaultValue = "false", category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "If passed, resource merge conflicts will be treated as errors instead of warnings" ) public boolean throwOnResourceConflict; } public static void main(String[] args) { Stopwatch timer = Stopwatch.createStarted(); OptionsParser optionsParser = OptionsParser.newOptionsParser(AarGeneratorOptions.class); optionsParser.enableParamsFileSupport( new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); optionsParser.parseAndExitUponError(args); AarGeneratorOptions options = optionsParser.getOptions(AarGeneratorOptions.class); checkFlags(options); try (ScopedTemporaryDirectory scopedTmp = new ScopedTemporaryDirectory("aar_gen_tmp")) { Path tmp = scopedTmp.getPath(); Path resourcesOut = tmp.resolve("merged_resources"); Files.createDirectories(resourcesOut); Path assetsOut = tmp.resolve("merged_assets"); Files.createDirectories(assetsOut); logger.fine(String.format("Setup finished at %dms", timer.elapsed(TimeUnit.MILLISECONDS))); // There aren't any dependencies, but we merge to combine primary resources from different // res/assets directories into a single res and single assets directory. MergedAndroidData mergedData = AndroidResourceMerger.mergeData( options.mainData, ImmutableList.of(), ImmutableList.of(), resourcesOut, assetsOut, null, VariantType.LIBRARY, null, /* filteredResources= */ ImmutableList.of(), options.throwOnResourceConflict); logger.fine(String.format("Merging finished at %dms", timer.elapsed(TimeUnit.MILLISECONDS))); writeAar( options.aarOutput, mergedData, options.manifest, options.rtxt, options.classes, options.proguardSpecs); logger.fine( String.format("Packaging finished at %dms", timer.elapsed(TimeUnit.MILLISECONDS))); } catch (MergeConflictException e) { logger.log(Level.SEVERE, e.getMessage()); System.exit(1); } catch (IOException | MergingException e) { logger.log(Level.SEVERE, "Error during merging resources", e); System.exit(1); } System.exit(0); } @VisibleForTesting static void checkFlags(AarGeneratorOptions options) { 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, List proguardSpecs) throws IOException { try (final ZipOutputStream zipOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(aar)))) { ZipEntry manifestEntry = new ZipEntry("AndroidManifest.xml"); manifestEntry.setTime(DEFAULT_TIMESTAMP); zipOut.putNextEntry(manifestEntry); zipOut.write(Files.readAllBytes(manifest)); zipOut.closeEntry(); ZipEntry classJar = new ZipEntry("classes.jar"); classJar.setTime(DEFAULT_TIMESTAMP); zipOut.putNextEntry(classJar); zipOut.write(Files.readAllBytes(classes)); zipOut.closeEntry(); ZipDirectoryWriter resWriter = new ZipDirectoryWriter(zipOut, data.getResourceDir(), "res"); Files.walkFileTree(data.getResourceDir(), resWriter); resWriter.writeEntries(); ZipEntry r = new ZipEntry("R.txt"); r.setTime(DEFAULT_TIMESTAMP); zipOut.putNextEntry(r); zipOut.write(Files.readAllBytes(rtxt)); zipOut.closeEntry(); if (!proguardSpecs.isEmpty()) { ZipEntry proguardTxt = new ZipEntry("proguard.txt"); proguardTxt.setTime(DEFAULT_TIMESTAMP); zipOut.putNextEntry(proguardTxt); for (Path proguardSpec : proguardSpecs) { zipOut.write(Files.readAllBytes(proguardSpec)); zipOut.write("\r\n".getBytes(UTF_8)); } zipOut.closeEntry(); } if (Files.exists(data.getAssetDir()) && data.getAssetDir().toFile().list().length > 0) { ZipDirectoryWriter assetWriter = new ZipDirectoryWriter(zipOut, data.getAssetDir(), "assets"); Files.walkFileTree(data.getAssetDir(), assetWriter); assetWriter.writeEntries(); } } aar.toFile().setLastModified(DEFAULT_TIMESTAMP); } private static class ZipDirectoryWriter extends SimpleFileVisitor { private final ZipOutputStream zipOut; private final Path root; private final String dirName; private final Collection directories = new ArrayList<>(); private final Collection files = new ArrayList<>(); 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 { files.add(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { directories.add(dir); return FileVisitResult.CONTINUE; } void writeEntries() throws IOException { for (Path dir : Ordering.natural().immutableSortedCopy(directories)) { writeDirectoryEntry(dir); } for (Path file : Ordering.natural().immutableSortedCopy(files)) { writeFileEntry(file); } } private void writeFileEntry(Path file) throws IOException { ZipEntry entry = new ZipEntry(new File(dirName, root.relativize(file).toString()).toString()); entry.setTime(DEFAULT_TIMESTAMP); zipOut.putNextEntry(entry); zipOut.write(Files.readAllBytes(file)); zipOut.closeEntry(); } private void writeDirectoryEntry(Path dir) throws IOException { ZipEntry entry = new ZipEntry(new File(dirName, root.relativize(dir).toString()).toString() + "/"); entry.setTime(DEFAULT_TIMESTAMP); zipOut.putNextEntry(entry); zipOut.closeEntry(); } } }