// 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.base.Joiner; import com.google.common.collect.Ordering; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; 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.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.Collection; import java.util.Objects; import java.util.jar.Attributes; import java.util.jar.JarFile; 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; /** Collects all the functionationality for an action to create the final output artifacts. */ public class AndroidResourceOutputs { /** A FileVisitor that will add all R class files to be stored in a zip archive. */ static final class ClassJarBuildingVisitor extends ZipBuilderVisitor { ClassJarBuildingVisitor(ZipOutputStream zip, Path root) { super(zip, root, null); } private byte[] manifestContent() 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"); } ByteArrayOutputStream out = new ByteArrayOutputStream(); manifest.write(out); return out.toByteArray(); } @Override protected void writeFileEntry(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() throws IOException { addEntry(root.resolve(JarFile.MANIFEST_NAME), manifestContent()); } } /** 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(ZipOutputStream zip, Path root, boolean staticIds) { super(zip, 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 writeFileEntry(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 to be stored in a zip archive. */ static class ZipBuilderVisitor extends SimpleFileVisitor<Path> { // 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 = 315561600000L; private final String directoryPrefix; private final Collection<Path> paths = new ArrayList<>(); protected final Path root; private int storageMethod = ZipEntry.STORED; private final ZipOutputStream zip; ZipBuilderVisitor(ZipOutputStream zip, Path root, String directory) { this.zip = zip; this.root = root; this.directoryPrefix = directory; } protected void addEntry(Path file, byte[] content) throws IOException { String prefix = directoryPrefix != null ? (directoryPrefix + "/") : ""; String relativeName = root.relativize(file).toString(); ZipEntry entry = new ZipEntry(prefix + relativeName); entry.setMethod(storageMethod); entry.setTime(normalizeTime(relativeName)); entry.setSize(content.length); CRC32 crc32 = new CRC32(); crc32.update(content); entry.setCrc(crc32.getValue()); zip.putNextEntry(entry); zip.write(content); zip.closeEntry(); } /** * 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; } } 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)) { writeFileEntry(path); } } protected void writeFileEntry(Path file) throws IOException { 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 androidData The MergedAndroidData which contains the manifest to be written to * manifestOut. * @param manifestOut The Path to write the AndroidManifest.xml. */ public static void copyManifestToOutput(MergedAndroidData androidData, Path manifestOut) { try { Files.createDirectories(manifestOut.getParent()); Files.copy(androidData.getManifest(), manifestOut); // Set to the epoch for caching purposes. Files.setLastModifiedTime(manifestOut, FileTime.fromMillis(0L)); } 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); } // Set to the epoch for caching purposes. Files.setLastModifiedTime(rOutput, FileTime.fromMillis(0L)); } 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) { try { Files.createDirectories(classJar.getParent()); try (final ZipOutputStream zip = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(classJar)))) { ClassJarBuildingVisitor visitor = new ClassJarBuildingVisitor(zip, generatedClassesRoot); Files.walkFileTree(generatedClassesRoot, visitor); visitor.writeEntries(); visitor.writeManifestContent(); } // Set to the epoch for caching purposes. Files.setLastModifiedTime(classJar, FileTime.fromMillis(0L)); } catch (IOException e) { throw new RuntimeException(e); } } /** * Creates a zip file containing the provided android resources and assets. * * @param resourcesRoot The root containing android resources to be written. * @param assetsRoot The root containing android assets to be written. * @param output The path to write the zip file * @param compress Whether or not to compress the content * @throws IOException */ public static void createResourcesZip( Path resourcesRoot, Path assetsRoot, Path output, boolean compress) throws IOException { try (ZipOutputStream zout = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(output)))) { if (Files.exists(resourcesRoot)) { ZipBuilderVisitor visitor = new ZipBuilderVisitor(zout, resourcesRoot, "res"); visitor.setCompress(compress); Files.walkFileTree(resourcesRoot, visitor); visitor.writeEntries(); } if (Files.exists(assetsRoot)) { ZipBuilderVisitor visitor = new ZipBuilderVisitor(zout, assetsRoot, "assets"); visitor.setCompress(compress); Files.walkFileTree(assetsRoot, visitor); visitor.writeEntries(); } } } /** 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 ZipOutputStream zip = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(srcJar)))) { SymbolFileSrcJarBuildingVisitor visitor = new SymbolFileSrcJarBuildingVisitor( zip, generatedSourcesRoot, staticIds); Files.walkFileTree(generatedSourcesRoot, visitor); visitor.writeEntries(); } // Set to the epoch for caching purposes. Files.setLastModifiedTime(srcJar, FileTime.fromMillis(0L)); } catch (IOException e) { throw new RuntimeException(e); } } }