// 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 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; /** * 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. *

* The serialized format read and written is equivalent and compatible with the * ".d" file produced by the -MM for a given out (.o) file. *

* The file format looks like: * *

 * {outfile}:  \
 *  {infile} \
 *   {include} \
 *   ... \
 *   {include}
 * 
* * @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 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 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 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. * *

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)); 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 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; } 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; } } }