| // 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 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.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. |
| * |
| * <p> |
| * <pre> |
| * 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 |
| * </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 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 = "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.<DependencyAndroidData>of(), |
| ImmutableList.<DependencyAndroidData>of(), |
| resourcesOut, |
| assetsOut, |
| null, |
| VariantType.LIBRARY, |
| null, |
| /* filteredResources= */ ImmutableList.<String>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); |
| 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<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 BufferedOutputStream(Files.newOutputStream(aar)))) { |
| ZipEntry manifestEntry = new ZipEntry("AndroidManifest.xml"); |
| manifestEntry.setTime(EPOCH); |
| zipOut.putNextEntry(manifestEntry); |
| zipOut.write(Files.readAllBytes(manifest)); |
| zipOut.closeEntry(); |
| |
| ZipEntry classJar = new ZipEntry("classes.jar"); |
| classJar.setTime(EPOCH); |
| 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(EPOCH); |
| zipOut.putNextEntry(r); |
| zipOut.write(Files.readAllBytes(rtxt)); |
| 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(EPOCH); |
| } |
| |
| private static class ZipDirectoryWriter extends SimpleFileVisitor<Path> { |
| private final ZipOutputStream zipOut; |
| private final Path root; |
| private final String dirName; |
| private final Collection<Path> directories = new ArrayList<>(); |
| private final Collection<Path> 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(EPOCH); |
| 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(EPOCH); |
| zipOut.putNextEntry(entry); |
| zipOut.closeEntry(); |
| } |
| } |
| } |