blob: 9f099cc3c8eace906fb638c94f4e0c2836bd4635 [file] [log] [blame]
// 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.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
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;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 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 PathFragment}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 {
private static final Pattern DOTD_MERGED_LINE_SEPARATOR = Pattern.compile("\\\\[\n\r]+");
private static final Pattern DOTD_LINE_SEPARATOR = Pattern.compile("[\n\r]+");
private static final Pattern DOTD_DEP = Pattern.compile("(?:[^\\s\\\\]++|\\\\ |\\\\)+");
/**
* The set of dependent files that this DependencySet embodies. May be
* relative or absolute PathFragments. A tree set is used to ensure that we
* write them out in a consistent order.
*/
private final Collection<PathFragment> 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 PathFragment form
* from this DependencySet instance.
*/
public Collection<PathFragment> getDependencies() {
return Collections.unmodifiableCollection(dependencies);
}
/**
* Adds a given collection of dependencies in Path form to this DependencySet
* instance. Paths are converted to root-relative
*/
public void addDependencies(Collection<Path> deps) {
for (Path d : deps) {
addDependency(d.relativeTo(root));
}
}
/**
* Adds a given dependency in PathFragment form to this DependencySet
* instance.
*/
public void addDependency(PathFragment dep) {
dependencies.add(Preconditions.checkNotNull(dep));
}
/**
* 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 {
if (content.length > 0 && content[content.length - 1] != '\n') {
throw new IOException("File does not end in a newline");
}
// true if there is a CR in the input.
boolean cr = content.length > 0 && content[0] == '\r';
// true if there is more than one line in the input, not counting \-wrapped lines.
boolean multiline = false;
byte prevByte = ' ';
for (int i = 1; i < content.length; i++) {
byte b = content[i];
if (cr || b == '\r') {
// CR found, abort since our little loop here does not deal with CR/LFs.
cr = true;
break;
}
if (b == '\n') {
// Merge lines wrapped using backslashes.
if (prevByte == '\\') {
content[i] = ' ';
content[i - 1] = ' ';
} else {
multiline = true;
}
}
prevByte = b;
}
if (!cr && content.length > 0 && content[content.length - 1] == '\n') {
content[content.length - 1] = ' ';
}
String s = new String(content, StandardCharsets.UTF_8);
if (cr) {
s = DOTD_MERGED_LINE_SEPARATOR.matcher(s).replaceAll(" ").trim();
multiline = true;
}
return process(s, multiline);
}
private DependencySet process(String contents, boolean multiline) {
String[] lines;
if (!multiline) {
// Microoptimization: skip the usually unnecessary expensive-ish splitting step if there is
// only one target. This saves about 20% of CPU time.
lines = new String[] { contents };
} else {
lines = DOTD_LINE_SEPARATOR.split(contents);
}
for (String line : lines) {
// Split off output file name.
int pos = line.indexOf(':');
if (pos == -1) {
continue;
}
outputFileName = line.substring(0, pos);
String deps = line.substring(pos + 1);
Matcher m = DOTD_DEP.matcher(deps);
while (m.find()) {
String token = m.group();
// Process escaped spaces.
if (token.contains("\\ ")) {
token = token.replace("\\ ", " ");
}
dependencies.add(new PathFragment(token).normalize());
}
}
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 (PathFragment 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();
}
}