| // 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. |
| * |
| * <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 { |
| 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<Path> 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.mergeDataAndWrite( |
| 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, |
| 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<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, |
| List<Path> 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<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(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(); |
| } |
| } |
| } |