// Copyright 2016 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.buildjar.instrumentation; import com.google.devtools.build.buildjar.InvalidCommandLineException; import com.google.devtools.build.buildjar.JavaLibraryBuildRequest; import com.google.devtools.build.buildjar.jarhelper.JarCreator; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.List; import org.jacoco.core.instr.Instrumenter; import org.jacoco.core.runtime.OfflineInstrumentationAccessGenerator; /** Instruments compiled java classes using Jacoco instrumentation library. */ public final class JacocoInstrumentationProcessor { public static JacocoInstrumentationProcessor create(List<String> args) throws InvalidCommandLineException { if (args.size() < 2) { throw new InvalidCommandLineException( "Number of arguments for Jacoco instrumentation should be 2+ (given " + args.size() + ": metadataOutput metadataDirectory [filters*]."); } // ignoring filters, they weren't used in the previous implementation // TODO(bazel-team): filters should be correctly handled return new JacocoInstrumentationProcessor(args.get(1), args.get(0)); } private final String metadataDir; private final String metadataOutput; private JacocoInstrumentationProcessor(String metadataDir, String metadataOutput) { this.metadataDir = metadataDir; this.metadataOutput = metadataOutput; } /** * Instruments classes using Jacoco and keeps copies of uninstrumented class files in * jacocoMetadataDir, to be zipped up in the output file jacocoMetadataOutput. */ public void processRequest(JavaLibraryBuildRequest build) throws IOException { // Clean up jacocoMetadataDir to be used by postprocessing steps. This is important when // running JavaBuilder locally, to remove stale entries from previous builds. if (metadataDir != null) { Path workDir = Paths.get(metadataDir); if (Files.exists(workDir)) { recursiveRemove(workDir); } Files.createDirectories(workDir); } JarCreator jar = new JarCreator(metadataOutput); jar.setNormalize(true); jar.setCompression(build.compressJar()); Instrumenter instr = new Instrumenter(new OfflineInstrumentationAccessGenerator()); // TODO(bazel-team): not sure whether Emma did anything fancier than this (multithreaded?) instrumentRecursively(instr, Paths.get(build.getClassDir())); jar.addDirectory(metadataDir); jar.execute(); } /** * Runs Jacoco instrumentation processor over all .class files recursively, starting with root. */ private void instrumentRecursively(Instrumenter instr, Path root) throws IOException { Files.walkFileTree( root, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (!file.getFileName().toString().endsWith(".class")) { return FileVisitResult.CONTINUE; } // TODO(bazel-team): filter with coverage_instrumentation_filter? // It's not clear whether there is any advantage in not instrumenting *Test classes, // apart from lowering the covered percentage in the aggregate statistics. // We first move the original .class file to our metadata directory, then instrument it // and output the instrumented version in the regular classes output directory. Path instrumentedCopy = file; Path uninstrumentedCopy = Paths.get(metadataDir).resolve(root.relativize(file)); Files.createDirectories(uninstrumentedCopy.getParent()); Files.move(file, uninstrumentedCopy); try (InputStream input = new BufferedInputStream(Files.newInputStream(uninstrumentedCopy)); OutputStream output = new BufferedOutputStream(Files.newOutputStream(instrumentedCopy))) { instr.instrument(input, output, file.toString()); } return FileVisitResult.CONTINUE; } }); } // TODO(b/27069912): handle symlinks private static void recursiveRemove(Path path) throws IOException { Files.walkFileTree( path, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); } }