blob: 20b0e93c83b88deaac81e0d219914e016b3ff338 [file] [log] [blame]
// 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.android.ide.common.res2.MergingException;
import com.android.utils.StdLogger;
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.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 java.io.BufferedOutputStream;
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.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 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 = "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;
// TODO: remove once blaze stops sending "--nostrictMerge" (since this is unused).
@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);
AndroidResourceProcessor resourceProcessor =
new AndroidResourceProcessor(new StdLogger(com.android.utils.StdLogger.Level.VERBOSE));
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 =
resourceProcessor.mergeData(
options.mainData,
ImmutableList.<DependencyAndroidData>of(),
ImmutableList.<DependencyAndroidData>of(),
resourcesOut,
assetsOut,
null,
VariantType.LIBRARY,
null);
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 (IOException | MergingException e) {
logger.log(Level.SEVERE, "Error during merging resources", e);
System.exit(1);
}
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 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();
Files.walkFileTree(
data.getResourceDir(), new ZipDirectoryWriter(zipOut, data.getResourceDir(), "res"));
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) {
Files.walkFileTree(
data.getAssetDir(), new ZipDirectoryWriter(zipOut, data.getAssetDir(), "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());
entry.setTime(EPOCH);
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() + "/");
entry.setTime(EPOCH);
zipOut.putNextEntry(entry);
zipOut.closeEntry();
return FileVisitResult.CONTINUE;
}
}
}