blob: cc19be8f0c92bd62c852d190d62b8a706c6ffe4f [file] [log] [blame]
// Copyright 2017 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.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Ordering;
import com.google.devtools.build.android.aapt2.ResourceCompiler;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
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.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Objects;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.annotation.Nullable;
/** Collects all the functionationality for an action to create the final output artifacts. */
public class AndroidResourceOutputs {
@VisibleForTesting
static class ZipBuilder implements Closeable {
// ZIP timestamps have a resolution of 2 seconds.
// see http://www.info-zip.org/FAQ.html#limits
private static final long MINIMUM_TIMESTAMP_INCREMENT = 2000L;
// The earliest date representable in a zip file, 1-1-1980 (the DOS epoch).
private static final long ZIP_EPOCH =
new GregorianCalendar(1980, Calendar.JANUARY, 01, 0, 0).getTimeInMillis();
private final ZipOutputStream zip;
private ZipBuilder(ZipOutputStream zip) {
this.zip = zip;
}
public static ZipBuilder createFor(Path archivePath) throws IOException {
return wrap(
new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(archivePath))));
}
public static ZipBuilder wrap(ZipOutputStream zip) {
return new ZipBuilder(zip);
}
/**
* Normalize timestamps for deterministic builds. Stamp .class files to be a bit newer than
* .java files. See: {@link
* com.google.devtools.build.buildjar.jarhelper.JarHelper#normalizedTimestamp(String)}
*/
protected long normalizeTime(String filename) {
if (filename.endsWith(".class")) {
return ZIP_EPOCH + MINIMUM_TIMESTAMP_INCREMENT;
} else {
return ZIP_EPOCH;
}
}
protected void addEntry(String rawName, byte[] content, int storageMethod) throws IOException {
addEntry(rawName, content, storageMethod, null);
}
protected void addEntry(
String rawName, byte[] content, int storageMethod, @Nullable String comment)
throws IOException {
// Fix the path for windows.
String relativeName = rawName.replace('\\', '/');
// Make sure the zip entry is not absolute.
Preconditions.checkArgument(
!relativeName.startsWith("/"), "Cannot add absolute resources %s", relativeName);
ZipEntry entry = new ZipEntry(relativeName);
entry.setMethod(storageMethod);
entry.setTime(normalizeTime(relativeName));
entry.setSize(content.length);
CRC32 crc32 = new CRC32();
crc32.update(content);
entry.setCrc(crc32.getValue());
if (!Strings.isNullOrEmpty(comment)) {
entry.setComment(comment);
}
zip.putNextEntry(entry);
zip.write(content);
zip.closeEntry();
}
protected void addEntry(ZipEntry entry, byte[] content) throws IOException {
// Create a new ZipEntry because there are occasional discrepancies
// between the metadata and written content.
ZipEntry newEntry = new ZipEntry(entry.getName());
zip.putNextEntry(newEntry);
zip.write(content);
zip.closeEntry();
}
@Override
public void close() throws IOException {
zip.close();
}
}
/** A FileVisitor that will add all R class files to be stored in a zip archive. */
static final class ClassJarBuildingVisitor extends ZipBuilderVisitor {
ClassJarBuildingVisitor(ZipBuilder zip, Path root) {
super(zip, root, null);
}
private byte[] manifestContent(@Nullable String targetLabel, @Nullable String injectingRuleKind)
throws IOException {
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
Attributes.Name createdBy = new Attributes.Name("Created-By");
if (attributes.getValue(createdBy) == null) {
attributes.put(createdBy, "bazel");
}
if (targetLabel != null) {
// Enable add_deps support. add_deps expects this attribute in the jar manifest.
attributes.putValue("Target-Label", targetLabel);
}
if (injectingRuleKind != null) {
// add_deps support for aspects. Usually null.
attributes.putValue("Injecting-Rule-Kind", injectingRuleKind);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
manifest.write(out);
return out.toByteArray();
}
@Override
protected void writeEntry(Path file) throws IOException {
Path filename = file.getFileName();
String name = filename.toString();
if (name.endsWith(".class")) {
byte[] content = Files.readAllBytes(file);
addEntry(file, content);
}
}
void writeManifestContent(@Nullable String targetLabel, @Nullable String injectingRuleKind)
throws IOException {
addEntry("META-INF/", new byte[] {});
addEntry("META-INF/MANIFEST.MF", manifestContent(targetLabel, injectingRuleKind));
}
}
/** A FileVisitor that will add all R.java files to be stored in a zip archive. */
static final class SymbolFileSrcJarBuildingVisitor extends ZipBuilderVisitor {
static final Pattern ID_PATTERN =
Pattern.compile("public static int ([\\w\\.]+)=0x[0-9A-fa-f]+;");
static final Pattern INNER_CLASS =
Pattern.compile("public static class ([a-z_]*) \\{(.*?)\\}", Pattern.DOTALL);
static final Pattern PACKAGE_PATTERN =
Pattern.compile("\\s*package ([a-zA-Z_$][a-zA-Z\\d_$]*(?:\\.[a-zA-Z_$][a-zA-Z\\d_$]*)*)");
private final boolean staticIds;
private SymbolFileSrcJarBuildingVisitor(ZipBuilder zipBuilder, Path root, boolean staticIds) {
super(zipBuilder, root, null);
this.staticIds = staticIds;
}
private String replaceIdsWithStaticIds(String contents) {
Matcher packageMatcher = PACKAGE_PATTERN.matcher(contents);
if (!packageMatcher.find()) {
return contents;
}
String pkg = packageMatcher.group(1);
StringBuffer out = new StringBuffer();
Matcher innerClassMatcher = INNER_CLASS.matcher(contents);
while (innerClassMatcher.find()) {
String resourceType = innerClassMatcher.group(1);
Matcher idMatcher = ID_PATTERN.matcher(innerClassMatcher.group(2));
StringBuffer resourceIds = new StringBuffer();
while (idMatcher.find()) {
String javaId = idMatcher.group(1);
idMatcher.appendReplacement(
resourceIds,
String.format(
"public static int %s=0x%08X;", javaId, Objects.hash(pkg, resourceType, javaId)));
}
idMatcher.appendTail(resourceIds);
innerClassMatcher.appendReplacement(
out,
String.format("public static class %s {%s}", resourceType, resourceIds.toString()));
}
innerClassMatcher.appendTail(out);
return out.toString();
}
@Override
protected void writeEntry(Path file) throws IOException {
if (file.getFileName().endsWith("R.java")) {
byte[] content = Files.readAllBytes(file);
if (staticIds) {
content =
replaceIdsWithStaticIds(UTF_8.decode(ByteBuffer.wrap(content)).toString())
.getBytes(UTF_8);
}
addEntry(file, content);
}
}
}
/** A FileVisitor that will add all files and dirents to be stored in a zip archive. */
static final class ZipBuilderVisitorWithDirectories extends ZipBuilderVisitor {
ZipBuilderVisitorWithDirectories(ZipBuilder zipBuilder, Path root, String directory) {
super(zipBuilder, root, directory);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
paths.add(dir);
return FileVisitResult.CONTINUE;
}
}
/** A FileVisitor that will add all files to be stored in a zip archive. */
static class ZipBuilderVisitor extends SimpleFileVisitor<Path> {
protected final String directoryPrefix;
protected final Collection<Path> paths = new ArrayList<>();
protected final Path root;
private int storageMethod = ZipEntry.STORED;
private ZipBuilder zipBuilder;
ZipBuilderVisitor(ZipBuilder zipBuilder, Path root, String directory) {
this.root = root;
this.directoryPrefix = directory != null ? (directory + File.separator) : "";
this.zipBuilder = zipBuilder;
}
protected void addEntry(Path file, byte[] content) throws IOException {
Preconditions.checkArgument(file.startsWith(root), "%s does not start with %s", file, root);
zipBuilder.addEntry(directoryPrefix + root.relativize(file), content, storageMethod);
}
protected void addEntry(String entry, byte[] content) throws IOException {
zipBuilder.addEntry(entry, content, storageMethod);
}
protected void addDirEntry(Path file) throws IOException {
Preconditions.checkArgument(file.startsWith(root), "%s does not start with %s", file, root);
String entryName = directoryPrefix + root.relativize(file);
if (!entryName.endsWith("/")) {
entryName += "/";
}
zipBuilder.addEntry(entryName, new byte[0], storageMethod);
}
public void setCompress(boolean compress) {
storageMethod = compress ? ZipEntry.DEFLATED : ZipEntry.STORED;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
paths.add(file);
return FileVisitResult.CONTINUE;
}
/**
* Iterate through collected file paths in a deterministic order and write to the zip.
*
* @throws IOException if there is an error reading from the source or writing to the zip.
*/
void writeEntries() throws IOException {
for (Path path : Ordering.natural().immutableSortedCopy(paths)) {
writeEntry(path);
}
}
protected void writeEntry(Path file) throws IOException {
if (Files.isDirectory(file)) {
addDirEntry(file);
} else {
byte[] content = Files.readAllBytes(file);
addEntry(file, content);
}
}
}
static final Pattern HEX_REGEX = Pattern.compile("0x[0-9A-Fa-f]{8}");
/**
* Copies the AndroidManifest.xml to the specified output location.
*
* @param provider The MergedAndroidData which contains the manifest to be written to manifestOut.
* @param manifestOut The Path to write the AndroidManifest.xml.
*/
public static void copyManifestToOutput(ManifestContainer provider, Path manifestOut) {
try {
Files.createDirectories(manifestOut.getParent());
Files.copy(provider.getManifest(), manifestOut);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Copies the R.txt to the expected place.
*
* @param generatedSourceRoot The path to the generated R.txt.
* @param rOutput The Path to write the R.txt.
* @param staticIds Boolean that indicates if the ids should be set to 0x1 for caching purposes.
*/
public static void copyRToOutput(Path generatedSourceRoot, Path rOutput, boolean staticIds) {
try {
Files.createDirectories(rOutput.getParent());
final Path source = generatedSourceRoot.resolve("R.txt");
if (Files.exists(source)) {
if (staticIds) {
String contents =
HEX_REGEX
.matcher(Joiner.on("\n").join(Files.readAllLines(source, UTF_8)))
.replaceAll("0x1");
Files.write(rOutput, contents.getBytes(UTF_8));
} else {
Files.copy(source, rOutput);
}
} else {
// The R.txt wasn't generated, create one for future inheritance, as Bazel always requires
// outputs. This state occurs when there are no resource directories.
Files.createFile(rOutput);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** Creates a zip archive from all found R.class (and inner class) files. */
public static void createClassJar(
Path generatedClassesRoot,
Path classJar,
@Nullable String targetLabel,
@Nullable String injectingRuleKind) {
try {
Files.createDirectories(classJar.getParent());
try (final ZipBuilder zip = ZipBuilder.createFor(classJar)) {
ClassJarBuildingVisitor visitor = new ClassJarBuildingVisitor(zip, generatedClassesRoot);
Files.walkFileTree(generatedClassesRoot, visitor);
visitor.writeManifestContent(targetLabel, injectingRuleKind);
visitor.writeEntries();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** Creates a zip archive from all files under the provided root. */
public static void archiveDirectory(Path root, Path archive) {
try {
Files.createDirectories(archive.getParent());
try (final ZipBuilder zip = ZipBuilder.createFor(archive)) {
ZipBuilderVisitor visitor = new ZipBuilderVisitor(zip, root, null);
visitor.setCompress(false);
Files.walkFileTree(root, visitor);
visitor.writeEntries();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** Creates a zip archive from all found R.java files. */
public static void createSrcJar(Path generatedSourcesRoot, Path srcJar, boolean staticIds) {
try {
Files.createDirectories(srcJar.getParent());
try (final ZipBuilder zip = ZipBuilder.createFor(srcJar)) {
SymbolFileSrcJarBuildingVisitor visitor =
new SymbolFileSrcJarBuildingVisitor(zip, generatedSourcesRoot, staticIds);
Files.walkFileTree(generatedSourcesRoot, visitor);
visitor.writeEntries();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** Collects all the compiled resources into an archive, normalizing the paths to the root. */
public static Path archiveCompiledResources(
final Path archiveOut,
final Path databindingResourcesRoot,
final Path compiledRoot,
final List<Path> compiledArtifacts)
throws IOException {
final Path relativeDatabindingProcessedResources =
databindingResourcesRoot.getRoot().relativize(databindingResourcesRoot);
try (ZipBuilder builder = ZipBuilder.createFor(archiveOut)) {
for (Path artifact : compiledArtifacts) {
Path relativeName = artifact;
// remove compiled resources prefix
if (artifact.startsWith(compiledRoot)) {
relativeName = compiledRoot.relativize(relativeName);
}
// remove databinding prefix
if (relativeName.startsWith(relativeDatabindingProcessedResources)) {
relativeName =
relativeName.subpath(
relativeDatabindingProcessedResources.getNameCount(),
relativeName.getNameCount());
}
builder.addEntry(
relativeName.toString(),
Files.readAllBytes(artifact),
ZipEntry.STORED,
ResourceCompiler.getCompiledType(relativeName.toString()).asComment());
}
}
return archiveOut;
}
}