// Copyright 2014 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.lib.util; import com.google.common.annotations.VisibleForTesting; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import java.io.IOException; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; /** * Representation of a set of file dependencies for a given output file. There * are generally one input dependency and a bunch of include dependencies. The * files are stored as {@code Path}s and may be relative or absolute. * <p> * The serialized format read and written is equivalent and compatible with the * ".d" file produced by the -MM for a given out (.o) file. * <p> * The file format looks like: * * <pre> * {outfile}: \ * {infile} \ * {include} \ * ... \ * {include} * </pre> * * @see "http://gcc.gnu.org/onlinedocs/gcc-4.2.1/gcc/Preprocessor-Options.html#Preprocessor-Options" */ public final class DependencySet { /** * The set of dependent files that this DependencySet embodies. They are all * Path with the same FileSystem A tree set is used to ensure that we * write them out in a consistent order. */ private final Collection<Path> dependencies = new ArrayList<>(); private final Path root; private String outputFileName; /** * Get output file name for which dependencies are included in this DependencySet. */ public String getOutputFileName() { return outputFileName; } public void setOutputFileName(String outputFileName) { this.outputFileName = outputFileName; } /** * Constructs a new empty DependencySet instance. */ public DependencySet(Path root) { this.root = root; } /** * Gets an unmodifiable view of the set of dependencies in {@link Path} form * from this DependencySet instance. */ public Collection<Path> getDependencies() { return Collections.unmodifiableCollection(dependencies); } /** * Adds a given collection of dependencies in Path form to this DependencySet * instance. Paths are converted to root-relative */ @VisibleForTesting // only called from DependencySetTest public void addDependencies(Collection<Path> deps) { for (Path d : deps) { Preconditions.checkArgument(d.startsWith(root)); dependencies.add(d); } } /** * Adds a given dependency to this DependencySet instance. */ private void addDependency(String dep) { Path depPath = root.getRelative(dep); dependencies.add(depPath); } /** * Reads a dotd file into this DependencySet instance. */ public DependencySet read(Path dotdFile) throws IOException { byte[] content = FileSystemUtils.readContent(dotdFile); try { return process(content); } catch (IOException e) { throw new IOException("Error processing " + dotdFile + ": " + e.getMessage()); } } /** * Parses a .d file. * * <p>Performance-critical! In large C++ builds there are lots of .d files to read, and some of * them reach into hundreds of kilobytes. */ public DependencySet process(byte[] content) throws IOException { final int n = content.length; if (n > 0 && content[n - 1] != '\n') { throw new IOException("File does not end in a newline"); // From now on, we can safely peek ahead one character when not at a newline. } // Our write position in content[]; we use the prefix as working space to build strings. int w = 0; // Have we seen a leading "mumble.o:" on this line yet? If not, we ignore // any dependencies we parse. This is bug-for-bug compatibility with our // MSVC wrapper, which generates invalid .d files :( boolean sawTarget = false; for (int r = 0; r < n; ) { final byte c = content[r++]; switch (c) { case ' ': // If we haven't yet seen the colon delimiting the target name, // keep scanning. We do this to cope with "foo.o : \" which is // valid Makefile syntax produced by the cuda compiler. if (sawTarget && w > 0) { addDependency(new String(content, 0, w, StandardCharsets.UTF_8)); w = 0; } continue; case '\r': // Ignore, should be followed by a \n. continue; case '\n': // This closes a filename. // (Arguably if !sawTarget && w > 0 we should report an error, // as that suggests the .d file is malformed.) if (sawTarget && w > 0) { addDependency(new String(content, 0, w, StandardCharsets.UTF_8)); } w = 0; sawTarget = false; // reset for new line continue; case ':': // Normally this indicates the target name, but it might be part of a // filename on Windows. Peek ahead at the next character. switch (content[r]) { case ' ': case '\n': case '\r': if (w > 0) { outputFileName = new String(content, 0, w, StandardCharsets.UTF_8); w = 0; sawTarget = true; } continue; default: content[w++] = c; // copy a colon to filename continue; } case '\\': // Peek ahead at the next character. switch (content[r]) { // Backslashes are taken literally except when followed by whitespace. // See the Windows tests for some of the nonsense we have to tolerate. case ' ': content[w++] = ' '; // copy a space to the filename ++r; // skip over the space continue; case '\n': ++r; // skip over the newline continue; case '\r': // One backslash can escape \r\n, so peek one more character. if (content[++r] == '\n') { ++r; } continue; default: content[w++] = c; // copy a backlash to the filename continue; } default: content[w++] = c; } } return this; } /** * Writes this DependencySet object for a specified output file under the root * dir, and with a given suffix. */ public void write(Path outFile, String suffix) throws IOException { Path dotdFile = outFile.getRelative(FileSystemUtils.replaceExtension(outFile.asFragment(), suffix)); PrintStream out = new PrintStream(dotdFile.getOutputStream()); try { out.print(outFile.relativeTo(root) + ": "); for (Path d : dependencies) { out.print(" \\\n " + d.getPathString()); // should already be root relative } out.println(); } finally { out.close(); } } @Override public boolean equals(Object other) { return other instanceof DependencySet && ((DependencySet) other).dependencies.equals(dependencies); } @Override public int hashCode() { return dependencies.hashCode(); } }