blob: 8c39bdb8d38a4fb14235373657a1147f062daae4 [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.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
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.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
/**
* 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) {
dep = translatePath(dep);
Path depPath = root.getRelative(dep);
dependencies.add(depPath);
}
private String translatePath(String path) {
if (OS.getCurrent() != OS.WINDOWS) {
return path;
}
return WindowsPath.translateWindowsPath(path);
}
/** 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.
*/
@CanIgnoreReturnValue
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;
}
case '$':
if (content[r] == '$') {
content[w++] = '$';
++r;
continue;
}
// I don't think this can ever happen, but fall through nevertheless...
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));
try (PrintStream out = new PrintStream(dotdFile.getOutputStream())) {
out.print(outFile.relativeTo(root) + ": ");
for (Path d : dependencies) {
out.print(" \\\n " + d.getPathString()); // should already be root relative
}
out.println();
}
}
@Override
public boolean equals(Object other) {
return other instanceof DependencySet
&& ((DependencySet) other).dependencies.equals(dependencies);
}
@Override
public int hashCode() {
return dependencies.hashCode();
}
private static final class WindowsPath {
private static final AtomicReference<String> UNIX_ROOT = new AtomicReference<>(null);
private static String translateWindowsPath(String path) {
int n = path.length();
if (n == 0 || path.charAt(0) != '/') {
return path;
}
if (n >= 2 && isAsciiLetter(path.charAt(1)) && (n == 2 || path.charAt(2) == '/')) {
StringBuilder sb = new StringBuilder(path.length());
sb.append(Character.toUpperCase(path.charAt(1)));
sb.append(":/");
sb.append(path, 2, path.length());
return sb.toString();
} else {
String unixRoot = getUnixRoot();
return unixRoot + path;
}
}
private static boolean isAsciiLetter(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
private static String getUnixRoot() {
String value = UNIX_ROOT.get();
if (value == null) {
String jvmFlag = "bazel.windows_unix_root";
value = determineUnixRoot(jvmFlag);
if (value == null) {
throw new IllegalStateException(
String.format(
"\"%1$s\" JVM flag is not set. Use the --host_jvm_args flag. "
+ "For example: "
+ "\"--host_jvm_args=-D%1$s=c:/tools/msys64\".",
jvmFlag));
}
value = value.replace('\\', '/');
if (value.length() > 3 && value.endsWith("/")) {
value = value.substring(0, value.length() - 1);
}
UNIX_ROOT.set(value);
}
return value;
}
@Nullable
private static String determineUnixRoot(String jvmArgName) {
// Get the path from a JVM flag, if specified.
String path = System.getProperty(jvmArgName);
if (path == null) {
return null;
}
path = path.trim();
if (path.isEmpty()) {
return null;
}
return path;
}
}
}